get this to work a slight bit more
This commit is contained in:
parent
ed87d3fb51
commit
5160929958
10 changed files with 484 additions and 108 deletions
|
@ -5,15 +5,19 @@ edition = "2021"
|
|||
|
||||
[dependencies]
|
||||
http = "1"
|
||||
axum = { version = "0.7.5", features = [ "json", "macros" ] }
|
||||
axum = { version = "0.7.5", features = [ "json", "macros", "ws" ] }
|
||||
axum-macros = "0.4.1"
|
||||
chrono = { version = "0.4", features = [ "serde" ] }
|
||||
chrono-tz = "0.10.0"
|
||||
maud = "0.26.0"
|
||||
sqlx = { version = "0.8.2", features = [ "postgres", "runtime-tokio", "tls-rustls-ring", "uuid", "chrono" ] }
|
||||
tokio = { version = "1.40.0", features = [ "full" ] }
|
||||
tokio-tungstenite = "0.24.0"
|
||||
dashmap = "6"
|
||||
serde = { version = "1", features = [ "derive" ] }
|
||||
thiserror = "1"
|
||||
anyhow = "1"
|
||||
uuid = { version = "1.10.0", features = [ "serde" ] }
|
||||
rand = "0.8.5"
|
||||
tracing = "0.1.40"
|
||||
tracing-subscriber = "0.3.18"
|
||||
axum_static = "1.7.1"
|
||||
|
|
|
@ -6,15 +6,15 @@ use axum::{
|
|||
use rand::distributions::{Alphanumeric, DistString};
|
||||
use sqlx::{Pool, Postgres, QueryBuilder};
|
||||
|
||||
use crate::model::Chat;
|
||||
use crate::{model::Chat, state::AppState};
|
||||
|
||||
pub fn router(pool: Pool<Postgres>) -> Router {
|
||||
pub fn router(state: AppState) -> Router {
|
||||
Router::new()
|
||||
.route("/new/:amount", get(create))
|
||||
.with_state(pool)
|
||||
.with_state(state)
|
||||
}
|
||||
|
||||
async fn create(Path(amount): Path<u8>, State(pool): State<Pool<Postgres>>) -> Json<Vec<Chat>> {
|
||||
async fn create(Path(amount): Path<u8>, State(state): State<AppState>) -> Json<Vec<Chat>> {
|
||||
let paths: Vec<String> = (0..amount)
|
||||
.map(|_| Alphanumeric.sample_string(&mut rand::thread_rng(), 6))
|
||||
.collect();
|
||||
|
@ -25,7 +25,7 @@ async fn create(Path(amount): Path<u8>, State(pool): State<Pool<Postgres>>) -> J
|
|||
})
|
||||
.push("returning *")
|
||||
.build_query_as()
|
||||
.fetch_all(&pool)
|
||||
.fetch_all(state.pool())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
|
|
|
@ -10,42 +10,55 @@ use axum::{
|
|||
use maud::{html, Render};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::{Pool, Postgres};
|
||||
use tracing::error;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::{
|
||||
markup_response::{simple_error_page, MarkupResponse},
|
||||
model::{Chat, Message},
|
||||
state::AppState,
|
||||
ADMIN_TOK,
|
||||
};
|
||||
|
||||
pub async fn get(
|
||||
Path(url_path): Path<String>,
|
||||
headers: HeaderMap,
|
||||
State(pool): State<Pool<Postgres>>,
|
||||
State(state): State<AppState>,
|
||||
) -> impl IntoResponse {
|
||||
println!("headers: {headers:#?}");
|
||||
let chat = sqlx::query_as!(Chat, r#"select * from chats where url_path = $1"#, url_path)
|
||||
.fetch_one(&pool)
|
||||
.await
|
||||
.unwrap();
|
||||
let messages = sqlx::query_as!(
|
||||
Message,
|
||||
r#"select * from messages where chat_id = $1"#,
|
||||
chat.id
|
||||
)
|
||||
.fetch_all(&pool)
|
||||
.await
|
||||
.unwrap();
|
||||
// TODO: Error handling
|
||||
let chat = match state.fetch_chat_by_url_path(&url_path).await {
|
||||
Ok(v) => v,
|
||||
Err(err) => {
|
||||
error!("Error fetching chat: {err:?}");
|
||||
return simple_error_page(err.into()).into_response();
|
||||
}
|
||||
};
|
||||
let messages = match state.fetch_messages(&chat).await {
|
||||
Ok(v) => v,
|
||||
Err(err) => {
|
||||
error!("Error fetching messages for chat {}: {err:?}", chat.id);
|
||||
return simple_error_page(err.into()).into_response();
|
||||
}
|
||||
};
|
||||
|
||||
if Some(&HeaderValue::from_static("application/json")) == headers.get(ACCEPT) {
|
||||
Json(messages).into_response()
|
||||
} else {
|
||||
Html(
|
||||
MarkupResponse::new(
|
||||
html! {
|
||||
template #chatmessage {
|
||||
div.message {
|
||||
p { }
|
||||
span.timestamp { }
|
||||
}
|
||||
}
|
||||
main {
|
||||
div #history {
|
||||
@for msg in &messages {
|
||||
div.message.(if msg.from_admin { "from_admin" } else { "from_user" }) {
|
||||
p { (msg.content) "(" (msg.timestamp) ")" }
|
||||
div.message.(if msg.from_admin { "from_admin" } else { "from_user" }) #(msg.id) {
|
||||
p { (msg.content) }
|
||||
span.timestamp { (msg.timestamp) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -54,24 +67,45 @@ pub async fn get(
|
|||
button type="submit" { "Send!" }
|
||||
}
|
||||
}
|
||||
}
|
||||
.into_string(),
|
||||
script src="/static/chat.js" {};
|
||||
|
||||
},
|
||||
"Cursed Messenger from hell",
|
||||
)
|
||||
.into_response()
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn poll(Path(message): Path<Uuid>, State(state): State<AppState>) -> impl IntoResponse {
|
||||
let message = sqlx::query_as!(Message, r#"select * from messages where id = $1"#, message)
|
||||
.fetch_one(state.pool())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let new_messages = sqlx::query_as!(
|
||||
Message,
|
||||
r#"select * from messages where chat_id = $1 and timestamp > $2;"#,
|
||||
message.chat_id,
|
||||
message.timestamp
|
||||
)
|
||||
.fetch_all(state.pool())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
Json(new_messages)
|
||||
}
|
||||
|
||||
// TODO:
|
||||
// - validation of msg length
|
||||
// - fix terrible returns lmao
|
||||
pub async fn post(
|
||||
Path(url_path): Path<String>,
|
||||
headers: HeaderMap,
|
||||
State(pool): State<Pool<Postgres>>,
|
||||
State(state): State<AppState>,
|
||||
Form(FormMessageBody { msgcontent: body }): Form<FormMessageBody>,
|
||||
) -> impl IntoResponse {
|
||||
let chat = sqlx::query_as!(Chat, r#"select * from chats where url_path = $1"#, url_path)
|
||||
.fetch_one(&pool)
|
||||
.fetch_one(state.pool())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
|
@ -85,7 +119,7 @@ pub async fn post(
|
|||
chat.id,
|
||||
body
|
||||
)
|
||||
.execute(&pool)
|
||||
.execute(state.pool())
|
||||
.await
|
||||
.unwrap();
|
||||
StatusCode::OK.into_response()
|
||||
|
@ -95,7 +129,7 @@ pub async fn post(
|
|||
chat.id,
|
||||
body
|
||||
)
|
||||
.execute(&pool)
|
||||
.execute(state.pool())
|
||||
.await
|
||||
.unwrap();
|
||||
Redirect::to(&format!("/{url_path}")).into_response()
|
||||
|
|
|
@ -1,26 +1,36 @@
|
|||
use axum::{routing::get, Router};
|
||||
use sqlx::{Pool, Postgres};
|
||||
use state::AppState;
|
||||
use tracing::Level;
|
||||
use tracing_subscriber::FmtSubscriber;
|
||||
|
||||
const DB_URL: &str = "postgres://localhost/chatdings";
|
||||
const ADMIN_TOK: &str = "meow";
|
||||
|
||||
mod admin;
|
||||
mod chat;
|
||||
mod markup_response;
|
||||
mod model;
|
||||
mod stat;
|
||||
mod state;
|
||||
mod ws;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> anyhow::Result<()> {
|
||||
let pool = Pool::<Postgres>::connect(DB_URL).await?;
|
||||
let subscriber = FmtSubscriber::builder()
|
||||
.with_max_level(Level::TRACE)
|
||||
.finish();
|
||||
tracing::subscriber::set_global_default(subscriber).expect("setting default subscriber failed");
|
||||
|
||||
sqlx::migrate!().run(&pool).await?;
|
||||
let state = AppState::init().await?;
|
||||
|
||||
let app = Router::new()
|
||||
.route("/", get(|| async { "<h1>gay</h1>" }))
|
||||
.route("/:path", get(chat::get).post(chat::post))
|
||||
.with_state(pool.clone())
|
||||
.nest("/stat", stat::router(pool.clone()))
|
||||
.nest("/admin", admin::router(pool.clone()));
|
||||
.route("/poll/:msg", get(chat::poll))
|
||||
.with_state(state.clone())
|
||||
.nest("/stat", stat::router(state.clone()))
|
||||
.nest("/admin", admin::router(state.clone()))
|
||||
.nest("/static", axum_static::static_router("static"));
|
||||
let listener = tokio::net::TcpListener::bind("0.0.0.0:8080").await.unwrap();
|
||||
axum::serve(listener, app).await?;
|
||||
|
||||
|
|
47
crates/backend/src/markup_response.rs
Normal file
47
crates/backend/src/markup_response.rs
Normal file
|
@ -0,0 +1,47 @@
|
|||
use axum::response::{Html, IntoResponse};
|
||||
use http::StatusCode;
|
||||
use maud::{html, Markup, DOCTYPE};
|
||||
|
||||
pub struct MarkupResponse {
|
||||
title: String,
|
||||
body: Markup,
|
||||
}
|
||||
|
||||
impl MarkupResponse {
|
||||
pub fn new(body: Markup, title: &str) -> Self {
|
||||
Self {
|
||||
title: title.to_owned(),
|
||||
body,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoResponse for MarkupResponse {
|
||||
fn into_response(self) -> axum::response::Response {
|
||||
Html::from(base_page(&self.title, &self.body).into_string()).into_response()
|
||||
}
|
||||
}
|
||||
|
||||
fn base_page(title: &str, markup: &Markup) -> Markup {
|
||||
html! {
|
||||
(DOCTYPE)
|
||||
html lang="en" {
|
||||
head {
|
||||
meta charset="utf-8";
|
||||
meta name="viewport" content="width=device-width, initial-scale=1";
|
||||
title { (title) }
|
||||
// link rel="stylesheet" href="/style";
|
||||
}
|
||||
body { (markup) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn simple_error_page(status: StatusCode) -> MarkupResponse {
|
||||
MarkupResponse::new(
|
||||
html! {
|
||||
img src=(format!("https://http.cat/{}", status.as_u16()));
|
||||
},
|
||||
"Error",
|
||||
)
|
||||
}
|
|
@ -3,16 +3,16 @@ use std::sync::Arc;
|
|||
use axum::{extract::State, routing::get, Json, Router};
|
||||
use sqlx::{types::Uuid, Pool, Postgres};
|
||||
|
||||
use crate::model::Chat;
|
||||
use crate::{model::Chat, state::AppState};
|
||||
|
||||
// TODO: /stat/* should require authentication
|
||||
pub fn router(pool: Pool<Postgres>) -> Router {
|
||||
Router::new().route("/chats", get(chats)).with_state(pool)
|
||||
pub fn router(state: AppState) -> Router {
|
||||
Router::new().route("/chats", get(chats)).with_state(state)
|
||||
}
|
||||
|
||||
async fn chats(State(pool): State<Pool<Postgres>>) -> Json<Vec<Chat>> {
|
||||
async fn chats(State(state): State<AppState>) -> Json<Vec<Chat>> {
|
||||
let r = sqlx::query_as!(Chat, "select * from chats;")
|
||||
.fetch_all(&pool)
|
||||
.fetch_all(state.pool())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
|
|
74
crates/backend/src/state.rs
Normal file
74
crates/backend/src/state.rs
Normal file
|
@ -0,0 +1,74 @@
|
|||
use axum::response::IntoResponse;
|
||||
use http::StatusCode;
|
||||
use sqlx::{Pool, Postgres};
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::model::{Chat, Message};
|
||||
|
||||
type Result<T> = std::result::Result<T, AppStateError>;
|
||||
const DB_URL: &str = "postgres://localhost/chatdings";
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct AppState {
|
||||
pool: Pool<Postgres>,
|
||||
}
|
||||
|
||||
impl AppState {
|
||||
pub async fn init() -> Result<Self> {
|
||||
let pool = Pool::<Postgres>::connect(DB_URL).await?;
|
||||
|
||||
sqlx::migrate!()
|
||||
.run(&pool)
|
||||
.await
|
||||
.expect("migration should not fail");
|
||||
|
||||
Ok(Self { pool })
|
||||
}
|
||||
|
||||
pub async fn fetch_chat_by_url_path(&self, url_path: &str) -> Result<Chat> {
|
||||
Ok(
|
||||
sqlx::query_as!(Chat, r#"select * from chats where url_path = $1"#, url_path)
|
||||
.fetch_one(&self.pool)
|
||||
.await?,
|
||||
)
|
||||
}
|
||||
|
||||
pub async fn fetch_messages(&self, chat: &Chat) -> Result<Vec<Message>> {
|
||||
Ok(sqlx::query_as!(
|
||||
Message,
|
||||
r#"select * from messages where chat_id = $1"#,
|
||||
chat.id
|
||||
)
|
||||
.fetch_all(&self.pool)
|
||||
.await?)
|
||||
}
|
||||
|
||||
pub async fn send_message(&self, chat: &Chat, content: String, from_admin: bool) -> Result<()> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
pub fn pool(&self) -> &Pool<Postgres> {
|
||||
&self.pool
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum AppStateError {
|
||||
#[error("database error")]
|
||||
Sqlx(#[from] sqlx::Error),
|
||||
}
|
||||
|
||||
impl IntoResponse for AppStateError {
|
||||
fn into_response(self) -> axum::response::Response {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<AppStateError> for StatusCode {
|
||||
fn from(value: AppStateError) -> Self {
|
||||
match value {
|
||||
AppStateError::Sqlx(sqlx::Error::RowNotFound) => Self::NOT_FOUND,
|
||||
AppStateError::Sqlx(_) => Self::INTERNAL_SERVER_ERROR,
|
||||
}
|
||||
}
|
||||
}
|
15
crates/backend/src/ws.rs
Normal file
15
crates/backend/src/ws.rs
Normal file
|
@ -0,0 +1,15 @@
|
|||
use axum::{
|
||||
extract::{ws::WebSocket, Path, State, WebSocketUpgrade},
|
||||
response::Response,
|
||||
};
|
||||
use sqlx::{Pool, Postgres};
|
||||
|
||||
fn get(
|
||||
Path(url_path): Path<String>,
|
||||
ws: WebSocketUpgrade,
|
||||
State(pool): State<Pool<Postgres>>,
|
||||
) -> Response {
|
||||
todo!()
|
||||
}
|
||||
|
||||
// fn handle_socket(socket: WebSocket, pool: Pool<Postgres>)
|
Loading…
Add table
Add a link
Reference in a new issue