aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorXe Iaso <me@christine.website>2022-07-10 20:29:07 +0000
committerXe Iaso <me@christine.website>2022-07-10 20:29:07 +0000
commit55bf7e4cb403566d7172ef69b8f2f7393ac8627d (patch)
tree7b3682e19c97672245cbb97e6c1d1069e812fd8b /src
parentb32f5a25afb7b9901476164663c1b7099dcec7a8 (diff)
downloadxesite-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.rs10
-rw-r--r--src/handlers/mod.rs19
-rw-r--r--src/handlers/notes.rs161
-rw-r--r--src/main.rs13
-rw-r--r--src/migrate/base_schema.sql11
-rw-r--r--src/migrate/mod.rs16
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(())
+}