summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.sqlx/query-79e6e76bc3f974248bf8a7f4f89221feb030ac8f42cf594c875ecd3bfeca3eb7.json26
-rw-r--r--Cargo.lock83
-rw-r--r--Cargo.toml2
-rw-r--r--js/channel.js34
-rw-r--r--src/channel/repo/channels.rs16
-rw-r--r--src/channel/routes.rs3
-rw-r--r--src/index/app.rs10
-rw-r--r--src/index/routes.rs45
-rw-r--r--src/index/templates.rs39
9 files changed, 251 insertions, 7 deletions
diff --git a/.sqlx/query-79e6e76bc3f974248bf8a7f4f89221feb030ac8f42cf594c875ecd3bfeca3eb7.json b/.sqlx/query-79e6e76bc3f974248bf8a7f4f89221feb030ac8f42cf594c875ecd3bfeca3eb7.json
new file mode 100644
index 0000000..b46d940
--- /dev/null
+++ b/.sqlx/query-79e6e76bc3f974248bf8a7f4f89221feb030ac8f42cf594c875ecd3bfeca3eb7.json
@@ -0,0 +1,26 @@
+{
+ "db_name": "SQLite",
+ "query": "\n select id as \"id: Id\", name\n from channel\n where id = $1\n ",
+ "describe": {
+ "columns": [
+ {
+ "name": "id: Id",
+ "ordinal": 0,
+ "type_info": "Text"
+ },
+ {
+ "name": "name",
+ "ordinal": 1,
+ "type_info": "Text"
+ }
+ ],
+ "parameters": {
+ "Right": 1
+ },
+ "nullable": [
+ false,
+ false
+ ]
+ },
+ "hash": "79e6e76bc3f974248bf8a7f4f89221feb030ac8f42cf594c875ecd3bfeca3eb7"
+}
diff --git a/Cargo.lock b/Cargo.lock
index 7268a4b..9f2c8f5 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -775,9 +775,11 @@ dependencies = [
"futures",
"headers",
"maud",
+ "mime_guess",
"password-hash",
"rand",
"rand_core",
+ "rust-embed",
"serde",
"serde_json",
"sqlx",
@@ -1064,6 +1066,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
[[package]]
+name = "mime_guess"
+version = "2.0.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e"
+dependencies = [
+ "mime",
+ "unicase",
+]
+
+[[package]]
name = "minimal-lexical"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1413,6 +1425,40 @@ dependencies = [
]
[[package]]
+name = "rust-embed"
+version = "8.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fa66af4a4fdd5e7ebc276f115e895611a34739a9c1c01028383d612d550953c0"
+dependencies = [
+ "rust-embed-impl",
+ "rust-embed-utils",
+ "walkdir",
+]
+
+[[package]]
+name = "rust-embed-impl"
+version = "8.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6125dbc8867951125eec87294137f4e9c2c96566e61bf72c45095a7c77761478"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "rust-embed-utils",
+ "syn",
+ "walkdir",
+]
+
+[[package]]
+name = "rust-embed-utils"
+version = "8.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2e5347777e9aacb56039b0e1f28785929a8a3b709e87482e7442c72e7c12529d"
+dependencies = [
+ "sha2",
+ "walkdir",
+]
+
+[[package]]
name = "rustc-demangle"
version = "0.1.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1444,6 +1490,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f"
[[package]]
+name = "same-file"
+version = "1.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
+dependencies = [
+ "winapi-util",
+]
+
+[[package]]
name = "scopeguard"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2040,6 +2095,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825"
[[package]]
+name = "unicase"
+version = "2.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f7d2d4dafb69621809a81864c9c1b864479e1235c0dd4e199924b9742439ed89"
+dependencies = [
+ "version_check",
+]
+
+[[package]]
name = "unicode-bidi"
version = "0.3.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2111,6 +2175,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
[[package]]
+name = "walkdir"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b"
+dependencies = [
+ "same-file",
+ "winapi-util",
+]
+
+[[package]]
name = "wasi"
version = "0.11.0+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2188,6 +2262,15 @@ dependencies = [
]
[[package]]
+name = "winapi-util"
+version = "0.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
+dependencies = [
+ "windows-sys 0.59.0",
+]
+
+[[package]]
name = "windows-core"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
diff --git a/Cargo.toml b/Cargo.toml
index 1f6893c..05a880b 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -13,9 +13,11 @@ clap = { version = "4.5.16", features = ["derive", "env"] }
futures = "0.3.30"
headers = "0.4.0"
maud = { version = "0.26.0", features = ["axum"] }
+mime_guess = "2.0.5"
password-hash = { version = "0.5.0", features = ["std"] }
rand = "0.8.5"
rand_core = { version = "0.6.4", features = ["getrandom"] }
+rust-embed = "8.5.0"
serde = { version = "1.0.209", features = ["derive"] }
serde_json = "1.0.128"
sqlx = { version = "0.8.1", features = ["chrono", "runtime-tokio", "sqlite"] }
diff --git a/js/channel.js b/js/channel.js
new file mode 100644
index 0000000..96d1061
--- /dev/null
+++ b/js/channel.js
@@ -0,0 +1,34 @@
+"use strict";
+
+function ready(callback) {
+ if (document.readyState === 'loading') {
+ document.addEventListener('DOMContentLoaded', callback);
+ } else {
+ callback();
+ }
+}
+
+ready(() => {
+ let template = document.querySelector('#message').content;
+
+ document.querySelectorAll('link[rel=events]').forEach(elem => {
+ let url = elem.getAttribute("href");
+ let source = new EventSource(url);
+ source.addEventListener('message', message => {
+ let body = JSON.parse(message.data);
+
+ document.querySelectorAll('.messages').forEach(elem => {
+ let message = template.cloneNode(true);
+
+ message.querySelectorAll('.sender')
+ .forEach(elem => elem.textContent = body.sender.name);
+ message.querySelectorAll('.message')
+ .forEach(elem => elem.textContent = body.body);
+ message.querySelectorAll('.sent_at')
+ .forEach(elem => elem.textContent = body.sent_at);
+
+ message.childNodes.forEach(node => elem.appendChild(node));
+ });
+ });
+ });
+})
diff --git a/src/channel/repo/channels.rs b/src/channel/repo/channels.rs
index 6fb0c23..fc52aa3 100644
--- a/src/channel/repo/channels.rs
+++ b/src/channel/repo/channels.rs
@@ -44,6 +44,22 @@ impl<'c> Channels<'c> {
Ok(channel)
}
+ pub async fn by_id(&mut self, channel: Id) -> Result<Channel, BoxedError> {
+ let channel = sqlx::query_as!(
+ Channel,
+ r#"
+ select id as "id: Id", name
+ from channel
+ where id = $1
+ "#,
+ channel,
+ )
+ .fetch_one(&mut *self.0)
+ .await?;
+
+ Ok(channel)
+ }
+
pub async fn all(&mut self) -> Result<Vec<Channel>, BoxedError> {
let channels = sqlx::query_as!(
Channel,
diff --git a/src/channel/routes.rs b/src/channel/routes.rs
index 4f83a8b..eae68a2 100644
--- a/src/channel/routes.rs
+++ b/src/channel/routes.rs
@@ -1,6 +1,5 @@
use axum::{
extract::{Form, Path, State},
- http::StatusCode,
response::{
sse::{self, Sse},
IntoResponse, Redirect,
@@ -59,7 +58,7 @@ async fn on_send(
.send(&login, &channel, &form.message, &sent_at)
.await?;
- Ok(StatusCode::ACCEPTED)
+ Ok(Redirect::to(&format!("/{}", channel)))
}
async fn on_events(
diff --git a/src/index/app.rs b/src/index/app.rs
index 6075c6f..b315b45 100644
--- a/src/index/app.rs
+++ b/src/index/app.rs
@@ -1,7 +1,7 @@
use sqlx::sqlite::SqlitePool;
use crate::{
- channel::repo::channels::{Channel, Provider as _},
+ channel::repo::channels::{Channel, Id as ChannelId, Provider as _},
error::BoxedError,
};
@@ -21,4 +21,12 @@ impl<'a> Index<'a> {
Ok(channels)
}
+
+ pub async fn channel(&self, channel: ChannelId) -> Result<Channel, BoxedError> {
+ let mut tx = self.db.begin().await?;
+ let channel = tx.channels().by_id(channel).await?;
+ tx.commit().await?;
+
+ Ok(channel)
+ }
}
diff --git a/src/index/routes.rs b/src/index/routes.rs
index c57278f..07b6001 100644
--- a/src/index/routes.rs
+++ b/src/index/routes.rs
@@ -1,8 +1,17 @@
-use axum::{extract::State, routing::get, Router};
+use axum::{
+ extract::{Path, State},
+ http::{header, StatusCode},
+ response::IntoResponse,
+ routing::get,
+ Router,
+};
use maud::Markup;
use super::templates;
-use crate::{app::App, error::InternalError, login::repo::logins::Login};
+use crate::{
+ app::App, channel::repo::channels::Id as ChannelId, error::InternalError,
+ login::repo::logins::Login,
+};
async fn index(State(app): State<App>, login: Option<Login>) -> Result<Markup, InternalError> {
match login {
@@ -17,6 +26,36 @@ async fn index_authenticated(app: App, login: Login) -> Result<Markup, InternalE
Ok(templates::authenticated(login, &channels))
}
+#[derive(rust_embed::Embed)]
+#[folder = "js"]
+struct Js;
+
+async fn js(Path(path): Path<String>) -> impl IntoResponse {
+ let mime = mime_guess::from_path(&path).first_or_octet_stream();
+
+ match Js::get(&path) {
+ Some(file) => (
+ StatusCode::OK,
+ [(header::CONTENT_TYPE, mime.as_ref())],
+ file.data,
+ )
+ .into_response(),
+ None => (StatusCode::NOT_FOUND, "").into_response(),
+ }
+}
+
+async fn channel(
+ State(app): State<App>,
+ _: Login,
+ Path(channel): Path<ChannelId>,
+) -> Result<Markup, InternalError> {
+ let channel = app.index().channel(channel).await?;
+ Ok(templates::channel(&channel))
+}
+
pub fn router() -> Router<App> {
- Router::new().route("/", get(index))
+ Router::new()
+ .route("/", get(index))
+ .route("/js/*path", get(js))
+ .route("/:channel", get(channel))
}
diff --git a/src/index/templates.rs b/src/index/templates.rs
index 38cd93f..7472fd0 100644
--- a/src/index/templates.rs
+++ b/src/index/templates.rs
@@ -33,7 +33,9 @@ fn channel_list<'c>(channels: impl IntoIterator<Item = &'c Channel>) -> Markup {
fn channel_list_entry(channel: &Channel) -> Markup {
html! {
li {
- (channel.name) " (" (channel.id) ")"
+ a href=(format!("/{}", channel.id)) {
+ (channel.name) " (" (channel.id) ")"
+ }
}
}
}
@@ -87,3 +89,38 @@ fn login_form() -> Markup {
}
}
}
+
+pub fn channel(channel: &Channel) -> Markup {
+ html! {
+ (DOCTYPE)
+ head {
+ title { "hi - " (channel.name) }
+ script src="/js/channel.js" {}
+ template id="message" {
+ p {
+ span.sender { "(sender)" }
+ ": "
+ span.message { "(message)" }
+ " (at "
+ span.sent_at { "(sent_at)" }
+ ")" }
+ }
+ link rel="events" href=(format!("/{}/events", channel.id)) {}
+ }
+ body {
+ section class="messages" {}
+ section {
+ form action=(format!("/{}/send", channel.id)) method="post" {
+ label {
+ "message"
+ input name="message" type="text" autofocus {}
+ }
+ button { "send" }
+ }
+ }
+ section {
+ a href="/" { "back" }
+ }
+ }
+ }
+}