diff options
| author | Xe Iaso <me@christine.website> | 2022-07-10 20:29:07 +0000 |
|---|---|---|
| committer | Xe Iaso <me@christine.website> | 2022-07-10 20:29:07 +0000 |
| commit | 55bf7e4cb403566d7172ef69b8f2f7393ac8627d (patch) | |
| tree | 7b3682e19c97672245cbb97e6c1d1069e812fd8b /src | |
| parent | b32f5a25afb7b9901476164663c1b7099dcec7a8 (diff) | |
| download | xesite-55bf7e4cb403566d7172ef69b8f2f7393ac8627d.tar.xz xesite-55bf7e4cb403566d7172ef69b8f2f7393ac8627d.zip | |
basic notes support
Signed-off-by: Xe Iaso <me@christine.website>
Diffstat (limited to 'src')
| -rw-r--r-- | src/app/mod.rs | 10 | ||||
| -rw-r--r-- | src/handlers/mod.rs | 19 | ||||
| -rw-r--r-- | src/handlers/notes.rs | 161 | ||||
| -rw-r--r-- | src/main.rs | 13 | ||||
| -rw-r--r-- | src/migrate/base_schema.sql | 11 | ||||
| -rw-r--r-- | src/migrate/mod.rs | 16 |
6 files changed, 229 insertions, 1 deletions
diff --git a/src/app/mod.rs b/src/app/mod.rs index 5b8e719..24eabc5 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -1,7 +1,9 @@ use crate::{post::Post, signalboost::Person}; +use bb8::Pool; +use bb8_rusqlite::RusqliteConnectionManager; use chrono::prelude::*; use color_eyre::eyre::Result; -use std::{fs, path::PathBuf, sync::Arc}; +use std::{env, fs, path::PathBuf, sync::Arc}; use tracing::{error, instrument}; pub mod config; @@ -58,6 +60,7 @@ pub struct State { pub sitemap: Vec<u8>, pub patrons: Option<patreon::Users>, pub mi: mi::Client, + pub pool: Pool<RusqliteConnectionManager>, } pub async fn init(cfg: PathBuf) -> Result<State> { @@ -73,6 +76,10 @@ pub async fn init(cfg: PathBuf) -> Result<State> { let gallery = crate::post::load(cfg.clone(), "gallery").await?; let talks = crate::post::load(cfg.clone(), "talks").await?; let mut everything: Vec<Post> = vec![]; + let mgr = RusqliteConnectionManager::new( + env::var("DATABASE_URL").unwrap_or("./var/waifud.db".to_string()), + ); + let pool = bb8::Pool::builder().build(mgr).await?; { let blog = blog.clone(); @@ -150,6 +157,7 @@ pub async fn init(cfg: PathBuf) -> Result<State> { jf: jfb.build(), sitemap: sm, patrons: patrons().await?, + pool, }) } diff --git a/src/handlers/mod.rs b/src/handlers/mod.rs index 37fcac1..87175ca 100644 --- a/src/handlers/mod.rs +++ b/src/handlers/mod.rs @@ -15,6 +15,7 @@ pub mod api; pub mod blog; pub mod feeds; pub mod gallery; +pub mod notes; pub mod talks; fn weekday_to_name(w: Weekday) -> &'static str { @@ -164,6 +165,24 @@ pub enum Error { #[error("string conversion error: {0}")] ToStr(#[from] http::header::ToStrError), + + #[error("database error: {0}")] + SQLite(#[from] rusqlite::Error), + + #[error("database pool error: {0}")] + SQLitePool(#[from] bb8_rusqlite::Error), + + #[error("other error: {0}")] + Catchall(String), +} + +impl<E> From<bb8::RunError<E>> for Error +where + E: std::error::Error + Send + 'static, +{ + fn from(err: bb8::RunError<E>) -> Self { + Self::Catchall(format!("{}", err)) + } } pub type Result<T = Html<Vec<u8>>> = std::result::Result<T, Error>; diff --git a/src/handlers/notes.rs b/src/handlers/notes.rs new file mode 100644 index 0000000..feb2129 --- /dev/null +++ b/src/handlers/notes.rs @@ -0,0 +1,161 @@ +use crate::templates; +use axum::{extract::Path, response::Html, Json}; +use chrono::prelude::*; +use maud::{html, Markup, PreEscaped}; +use rusqlite::params; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct Note { + pub id: u64, + pub content: String, + pub content_html: String, + pub created_at: DateTime<Utc>, + pub updated_at: Option<DateTime<Utc>>, + pub deleted_at: Option<DateTime<Utc>>, + pub reply_to: Option<String>, +} + +impl Note { + pub fn to_html(&self) -> Markup { + html! { + article."h-entry" { + time."dt-published" datetime=(self.created_at) { + {(self.detrytemci())} + } + a href={"/notes/" (self.id)} { + "🔗" + } + + @if let Some(reply_to) = &self.reply_to { + p { + "In reply to " + a href=(reply_to) {(reply_to)} + "." + } + } + + div."e-content" { + (PreEscaped(self.content_html.clone())) + } + } + } + } + + pub fn detrytemci(&self) -> String { + self.created_at.format("M%m %d %Y %M:%H").to_string() + } +} + +impl Into<xe_jsonfeed::Item> for Note { + fn into(self) -> xe_jsonfeed::Item { + let url = format!("https://xeiaso.net/note/{}", self.id); + let mut builder = xe_jsonfeed::Item::builder() + .content_html(self.content_html) + .id(url.clone()) + .url(url.clone()) + .date_published(self.created_at.to_rfc3339()) + .author( + xe_jsonfeed::Author::new() + .name("Xe Iaso") + .url("https://xeiaso.net") + .avatar("https://xeiaso.net/static/img/avatar.png"), + ); + + if let Some(updated_at) = self.updated_at { + builder = builder.date_modified(updated_at.to_rfc3339()); + } + + builder.build().unwrap() + } +} + +#[instrument(err)] +pub async fn index() -> super::Result { + let conn = crate::establish_connection()?; + + let mut stmt = conn.prepare("SELECT id, content, content_html, created_at, updated_at, deleted_at, reply_to FROM notes ORDER BY id DESC LIMIT 25")?; + let notes = stmt + .query_map(params![], |row| { + Ok(Note { + id: row.get(0)?, + content: row.get(1)?, + content_html: row.get(2)?, + created_at: row.get(3)?, + updated_at: row.get(4)?, + deleted_at: row.get(5)?, + reply_to: row.get(6)?, + }) + })? + .filter(Result::is_ok) + .map(Result::unwrap) + .collect::<Vec<Note>>(); + + let mut result: Vec<u8> = vec![]; + templates::notesindex_html(&mut result, notes)?; + Ok(Html(result)) +} + +#[instrument(err)] +pub async fn feed() -> super::Result<Json<xe_jsonfeed::Feed>> { + let conn = crate::establish_connection()?; + + let mut stmt = conn.prepare("SELECT id, content, content_html, created_at, updated_at, deleted_at, reply_to FROM notes ORDER BY id DESC LIMIT 25")?; + let notes = stmt + .query_map(params![], |row| { + Ok(Note { + id: row.get(0)?, + content: row.get(1)?, + content_html: row.get(2)?, + created_at: row.get(3)?, + updated_at: row.get(4)?, + deleted_at: row.get(5)?, + reply_to: row.get(6)?, + }) + })? + .filter(Result::is_ok) + .map(Result::unwrap) + .collect::<Vec<Note>>(); + + let mut feed = xe_jsonfeed::Feed::builder() + .author( + xe_jsonfeed::Author::new() + .name("Xe Iaso") + .url("https://xeiaso.net") + .avatar("https://xeiaso.net/static/img/avatar.png"), + ) + .description("Short posts that aren't to the same quality level as mainline blogposts") + .feed_url("https://xeiaso.net/notes.json") + .title("Xe's Notes"); + + for note in notes { + feed = feed.item(note.into()); + } + + Ok(Json(feed.build())) +} + +#[instrument(err)] +pub async fn view(Path(id): Path<u64>) -> super::Result { + let conn = crate::establish_connection()?; + + let mut stmt = conn.prepare( + "SELECT id, content, content_html, created_at, updated_at, deleted_at, reply_to FROM notes WHERE id = ?1" + )?; + + let note = stmt.query_row(params![id], |row| { + Ok(Note { + id: row.get(0)?, + content: row.get(1)?, + content_html: row.get(2)?, + created_at: row.get(3)?, + updated_at: row.get(4)?, + deleted_at: row.get(5)?, + reply_to: row.get(6)?, + }) + })?; + + let mut result: Vec<u8> = vec![]; + templates::notepost_html(&mut result, note)?; + Ok(Html(result)) +} diff --git a/src/main.rs b/src/main.rs index cdcae68..76196a8 100644 --- a/src/main.rs +++ b/src/main.rs @@ -12,6 +12,7 @@ use axum::{ use color_eyre::eyre::Result; use hyper::StatusCode; use prometheus::{Encoder, TextEncoder}; +use rusqlite::Connection; use sdnotify::SdNotify; use std::{ env, io, @@ -28,6 +29,7 @@ use tower_http::{ pub mod app; pub mod handlers; +pub mod migrate; pub mod post; pub mod signalboost; pub mod tmpl; @@ -39,6 +41,11 @@ use crate::app::poke; const APPLICATION_NAME: &str = concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION")); +pub fn establish_connection() -> handlers::Result<Connection> { + let database_url = env::var("DATABASE_URL").unwrap_or("./xesite.db".to_string()); + Ok(Connection::open(&database_url)?) +} + async fn healthcheck() -> &'static str { "OK" } @@ -72,6 +79,8 @@ async fn main() -> Result<()> { tracing_subscriber::fmt::init(); info!("starting up commit {}", env!("GITHUB_SHA")); + migrate::run()?; + let state = Arc::new( app::init( env::var("CONFIG_FNAME") @@ -189,6 +198,10 @@ async fn main() -> Result<()> { .route("/talks", get(handlers::talks::index)) .route("/talks/", get(handlers::talks::index)) .route("/talks/:name", get(handlers::talks::post_view)) + // notes + .route("/notes", get(handlers::notes::index)) + .route("/notes.json", get(handlers::notes::feed)) + .route("/notes/:id", get(handlers::notes::view)) // junk google wants .route("/sitemap.xml", get(handlers::feeds::sitemap)) // static files diff --git a/src/migrate/base_schema.sql b/src/migrate/base_schema.sql new file mode 100644 index 0000000..65d19d0 --- /dev/null +++ b/src/migrate/base_schema.sql @@ -0,0 +1,11 @@ +CREATE TABLE IF NOT EXISTS notes + ( id INTEGER PRIMARY KEY + , content TEXT NOT NULL + , content_html TEXT NOT NULL + , created_at TEXT NOT NULL -- Unix epoch timestamp + , updated_at TEXT -- Unix epoch timestamp + , deleted_at TEXT -- Unix epoch timestamp + , reply_to TEXT + ); + +CREATE INDEX IF NOT EXISTS notes_reply_to ON notes(reply_to); diff --git a/src/migrate/mod.rs b/src/migrate/mod.rs new file mode 100644 index 0000000..a2eb9c3 --- /dev/null +++ b/src/migrate/mod.rs @@ -0,0 +1,16 @@ +use super::establish_connection; +use color_eyre::eyre::Result; +use rusqlite_migration::{Migrations, M}; + +#[instrument(err)] +pub fn run() -> Result<()> { + info!("running"); + let mut conn = establish_connection()?; + + let migrations = Migrations::new(vec![M::up(include_str!("./base_schema.sql"))]); + conn.pragma_update(None, "journal_mode", &"WAL").unwrap(); + + migrations.to_latest(&mut conn)?; + + Ok(()) +} |
