get this to work a slight bit more

This commit is contained in:
Schrottkatze 2024-10-07 21:12:49 +02:00
parent ed87d3fb51
commit 5160929958
Signed by: schrottkatze
SSH key fingerprint: SHA256:hXb3t1vINBFCiDCmhRABHX5ocdbLiKyCdKI4HK2Rbbc
10 changed files with 484 additions and 108 deletions

View file

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

View file

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

View file

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

View file

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

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

View file

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

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