diff --git a/Cargo.lock b/Cargo.lock index fd75150..ef8a5e3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -284,6 +284,15 @@ version = "2.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "630be753d4e58660abd17930c71b647fe46c27ea6b63cc59e1e3851406972e42" +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + [[package]] name = "bstr" version = "0.2.17" @@ -307,6 +316,12 @@ version = "3.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a3e2c3daef883ecc1b5d58c15adae93470a91d425f3532ba1695849656af3fc1" +[[package]] +name = "byteorder" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" + [[package]] name = "bytes" version = "1.4.0" @@ -329,6 +344,7 @@ dependencies = [ "serde_yaml", "systemd", "tokio", + "tokio-postgres", "tracing", "tracing-subscriber", ] @@ -369,10 +385,12 @@ dependencies = [ "metrics-exporter-prometheus", "metrics-util", "redis", + "reqwest", "serde", "serde_yaml", "tokio", "tracing", + "urlencoding", ] [[package]] @@ -394,6 +412,7 @@ dependencies = [ "tokio", "tokio-uring", "tracing", + "urlencoding", ] [[package]] @@ -466,6 +485,15 @@ version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa" +[[package]] +name = "cpufeatures" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a17b76ff3a4162b0b27f354a0c87015ddad39d35f9c0c36607a3bdd175dde1f1" +dependencies = [ + "libc", +] + [[package]] name = "crc32fast" version = "1.3.2" @@ -497,6 +525,16 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + [[package]] name = "cstr-argument" version = "0.1.2" @@ -513,6 +551,17 @@ version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2e66c9d817f1720209181c316d28635c050fa304f9c79e47a520882661b7308" +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", + "subtle", +] + [[package]] name = "encoding_rs" version = "0.8.32" @@ -578,6 +627,12 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "fallible-iterator" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4443176a9f2c162692bd3d352d745ef9413eec5782a80d8fd6f8a1ac692a07f7" + [[package]] name = "fastrand" version = "2.0.0" @@ -740,6 +795,16 @@ dependencies = [ "slab", ] +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + [[package]] name = "getrandom" version = "0.2.10" @@ -803,6 +868,15 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "443144c8cdadd93ebf52ddb4056d257f5b52c04d3c804e657d19eb73fc33668b" +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + [[package]] name = "hostname" version = "0.3.1" @@ -1142,6 +1216,15 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73cbba799671b762df5a175adf59ce145165747bb891505c43d09aefbbf38beb" +[[package]] +name = "md-5" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6365506850d44bff6e2fbcb5176cf63650e48bd45ef2fe2665ae1570e0f4b9ca" +dependencies = [ + "digest", +] + [[package]] name = "memchr" version = "2.5.0" @@ -1430,6 +1513,24 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94" +[[package]] +name = "phf" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc" +dependencies = [ + "phf_shared", +] + +[[package]] +name = "phf_shared" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90fcb95eef784c2ac79119d1dd819e162b5da872ce6f3c3abe1e8ca1c082f72b" +dependencies = [ + "siphasher", +] + [[package]] name = "pin-project" version = "1.1.2" @@ -1483,6 +1584,35 @@ version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "edc55135a600d700580e406b4de0d59cb9ad25e344a3a091a97ded2622ec4ec6" +[[package]] +name = "postgres-protocol" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78b7fa9f396f51dffd61546fd8573ee20592287996568e6175ceb0f8699ad75d" +dependencies = [ + "base64", + "byteorder", + "bytes", + "fallible-iterator", + "hmac", + "md-5", + "memchr", + "rand", + "sha2", + "stringprep", +] + +[[package]] +name = "postgres-types" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f028f05971fe20f512bcc679e2c10227e57809a3af86a7606304435bc8896cd6" +dependencies = [ + "bytes", + "fallible-iterator", + "postgres-protocol", +] + [[package]] name = "ppv-lite86" version = "0.2.17" @@ -1835,6 +1965,17 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ae1a47186c03a32177042e55dbc5fd5aee900b8e0069a8d70fba96a9375cd012" +[[package]] +name = "sha2" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479fb9d862239e610720565ca91403019f2f00410f1864c5aa7479b950a76ed8" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + [[package]] name = "sharded-slab" version = "0.1.4" @@ -1853,6 +1994,12 @@ dependencies = [ "libc", ] +[[package]] +name = "siphasher" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7bd3e3206899af3f8b12af284fafc038cc1dc2b41d1b89dd17297221c5d225de" + [[package]] name = "sketches-ddsketch" version = "0.2.1" @@ -1894,6 +2041,22 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "stringprep" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db3737bde7edce97102e0e2b15365bf7a20bfdb5f60f4f9e8d7004258a51a8da" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "subtle" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" + [[package]] name = "syn" version = "1.0.109" @@ -2063,6 +2226,30 @@ dependencies = [ "tokio", ] +[[package]] +name = "tokio-postgres" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e89f6234aa8fd43779746012fcf53603cdb91fdd8399aa0de868c2d56b6dde1" +dependencies = [ + "async-trait", + "byteorder", + "bytes", + "fallible-iterator", + "futures-channel", + "futures-util", + "log", + "parking_lot", + "percent-encoding", + "phf", + "pin-project-lite", + "postgres-protocol", + "postgres-types", + "socket2 0.5.3", + "tokio", + "tokio-util", +] + [[package]] name = "tokio-uring" version = "0.4.0" @@ -2267,6 +2454,12 @@ version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed" +[[package]] +name = "typenum" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba" + [[package]] name = "unicase" version = "2.6.0" @@ -2314,6 +2507,12 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "urlencoding" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a1f0175e03a0973cf4afd476bef05c26e228520400eb1fd473ad417b1c00ffb" + [[package]] name = "utf8-cstr" version = "0.1.6" diff --git a/cave/Cargo.toml b/cave/Cargo.toml index 9e6a18f..e964824 100644 --- a/cave/Cargo.toml +++ b/cave/Cargo.toml @@ -19,3 +19,4 @@ reqwest = { version = "0.11", features = ["json", "deflate", "gzip", "stream"] } eventsource-stream = "0.2" tracing-subscriber = { version = "0.3", features = ["env-filter"] } inotify = "0.10" +tokio-postgres = "0.7" diff --git a/cave/src/db.rs b/cave/src/db.rs new file mode 100644 index 0000000..9f26b36 --- /dev/null +++ b/cave/src/db.rs @@ -0,0 +1,117 @@ +use std::sync::Arc; +use tokio_postgres::{Client, Error, NoTls, Statement}; + + +const CREATE_SCHEMA_COMMANDS: &[&str] = &[ + "CREATE TABLE IF NOT EXISTS instance_apps (host TEXT NOT NULL, client_id TEXT NOT NULL, client_secret TEXT NOT NULL, UNIQUE (host, client_id))", + "CREATE TABLE IF NOT EXISTS instance_tokens (host TEXT NOT NULL, client_id TEXT NOT NULL, token TEXT NOT NULL, created TIMESTAMP)", + "CREATE INDEX IF NOT EXISTS instance_tokens_host ON instance_tokens (host, created)", +]; + +#[derive(Clone)] +pub struct Database { + inner: Arc, +} + +struct DatabaseInner { + client: Client, + add_app: Statement, + get_apps: Statement, + delete_app: Statement, + add_token: Statement, + get_token: Statement, + delete_token: Statement, +} + +impl Database { + pub async fn connect(conn_str: &str) -> Self { + let (client, connection) = tokio_postgres::connect(conn_str, NoTls) + .await + .unwrap(); + + tokio::spawn(async move { + if let Err(e) = connection.await { + tracing::error!("postgresql: {}", e); + } + }); + + let add_app = client.prepare("INSERT INTO instance_apps (host, client_id, client_secret) VALUES ($1, $2, $3)") + .await + .unwrap(); + let get_apps = client.prepare("SELECT client_id, client_secret FROM instance_apps WHERE host=$1 LIMIT 32") + .await + .unwrap(); + let delete_app = client.prepare("DELETE FROM instance_apps WHERE host=$1 AND client_id=$2") + .await + .unwrap(); + let add_token = client.prepare("INSERT INTO instance_tokens (host, client_id, token, created) VALUES ($1, $2, $3, NOW())") + .await + .unwrap(); + let get_token = client.prepare("SELECT token FROM instance_tokens WHERE host=$1 ORDER BY created ASC LIMIT 1") + .await + .unwrap(); + let delete_token = client.prepare("DELETE FROM instance_tokens WHERE host=$1 AND token=$2") + .await + .unwrap(); + + Database { + inner: Arc::new(DatabaseInner { + client, + add_app, + get_apps, + delete_app, + add_token, + get_token, + delete_token, + }), + } + } + + pub async fn create_schema(&self) -> Result<(), Error> { + for command in CREATE_SCHEMA_COMMANDS { + self.inner.client.execute(*command, &[]) + .await?; + } + Ok(()) + } + + pub async fn add_app(&self, host: &str, client_id: &str, client_secret: &str) -> Result<(), Error> { + self.inner.client.execute(&self.inner.add_app, &[&host, &client_id, &client_secret]) + .await?; + Ok(()) + } + + pub async fn get_apps(&self, host: &str) -> Result, Error> { + let rows = self.inner.client.query(&self.inner.get_apps, &[&host]) + .await?; + Ok(rows.into_iter().filter_map(|row| { + let client_id = row.try_get(0).ok()?; + let client_token = row.try_get(1).ok()?; + Some((client_id, client_token)) + }).collect()) + } + + pub async fn delete_app(&self, host: &str, client_id: &str) -> Result<(), Error> { + self.inner.client.execute(&self.inner.delete_app, &[&host, &client_id]) + .await?; + Ok(()) + } + + pub async fn add_token(&self, host: &str, client_id: &str, token: &str) -> Result<(), Error> { + self.inner.client.execute(&self.inner.add_token, &[&host, &client_id, &token]) + .await?; + Ok(()) + } + + pub async fn get_token(&self, host: &str) -> Result, Error> { + let rows = self.inner.client.query(&self.inner.get_token, &[&host]) + .await?; + Ok(rows.first().and_then(|row| row.try_get(0).ok())) + } + + pub async fn delete_token(&self, host: &str, token: &str) -> Result<(), Error> { + self.inner.client.execute(&self.inner.delete_token, &[&host, &token]) + .await?; + Ok(()) + } +} diff --git a/cave/src/feed.rs b/cave/src/feed.rs index d0896f0..b6eae34 100644 --- a/cave/src/feed.rs +++ b/cave/src/feed.rs @@ -2,6 +2,7 @@ use std::{collections::{HashMap, HashSet}, time::Duration, ops::Deref}; use chrono::{DateTime, FixedOffset}; use futures::{Stream, StreamExt}; use eventsource_stream::Eventsource; +use reqwest::StatusCode; pub fn url_host(url: &str) -> Option { reqwest::Url::parse(url) @@ -237,19 +238,19 @@ impl Feed { Ok(Feed { posts }) } - pub async fn stream(client: &reqwest::Client, url: &str) -> Result, String> { + pub async fn stream(client: &reqwest::Client, url: &str) -> Result, StreamError> { let res = client.get(url) .timeout(Duration::MAX) .send() .await - .map_err(|e| format!("{}", e))?; + .map_err(StreamError::Http)?; if res.status() != 200 { - return Err(format!("HTTP {}", res.status())); + return Err(StreamError::HttpStatus(res.status())); } let ct = res.headers().get("content-type") .and_then(|c| c.to_str().ok()); if ct.map_or(true, |ct| ct != "text/event-stream") { - return Err(format!("Invalid Content-Type: {:?}", ct)); + return Err(StreamError::InvalidContentType(ct.unwrap_or("").to_owned())); } let src = res.bytes_stream().eventsource() @@ -267,3 +268,22 @@ impl Feed { Ok(src) } } + +pub enum StreamError { + HttpStatus(StatusCode), + Http(reqwest::Error), + InvalidContentType(String), +} + +impl std::fmt::Display for StreamError { + fn fmt(&self, fmt: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> { + match self { + StreamError::HttpStatus(code) => + write!(fmt, "HTTP/{}", code), + StreamError::Http(e) => + e.fmt(fmt), + StreamError::InvalidContentType(ct) => + write!(fmt, "Invalid Content-Type: {}", ct), + } + } +} diff --git a/cave/src/lib.rs b/cave/src/lib.rs index f5c45a8..b6140ef 100644 --- a/cave/src/lib.rs +++ b/cave/src/lib.rs @@ -7,6 +7,7 @@ pub mod trend_tag; pub mod firehose; pub mod live_file; pub mod word_list; +pub mod db; pub const PERIODS: &[u64] = &[4, 24, 7 * 24]; diff --git a/gatherer/Cargo.toml b/gatherer/Cargo.toml index ee4a8d4..1d1e3c9 100644 --- a/gatherer/Cargo.toml +++ b/gatherer/Cargo.toml @@ -21,3 +21,5 @@ askama = "0.11" metrics = "0.20" metrics-util = "0.14" metrics-exporter-prometheus = "0.11" +reqwest = "0.11" +urlencoding = "1" diff --git a/gatherer/config.yaml b/gatherer/config.yaml index 7ba5e01..9e5e968 100644 --- a/gatherer/config.yaml +++ b/gatherer/config.yaml @@ -1,3 +1,3 @@ redis: redis://10.233.12.2:6379/ - +database: host=127.0.0.1 port=5433 dbname=caveman username=caveman-gatherer password=c listen_port: 8000 diff --git a/gatherer/src/config.rs b/gatherer/src/config.rs index d57b322..df09a43 100644 --- a/gatherer/src/config.rs +++ b/gatherer/src/config.rs @@ -1,5 +1,6 @@ #[derive(Debug, serde::Deserialize)] pub struct Config { pub redis: String, + pub database: String, pub listen_port: u16, } diff --git a/gatherer/src/http_server.rs b/gatherer/src/http_server.rs index 89a19a5..2a0a27e 100644 --- a/gatherer/src/http_server.rs +++ b/gatherer/src/http_server.rs @@ -21,7 +21,7 @@ use futures::future::{join, join_all}; use cave::{ firehose::FirehoseFactory, store::{Store, TREND_POOL_SIZE}, PERIODS, - systemd, + systemd, db::Database, }; use http_body::combinators::UnsyncBoxBody; use metrics_exporter_prometheus::PrometheusHandle; @@ -30,12 +30,15 @@ use crate::{ trends::{TrendAnalyzer, TrendsResults}, }; +mod token_donate; +mod token_collect; type Languages = Vec; #[derive(Clone)] pub struct ServerState { store: Store, + db: Database, } impl ServerState { @@ -185,7 +188,6 @@ async fn streaming_api( .expect("Response") } - async fn print_request( req: Request, next: Next, @@ -206,6 +208,7 @@ async fn print_request( pub async fn start( listen_port: u16, store: Store, + db: Database, firehose_factory: FirehoseFactory, recorder: PrometheusHandle, ) { @@ -216,7 +219,10 @@ pub async fn start( .route("/", get(home)) .route("/in/:language", get(in_language)) .route("/api/v1/streaming/public", get(streaming_api)) - .layer(Extension(ServerState { store })) + .route("/token/donate", get(token_donate::get_token_donate).post(token_donate::post_token_donate)) + .route("/token/collect/:host", get(token_collect::get_token_collect)) + .route("/token/thanks", get(token_collect::get_token_thanks)) + .layer(Extension(ServerState { store, db })) .layer(Extension(firehose_factory)) .route("/metrics", get(|| async move { recorder.render().into_response() diff --git a/gatherer/src/http_server/token_collect.rs b/gatherer/src/http_server/token_collect.rs new file mode 100644 index 0000000..d1a73af --- /dev/null +++ b/gatherer/src/http_server/token_collect.rs @@ -0,0 +1,69 @@ +use axum::{ + Extension, + extract, + http::StatusCode, + response::{Html, IntoResponse}, +}; +use cave::db::Database; +use crate::{ + http_server::ServerState, + oauth, +}; + +async fn collect_token(db: Database, host: &str, code: String) -> Result<(), String> { + // try a few registered apps until one works + for (client_id, client_secret) in db.get_apps(&host).await + .map_err(|e| format!("{}", e))? + { + let app = oauth::Application { + client_id, + client_secret, + }; + match app.obtain_token(&host, code.clone()).await { + Ok(token) => { + db.add_token(&host, &app.client_id, &token).await + .expect("db.add_token"); + // success, done! + return Ok(()); + } + Err(e) => { + tracing::error!("obtain_token for {}: {}", host, e); + // app seems blocked, remove + let _ = db.delete_app(host, &app.client_id).await; + } + } + } + + Err(format!("No registered app found for instance {}", host)) +} + +#[derive(serde::Deserialize)] +pub struct OAuthCode { + code: String, +} + +pub async fn get_token_collect( + Extension(ServerState { db, .. }): Extension, + extract::Path(host): extract::Path, + extract::Query(OAuthCode { code }): extract::Query, +) -> impl IntoResponse { + match collect_token(db, &host, code.clone()).await { + Ok(()) => + ( + StatusCode::SEE_OTHER, + [("location", "/token/thanks")] + ).into_response(), + Err(e) => { + tracing::error!("{}", e); + return ( + StatusCode::INTERNAL_SERVER_ERROR, + [("content-type", "text/plain")], + e + ).into_response(); + } + } +} + +pub async fn get_token_thanks() -> impl IntoResponse { + Html(&include_bytes!("../../templates/token_thanks.html")[..]) +} diff --git a/gatherer/src/http_server/token_donate.rs b/gatherer/src/http_server/token_donate.rs new file mode 100644 index 0000000..8582d4e --- /dev/null +++ b/gatherer/src/http_server/token_donate.rs @@ -0,0 +1,74 @@ +use axum::{ + Extension, + extract, + http::StatusCode, + response::{Html, IntoResponse}, +}; +use crate::{ + http_server::ServerState, + oauth, +}; + +pub async fn get_token_donate() -> impl IntoResponse { + Html(&include_bytes!("../../templates/token_donate.html")[..]) +} + +#[derive(serde::Deserialize, Debug)] +//#[allow(dead_code)] +pub struct TokenDonateForm { + instance: String, +} + +pub enum PostTokenDonateResult { + OauthRedirect(String), + Error(String), +} + +impl IntoResponse for PostTokenDonateResult { + fn into_response(self) -> axum::response::Response { + match self { + PostTokenDonateResult::OauthRedirect(url) => ( + StatusCode::SEE_OTHER, + [("location", url)] + ).into_response(), + + PostTokenDonateResult::Error(e) => ( + StatusCode::INTERNAL_SERVER_ERROR, + [("content-type", "text/plain")], + e + ).into_response(), + } + } +} + +pub async fn post_token_donate( + Extension(ServerState { db, .. }): Extension, + extract::Form(form): extract::Form, +) -> PostTokenDonateResult { + let apps = db.get_apps(&form.instance).await + .expect("db.get_apps"); + + let app; + if let Some((client_id, client_secret)) = apps.into_iter().next() { + // existing app + app = oauth::Application { + client_id, + client_secret, + }; + } else { + // register a new app with this instance + app = match oauth::Application::register(&form.instance).await { + Ok(app) => app, + Err(e) => { + tracing::error!("Canont register OAuth app: {}", e); + return PostTokenDonateResult::Error(format!("{}", e)); + } + }; + db.add_app(&form.instance, &app.client_id, &app.client_secret).await + .expect("db.add_app"); + tracing::info!("Added app for {}: {}", form.instance, app.client_id); + }; + + let url = app.generate_auth_url(&form.instance); + PostTokenDonateResult::OauthRedirect(url) +} diff --git a/gatherer/src/main.rs b/gatherer/src/main.rs index 288cf19..63dd688 100644 --- a/gatherer/src/main.rs +++ b/gatherer/src/main.rs @@ -11,6 +11,7 @@ mod config; mod trends; mod html_template; mod http_server; +mod oauth; #[tokio::main] async fn main() { @@ -25,6 +26,8 @@ async fn main() { .install_recorder() .unwrap(); + cave::systemd::status("Connecting to database"); + let db = cave::db::Database::connect(&config.database).await; cave::systemd::status("Starting redis client"); let store = cave::store::Store::new(8, config.redis.clone()).await; @@ -33,6 +36,7 @@ async fn main() { let http = http_server::start( config.listen_port, store, + db, firehose_factory, recorder, ); diff --git a/gatherer/src/oauth.rs b/gatherer/src/oauth.rs new file mode 100644 index 0000000..2ba33d3 --- /dev/null +++ b/gatherer/src/oauth.rs @@ -0,0 +1,85 @@ +#[derive(serde::Serialize)] +struct AppRegistration { + client_name: String, + redirect_uris: String, + scopes: String, + website: String, +} + +#[derive(serde::Serialize)] +struct TokenRequest { + grant_type: String, + scope: String, + client_id: String, + client_secret: String, + redirect_uri: String, + code: String, +} + + +#[derive(serde::Deserialize)] +struct TokenResult { + pub access_token: String, +} + +#[derive(serde::Deserialize)] +pub struct Application { + pub client_id: String, + pub client_secret: String, +} + +const SCOPES: &str = "read:statuses"; + +impl Application { + pub async fn register(host: &str) -> Result { + let url = format!("https://{}/api/v1/apps", host); + let form = AppRegistration { + client_name: "#FediBuzz".to_string(), + website: "https://fedi.buzz/".to_string(), + redirect_uris: Self::generate_redirect_url(host), + scopes: SCOPES.to_string(), + }; + let client = reqwest::Client::new(); + let res = client.post(url) + .form(&form) + .send() + .await? + .json() + .await?; + Ok(res) + } + + pub fn generate_redirect_url(host: &str) -> String { + format!("https://fedi.buzz/token/collect/{}", host) + } + + pub fn generate_auth_url(&self, host: &str) -> String { + format!( + "https://{}/oauth/authorize?client_id={}&scope={}&response_type=code&redirect_uri={}", + host, + urlencoding::encode(&self.client_id), + urlencoding::encode(SCOPES), + urlencoding::encode(&Self::generate_redirect_url(host)), + ) + } + + pub async fn obtain_token(&self, host: &str, code: String) -> Result { + let url = format!("https://{}/oauth/token", host); + let form = TokenRequest { + grant_type: "authorization_code".to_string(), + scope: SCOPES.to_string(), + client_id: self.client_id.clone(), + client_secret: self.client_secret.clone(), + redirect_uri: Self::generate_redirect_url(host), + code, + }; + let client = reqwest::Client::new(); + let res: TokenResult = client.post(url) + .form(&form) + .send() + .await? + .json() + .await?; + Ok(res.access_token) + } +} diff --git a/gatherer/templates/token_donate.html b/gatherer/templates/token_donate.html new file mode 100644 index 0000000..4db144e --- /dev/null +++ b/gatherer/templates/token_donate.html @@ -0,0 +1,49 @@ + + + + + #FediBuzz: Donate a token + + + + + + +
+

Fedi­Buzz

+

Donate an API token

+
+ +
+
+
+
+ What is your home instance? + + +
+
+ +

Reason

+

+ A token lets us access the public timeline streaming API of your instance so that we can include it in our sampling of the Fediverse. +

+ +

Background

+

+ Mastodon is breaking unauthenticated access in future versions. +

+
+ + + + + + diff --git a/gatherer/templates/token_thanks.html b/gatherer/templates/token_thanks.html new file mode 100644 index 0000000..200db12 --- /dev/null +++ b/gatherer/templates/token_thanks.html @@ -0,0 +1,39 @@ + + + + + #FediBuzz: Donate a token + + + + + + +
+

Thank you!

+

You donated an API token.

+
+ +
+
+
+
+ Donate a token for another instance? + + +
+
+
+
+ + + + + diff --git a/gatherer/templates/trends.html b/gatherer/templates/trends.html index bde1a33..b30c83b 100644 --- a/gatherer/templates/trends.html +++ b/gatherer/templates/trends.html @@ -20,6 +20,10 @@

Fedi­Buzz

Trends in the Fedi­verse

+