begin collecting tokens

This commit is contained in:
Astro 2023-08-08 18:42:34 +02:00
parent 5839a02e55
commit aa1d0f1009
22 changed files with 727 additions and 13 deletions

199
Cargo.lock generated
View File

@ -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"

View File

@ -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"

117
cave/src/db.rs Normal file
View File

@ -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<DatabaseInner>,
}
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<Vec<(String, String)>, 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<Option<String>, 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(())
}
}

View File

@ -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<String> {
reqwest::Url::parse(url)
@ -237,19 +238,19 @@ impl Feed {
Ok(Feed { posts })
}
pub async fn stream(client: &reqwest::Client, url: &str) -> Result<impl Stream<Item = EncodablePost>, String> {
pub async fn stream(client: &reqwest::Client, url: &str) -> Result<impl Stream<Item = EncodablePost>, 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),
}
}
}

View File

@ -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];

View File

@ -21,3 +21,5 @@ askama = "0.11"
metrics = "0.20"
metrics-util = "0.14"
metrics-exporter-prometheus = "0.11"
reqwest = "0.11"
urlencoding = "1"

View File

@ -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

View File

@ -1,5 +1,6 @@
#[derive(Debug, serde::Deserialize)]
pub struct Config {
pub redis: String,
pub database: String,
pub listen_port: u16,
}

View File

@ -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<String>;
#[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<Body>,
next: Next<Body>,
@ -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()

View File

@ -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<ServerState>,
extract::Path(host): extract::Path<String>,
extract::Query(OAuthCode { code }): extract::Query<OAuthCode>,
) -> 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")[..])
}

View File

@ -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<ServerState>,
extract::Form(form): extract::Form<TokenDonateForm>,
) -> 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)
}

View File

@ -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,
);

85
gatherer/src/oauth.rs Normal file
View File

@ -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<Self, reqwest::Error> {
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<String, reqwest::Error> {
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)
}
}

View File

@ -0,0 +1,49 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<title>#FediBuzz: Donate a token</title>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<link rel="icon" type="image/png" sizes="16x16" href="/assets/favicon16.png"/>
<link rel="icon" type="image/png" sizes="48x48" href="/assets/favicon48.png"/>
<link rel="stylesheet" title="Default" type="text/css" href="/assets/style.css"/>
</head>
<body>
<header>
<h1>Fedi&#173;Buzz</h1>
<p>Donate an API token</p>
</header>
<main>
<div>
<form method="post">
<fieldset>
<legend>What is your home instance?</legend>
<input name="instance" size="40">
<input type="submit" value="Ok">
</fieldset>
</form>
<h2>Reason</h2>
<p>
A token lets us access <a href="https://docs.joinmastodon.org/methods/streaming/#public">the public timeline streaming API</a> of your instance so that we can include it in our sampling of the Fediverse.
</p>
<h2>Background</h2>
<p>
Mastodon is <a href="https://github.com/mastodon/mastodon/pull/23989#issuecomment-1628961709">breaking unauthenticated access</a> in future versions.
</p>
</main>
<footer>
<p>
run by <a rel="me" href="https://c3d2.social/@astro">@astro&#173;@c3d2.social</a>
</p>
<p>
Get live ActivityPub content with the <a rel="me" href="https://relay.fedi.buzz">#FediBuzz relay</a>
</p>
</footer>
</div>
</body>
</html>

View File

@ -0,0 +1,39 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<title>#FediBuzz: Donate a token</title>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<link rel="icon" type="image/png" sizes="16x16" href="/assets/favicon16.png"/>
<link rel="icon" type="image/png" sizes="48x48" href="/assets/favicon48.png"/>
<link rel="stylesheet" title="Default" type="text/css" href="/assets/style.css"/>
</head>
<body>
<header>
<h1>Thank you!</h1>
<p>You donated an API token.</p>
</header>
<main>
<div>
<form method="post">
<fieldset>
<legend>Donate a token for another instance?</legend>
<input name="instance" size="40">
<input type="submit" value="Ok">
</fieldset>
</form>
</div>
</main>
<footer>
<p>
run by <a rel="me" href="https://c3d2.social/@astro">@astro&#173;@c3d2.social</a>
</p>
<p>
Get live ActivityPub content with the <a rel="me" href="https://relay.fedi.buzz">#FediBuzz relay</a>
</p>
</footer>
</body>
</html>

View File

@ -20,6 +20,10 @@
<header>
<h1>Fedi&#173;Buzz</h1>
<p>Trends in the Fedi&#173;verse</p>
<!--p class="plea">
#FediBuzz needs your help!
<a href="/token/donate">Donate a token...</a>
</p-->
</header>
<nav>

View File

@ -21,6 +21,7 @@ metrics = "0.20"
metrics-util = "0.14"
metrics-exporter-prometheus = "0.11"
serde_json = "1"
urlencoding = "1"
[target.'cfg(not(target_env = "msvc"))'.dependencies]
jemallocator = "0.5"

View File

@ -1,4 +1,5 @@
redis: redis://10.233.12.2:6379/
database: host=127.0.0.1 port=5433 dbname=caveman username=caveman-gatherer password=c
hosts:
- chaos.social

View File

@ -1,6 +1,7 @@
#[derive(Debug, serde::Deserialize)]
pub struct Config {
pub redis: String,
pub database: String,
pub hosts: Vec<String>,
pub max_workers: usize,
pub prometheus_port: u16,

View File

@ -41,6 +41,7 @@ async fn run() {
.install()
.unwrap();
let db = cave::db::Database::connect(&config.database).await;
let mut store = cave::store::Store::new(16, config.redis).await;
let posts_cache = posts_cache::PostsCache::new(65536);
@ -128,6 +129,7 @@ async fn run() {
.spawn(worker::run(
message_tx.clone(),
store.clone(),
db.clone(),
posts_cache.clone(),
client.clone(),
host

View File

@ -2,12 +2,13 @@ use std::collections::HashSet;
use std::future::Future;
use std::sync::Arc;
use std::time::{Duration, Instant};
use cave::feed::Post;
use cave::{
feed::{Feed, EncodablePost},
db::Database,
feed::{Feed, EncodablePost, Post, StreamError},
store::Store,
};
use futures::{StreamExt, future};
use reqwest::StatusCode;
use crate::posts_cache::PostsCache;
use crate::scheduler::{Host, InstanceHost};
use crate::webfinger;
@ -72,6 +73,7 @@ pub enum Message {
pub async fn run(
message_tx: tokio::sync::mpsc::UnboundedSender<Message>,
store: Store,
db: Database,
posts_cache: PostsCache,
client: reqwest::Client,
host: InstanceHost,
@ -87,7 +89,7 @@ pub async fn run(
&client, robots_txt.clone(), &host.host
),
open_stream(
message_tx.clone(), store.clone(),
message_tx.clone(), store.clone(), db.clone(),
&posts_cache,
&client, robots_txt, host.host.clone()
),
@ -274,6 +276,7 @@ async fn process_posts(
async fn open_stream(
message_tx: tokio::sync::mpsc::UnboundedSender<Message>,
store: Store,
db: Database,
posts_cache: &PostsCache,
client: &reqwest::Client,
robots_txt: RobotsTxt,
@ -288,9 +291,32 @@ async fn open_stream(
let posts_cache = posts_cache.clone();
metrics::increment_gauge!("hunter_requests", 1.0, "type" => "stream_open");
let stream = Feed::stream(client, &url).await;
let mut stream = Feed::stream(client, &url).await;
metrics::decrement_gauge!("hunter_requests", 1.0, "type" => "stream_open");
let mut prev_token: Option<String> = None;
while let Err(StreamError::HttpStatus(StatusCode::UNAUTHORIZED)) = &stream {
if let Some(invalid_token) = prev_token {
// If we tried with a token before but it's Unauthorized, delete it.
let _ = db.delete_token(&host, &invalid_token).await;
}
let token = db.get_token(&host).await
.expect("db.get_token()");
if let Some(token) = &token {
let url = format!("https://{}/api/v1/streaming/public?access_token={}", host, urlencoding::encode(token));
stream = Feed::stream(client, &url).await;
} else {
tracing::info!("No working token for {}", host);
break;
}
prev_token = token;
}
if let Err(e) = &stream {
tracing::error!("Error opening stream to {}: {}", host, e);
}
let stream = stream.map_err(|e| {
format!("Stream error for {}: {}", host, e)
})?;

View File

@ -8,7 +8,8 @@ let
hunterDefaultSettings = {
redis = "redis://127.0.0.1:${toString cfg.redis.port}/";
hosts = [ "mastodon.social" "fosstodon.org" "chaos.social" "dresden.network" ];
database = "host=/var/run/postgresql user=caveman-hunter dbname=caveman";
hosts = [ "mastodon.social" ];
max_workers = 16;
prometheus_port = 9101;
blocklist = blocklistPath;
@ -33,6 +34,7 @@ let
gathererDefaultSettings = {
redis = "redis://127.0.0.1:${toString cfg.redis.port}/";
database = "host=/var/run/postgresql user=caveman-gatherer dbname=caveman";
listen_port = 8000;
};
@ -144,6 +146,16 @@ in
maxmemory-policy = "allkeys-lru";
};
};
services.postgresql = {
enable = true;
ensureDatabases = [ "caveman" ];
ensureUsers = [ {
name = databaseUser;
ensurePermissions = {
"DATABASE caveman" = "ALL PRIVILEGES";
};
} ];
};
systemd.services.caveman-hunter = lib.mkIf cfg.hunter.enable {
wantedBy = [ "multi-user.target" ];