use super::{Error, Result}; use crate::{ models, paseto, schema, web::{self, discord_webhook::Client as DiscordWebhook, Error as WebError}, MainDatabase, }; use diesel::prelude::*; use readability_fork::extractor::{self, Product}; use rocket::{ http::Status, request::Form, response::{self, Responder}, Request, Response, State, }; use rocket_contrib::json::Json; use rusty_ulid::generate_ulid_string; use serde::Serialize; use url::Url; #[derive(FromForm, Debug, Serialize)] pub struct WebMention { source: String, target: String, title: Option, } impl WebMention { fn check(&self) -> Result { if self.source == self.target { return Err(Error::InvalidWebMention("source == target".into())); } let u: Url = Url::parse(&self.source)?; match u.scheme() { "http" | "https" => {} _ => return Err(Error::InvalidWebMention("invalid source scheme".into())), } u.host_str() .ok_or(Error::InvalidWebMention("no host found in target".into()))?; let u: Url = Url::parse(&self.target)?; match u.scheme() { "http" | "https" => {} _ => return Err(Error::InvalidWebMention("invalid target scheme".into())), } match u .host_str() .ok_or(Error::InvalidWebMention("no host found in target".into()))? { "christine.website" | "cetacean.club" => {} _ => return Err(Error::InvalidWebMention("invalid target host".into())), } Ok(()) } fn extract(&self) -> Result { let resp = ureq::get(&self.source) .set("User-Agent", crate::APPLICATION_NAME) .set("Mi-Mentioned-Url", &self.target) .call()?; let body = resp .into_string() .map_err(|why| Error::Web(web::Error::FuturesIO(why)))?; Ok(extractor::extract( &mut body.as_bytes(), &url::Url::parse(&self.source)?, )?) } } impl Into for WebMention { fn into(self) -> models::WebMention { models::WebMention { id: generate_ulid_string(), source_url: self.source, target_url: self.target, title: self.title, } } } impl Into for models::WebMention { fn into(self) -> WebMention { WebMention { source: self.source_url, target: self.target_url, title: self.title, } } } impl<'a> Responder<'a> for models::WebMention { fn respond_to(self, _: &Request) -> response::Result<'a> { Response::build() .raw_header( "Location", format!("https://mi.christine.website/api/webmention/{}", self.id), ) .status(Status::Created) .ok() } } #[get("/webmention/for?")] #[instrument(skip(conn), err)] pub fn lookup_target(conn: MainDatabase, target: String) -> Result>> { use schema::webmentions::dsl::*; Ok(Json( webmentions .filter(target_url.eq(target)) .load::(&*conn) .map_err(Error::Database)? .into_iter() .map(Into::into) .filter(|wm: &WebMention| { !wm.source.contains("like/twitter") || !wm.source.contains("like/mastodon") }) .collect::>(), )) } #[post("/webmention/accept", data = "")] #[instrument(skip(conn, dw), err)] pub fn accept( conn: MainDatabase, mention: Form, dw: State, ) -> Result { use schema::webmentions; let mut mention = mention.into_inner(); mention.check()?; mention .extract() .map_err(|why| { error!( "error extracting information from {}: {}", mention.source, why ); why }) .iter_mut() .for_each(|info| mention.title = Some(info.title.clone())); info!( source = &mention.source[..], target = &mention.target[..], "webmention received: {:?}", mention.title, ); let wm: models::WebMention = mention.into(); diesel::insert_into(webmentions::table) .values(&wm) .execute(&*conn) .map_err(Error::Database)?; bridgy_expand(conn, wm.clone())?; dw.send(format!( "<{}> mentioned <{}> ()", wm.source_url, wm.target_url, wm.id )) .map_err(|why| { error!("webhook send failed: {}", why); Error::Web(why) })?; Ok(wm) } #[get("/webmention/")] #[instrument(skip(conn), err)] pub fn get(conn: MainDatabase, mention_id: String) -> Result> { use schema::webmentions::dsl::webmentions; Ok(Json( webmentions .find(mention_id) .get_result(&*conn) .map_err(Error::Database)?, )) } #[get("/webmention?&")] #[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)?, )) } pub fn bridgy_expand(conn: MainDatabase, wm: models::WebMention) -> Result { use crate::web::bridgy::parse; use schema::webmentions::dsl::*; if !wm.source_url.contains("https://brid-gy.appspot.com") { return Ok(()); } if wm.source_url.contains("like/twitter") { return Ok(()); } let resp = ureq::get(&wm.source_url) .set("User-Agent", crate::APPLICATION_NAME) .set("Mi-Mentioned-Url", &wm.target_url) .call() .map_err(WebError::UReq)?; let body: String = resp.into_string()?; let result = parse(&body).unwrap().unwrap(); diesel::update(webmentions.find(wm.id)) .set(&models::UpdateWebMentionSource { source_url: result.target, }) .execute(&*conn) .map_err(Error::Database) .unwrap(); Ok(()) }