diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..cbe6f8d --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ + +*.test +target \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..f347bcd --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,710 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "android_system_properties" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7ed72e1635e121ca3e79420540282af22da58be50de153d36f81ddc6b83aa9e" +dependencies = [ + "libc", +] + +[[package]] +name = "async-trait" +version = "0.1.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76464446b8bc32758d7e88ee1a804d9914cd9b1cb264c029899680b0be29826f" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bumpalo" +version = "3.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1ad822118d20d2c234f427000d5acc36eabe1e29a348c89b63dd60b13f28e5d" + +[[package]] +name = "byteorder" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" + +[[package]] +name = "bytes" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec8a7b6a70fde80372154c65702f00a0f56f3e1c36abbc6c440484be248856db" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "chrono" +version = "0.4.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfd4d1b31faaa3a89d7934dbded3111da0d2ef28e3ebccdb4f0179f5929d1ef1" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-integer", + "num-traits", + "time", + "wasm-bindgen", + "winapi", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc" + +[[package]] +name = "crc32fast" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "045ebe27666471bb549370b4b0b3e51b07f56325befa4284db65fc89c02511b1" +dependencies = [ + "autocfg", + "cfg-if", + "crossbeam-utils", + "memoffset", + "once_cell", + "scopeguard", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51887d4adc7b564537b15adcfb307936f8075dfcd5f00dde9a9f1d29383682bc" +dependencies = [ + "cfg-if", + "once_cell", +] + +[[package]] +name = "fs2" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9564fc758e15025b46aa6643b1b77d047d1a56a1aea6e01002ac0c7026876213" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "fxhash" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c" +dependencies = [ + "byteorder", +] + +[[package]] +name = "getrandom" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4eb1a864a501629691edf6c15a593b7a51eebaa1e8468e9ddc623de7c9b58ec6" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.11.0+wasi-snapshot-preview1", +] + +[[package]] +name = "harsh-client" +version = "0.1.0" +dependencies = [ + "harsh_common", + "sled", + "telecomande", + "tokio", +] + +[[package]] +name = "harsh_common" +version = "0.1.0" +dependencies = [ + "serde", + "serde_json", +] + +[[package]] +name = "harsh_server" +version = "0.1.0" +dependencies = [ + "chrono", + "harsh_common", + "rand", + "serde", + "serde_json", + "sled", + "telecomande", + "tokio", +] + +[[package]] +name = "hermit-abi" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" +dependencies = [ + "libc", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad2bfd338099682614d3ee3fe0cd72e0b6a41ca6a87f6a74a3bd593c91650501" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "js-sys", + "wasm-bindgen", + "winapi", +] + +[[package]] +name = "instant" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "itoa" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c8af84674fe1f223a982c933a0ee1086ac4d4052aa0fb8060c12c6ad838e754" + +[[package]] +name = "js-sys" +version = "0.3.59" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "258451ab10b34f8af53416d1fdab72c22e805f0c92a1136d59470ec0b11138b2" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "libc" +version = "0.2.132" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8371e4e5341c3a96db127eb2465ac681ced4c433e01dd0e938adbef26ba93ba5" + +[[package]] +name = "lock_api" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "327fa5b6a6940e4699ec49a9beae1ea4845c6bab9314e4f84ac68742139d8c53" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "memchr" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" + +[[package]] +name = "memoffset" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aa361d4faea93603064a027415f07bd8e1d5c88c9fbf68bf56a285428fd79ce" +dependencies = [ + "autocfg", +] + +[[package]] +name = "mio" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57ee1c23c7c63b0c9250c339ffdc69255f110b298b901b9f6c82547b7b87caaf" +dependencies = [ + "libc", + "log", + "wasi 0.11.0+wasi-snapshot-preview1", + "windows-sys", +] + +[[package]] +name = "num-integer" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" +dependencies = [ + "autocfg", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_cpus" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19e64526ebdee182341572e50e9ad03965aa510cd94427a4549448f285e957a1" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "once_cell" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "074864da206b4973b84eb91683020dbefd6a8c3f0f38e054d93954e891935e4e" + +[[package]] +name = "parking_lot" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99" +dependencies = [ + "instant", + "lock_api", + "parking_lot_core 0.8.5", +] + +[[package]] +name = "parking_lot" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" +dependencies = [ + "lock_api", + "parking_lot_core 0.9.3", +] + +[[package]] +name = "parking_lot_core" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d76e8e1493bcac0d2766c42737f34458f1c8c50c0d23bcb24ea953affb273216" +dependencies = [ + "cfg-if", + "instant", + "libc", + "redox_syscall", + "smallvec", + "winapi", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09a279cbf25cb0757810394fbc1e359949b59e348145c643a939a525692e6929" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-sys", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116" + +[[package]] +name = "ppv-lite86" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb9f9e6e233e5c4a35559a617bf40a4ec447db2e84c20b55a6f83167b7e57872" + +[[package]] +name = "proc-macro2" +version = "1.0.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a2ca2c61bc9f3d74d2886294ab7b9853abd9c1ad903a3ac7815c58989bb7bab" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbe448f377a7d6961e30f5955f9b8d106c3f5e449d493ee1b125c1d43c2b5179" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7" +dependencies = [ + "getrandom", +] + +[[package]] +name = "redox_syscall" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" +dependencies = [ + "bitflags", +] + +[[package]] +name = "ryu" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4501abdff3ae82a1c1b477a17252eb69cee9e66eb915c1abaa4f44d873df9f09" + +[[package]] +name = "scopeguard" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" + +[[package]] +name = "serde" +version = "1.0.144" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f747710de3dcd43b88c9168773254e809d8ddbdf9653b84e2554ab219f17860" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.144" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94ed3a816fb1d101812f83e789f888322c34e291f894f19590dc310963e87a00" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38dd04e3c8279e75b31ef29dbdceebfe5ad89f4d0937213c53f7d49d01b3d5a7" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e51e73328dc4ac0c7ccbda3a494dfa03df1de2f46018127f60c693f2648455b0" +dependencies = [ + "libc", +] + +[[package]] +name = "sled" +version = "0.34.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f96b4737c2ce5987354855aed3797279def4ebf734436c6aa4552cf8e169935" +dependencies = [ + "crc32fast", + "crossbeam-epoch", + "crossbeam-utils", + "fs2", + "fxhash", + "libc", + "log", + "parking_lot 0.11.2", +] + +[[package]] +name = "smallvec" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fd0db749597d91ff862fd1d55ea87f7855a744a8425a64695b6fca237d1dad1" + +[[package]] +name = "socket2" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66d72b759436ae32898a2af0a14218dbf55efde3feeb170eb623637db85ee1e0" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "syn" +version = "1.0.99" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58dbef6ec655055e20b86b15a8cc6d439cca19b667537ac6a1369572d151ab13" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "telecomande" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e2911385f0e73674cd9a881c3121b27a78bbc1a4f25fce42989e85c6b571679" +dependencies = [ + "async-trait", + "tokio", +] + +[[package]] +name = "time" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db9e6914ab8b1ae1c260a4ae7a49b6c5611b40328a735b21862567685e73255" +dependencies = [ + "libc", + "wasi 0.10.0+wasi-snapshot-preview1", + "winapi", +] + +[[package]] +name = "tokio" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a8325f63a7d4774dd041e363b2409ed1c5cbbd0f867795e661df066b2b0a581" +dependencies = [ + "autocfg", + "bytes", + "libc", + "memchr", + "mio", + "num_cpus", + "once_cell", + "parking_lot 0.12.1", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "winapi", +] + +[[package]] +name = "tokio-macros" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9724f9a975fb987ef7a3cd9be0350edcbe130698af5b8f7a631e23d42d052484" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "unicode-ident" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4f5b37a154999a8f3f98cc23a628d850e154479cd94decf3414696e12e31aaf" + +[[package]] +name = "wasi" +version = "0.10.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasm-bindgen" +version = "0.2.82" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc7652e3f6c4706c8d9cd54832c4a4ccb9b5336e2c3bd154d5cccfbf1c1f5f7d" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.82" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "662cd44805586bd52971b9586b1df85cdbbd9112e4ef4d8f41559c334dc6ac3f" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.82" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b260f13d3012071dfb1512849c033b1925038373aea48ced3012c09df952c602" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.82" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5be8e654bdd9b79216c2929ab90721aa82faf65c48cdf08bdc4e7f51357b80da" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.82" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6598dd0bd3c7d51095ff6531a5b23e02acdc81804e30d8f07afb77b7215a140a" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-sys" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea04155a16a59f9eab786fe12a4a450e75cdb175f9e0d80da1e17db09f55b8d2" +dependencies = [ + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_msvc" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bb8c3fd39ade2d67e9874ac4f3db21f0d710bee00fe7cab16949ec184eeaa47" + +[[package]] +name = "windows_i686_gnu" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "180e6ccf01daf4c426b846dfc66db1fc518f074baa793aa7d9b9aaeffad6a3b6" + +[[package]] +name = "windows_i686_msvc" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2e7917148b2812d1eeafaeb22a97e4813dfa60a3f8f78ebe204bcc88f12f024" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4dcd171b8776c41b97521e5da127a2d86ad280114807d0b2ab1e462bc764d9e1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c811ca4a8c853ef420abd8592ba53ddbbac90410fab6903b3e79972a631f7680" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..75cb270 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,3 @@ +[workspace] +members = ["harsh-client", "harsh-server", "harsh-common"] + diff --git a/README.md b/README.md new file mode 100644 index 0000000..fcc3165 --- /dev/null +++ b/README.md @@ -0,0 +1,18 @@ +# Harsh + +![harsh icon](./assets/icon_256.png) + +--- + +## Description + +An enbeded Harmony server implementation written in rust. + +> **Warning** +> This project is in early design phase, it is non conforming and very few features are implemented. + +## Usage + +To launch the server, use the `start-server.sh` script + +To launch the debug client, use the `start-client.sh` script diff --git a/assets/icon_256.png b/assets/icon_256.png new file mode 100644 index 0000000..5a1cd30 Binary files /dev/null and b/assets/icon_256.png differ diff --git a/assets/icon_512.png b/assets/icon_512.png new file mode 100644 index 0000000..5b09baa Binary files /dev/null and b/assets/icon_512.png differ diff --git a/harsh-client/.gitignore b/harsh-client/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/harsh-client/.gitignore @@ -0,0 +1 @@ +/target diff --git a/harsh-client/Cargo.toml b/harsh-client/Cargo.toml new file mode 100644 index 0000000..f9c1838 --- /dev/null +++ b/harsh-client/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "harsh-client" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +sled = "0.34.7" +telecomande = "1.2.2" +tokio = { version = "1.20.1", features = ["full"] } +harsh_common = { path = "../harsh-common" } \ No newline at end of file diff --git a/harsh-client/src/commands.rs b/harsh-client/src/commands.rs new file mode 100644 index 0000000..5be95e9 --- /dev/null +++ b/harsh-client/src/commands.rs @@ -0,0 +1,192 @@ +use harsh_common::ClientRequest; + +pub enum Command { + Help, + Request(ClientRequest), +} + +pub fn parse(input: &str) -> Option { + let mut parts = smart_split(input).into_iter(); + let command = match parts.next()?.as_str() { + "help" => return Some(Command::Help), + "ping" => { + let rest = parts.collect::>(); + let content = rest.join(" "); + ClientRequest::new_ping(content) + } + "chanls" => ClientRequest::new_channel_list(), + "chanadd" => { + let name = parts.next()?; + ClientRequest::new_channel_create(name) + } + "chandel" => { + let id = parts.next()?.parse().ok()?; + ClientRequest::new_channel_delete(id) + } + "changname" => { + let id = parts.next()?.parse().ok()?; + ClientRequest::new_channel_get_name(id) + } + "chansname" => { + let id = parts.next()?.parse().ok()?; + let name = parts.next()?; + ClientRequest::new_channel_set_name(id, name) + } + "msgls" => { + let channel_id = parts.next()?.parse().ok()?; + ClientRequest::new_message_list(channel_id) + } + "msgadd" => { + let channel_id = parts.next()?.parse().ok()?; + let content = parts.next()?; + ClientRequest::new_message_create(channel_id, content) + } + "msgdel" => { + let channel_id = parts.next()?.parse().ok()?; + let id = parts.next()?.parse().ok()?; + ClientRequest::new_message_delete(channel_id, id) + } + "msggcont" => { + let channel_id = parts.next()?.parse().ok()?; + let id = parts.next()?.parse().ok()?; + ClientRequest::new_message_get_content(channel_id, id) + } + "msgscont" => { + let channel_id = parts.next()?.parse().ok()?; + let id = parts.next()?.parse().ok()?; + let content = parts.next()?; + ClientRequest::new_message_set_content(channel_id, id, content) + } + "usrls" => ClientRequest::new_user_list(), + "usradd" => { + let name = parts.next()?; + let pass = parts.next()?; + ClientRequest::new_user_create(name, pass) + } + "usrdel" => { + let id = parts.next()?.parse().ok()?; + ClientRequest::new_user_delete(id) + } + "usrgname" => { + let id = parts.next()?.parse().ok()?; + ClientRequest::new_user_get_name(id) + } + "usrsname" => { + let id = parts.next()?.parse().ok()?; + let name = parts.next()?; + ClientRequest::new_user_set_name(id, name) + } + "usrspass" => { + let id = parts.next()?.parse().ok()?; + let pass = parts.next()?; + ClientRequest::new_user_set_pass(id, pass) + } + _ => return None, + }; + + Some(Command::Request(command)) +} + +pub const CMDS: &'static [Description] = &[ + // all commands + Description::new("help", &[], "returns a help message"), + Description::new( + "ping", + &["content"], + "sends a ping with the specified content", + ), + Description::new("chanls", &[], "list channels"), + Description::new("chanadd", &["name"], "creates a new channel"), + Description::new("chandel", &["id"], "delete a channel by its id"), + Description::new("changname", &["id"], "get a channel's name"), + Description::new("chansname", &["id", "name"], "set a channel's name"), + Description::new("msgls", &["channel_id"], "list messages"), + Description::new("msgadd", &["channel_id", "content"], "create a message"), + Description::new("msgdel", &["channel_id", "id"], "delete a message"), + Description::new("msggcont", &["channel_id", "id"], "get a message's content"), + Description::new( + "msgscont", + &["channel_id", "id", "content"], + "set a message's content", + ), + Description::new("usrls", &[], "list users"), + Description::new("usradd", &["name", "pass"], "add a user"), + Description::new("usrdel", &["id"], "delete a user"), + Description::new("usrgname", &["id"], "get a user name"), + Description::new("usrsname", &["id", "name"], "set a user name"), + Description::new("usrspass", &["id", "pass"], "set a user pass"), +]; + +pub fn smart_split(input: &str) -> Vec { + let input = input.trim(); + let mut result = Vec::new(); + + let mut capturing = false; + let mut ignoring = false; + let mut current = String::new(); + for char in input.chars() { + let char: char = char; + if ignoring { + current.push(char); + ignoring = false; + continue; + } + + match char { + '\\' => ignoring = true, + '"' => capturing = !capturing, + ' ' if !capturing => { + result.push(current); + current = String::new(); + } + _ => current.push(char), + } + } + result.push(current); + result +} + +#[test] +fn test_smart_split() { + assert_eq!( + smart_split("hello world"), + vec!["hello".to_string(), "world".to_string()] + ); + assert_eq!( + smart_split(r#""lorem ipsum" "dolor amit""#), + vec!["lorem ipsum".to_string(), "dolor amit".to_string()] + ); + assert_eq!( + smart_split(r#"lorem "ipsum do"lor "amit""#), + vec![ + "lorem".to_string(), + "ipsum dolor".to_string(), + "amit".to_string() + ] + ); +} + +pub struct Description { + name: &'static str, + params: &'static [&'static str], + desc: &'static str, +} + +impl Description { + pub const fn new( + name: &'static str, + params: &'static [&'static str], + desc: &'static str, + ) -> Self { + Self { name, desc, params } + } +} + +pub fn help() { + for &Description { name, params, desc } in CMDS { + let mut usage = params.iter().map(|s| s.to_string()).collect::>(); + usage.insert(0, name.to_string()); + let usage = usage.join(" "); + println!("{name}:\n\tusage:\t\t{usage}\n\tdescription:\t{desc}"); + } +} diff --git a/harsh-client/src/main.rs b/harsh-client/src/main.rs new file mode 100644 index 0000000..73d4442 --- /dev/null +++ b/harsh-client/src/main.rs @@ -0,0 +1,64 @@ +use std::{ + io::{stdout, Write}, + process::exit, +}; + +use harsh_common::ServerRequest; +use tokio::{ + io::{stdin, AsyncBufReadExt, AsyncWriteExt, BufReader}, + net::TcpStream, +}; + +const ADDRESS: &'static str = "localhost:42069"; + +#[tokio::main] +async fn main() { + println!("[main/info] starting client ..."); + let stream = TcpStream::connect(ADDRESS).await.unwrap(); + println!("[main/info] connected to '{ADDRESS}'"); + let (reader, writer) = stream.into_split(); + tokio::spawn(async { + let mut reader = BufReader::new(reader); + loop { + let mut line = String::new(); + match reader.read_line(&mut line).await { + Ok(0) => { + break; + } + _ => (), + } + if let Some(parsed) = ServerRequest::try_parse(&line) { + println!("[main/info] received '{parsed:?}'"); + } + } + println!("[main/info] connection closed, goodbye."); + exit(0); + }); + + let input_loop = tokio::spawn(async { + let mut input = BufReader::new(stdin()); + let mut writer = writer; + + loop { + print!("$> "); + stdout().lock().flush().unwrap(); + let mut line = String::new(); + input.read_line(&mut line).await.unwrap(); + let input = commands::parse(&line); + match input { + None => println!("[main/warn] failed to parse command"), + Some(commands::Command::Help) => commands::help(), + Some(commands::Command::Request(cmd)) => { + println!("[main/info] sending.."); + writer.write_all(cmd.serialize().as_bytes()).await.unwrap(); + writer.write_all(b"\n").await.unwrap(); + } + } + } + }); + + println!("[main/info] awaiting input ..."); + input_loop.await.unwrap(); +} + +mod commands; diff --git a/harsh-common/.gitignore b/harsh-common/.gitignore new file mode 100644 index 0000000..4fffb2f --- /dev/null +++ b/harsh-common/.gitignore @@ -0,0 +1,2 @@ +/target +/Cargo.lock diff --git a/harsh-common/Cargo.toml b/harsh-common/Cargo.toml new file mode 100644 index 0000000..c853431 --- /dev/null +++ b/harsh-common/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "harsh_common" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +serde = { version = "1.0.144", features = ["derive"] } +serde_json = "1.0.83" diff --git a/harsh-common/src/client.rs b/harsh-common/src/client.rs new file mode 100644 index 0000000..13540be --- /dev/null +++ b/harsh-common/src/client.rs @@ -0,0 +1,355 @@ +#[derive(Debug)] +pub struct Ping { + pub content: String, +} + +#[derive(Debug)] +pub struct ChannelList {} + +#[derive(Debug)] +pub struct ChannelCreate { + pub name: String, +} + +#[derive(Debug)] +pub struct ChannelDelete { + pub id: u64, +} + +#[derive(Debug)] +pub struct ChannelGetName { + pub id: u64, +} + +#[derive(Debug)] +pub struct ChannelSetName { + pub id: u64, + pub name: String, +} + +#[derive(Debug)] +pub struct MessageList { + pub channel_id: u64, +} +#[derive(Debug)] +pub struct MessageCreate { + pub channel_id: u64, + pub content: String, +} +#[derive(Debug)] +pub struct MessageDelete { + pub channel_id: u64, + pub id: u64, +} +#[derive(Debug)] +pub struct MessageGetContent { + pub channel_id: u64, + pub id: u64, +} +#[derive(Debug)] +pub struct MessageSetContent { + pub channel_id: u64, + pub id: u64, + pub content: String, +} + +#[derive(Debug)] +pub struct UserList {} + +#[derive(Debug)] +pub struct UserCreate { + pub name: String, + pub pass: String, +} + +#[derive(Debug)] +pub struct UserDelete { + pub id: u64, +} + +#[derive(Debug)] +pub struct UserGetName { + pub id: u64, +} + +#[derive(Debug)] +pub struct UserSetName { + pub id: u64, + pub name: String, +} + +#[derive(Debug)] +pub struct UserGetPass { + pub id: u64, +} + +#[derive(Debug)] +pub struct UserSetPass { + pub id: u64, + pub pass: String, +} + +#[derive(Debug)] +pub enum ClientRequest { + Ping(Ping), + ChannelList(ChannelList), + ChannelCreate(ChannelCreate), + ChannelDelete(ChannelDelete), + ChannelGetName(ChannelGetName), + ChannelSetName(ChannelSetName), + + MessageList(MessageList), + MessageCreate(MessageCreate), + MessageDelete(MessageDelete), + MessageGetContent(MessageGetContent), + MessageSetContent(MessageSetContent), + + UserList(UserList), + UserCreate(UserCreate), + UserDelete(UserDelete), + UserGetName(UserGetName), + UserSetName(UserSetName), + UserSetPass(UserSetPass), +} + +impl ClientRequest { + pub fn new_ping(content: String) -> Self { + Self::Ping(Ping { content }) + } + + pub fn new_channel_list() -> Self { + Self::ChannelList(ChannelList {}) + } + + pub fn new_channel_create(name: String) -> Self { + Self::ChannelCreate(ChannelCreate { name }) + } + + pub fn new_channel_delete(channel_id: u64) -> Self { + Self::ChannelDelete(ChannelDelete { id: channel_id }) + } + + pub fn new_channel_get_name(channel_id: u64) -> Self { + Self::ChannelGetName(ChannelGetName { id: channel_id }) + } + + pub fn new_channel_set_name(channel_id: u64, name: String) -> Self { + Self::ChannelSetName(ChannelSetName { + id: channel_id, + name, + }) + } + + pub fn new_message_list(channel_id: u64) -> Self { + Self::MessageList(MessageList { channel_id }) + } + pub fn new_message_create(channel_id: u64, content: String) -> Self { + Self::MessageCreate(MessageCreate { + channel_id, + content, + }) + } + pub fn new_message_delete(channel_id: u64, id: u64) -> Self { + Self::MessageDelete(MessageDelete { channel_id, id }) + } + pub fn new_message_get_content(channel_id: u64, id: u64) -> Self { + Self::MessageGetContent(MessageGetContent { channel_id, id }) + } + pub fn new_message_set_content(channel_id: u64, id: u64, content: String) -> Self { + Self::MessageSetContent(MessageSetContent { + channel_id, + id, + content, + }) + } + pub fn new_user_list() -> Self { + Self::UserList(UserList {}) + } + pub fn new_user_create(name: String, pass: String) -> Self { + Self::UserCreate(UserCreate { name, pass }) + } + pub fn new_user_delete(id: u64) -> Self { + Self::UserDelete(UserDelete { id }) + } + pub fn new_user_get_name(id: u64) -> Self { + Self::UserGetName(UserGetName { id }) + } + pub fn new_user_set_name(id: u64, name: String) -> Self { + Self::UserSetName(UserSetName { id, name }) + } + pub fn new_user_set_pass(id: u64, pass: String) -> Self { + Self::UserSetPass(UserSetPass { id, pass }) + } + + pub fn try_parse(line: &str) -> Option { + use repr::Command::*; + let command: repr::Command = serde_json::from_str(line).ok()?; + let mapped = match command { + ping { content } => Self::Ping(Ping { content }), + channel_list {} => Self::ChannelList(ChannelList {}), + channel_create { name } => Self::ChannelCreate(ChannelCreate { name }), + channel_delete { id: channel_id } => { + Self::ChannelDelete(ChannelDelete { id: channel_id }) + } + channel_get_name { id: channel_id } => { + Self::ChannelGetName(ChannelGetName { id: channel_id }) + } + channel_set_name { + id: channel_id, + name, + } => Self::ChannelSetName(ChannelSetName { + id: channel_id, + name, + }), + message_list { channel_id } => Self::MessageList(MessageList { channel_id }), + message_create { + channel_id, + content, + } => Self::MessageCreate(MessageCreate { + channel_id, + content, + }), + message_delete { id, channel_id } => { + Self::MessageDelete(MessageDelete { id, channel_id }) + } + message_get_content { id, channel_id } => { + Self::MessageGetContent(MessageGetContent { id, channel_id }) + } + message_set_content { + id, + channel_id, + content, + } => Self::MessageSetContent(MessageSetContent { + content, + id, + channel_id, + }), + user_list {} => Self::UserList(UserList {}), + user_create { name, pass } => Self::UserCreate(UserCreate { name, pass }), + user_delete { id } => Self::UserDelete(UserDelete { id }), + user_get_name { id } => Self::UserGetName(UserGetName { id }), + user_set_name { id, name } => Self::UserSetName(UserSetName { id, name }), + user_set_pass { id, pass } => Self::UserSetPass(UserSetPass { id, pass }), + }; + Some(mapped) + } + + pub fn serialize(self) -> String { + use repr::Command::*; + let mapped = match self { + Self::Ping(Ping { content }) => ping { content }, + Self::ChannelList(ChannelList {}) => repr::Command::channel_list {}, + Self::ChannelCreate(ChannelCreate { name }) => channel_create { name }, + Self::ChannelDelete(ChannelDelete { id: channel_id }) => { + channel_delete { id: channel_id } + } + Self::ChannelGetName(ChannelGetName { id: channel_id }) => { + channel_get_name { id: channel_id } + } + Self::ChannelSetName(ChannelSetName { + id: channel_id, + name, + }) => channel_set_name { + id: channel_id, + name, + }, + Self::MessageList(MessageList { channel_id }) => message_list { channel_id }, + Self::MessageCreate(MessageCreate { + channel_id, + content, + }) => message_create { + channel_id, + content, + }, + Self::MessageDelete(MessageDelete { id, channel_id }) => { + message_delete { id, channel_id } + } + Self::MessageGetContent(MessageGetContent { id, channel_id }) => { + message_get_content { id, channel_id } + } + Self::MessageSetContent(MessageSetContent { + content, + id, + channel_id, + }) => message_set_content { + id, + channel_id, + content, + }, + Self::UserList(UserList {}) => user_list {}, + Self::UserCreate(UserCreate { name, pass }) => user_create { name, pass }, + Self::UserDelete(UserDelete { id }) => user_delete { id }, + Self::UserGetName(UserGetName { id }) => user_get_name { id }, + Self::UserSetName(UserSetName { id, name }) => user_set_name { id, name }, + Self::UserSetPass(UserSetPass { id, pass }) => user_set_pass { id, pass }, + }; + serde_json::to_string(&mapped).unwrap() + } +} + +mod repr { + #![allow(non_camel_case_types)] + + use serde::{Deserialize, Serialize}; + + #[derive(Serialize, Deserialize)] + #[serde(tag = "type")] + pub enum Command { + ping { + content: String, + }, + channel_list {}, + channel_create { + name: String, + }, + channel_delete { + id: u64, + }, + channel_get_name { + id: u64, + }, + channel_set_name { + id: u64, + name: String, + }, + message_list { + channel_id: u64, + }, + message_create { + channel_id: u64, + content: String, + }, + message_delete { + channel_id: u64, + id: u64, + }, + message_get_content { + channel_id: u64, + id: u64, + }, + message_set_content { + channel_id: u64, + id: u64, + content: String, + }, + user_list {}, + user_create { + name: String, + pass: String, + }, + user_delete { + id: u64, + }, + user_get_name { + id: u64, + }, + user_set_name { + id: u64, + name: String, + }, + user_set_pass { + id: u64, + pass: String, + }, + } +} diff --git a/harsh-common/src/lib.rs b/harsh-common/src/lib.rs new file mode 100644 index 0000000..38e31f5 --- /dev/null +++ b/harsh-common/src/lib.rs @@ -0,0 +1,14 @@ +#[cfg(test)] +mod tests { + #[test] + fn it_works() { + let result = 2 + 2; + assert_eq!(result, 4); + } +} + +pub use client::ClientRequest; +pub mod client; + +pub use server::ServerRequest; +pub mod server; diff --git a/harsh-common/src/server.rs b/harsh-common/src/server.rs new file mode 100644 index 0000000..b19900d --- /dev/null +++ b/harsh-common/src/server.rs @@ -0,0 +1,393 @@ +#[derive(Debug)] +pub struct Pong { + pub content: String, +} + +#[derive(Debug)] +pub struct ChannelList { + pub channels: Vec, +} + +#[derive(Debug)] +pub struct ChannelGetName { + pub id: u64, + pub name: Option, +} + +#[derive(Debug)] +pub struct ChannelCreate { + pub id: u64, + pub name: String, +} + +#[derive(Debug)] +pub struct ChannelDelete { + pub id: u64, +} + +#[derive(Debug)] +pub struct ChannelSetName { + pub id: u64, + pub name: String, +} + +#[derive(Debug)] +pub struct MessageList { + pub channel_id: u64, + pub messages: Vec, +} +#[derive(Debug)] +pub struct MessageCreate { + pub channel_id: u64, + pub id: u64, + pub content: String, +} +#[derive(Debug)] +pub struct MessageDelete { + pub channel_id: u64, + pub id: u64, +} +#[derive(Debug)] +pub struct MessageGetContent { + pub channel_id: u64, + pub id: u64, + pub content: Option, +} +#[derive(Debug)] +pub struct MessageSetContent { + pub channel_id: u64, + pub id: u64, + pub content: String, +} + +#[derive(Debug)] +pub struct UserList { + pub users: Vec, +} + +#[derive(Debug)] +pub struct UserCreate { + pub id: u64, + pub name: String, +} + +#[derive(Debug)] +pub struct UserDelete { + pub id: u64, +} + +#[derive(Debug)] +pub struct UserGetName { + pub id: u64, + pub name: Option, +} + +#[derive(Debug)] +pub struct UserSetName { + pub id: u64, + pub name: String, +} + +#[derive(Debug)] +pub struct UserSetPass { + pub id: u64, +} + +#[derive(Debug)] +pub enum ServerRequest { + Pong(Pong), + + ChannelCreate(ChannelCreate), + ChannelDelete(ChannelDelete), + ChannelList(ChannelList), + ChannelGetName(ChannelGetName), + ChannelSetName(ChannelSetName), + + MessageList(MessageList), + MessageCreate(MessageCreate), + MessageDelete(MessageDelete), + MessageGetContent(MessageGetContent), + MessageSetContent(MessageSetContent), + + UserList(UserList), + UserCreate(UserCreate), + UserDelete(UserDelete), + UserGetName(UserGetName), + UserSetName(UserSetName), + UserSetPass(UserSetPass), +} + +impl ServerRequest { + pub fn new_pong(content: String) -> Self { + Self::Pong(Pong { content }) + } + + pub fn new_channel_list(channels: Vec) -> Self { + Self::ChannelList(ChannelList { channels }) + } + + pub fn new_channel_get_name(id: u64, name: Option) -> Self { + Self::ChannelGetName(ChannelGetName { name, id }) + } + + pub fn new_channel_create(id: u64, name: String) -> Self { + Self::ChannelCreate(ChannelCreate { id, name }) + } + + pub fn new_channel_delete(id: u64) -> Self { + Self::ChannelDelete(ChannelDelete { id }) + } + + pub fn new_channel_set_name(id: u64, name: String) -> Self { + Self::ChannelSetName(ChannelSetName { id, name }) + } + + pub fn new_message_list(channel_id: u64, messages: Vec) -> Self { + Self::MessageList(MessageList { + channel_id, + messages, + }) + } + + pub fn new_message_create(channel_id: u64, id: u64, content: String) -> Self { + Self::MessageCreate(MessageCreate { + channel_id, + content, + id, + }) + } + pub fn new_message_delete(channel_id: u64, id: u64) -> Self { + Self::MessageDelete(MessageDelete { channel_id, id }) + } + + pub fn new_message_get_content(channel_id: u64, id: u64, content: Option) -> Self { + Self::MessageGetContent(MessageGetContent { + channel_id, + content, + id, + }) + } + + pub fn new_message_set_content(channel_id: u64, id: u64, content: String) -> Self { + Self::MessageSetContent(MessageSetContent { + channel_id, + content, + id, + }) + } + + pub fn new_user_list(users: Vec) -> Self { + Self::UserList(UserList { users }) + } + + pub fn new_user_create(id: u64, name: String) -> Self { + Self::UserCreate(UserCreate { id, name }) + } + + pub fn new_user_delete(id: u64) -> Self { + Self::UserDelete(UserDelete { id }) + } + + pub fn new_user_get_name(id: u64, name: Option) -> Self { + Self::UserGetName(UserGetName { id, name }) + } + + pub fn new_user_set_name(id: u64, name: String) -> Self { + Self::UserSetName(UserSetName { id, name }) + } + + pub fn new_user_set_pass(id: u64) -> Self { + Self::UserSetPass(UserSetPass { id }) + } + + pub fn try_parse(line: &str) -> Option { + use repr::Command::*; + let command: repr::Command = serde_json::from_str(line).ok()?; + let mapped = match command { + pong { content } => Self::Pong(Pong { content }), + channel_list { channels } => Self::ChannelList(ChannelList { channels }), + channel_get_name { id, name } => Self::ChannelGetName(ChannelGetName { id, name }), + channel_create { id, name } => Self::ChannelCreate(ChannelCreate { id, name }), + channel_set_name { id, name } => Self::ChannelSetName(ChannelSetName { id, name }), + channel_delete { id } => Self::ChannelDelete(ChannelDelete { id }), + message_list { + channel_id, + messages, + } => Self::MessageList(MessageList { + channel_id, + messages, + }), + message_create { + channel_id, + id, + content, + } => Self::MessageCreate(MessageCreate { + channel_id, + content, + id, + }), + message_delete { channel_id, id } => { + Self::MessageDelete(MessageDelete { channel_id, id }) + } + message_get_content { + channel_id, + id, + content, + } => Self::MessageGetContent(MessageGetContent { + channel_id, + content, + id, + }), + message_set_content { + channel_id, + id, + content, + } => Self::MessageSetContent(MessageSetContent { + channel_id, + content, + id, + }), + user_list { users } => Self::UserList(UserList { users }), + user_create { id, name } => Self::UserCreate(UserCreate { id, name }), + user_delete { id } => Self::UserDelete(UserDelete { id }), + user_get_name { id, name } => Self::UserGetName(UserGetName { id, name }), + user_set_name { id, name } => Self::UserSetName(UserSetName { id, name }), + user_set_pass { id } => Self::UserSetPass(UserSetPass { id }), + }; + Some(mapped) + } + + pub fn serialize(self) -> String { + use repr::Command::*; + let mapped = match self { + Self::Pong(Pong { content }) => pong { content }, + Self::ChannelList(ChannelList { channels }) => channel_list { channels }, + Self::ChannelGetName(ChannelGetName { id, name }) => channel_get_name { id, name }, + Self::ChannelCreate(ChannelCreate { id, name }) => channel_create { id, name }, + Self::ChannelSetName(ChannelSetName { id, name }) => channel_set_name { id, name }, + Self::ChannelDelete(ChannelDelete { id }) => channel_delete { id }, + + Self::MessageList(MessageList { + channel_id, + messages, + }) => message_list { + channel_id, + messages, + }, + Self::MessageCreate(MessageCreate { + channel_id, + content, + id, + }) => message_create { + channel_id, + id, + content, + }, + Self::MessageDelete(MessageDelete { channel_id, id }) => { + message_delete { channel_id, id } + } + Self::MessageGetContent(MessageGetContent { + channel_id, + content, + id, + }) => message_get_content { + channel_id, + id, + content, + }, + Self::MessageSetContent(MessageSetContent { + channel_id, + content, + id, + }) => message_set_content { + channel_id, + id, + content, + }, + Self::UserList(UserList { users }) => user_list { users }, + Self::UserCreate(UserCreate { id, name }) => user_create { id, name }, + Self::UserDelete(UserDelete { id }) => user_delete { id }, + Self::UserGetName(UserGetName { id, name }) => user_get_name { id, name }, + Self::UserSetName(UserSetName { id, name }) => user_set_name { id, name }, + Self::UserSetPass(UserSetPass { id }) => user_set_pass { id }, + }; + + serde_json::to_string(&mapped).unwrap() + } +} + +mod repr { + #![allow(non_camel_case_types)] + + use serde::{Deserialize, Serialize}; + + #[derive(Serialize, Deserialize)] + #[serde(tag = "type")] + pub enum Command { + pong { + content: String, + }, + channel_list { + channels: Vec, + }, + channel_get_name { + id: u64, + name: Option, + }, + channel_create { + id: u64, + name: String, + }, + channel_delete { + id: u64, + }, + channel_set_name { + id: u64, + name: String, + }, + message_list { + channel_id: u64, + messages: Vec, + }, + message_create { + channel_id: u64, + id: u64, + content: String, + }, + message_delete { + channel_id: u64, + id: u64, + }, + message_get_content { + channel_id: u64, + id: u64, + content: Option, + }, + message_set_content { + channel_id: u64, + id: u64, + content: String, + }, + user_list { + users: Vec, + }, + user_create { + id: u64, + name: String, + }, + user_delete { + id: u64, + }, + user_get_name { + id: u64, + name: Option, + }, + user_set_name { + id: u64, + name: String, + }, + user_set_pass { + id: u64, + }, + } +} diff --git a/harsh-server/.gitignore b/harsh-server/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/harsh-server/.gitignore @@ -0,0 +1 @@ +/target diff --git a/harsh-server/Cargo.toml b/harsh-server/Cargo.toml new file mode 100644 index 0000000..cd7b892 --- /dev/null +++ b/harsh-server/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "harsh_server" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +#sleded = "1.0" +sled = "0.34" +telecomande = "1.2" +tokio = { version = "1.20", features = ["full"] } +harsh_common = { path = "../harsh-common/" } +chrono = "0.4" +rand = "0.8.5" diff --git a/harsh-server/src/gateway.rs b/harsh-server/src/gateway.rs new file mode 100644 index 0000000..d1c61b0 --- /dev/null +++ b/harsh-server/src/gateway.rs @@ -0,0 +1,225 @@ +use harsh_common::{client, server, ClientRequest, ServerRequest}; +use telecomande::{Processor, Remote}; + +use crate::{Addr, Id, SessionCmd, SessionProc, StorageCmd, StorageProc}; + +#[derive(Debug)] +pub enum GatewayCmd { + Request(Addr, String), + ClosedConnection(Addr), +} + +pub struct GatewayProc { + sessions: Remote, + storage: Remote, +} + +impl GatewayProc { + async fn handle_request(&mut self, address: Addr, request: ClientRequest) { + use client as c; + use ClientRequest::*; + match request { + Ping(c::Ping { content }) => self.on_ping(content, address), + + ChannelCreate(c::ChannelCreate { name }) => self.on_channel_create(name).await, + ChannelDelete(c::ChannelDelete { id }) => self.on_channel_delete(id), + ChannelList(c::ChannelList {}) => self.on_channel_list(address).await, + ChannelGetName(c::ChannelGetName { id }) => self.on_channel_get_name(id, address).await, + ChannelSetName(c::ChannelSetName { id, name }) => self.on_channel_set_name(id, name), + + MessageList(c::MessageList { channel_id }) => { + self.on_message_list(channel_id, address).await + } + MessageCreate(c::MessageCreate { + channel_id, + content, + }) => self.on_message_create(channel_id, content).await, + MessageDelete(c::MessageDelete { channel_id, id }) => { + self.on_message_delete(channel_id, id) + } + MessageGetContent(c::MessageGetContent { channel_id, id }) => { + self.on_message_get_content(channel_id, id, address).await + } + + MessageSetContent(c::MessageSetContent { + channel_id, + id, + content, + }) => { + self.on_message_set_content(channel_id, id, content); + } + + // TODO: user + UserList(c::UserList {}) => { + let (cmd, rec) = StorageCmd::new_user_list(); + self.storage.send(cmd).unwrap(); + let result = rec.await.unwrap().iter().map(Id::to_u64).collect(); + let request = ServerRequest::new_user_list(result); + let command = SessionCmd::new_send(address, request); + self.sessions.send(command).unwrap(); + } + + UserCreate(c::UserCreate { name, pass }) => { + let (cmd, rec) = StorageCmd::new_user_create(name.clone(), pass); + self.storage.send(cmd).unwrap(); + let id = rec.await.unwrap(); + let request = ServerRequest::new_user_create(id.into(), name); + let command = SessionCmd::new_broadcast(request); + self.sessions.send(command).unwrap(); + } + + UserDelete(c::UserDelete { id }) => { + let command = StorageCmd::new_user_delete(id.into()); + self.storage.send(command).unwrap(); + let request = ServerRequest::new_user_delete(id.into()); + let command = SessionCmd::new_broadcast(request); + self.sessions.send(command).unwrap(); + } + + UserGetName(c::UserGetName { id }) => { + let (cmd, rec) = StorageCmd::new_user_get_name(id.into()); + self.storage.send(cmd).unwrap(); + let name = rec.await.unwrap(); + let request = ServerRequest::new_user_get_name(id.into(), name); + let command = SessionCmd::new_send(address, request); + self.sessions.send(command).unwrap(); + } + + UserSetName(c::UserSetName { id, name }) => { + let command = StorageCmd::new_user_set_name(id.into(), name.clone()); + self.storage.send(command).unwrap(); + let request = ServerRequest::new_user_set_name(id.into(), name); + let command = SessionCmd::new_broadcast(request); + self.sessions.send(command).unwrap(); + } + + UserSetPass(c::UserSetPass { id, pass }) => { + let command = StorageCmd::new_user_set_pass(id.into(), pass); + self.storage.send(command).unwrap(); + let request = ServerRequest::new_user_set_pass(id.into()); + let command = SessionCmd::new_send(address, request); + self.sessions.send(command).unwrap(); + } + } + } + + pub fn new(sessions: Remote, storage: Remote) -> Self { + Self { sessions, storage } + } + + fn on_ping(&mut self, content: String, address: Addr) { + println!("[gateway/PING] '{content:?}'"); + let request = ServerRequest::Pong(server::Pong { content }); + let command = SessionCmd::new_send(address, request); + self.sessions.send(command).unwrap(); + } + + async fn on_channel_create(&mut self, name: String) { + let (cmd, rec) = StorageCmd::new_channel_create(name.clone()); + self.storage.send(cmd).unwrap(); + let id = rec.await.unwrap().to_u64(); + let request = ServerRequest::new_channel_create(id, name); + let command = SessionCmd::new_broadcast(request); + self.sessions.send(command).unwrap(); + } + + fn on_channel_delete(&mut self, id: u64) { + let command = StorageCmd::new_channel_delete(id.into()); + self.storage.send(command).unwrap(); + let request = ServerRequest::new_channel_delete(id); + let command = SessionCmd::new_broadcast(request); + self.sessions.send(command).unwrap(); + } + + async fn on_channel_list(&mut self, address: Addr) { + let (cmd, rec) = StorageCmd::new_channel_list(); + self.storage.send(cmd).unwrap(); + let channels = rec.await.unwrap().iter().map(|id| id.to_u64()).collect(); + let request = ServerRequest::new_channel_list(channels); + let command = SessionCmd::new_send(address, request); + self.sessions.send(command).unwrap(); + } + + async fn on_channel_get_name(&mut self, id: u64, address: Addr) { + let (cmd, rec) = StorageCmd::new_channel_get_name(id.into()); + self.storage.send(cmd).unwrap(); + let name = rec.await.unwrap(); + let request = ServerRequest::new_channel_get_name(id, name); + let command = SessionCmd::new_send(address, request); + self.sessions.send(command).unwrap(); + } + + fn on_channel_set_name(&mut self, id: u64, name: String) { + let command = StorageCmd::new_channel_set_name(id.into(), name.clone()); + self.storage.send(command).unwrap(); + let request = ServerRequest::new_channel_set_name(id, name); + let command = SessionCmd::new_broadcast(request); + self.sessions.send(command).unwrap(); + } + + async fn on_message_list(&mut self, channel_id: u64, address: Addr) { + let (cmd, rec) = StorageCmd::new_message_list(channel_id.into()); + self.storage.send(cmd).unwrap(); + let messages = rec.await.unwrap().iter().map(Id::to_u64).collect(); + let request = ServerRequest::new_message_list(channel_id, messages); + let command = SessionCmd::new_send(address, request); + self.sessions.send(command).unwrap(); + } + + async fn on_message_create(&mut self, channel_id: u64, content: String) { + let (cmd, rec) = StorageCmd::new_message_create(channel_id.into(), content.clone()); + self.storage.send(cmd).unwrap(); + let id = rec.await.unwrap(); + let request = ServerRequest::new_message_create(channel_id, id.to_u64(), content); + let command = SessionCmd::new_broadcast(request); + self.sessions.send(command).unwrap(); + } + + fn on_message_delete(&mut self, channel_id: u64, id: u64) { + let command = StorageCmd::new_message_delete(channel_id.into(), id.into()); + self.storage.send(command).unwrap(); + let request = ServerRequest::new_message_delete(channel_id, id); + let command = SessionCmd::new_broadcast(request); + self.sessions.send(command).unwrap(); + } + + async fn on_message_get_content(&mut self, channel_id: u64, id: u64, address: Addr) { + let (cmd, rec) = StorageCmd::new_message_get_content(channel_id.into(), id.into()); + self.storage.send(cmd).unwrap(); + let request = ServerRequest::new_message_get_content(channel_id, id, rec.await.unwrap()); + let command = SessionCmd::new_send(address, request); + self.sessions.send(command).unwrap(); + } + + fn on_message_set_content(&mut self, channel_id: u64, id: u64, content: String) { + let command = + StorageCmd::new_message_set_content(channel_id.into(), id.into(), content.clone()); + self.storage.send(command).unwrap(); + let request = ServerRequest::new_message_set_content(channel_id, id, content); + let command = SessionCmd::new_broadcast(request); + self.sessions.send(command).unwrap(); + } +} + +#[telecomande::async_trait] +impl Processor for GatewayProc { + type Command = GatewayCmd; + type Error = (); + async fn handle(&mut self, command: Self::Command) -> Result<(), Self::Error> { + match command { + GatewayCmd::Request(address, request) => { + if let Some(request) = ClientRequest::try_parse(&request) { + println!("[session/info] received command '{request:?}'"); + self.handle_request(address, request).await; + } else { + println!("[session/warn] failed to parse command"); + } + } + GatewayCmd::ClosedConnection(address) => self + .sessions + .send(SessionCmd::RemoveSession(address)) + .unwrap(), + } + Ok(()) + } +} diff --git a/harsh-server/src/main.rs b/harsh-server/src/main.rs new file mode 100644 index 0000000..bad085c --- /dev/null +++ b/harsh-server/src/main.rs @@ -0,0 +1,44 @@ +use telecomande::{Executor, SimpleExecutor}; +use tokio::net::TcpListener; + +const ADDRESS: &'static str = "localhost:42069"; +const DB_PATH: &'static str = "./db.test"; + +#[tokio::main] +async fn main() { + println!("[main/info] starting server ..."); + let sessions = SimpleExecutor::new(SessionProc::default()).spawn(); + println!("[main/info] spawned sessions"); + let storage = SimpleExecutor::new(StorageProc::new(DB_PATH)).spawn(); + println!("[main/info] spawned storage"); + let gateway = + SimpleExecutor::new(GatewayProc::new(sessions.remote(), storage.remote())).spawn(); + println!("[main/info] spawned gateway"); + + let listener = TcpListener::bind(ADDRESS).await.unwrap(); + println!("[main/info] listening on '{ADDRESS}' ..."); + + let client_handler = sessions.remote(); + loop { + let (stream, address) = listener.accept().await.unwrap(); + client_handler + .send(sessions::SessionCmd::AddSession( + stream, + address, + gateway.remote(), + )) + .unwrap(); + } +} + +mod utils; +pub use utils::{Addr, Id}; + +mod gateway; +pub use gateway::{GatewayCmd, GatewayProc}; + +mod sessions; +pub use sessions::{SessionCmd, SessionProc}; + +mod storage; +pub use storage::{StorageCmd, StorageProc}; diff --git a/harsh-server/src/sessions.rs b/harsh-server/src/sessions.rs new file mode 100644 index 0000000..9b9158a --- /dev/null +++ b/harsh-server/src/sessions.rs @@ -0,0 +1,109 @@ +use std::{collections::HashMap, net::SocketAddr}; + +use harsh_common::ServerRequest; +use telecomande::{Processor, Remote}; +use tokio::{ + io::{AsyncBufReadExt, AsyncWriteExt, BufReader}, + net::{ + tcp::{OwnedReadHalf, OwnedWriteHalf}, + TcpStream, + }, + task::JoinHandle, +}; + +use crate::{gateway, Addr}; +#[derive(Debug)] +pub enum SessionCmd { + AddSession(TcpStream, SocketAddr, Remote), + RemoveSession(Addr), + Send(Addr, String), + Broadcast(String), +} + +impl SessionCmd { + pub fn new_send(address: Addr, request: ServerRequest) -> Self { + let content = request.serialize(); + Self::Send(address, content) + } + + pub fn new_broadcast(request: ServerRequest) -> Self { + let content = request.serialize(); + Self::Broadcast(content) + } +} + +#[derive(Debug, Default)] +pub struct SessionProc { + clients: HashMap)>, +} + +impl SessionProc { + fn add_client( + &mut self, + stream: TcpStream, + address: Addr, + remote: Remote, + ) { + let (reader, writer) = stream.into_split(); + let handle = tokio::spawn(session(address.clone(), reader, remote)); + self.clients.insert(address, (writer, handle)); + } +} + +#[telecomande::async_trait] +impl Processor for SessionProc { + type Command = SessionCmd; + + type Error = (); + + async fn handle(&mut self, command: Self::Command) -> Result<(), Self::Error> { + match command { + SessionCmd::AddSession(stream, address, remote) => { + println!("[sessions/info] new connection from '{address:?}'"); + let address = Addr::new(address); + self.add_client(stream, address, remote) + } + SessionCmd::RemoveSession(address) => { + println!("[sessions/info] closed connection from '{address:?}'"); + if let Some((_writer, handle)) = self.clients.remove(&address) { + handle.await.unwrap(); + } + } + SessionCmd::Send(address, content) => { + if let Some((client, _)) = self.clients.get_mut(&address) { + println!("[session/info] sending '{content}' to '{address:?}'"); + client.write_all(content.as_bytes()).await.unwrap(); + client.write_all(b"\n").await.unwrap(); + } else { + eprintln!("failed to find session with address '{address:?}'") + } + } + SessionCmd::Broadcast(content) => { + for (client, _) in self.clients.values_mut() { + println!("[session/info] broadcasting '{content}'"); + client.write_all(content.as_bytes()).await.unwrap(); + client.write_all(b"\n").await.unwrap(); + } + } + }; + Ok(()) + } +} + +async fn session(address: Addr, reader: OwnedReadHalf, remote: Remote) { + let mut reader = BufReader::new(reader); + loop { + let mut line = String::new(); + match reader.read_line(&mut line).await { + Err(error) => eprintln!("[session/error] {error}"), + Ok(0) => break, + _ => (), + } + remote + .send(gateway::GatewayCmd::Request(address.clone(), line.clone())) + .unwrap(); + } + remote + .send(gateway::GatewayCmd::ClosedConnection(address)) + .unwrap(); +} diff --git a/harsh-server/src/storage.rs b/harsh-server/src/storage.rs new file mode 100644 index 0000000..c8ebd5e --- /dev/null +++ b/harsh-server/src/storage.rs @@ -0,0 +1,341 @@ +use sled::Db; +use telecomande::Processor; +use tokio::sync::oneshot::{self, Receiver, Sender}; + +use crate::Id; + +#[derive(Debug)] +pub enum StorageCmd { + ChannelList(Sender>), + ChannelCreate(String, Sender), + ChannelDelete(Id), + ChannelGetName(Id, Sender>), + ChannelSetName(Id, String), + MessageList(Id, Sender>), + MessageCreate(Id, String, Sender), + MessageDelete(Id, Id), + MessageGetContent(Id, Id, Sender>), + MessageSetContent(Id, Id, String), + UserList(Sender>), + UserCreate(String, String, Sender), + UserDelete(Id), + UserGetName(Id, Sender>), + UserSetName(Id, String), + UserGetPass(Id, Sender>), + UserSetPass(Id, String), +} + +impl StorageCmd { + pub fn new_channel_list() -> (Self, Receiver>) { + let (s, r) = oneshot::channel(); + (Self::ChannelList(s), r) + } + + pub fn new_channel_create(name: impl ToString) -> (Self, Receiver) { + let (s, r) = oneshot::channel(); + (Self::ChannelCreate(name.to_string(), s), r) + } + + pub fn new_channel_delete(id: Id) -> Self { + Self::ChannelDelete(id) + } + + pub fn new_channel_get_name(id: Id) -> (Self, Receiver>) { + let (s, r) = oneshot::channel(); + (Self::ChannelGetName(id, s), r) + } + + pub fn new_channel_set_name(id: Id, name: String) -> Self { + Self::ChannelSetName(id, name) + } + + pub fn new_message_list(channel_id: Id) -> (Self, Receiver>) { + let (sender, receiver) = oneshot::channel(); + let cmd = Self::MessageList(channel_id, sender); + (cmd, receiver) + } + + pub fn new_message_create(channel_id: Id, content: String) -> (Self, Receiver) { + let (sender, receiver) = oneshot::channel(); + let cmd = Self::MessageCreate(channel_id, content, sender); + (cmd, receiver) + } + + pub fn new_message_delete(channel_id: Id, id: Id) -> Self { + Self::MessageDelete(channel_id, id) + } + + pub fn new_message_get_content(channel_id: Id, id: Id) -> (Self, Receiver>) { + let (sender, receiver) = oneshot::channel(); + let cmd = Self::MessageGetContent(channel_id, id, sender); + (cmd, receiver) + } + + pub fn new_message_set_content(channel_id: Id, id: Id, content: String) -> Self { + Self::MessageSetContent(channel_id, id, content) + } + + pub fn new_user_list() -> (Self, Receiver>) { + let (sender, receiver) = oneshot::channel(); + let cmd = Self::UserList(sender); + (cmd, receiver) + } + + pub fn new_user_create(name: String, pass: String) -> (Self, Receiver) { + let (sender, receiver) = oneshot::channel(); + let cmd = Self::UserCreate(name, pass, sender); + (cmd, receiver) + } + + pub fn new_user_delete(id: Id) -> Self { + Self::UserDelete(id) + } + + pub fn new_user_get_name(id: Id) -> (Self, Receiver>) { + let (sender, receiver) = oneshot::channel(); + let cmd = Self::UserGetName(id, sender); + (cmd, receiver) + } + + pub fn new_user_set_name(id: Id, name: String) -> Self { + Self::UserSetName(id, name) + } + + pub fn new_user_get_pass(id: Id) -> (Self, Receiver>) { + let (sender, receiver) = oneshot::channel(); + let cmd = Self::UserGetPass(id, sender); + (cmd, receiver) + } + + pub fn new_user_set_pass(id: Id, pass: String) -> Self { + Self::UserSetPass(id, pass) + } +} + +pub struct StorageProc { + base: Db, +} + +impl StorageProc { + pub fn new(path: S) -> Self + where + S: ToString, + { + let path = path.to_string(); + let base = sled::open(path).unwrap(); + Self { base } + } + + fn get(&self, path: S) -> Option + where + S: ToString, + T: SerDeser, + { + let path = path.to_string(); + println!("[storage/info] setting entry at '{path}'"); + T::read(&self.base, path) + } + fn set(&self, path: S, item: T) + where + S: ToString, + T: SerDeser, + { + let path = path.to_string(); + println!("[storage/info] getting entry at '{path}'"); + item.write(&self.base, path) + } + + fn list(&self, path: impl ToString) -> Vec { + let path = path.to_string(); + println!("[storage/info] listing entries in '{path}'"); + let db = &self.base; + list(db, path) + } + + // firsts (x) + // lasts (x) + // from (id, x) + // to (id, x) + + fn remove(&self, path: impl ToString) { + let path = path.to_string(); + println!("[storage/info] removing entry at '{path}'"); + self.base.remove(path).unwrap(); + } + + async fn handle_command(&mut self, command: StorageCmd) { + use StorageCmd::*; + match command { + // + // Channel + // + ChannelList(sender) => self.on_channel_list(sender), + ChannelCreate(name, sender) => self.on_channel_create(name, sender), + ChannelDelete(id) => self.on_channel_remove(id), + ChannelGetName(id, sender) => self.on_channel_get_name(id, sender), + ChannelSetName(id, name) => self.on_channel_set_name(id, name), + // ChannelGetParent / Set + + // + // User + // + MessageList(channel_id, sender) => self.on_message_list(channel_id, sender), + MessageCreate(channel_id, content, sender) => { + self.on_message_create(channel_id, content, sender) + } + MessageDelete(channel_id, id) => self.on_message_delete(channel_id, id), + MessageGetContent(channel_id, id, sender) => { + self.on_message_get_content(channel_id, id, sender) + } + MessageSetContent(channel_id, id, content) => { + self.on_message_set_content(channel_id, id, content) + } + + // + // User + // + UserList(sender) => { + let users = self.list("/users/"); + sender.send(users).unwrap(); + } + + UserCreate(name, pass, sender) => { + let user = User::new(name, pass); + let id = user.get_id(); + self.set(format!("/users/{id}"), user); + sender.send(id).unwrap(); + } + + UserDelete(id) => { + self.remove(format!("/users/{id}")); + } + + UserGetName(id, sender) => { + let user = self.get::<_, User>(format!("/users/{id}")); + let name = user.map(|u| u.get_name().to_string()); + sender.send(name).unwrap(); + } + + UserSetName(id, name) => { + let path = format!("/users/{id}"); + if let Some(mut user) = self.get::<_, User>(&path) { + user.set_name(name); + self.set(path, user); + } + } + + UserGetPass(id, sender) => { + let user = self.get::<_, User>(format!("/users/{id}")); + let name = user.map(|u| u.get_pass().to_string()); + sender.send(name).unwrap(); + } + + UserSetPass(id, pass) => { + let path = format!("/users/{id}"); + if let Some(mut user) = self.get::<_, User>(&path) { + user.set_pass(pass); + self.set(path, user); + } + } + }; + } + + // + // Channels + // + fn on_channel_list(&mut self, sender: Sender>) { + let results = self.list("/channels/"); + sender.send(results).unwrap(); + } + + fn on_channel_create(&mut self, name: String, sender: Sender) { + let item = Channel::new(name); + let id = item.get_id(); + self.set(format!("/channels/{id}"), item); + sender.send(id).unwrap(); + } + + fn on_channel_remove(&mut self, id: Id) { + for message_id in self.list(format!("/messages/{id}/")) { + self.remove(format!("/messages/{id}/{message_id}")) + } + self.remove(format!("/channels/{id}")) + } + + fn on_channel_get_name(&mut self, id: Id, sender: Sender>) { + let channel = self.get::<_, Channel>(format!("/channels/{id}")); + let name = channel.map(|channel| channel.get_name().to_string()); + sender.send(name).unwrap(); + } + + fn on_channel_set_name(&mut self, id: Id, name: String) { + let path = format!("/channels/{id}"); + if let Some(mut channel) = self.get::<_, Channel>(&path) { + channel.set_name(name); + self.set(path, channel); + } + } + + // + // Messages + // + fn on_message_list(&mut self, channel_id: Id, sender: Sender>) { + let items = self.list(format!("/messages/{channel_id}/")); + sender.send(items).unwrap(); + } + + fn on_message_create(&mut self, channel_id: Id, content: String, sender: Sender) { + let message = Message::new(content); + let id = message.get_id(); + self.set(format!("/messages/{channel_id}/{id}"), message); + sender.send(id).unwrap(); + } + + fn on_message_delete(&mut self, channel_id: Id, id: Id) { + self.remove(format!("/messages/{channel_id}/{id}")); + } + + fn on_message_get_content(&mut self, channel_id: Id, id: Id, sender: Sender>) { + let message = self.get::<_, Message>(format!("/messages/{channel_id}/{id}")); + let content = message.map(|m| m.get_content().to_string()); + sender.send(content).unwrap() + } + + fn on_message_set_content(&mut self, channel_id: Id, id: Id, content: String) { + let path = format!("/messages/{channel_id}/{id}"); + if let Some(mut message) = self.get::<_, Message>(&path) { + message.set_content(content); + self.set(path, message); + } + } +} + +#[telecomande::async_trait] +impl Processor for StorageProc { + type Command = StorageCmd; + + type Error = (); + + async fn handle(&mut self, command: Self::Command) -> Result<(), Self::Error> { + self.handle_command(command).await; + Ok(()) + } +} + +mod models; +pub use models::{Channel, Message, SerDeser, User}; + +fn list(db: &Db, path: String) -> Vec { + let len = path.len(); + db.scan_prefix(path) + .filter_map(move |result| -> Option { + let (key, _) = result.ok()?; + let string = String::from_utf8(key.iter().cloned().collect()).unwrap(); + let suffix = &string[len..]; + Id::from_string(suffix) + }) + .collect() // TODO: turn into iterator with limits +} + +#[cfg(test)] +mod tests; diff --git a/harsh-server/src/storage/models.rs b/harsh-server/src/storage/models.rs new file mode 100644 index 0000000..63cd8bd --- /dev/null +++ b/harsh-server/src/storage/models.rs @@ -0,0 +1,116 @@ +use serde::{de::DeserializeOwned, Deserialize, Serialize}; +use sled::Db; + +use crate::Id; + +#[derive(Debug, Serialize, Deserialize)] +pub struct Channel { + id: Id, + name: String, +} + +impl Channel { + pub fn new(name: String) -> Self { + let id = Id::from_now(); + Self { id, name } + } + + pub fn get_id(&self) -> Id { + self.id + } + + pub fn get_name(&self) -> &str { + &self.name + } + + pub fn set_name(&mut self, name: String) { + self.name = name; + } +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct User { + id: Id, + name: String, + pass: String, +} + +impl User { + pub fn new(name: String, pass: String) -> Self { + let id = Id::from_now(); + Self { id, name, pass } + } + + pub fn get_id(&self) -> Id { + self.id + } + + pub fn get_name(&self) -> &str { + &self.name + } + + pub fn set_name(&mut self, name: String) { + self.name = name + } + + pub fn get_pass(&self) -> &str { + &self.pass + } + + pub fn set_pass(&mut self, pass: String) { + self.pass = pass + } +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct Message { + id: Id, + content: String, +} + +impl Message { + pub fn new(content: String) -> Self { + let id = Id::from_now(); + Self { id, content } + } + + pub fn get_id(&self) -> Id { + self.id + } + pub fn get_content(&self) -> &str { + &self.content + } + pub fn set_content(&mut self, content: String) { + self.content = content; + } +} + +pub trait SerDeser: Serialize + DeserializeOwned { + fn ser(&self) -> Vec; + fn deser(input: &[u8]) -> Option; + fn read(db: &Db, path: String) -> Option; + fn write(&self, db: &Db, path: String); +} + +impl SerDeser for T +where + T: Serialize + DeserializeOwned, +{ + fn ser(&self) -> Vec { + serde_json::to_vec(self).unwrap() + } + + fn deser(input: &[u8]) -> Option { + serde_json::from_slice(input).ok() + } + + fn read(db: &Db, path: String) -> Option { + let bytes = db.get(path).unwrap()?; + Self::deser(&bytes) + } + + fn write(&self, db: &Db, path: String) { + let bytes = self.ser(); + db.insert(path, bytes).unwrap(); + } +} diff --git a/harsh-server/src/storage/tests.rs b/harsh-server/src/storage/tests.rs new file mode 100644 index 0000000..657c6bb --- /dev/null +++ b/harsh-server/src/storage/tests.rs @@ -0,0 +1,64 @@ +use super::*; +#[test] +fn test_list() { + let db = sled::open("/tmp/test-db").unwrap(); + db.insert("/some/path/123", b"hello1").unwrap(); + db.insert("/some/path/1234", b"hello2").unwrap(); + db.insert("/some/path/12345", b"hello3").unwrap(); + let results = list(&db, "/some/path/".to_string()); + assert_eq!( + results, + vec![ + Id::from_string("123").unwrap(), + Id::from_string("1234").unwrap(), + Id::from_string("12345").unwrap() + ] + ); +} + +#[tokio::test] +async fn test_channels() { + use telecomande::{Executor, SimpleExecutor}; + // cleaning; + std::fs::remove_dir_all("/tmp/db-test").ok(); + + // instantiation + let store = SimpleExecutor::new(StorageProc::new("/tmp/db-test")).spawn(); + let remote = store.remote(); + + // insertion + let (cmd, rec) = StorageCmd::new_channel_create("a-channel"); + remote.send(cmd).unwrap(); + let id = rec.await.unwrap(); + + // query all + let (cmd, rec) = StorageCmd::new_channel_list(); + remote.send(cmd).unwrap(); + let result = rec.await.unwrap(); + assert_eq!(result.len(), 1); + let first = result[0]; + assert_eq!(first, id); + + // query property + let (cmd, rec) = StorageCmd::new_channel_get_name(id); + remote.send(cmd).unwrap(); + let result = rec.await.unwrap(); + assert_eq!(result.unwrap(), "a-channel".to_string()); + + // insertion + let (cmd, rec) = StorageCmd::new_channel_create("b-channel"); + remote.send(cmd).unwrap(); + let id2 = rec.await.unwrap(); + + // query all + let (cmd, rec) = StorageCmd::new_channel_list(); + remote.send(cmd).unwrap(); + let result = rec.await.unwrap(); + assert_eq!(result.len(), 2); + + // query property + let (cmd, rec) = StorageCmd::new_channel_get_name(id2); + remote.send(cmd).unwrap(); + let result = rec.await.unwrap(); + assert_eq!(result.unwrap(), "b-channel".to_string()); +} diff --git a/harsh-server/src/utils.rs b/harsh-server/src/utils.rs new file mode 100644 index 0000000..f5c0b5e --- /dev/null +++ b/harsh-server/src/utils.rs @@ -0,0 +1,76 @@ +use std::{fmt::Display, net::SocketAddr}; + +use rand::random; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct Addr(String); + +impl Addr { + pub fn new(address: SocketAddr) -> Self { + let string = format!("{address:?}"); + Self(string) + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub struct Id(u64); + +impl Id { + pub fn from_now() -> Self { + let ms = chrono::Utc::now().timestamp_millis() as u64; + let total = (ms * 1000) + rand_range(1000); + Self(total) + } + + pub fn from_string(input: &str) -> Option { + let inner: u64 = input.parse().ok()?; + Some(Self(inner)) + } + + pub fn from_u64(input: u64) -> Self { + Self(input) + } + + pub fn to_u64(&self) -> u64 { + self.0 + } +} + +impl From for Id { + fn from(input: u64) -> Self { + Self::from_u64(input) + } +} + +impl From for u64 { + fn from(input: Id) -> Self { + input.to_u64() + } +} + +#[test] +fn test_string_convertion() { + let id = Id::from_now(); + let str = id.to_string(); + assert_eq!(id, Id::from_string(&str).unwrap()); +} + +fn rand_range(n: u64) -> u64 { + let random: u64 = random(); + random % n +} + +impl Display for Id { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let inner = self.0; + let padded = format!("{inner:0>20}"); // pads to the left to make 20 chars of length + f.write_str(&padded) + } +} + +#[test] +fn length_of_max() { + assert_eq!(u64::MAX, 18446744073709551615_u64); + assert_eq!(20, "18446744073709551615".len()) +} diff --git a/start-client.sh b/start-client.sh new file mode 100755 index 0000000..3053c8a --- /dev/null +++ b/start-client.sh @@ -0,0 +1,4 @@ +#!/bin/sh + +cd harsh-client; +cargo run; diff --git a/start-server.sh b/start-server.sh new file mode 100755 index 0000000..5ceebc0 --- /dev/null +++ b/start-server.sh @@ -0,0 +1,4 @@ +#!/bin/sh + +cd harsh-server; +cargo run; diff --git a/watch-client.sh b/watch-client.sh new file mode 100755 index 0000000..cff9ea1 --- /dev/null +++ b/watch-client.sh @@ -0,0 +1,3 @@ +#!/bin/sh + +nodemon "harsh-client/src" "harsh-server/src" "harsh-common/src" -x "./start-client.sh" -e "rs" diff --git a/watch-server.sh b/watch-server.sh new file mode 100755 index 0000000..7a82673 --- /dev/null +++ b/watch-server.sh @@ -0,0 +1,3 @@ +#!/bin/sh + +nodemon "harsh-client/src" "harsh-server/src" "harsh-common/src" -x "./start-server.sh" -e "rs"