From c3108917438e6f63223666fb23d04e9f5224647d Mon Sep 17 00:00:00 2001 From: Christine Dodrill Date: Wed, 4 Nov 2020 13:41:46 -0500 Subject: [PATCH] begin work on posse support --- backend/Cargo.lock | 13 -- backend/Cargo.toml | 2 - backend/Rocket.example.toml | 7 - .../2020-11-04-153112_blogposts/up.sql | 2 +- backend/src/api/mod.rs | 1 + backend/src/api/posse.rs | 122 ++++++++++++++++++ backend/src/api/webmention.rs | 26 +++- backend/src/bin/import_blogposts.rs | 39 ++++++ backend/src/main.rs | 2 + backend/src/models.rs | 7 + backend/src/schema.rs | 2 +- 11 files changed, 198 insertions(+), 25 deletions(-) create mode 100644 backend/src/api/posse.rs create mode 100644 backend/src/bin/import_blogposts.rs diff --git a/backend/Cargo.lock b/backend/Cargo.lock index 968a4d8..1b4628e 100644 --- a/backend/Cargo.lock +++ b/backend/Cargo.lock @@ -533,7 +533,6 @@ version = "0.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2d2f06b9cac1506ece98fe3231e3cc9c4410ec3d5b1f24ae1c8946f0742cdefc" dependencies = [ - "backtrace", "version_check 0.9.2", ] @@ -866,17 +865,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "jsonfeed" -version = "0.3.0" -source = "git+https://github.com/Xe/site#22575fca380f561006f44079ebe59019babe24d7" -dependencies = [ - "error-chain", - "serde", - "serde_derive", - "serde_json", -] - [[package]] name = "kankyo" version = "0.3.0" @@ -1013,7 +1001,6 @@ dependencies = [ "diesel", "futures-io", "hex", - "jsonfeed", "kankyo", "log 0.4.11", "mime 0.3.16", diff --git a/backend/Cargo.toml b/backend/Cargo.toml index 7b90fb6..de93b34 100644 --- a/backend/Cargo.toml +++ b/backend/Cargo.toml @@ -32,8 +32,6 @@ ureq = { version = "1", features = ["json", "charset"] } uuid = { version = "0.7", features = ["serde", "v4"] } url = "2" -jsonfeed = { git = "https://github.com/Xe/site" } - [dependencies.diesel] version = "1" features = ["sqlite", "r2d2", "uuidv07", "chrono"] diff --git a/backend/Rocket.example.toml b/backend/Rocket.example.toml index bbd5327..d5f04c9 100644 --- a/backend/Rocket.example.toml +++ b/backend/Rocket.example.toml @@ -31,10 +31,3 @@ api_secret = "..." instance = "..." token = "..." account = "..." - -# Get these values by creating an oAuth app -[global.reddit] -app_id = "..." -app_secret = "..." -username = "..." -password = "..." \ No newline at end of file diff --git a/backend/migrations/2020-11-04-153112_blogposts/up.sql b/backend/migrations/2020-11-04-153112_blogposts/up.sql index add9329..31a6b06 100644 --- a/backend/migrations/2020-11-04-153112_blogposts/up.sql +++ b/backend/migrations/2020-11-04-153112_blogposts/up.sql @@ -1,4 +1,4 @@ CREATE TABLE IF NOT EXISTS blogposts - ( url UNIQUE NOT NULL PRIMARY KEY + ( url TEXT UNIQUE NOT NULL PRIMARY KEY , title TEXT NOT NULL ) diff --git a/backend/src/api/mod.rs b/backend/src/api/mod.rs index 2095adb..15520c7 100644 --- a/backend/src/api/mod.rs +++ b/backend/src/api/mod.rs @@ -12,6 +12,7 @@ use rocket::{ use rocket_contrib::json::Json; use std::io::Read; +pub mod posse; pub mod switch; pub mod webmention; diff --git a/backend/src/api/posse.rs b/backend/src/api/posse.rs new file mode 100644 index 0000000..b0f967a --- /dev/null +++ b/backend/src/api/posse.rs @@ -0,0 +1,122 @@ +use super::Result; +use crate::{ + models, paseto, schema, + web::{DiscordWebhook, Error as WebError, Mastodon, Result as WebResult, Twitter}, + MainDatabase, +}; +use diesel::prelude::*; +use rocket::State; +use serde::Deserialize; +use std::fmt::Write; + +#[derive(Deserialize)] +pub struct Jsonfeed { + pub version: String, + pub home_page_url: String, + pub items: Vec, +} + +#[derive(Deserialize, Clone, Debug)] +pub struct Item { + pub url: String, + pub title: String, + pub tags: Option>, +} + +impl Item { + fn render(self) -> String { + let mut result = String::new(); + + write!(result, "{}\n\n{}", self.title, self.url).unwrap(); + + if let Some(tags) = self.tags { + write!(result, "\n\n").unwrap(); + + for tag in tags.iter() { + write!(result, "#{} ", tag).unwrap(); + } + } + + result + } +} + +impl Into for Item { + fn into(self) -> models::Blogpost { + models::Blogpost { + url: self.url, + title: self.title, + } + } +} + +pub fn read_jsonfeed(url: String) -> WebResult { + let resp = ureq::get(&url) + .set("User-Agent", crate::APPLICATION_NAME) + .call(); + + if resp.ok() { + Ok(resp.into_json_deserialize()?) + } else { + Err(match resp.synthetic_error() { + Some(why) => WebError::UReq(why.to_string()), + None => WebError::HttpStatus(resp.status()), + }) + } +} + +#[instrument(skip(dw, tw, ma), err)] +fn posse(item: Item, dw: &DiscordWebhook, tw: &Twitter, ma: &Mastodon) -> WebResult { + let message = item.render(); + + dw.send(message.clone())?; + tw.tweet(message.clone())?; + ma.toot(message.clone())?; + + Ok(()) +} + +pub static BLOG_FEED_URL: &'static str = "https://christine.website/blog.json"; + +#[post("/blog/refresh")] +#[instrument(skip(conn, dw, tw, ma), err)] +pub fn refresh_blog( + tok: paseto::Token, + conn: MainDatabase, + dw: State, + tw: State, + ma: State, +) -> Result { + use schema::blogposts::dsl::blogposts; + let feed = read_jsonfeed(BLOG_FEED_URL.to_string())?; + + for item in feed.items.into_iter() { + match blogposts + .find(item.url.clone()) + .get_result::(&*conn) + { + Ok(_) => continue, + Err(_) => { + diesel::insert_into(schema::blogposts::table) + .values(&{ + let post: models::Blogpost = item.clone().into(); + post + }) + .execute(&*conn)?; + posse(item, &dw, &tw, &ma)? + } + } + } + + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::{read_jsonfeed, BLOG_FEED_URL}; + + #[test] + fn valid_jsonfeed() { + read_jsonfeed(BLOG_FEED_URL.to_string()).unwrap(); + } +} diff --git a/backend/src/api/webmention.rs b/backend/src/api/webmention.rs index 8824dbf..da0cfc6 100644 --- a/backend/src/api/webmention.rs +++ b/backend/src/api/webmention.rs @@ -1,5 +1,5 @@ use super::{Error, Result}; -use crate::{models, schema, web::discord_webhook::Client as DiscordWebhook, MainDatabase}; +use crate::{models, paseto, schema, web::discord_webhook::Client as DiscordWebhook, MainDatabase}; use diesel::prelude::*; use rocket::{ request::Form, @@ -118,3 +118,27 @@ pub fn get(conn: MainDatabase, mention_id: String) -> Result&")] +#[instrument(skip(conn), err)] +pub fn list( + conn: MainDatabase, + count: Option, + page: Option, + tok: paseto::Token, +) -> Result>> { + use schema::webmentions; + + let count = count.unwrap_or(30); + let page = page.unwrap_or(0); + + let count = if count < 100 { count } else { 100 }; + + Ok(Json( + webmentions::table + .limit(count) + .offset(count * (page - 1)) + .load::(&*conn) + .map_err(Error::Database)?, + )) +} diff --git a/backend/src/bin/import_blogposts.rs b/backend/src/bin/import_blogposts.rs new file mode 100644 index 0000000..16d7319 --- /dev/null +++ b/backend/src/bin/import_blogposts.rs @@ -0,0 +1,39 @@ +#[macro_use] +extern crate tracing; + +use color_eyre::eyre::Result; +use diesel::{prelude::*, SqliteConnection}; +use std::env; + +use mi::{api::posse::*, *}; + +fn main() -> Result<()> { + let _ = kankyo::init(); + color_eyre::install()?; + tracing_subscriber::fmt::init(); + + info!("{} blogpost importer starting up", mi::APPLICATION_NAME); + + let conn = establish_connection(); + + let feed = read_jsonfeed(BLOG_FEED_URL.to_string())?; + let posts: Vec = feed + .items + .into_iter() + .map(|item| { + let post: models::Blogpost = item.into(); + post + }) + .collect(); + diesel::insert_into(schema::blogposts::table) + .values(&posts) + .execute(&conn)?; + + Ok(()) +} + +pub fn establish_connection() -> SqliteConnection { + let database_url = env::var("DATABASE_URL").expect("DATABASE_URL must be set"); + SqliteConnection::establish(&database_url) + .expect(&format!("Error connecting to {}", database_url)) +} diff --git a/backend/src/main.rs b/backend/src/main.rs index b0cd013..e848942 100644 --- a/backend/src/main.rs +++ b/backend/src/main.rs @@ -53,12 +53,14 @@ fn main() -> Result<()> { .mount( "/api", routes![ + api::posse::refresh_blog, api::switch::current_front, api::switch::get, api::switch::list, api::switch::switch, api::webmention::accept, api::webmention::get, + api::webmention::list, api::get_members, api::token_info, api::tweet, diff --git a/backend/src/models.rs b/backend/src/models.rs index 2177f3f..eb6e644 100644 --- a/backend/src/models.rs +++ b/backend/src/models.rs @@ -58,3 +58,10 @@ pub struct WebMention { pub source_url: String, pub target_url: String, } + +#[derive(Queryable, Associations, Insertable)] +#[table_name = "blogposts"] +pub struct Blogpost { + pub url: String, + pub title: String, +} diff --git a/backend/src/schema.rs b/backend/src/schema.rs index fcaf230..3075b35 100644 --- a/backend/src/schema.rs +++ b/backend/src/schema.rs @@ -1,6 +1,6 @@ table! { blogposts (url) { - url -> Binary, + url -> Text, title -> Text, } }