Serve the feed, according to all the Atproto endpoints
This commit is contained in:
		
							parent
							
								
									c2899951f6
								
							
						
					
					
						commit
						b4250e12cd
					
				| 
						 | 
					@ -1,2 +1,3 @@
 | 
				
			||||||
CHAT_GPT_API_KEY="fake-chat-gpt-key"
 | 
					CHAT_GPT_API_KEY="fake-chat-gpt-key"
 | 
				
			||||||
DATABASE_URL="postgres://postgres:password@localhost/nederlandskie"
 | 
					DATABASE_URL="postgres://postgres:password@localhost/nederlandskie"
 | 
				
			||||||
 | 
					HOSTNAME="..."
 | 
				
			||||||
| 
						 | 
					@ -132,6 +132,55 @@ version = "1.1.0"
 | 
				
			||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
					source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
				
			||||||
checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
 | 
					checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[[package]]
 | 
				
			||||||
 | 
					name = "axum"
 | 
				
			||||||
 | 
					version = "0.6.20"
 | 
				
			||||||
 | 
					source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
				
			||||||
 | 
					checksum = "3b829e4e32b91e643de6eafe82b1d90675f5874230191a4ffbc1b336dec4d6bf"
 | 
				
			||||||
 | 
					dependencies = [
 | 
				
			||||||
 | 
					 "async-trait",
 | 
				
			||||||
 | 
					 "axum-core",
 | 
				
			||||||
 | 
					 "bitflags 1.3.2",
 | 
				
			||||||
 | 
					 "bytes",
 | 
				
			||||||
 | 
					 "futures-util",
 | 
				
			||||||
 | 
					 "http",
 | 
				
			||||||
 | 
					 "http-body",
 | 
				
			||||||
 | 
					 "hyper",
 | 
				
			||||||
 | 
					 "itoa",
 | 
				
			||||||
 | 
					 "matchit",
 | 
				
			||||||
 | 
					 "memchr",
 | 
				
			||||||
 | 
					 "mime",
 | 
				
			||||||
 | 
					 "percent-encoding",
 | 
				
			||||||
 | 
					 "pin-project-lite",
 | 
				
			||||||
 | 
					 "rustversion",
 | 
				
			||||||
 | 
					 "serde",
 | 
				
			||||||
 | 
					 "serde_json",
 | 
				
			||||||
 | 
					 "serde_path_to_error",
 | 
				
			||||||
 | 
					 "serde_urlencoded",
 | 
				
			||||||
 | 
					 "sync_wrapper",
 | 
				
			||||||
 | 
					 "tokio",
 | 
				
			||||||
 | 
					 "tower",
 | 
				
			||||||
 | 
					 "tower-layer",
 | 
				
			||||||
 | 
					 "tower-service",
 | 
				
			||||||
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[[package]]
 | 
				
			||||||
 | 
					name = "axum-core"
 | 
				
			||||||
 | 
					version = "0.3.4"
 | 
				
			||||||
 | 
					source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
				
			||||||
 | 
					checksum = "759fa577a247914fd3f7f76d62972792636412fbfd634cd452f6a385a74d2d2c"
 | 
				
			||||||
 | 
					dependencies = [
 | 
				
			||||||
 | 
					 "async-trait",
 | 
				
			||||||
 | 
					 "bytes",
 | 
				
			||||||
 | 
					 "futures-util",
 | 
				
			||||||
 | 
					 "http",
 | 
				
			||||||
 | 
					 "http-body",
 | 
				
			||||||
 | 
					 "mime",
 | 
				
			||||||
 | 
					 "rustversion",
 | 
				
			||||||
 | 
					 "tower-layer",
 | 
				
			||||||
 | 
					 "tower-service",
 | 
				
			||||||
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[[package]]
 | 
					[[package]]
 | 
				
			||||||
name = "backtrace"
 | 
					name = "backtrace"
 | 
				
			||||||
version = "0.3.69"
 | 
					version = "0.3.69"
 | 
				
			||||||
| 
						 | 
					@ -1125,6 +1174,12 @@ version = "0.4.20"
 | 
				
			||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
					source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
				
			||||||
checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f"
 | 
					checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[[package]]
 | 
				
			||||||
 | 
					name = "matchit"
 | 
				
			||||||
 | 
					version = "0.7.2"
 | 
				
			||||||
 | 
					source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
				
			||||||
 | 
					checksum = "ed1202b2a6f884ae56f04cff409ab315c5ce26b5e58d7412e484f01fd52f52ef"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[[package]]
 | 
					[[package]]
 | 
				
			||||||
name = "md-5"
 | 
					name = "md-5"
 | 
				
			||||||
version = "0.10.5"
 | 
					version = "0.10.5"
 | 
				
			||||||
| 
						 | 
					@ -1242,6 +1297,7 @@ dependencies = [
 | 
				
			||||||
 "async-trait",
 | 
					 "async-trait",
 | 
				
			||||||
 "atrium-api",
 | 
					 "atrium-api",
 | 
				
			||||||
 "atrium-xrpc",
 | 
					 "atrium-xrpc",
 | 
				
			||||||
 | 
					 "axum",
 | 
				
			||||||
 "chat-gpt-lib-rs",
 | 
					 "chat-gpt-lib-rs",
 | 
				
			||||||
 "chrono",
 | 
					 "chrono",
 | 
				
			||||||
 "ciborium",
 | 
					 "ciborium",
 | 
				
			||||||
| 
						 | 
					@ -1250,6 +1306,7 @@ dependencies = [
 | 
				
			||||||
 "libipld-core",
 | 
					 "libipld-core",
 | 
				
			||||||
 "rs-car",
 | 
					 "rs-car",
 | 
				
			||||||
 "scooby",
 | 
					 "scooby",
 | 
				
			||||||
 | 
					 "serde",
 | 
				
			||||||
 "serde_ipld_dagcbor",
 | 
					 "serde_ipld_dagcbor",
 | 
				
			||||||
 "sqlx",
 | 
					 "sqlx",
 | 
				
			||||||
 "tokio",
 | 
					 "tokio",
 | 
				
			||||||
| 
						 | 
					@ -1702,6 +1759,12 @@ dependencies = [
 | 
				
			||||||
 "windows-sys",
 | 
					 "windows-sys",
 | 
				
			||||||
]
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[[package]]
 | 
				
			||||||
 | 
					name = "rustversion"
 | 
				
			||||||
 | 
					version = "1.0.14"
 | 
				
			||||||
 | 
					source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
				
			||||||
 | 
					checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[[package]]
 | 
					[[package]]
 | 
				
			||||||
name = "ryu"
 | 
					name = "ryu"
 | 
				
			||||||
version = "1.0.15"
 | 
					version = "1.0.15"
 | 
				
			||||||
| 
						 | 
					@ -1813,6 +1876,16 @@ dependencies = [
 | 
				
			||||||
 "serde",
 | 
					 "serde",
 | 
				
			||||||
]
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[[package]]
 | 
				
			||||||
 | 
					name = "serde_path_to_error"
 | 
				
			||||||
 | 
					version = "0.1.14"
 | 
				
			||||||
 | 
					source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
				
			||||||
 | 
					checksum = "4beec8bce849d58d06238cb50db2e1c417cfeafa4c63f692b15c82b7c80f8335"
 | 
				
			||||||
 | 
					dependencies = [
 | 
				
			||||||
 | 
					 "itoa",
 | 
				
			||||||
 | 
					 "serde",
 | 
				
			||||||
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[[package]]
 | 
					[[package]]
 | 
				
			||||||
name = "serde_qs"
 | 
					name = "serde_qs"
 | 
				
			||||||
version = "0.12.0"
 | 
					version = "0.12.0"
 | 
				
			||||||
| 
						 | 
					@ -2196,6 +2269,12 @@ dependencies = [
 | 
				
			||||||
 "unicode-ident",
 | 
					 "unicode-ident",
 | 
				
			||||||
]
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[[package]]
 | 
				
			||||||
 | 
					name = "sync_wrapper"
 | 
				
			||||||
 | 
					version = "0.1.2"
 | 
				
			||||||
 | 
					source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
				
			||||||
 | 
					checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[[package]]
 | 
					[[package]]
 | 
				
			||||||
name = "synstructure"
 | 
					name = "synstructure"
 | 
				
			||||||
version = "0.12.6"
 | 
					version = "0.12.6"
 | 
				
			||||||
| 
						 | 
					@ -2364,6 +2443,28 @@ dependencies = [
 | 
				
			||||||
 "serde",
 | 
					 "serde",
 | 
				
			||||||
]
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[[package]]
 | 
				
			||||||
 | 
					name = "tower"
 | 
				
			||||||
 | 
					version = "0.4.13"
 | 
				
			||||||
 | 
					source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
				
			||||||
 | 
					checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c"
 | 
				
			||||||
 | 
					dependencies = [
 | 
				
			||||||
 | 
					 "futures-core",
 | 
				
			||||||
 | 
					 "futures-util",
 | 
				
			||||||
 | 
					 "pin-project",
 | 
				
			||||||
 | 
					 "pin-project-lite",
 | 
				
			||||||
 | 
					 "tokio",
 | 
				
			||||||
 | 
					 "tower-layer",
 | 
				
			||||||
 | 
					 "tower-service",
 | 
				
			||||||
 | 
					 "tracing",
 | 
				
			||||||
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[[package]]
 | 
				
			||||||
 | 
					name = "tower-layer"
 | 
				
			||||||
 | 
					version = "0.3.2"
 | 
				
			||||||
 | 
					source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
				
			||||||
 | 
					checksum = "c20c8dbed6283a09604c3e69b4b7eeb54e298b8a600d4d5ecb5ad39de609f1d0"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[[package]]
 | 
					[[package]]
 | 
				
			||||||
name = "tower-service"
 | 
					name = "tower-service"
 | 
				
			||||||
version = "0.3.2"
 | 
					version = "0.3.2"
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -10,6 +10,7 @@ anyhow = "1.0.75"
 | 
				
			||||||
async-trait = "0.1.73"
 | 
					async-trait = "0.1.73"
 | 
				
			||||||
atrium-api = "0.6.0"
 | 
					atrium-api = "0.6.0"
 | 
				
			||||||
atrium-xrpc = "0.4.0"
 | 
					atrium-xrpc = "0.4.0"
 | 
				
			||||||
 | 
					axum = "0.6.20"
 | 
				
			||||||
chat-gpt-lib-rs = "0.2.1"
 | 
					chat-gpt-lib-rs = "0.2.1"
 | 
				
			||||||
chrono = "0.4.29"
 | 
					chrono = "0.4.29"
 | 
				
			||||||
ciborium = "0.2.1"
 | 
					ciborium = "0.2.1"
 | 
				
			||||||
| 
						 | 
					@ -18,6 +19,7 @@ futures = "0.3.28"
 | 
				
			||||||
libipld-core = { version = "0.16.0", features = ["serde-codec"] }
 | 
					libipld-core = { version = "0.16.0", features = ["serde-codec"] }
 | 
				
			||||||
rs-car = "0.4.1"
 | 
					rs-car = "0.4.1"
 | 
				
			||||||
scooby = "0.5.0"
 | 
					scooby = "0.5.0"
 | 
				
			||||||
 | 
					serde = "1.0.188"
 | 
				
			||||||
serde_ipld_dagcbor = "0.4.1"
 | 
					serde_ipld_dagcbor = "0.4.1"
 | 
				
			||||||
sqlx = { version = "0.7.1", default-features = false, features = ["postgres", "runtime-tokio-native-tls", "chrono"] }
 | 
					sqlx = { version = "0.7.1", default-features = false, features = ["postgres", "runtime-tokio-native-tls", "chrono"] }
 | 
				
			||||||
tokio = { version = "1.32.0", features = ["full"] }
 | 
					tokio = { version = "1.32.0", features = ["full"] }
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -21,6 +21,7 @@ Copy `.env.example` into `.env` and set up the environment variables within:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
- `CHAT_GPT_API_KEY` for your ChatGPT key
 | 
					- `CHAT_GPT_API_KEY` for your ChatGPT key
 | 
				
			||||||
- `DATABASE_URL` for PostgreSQL credentials
 | 
					- `DATABASE_URL` for PostgreSQL credentials
 | 
				
			||||||
 | 
					- `HOSTNAME` to the hostname of where you intend to host the feed
 | 
				
			||||||
 | 
					
 | 
				
			||||||
## Running
 | 
					## Running
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,26 @@
 | 
				
			||||||
 | 
					use anyhow::Result;
 | 
				
			||||||
 | 
					use dotenv::dotenv;
 | 
				
			||||||
 | 
					use std::env;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[derive(Clone)]
 | 
				
			||||||
 | 
					pub struct Config {
 | 
				
			||||||
 | 
					    pub chat_gpt_api_key: String,
 | 
				
			||||||
 | 
					    pub database_url: String,
 | 
				
			||||||
 | 
					    pub service_did: String,
 | 
				
			||||||
 | 
					    pub publisher_did: String,
 | 
				
			||||||
 | 
					    pub hostname: String,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl Config {
 | 
				
			||||||
 | 
					    pub fn load() -> Result<Self> {
 | 
				
			||||||
 | 
					        dotenv()?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Ok(Self {
 | 
				
			||||||
 | 
					            chat_gpt_api_key: env::var("CHAT_GPT_API_KEY")?,
 | 
				
			||||||
 | 
					            database_url: env::var("DATABASE_URL")?,
 | 
				
			||||||
 | 
					            hostname: env::var("HOSTNAME")?,
 | 
				
			||||||
 | 
					            service_did: format!("did:web:{}", env::var("HOSTNAME")?),
 | 
				
			||||||
 | 
					            publisher_did: "".to_owned(), // TODO
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										29
									
								
								src/main.rs
								
								
								
								
							
							
						
						
									
										29
									
								
								src/main.rs
								
								
								
								
							| 
						 | 
					@ -1,33 +1,17 @@
 | 
				
			||||||
 | 
					mod config;
 | 
				
			||||||
mod processes;
 | 
					mod processes;
 | 
				
			||||||
mod services;
 | 
					mod services;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
use std::env;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
use anyhow::Result;
 | 
					use anyhow::Result;
 | 
				
			||||||
use dotenv::dotenv;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use crate::config::Config;
 | 
				
			||||||
 | 
					use crate::processes::feed_server::FeedServer;
 | 
				
			||||||
use crate::processes::post_saver::PostSaver;
 | 
					use crate::processes::post_saver::PostSaver;
 | 
				
			||||||
use crate::processes::profile_classifier::ProfileClassifier;
 | 
					use crate::processes::profile_classifier::ProfileClassifier;
 | 
				
			||||||
use crate::services::ai::AI;
 | 
					use crate::services::ai::AI;
 | 
				
			||||||
use crate::services::bluesky::Bluesky;
 | 
					use crate::services::bluesky::Bluesky;
 | 
				
			||||||
use crate::services::database::Database;
 | 
					use crate::services::database::Database;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
struct Config {
 | 
					 | 
				
			||||||
    chat_gpt_api_key: String,
 | 
					 | 
				
			||||||
    database_url: String,
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
impl Config {
 | 
					 | 
				
			||||||
    fn load() -> Result<Self> {
 | 
					 | 
				
			||||||
        dotenv()?;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        Ok(Self {
 | 
					 | 
				
			||||||
            chat_gpt_api_key: env::var("CHAT_GPT_API_KEY")?,
 | 
					 | 
				
			||||||
            database_url: env::var("DATABASE_URL")?,
 | 
					 | 
				
			||||||
        })
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#[tokio::main]
 | 
					#[tokio::main]
 | 
				
			||||||
async fn main() -> Result<()> {
 | 
					async fn main() -> Result<()> {
 | 
				
			||||||
    let config = Config::load()?;
 | 
					    let config = Config::load()?;
 | 
				
			||||||
| 
						 | 
					@ -38,8 +22,13 @@ async fn main() -> Result<()> {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    let post_saver = PostSaver::new(&database, &bluesky);
 | 
					    let post_saver = PostSaver::new(&database, &bluesky);
 | 
				
			||||||
    let profile_classifier = ProfileClassifier::new(&database, &ai, &bluesky);
 | 
					    let profile_classifier = ProfileClassifier::new(&database, &ai, &bluesky);
 | 
				
			||||||
 | 
					    let feed_server = FeedServer::new(&database, &config);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    tokio::try_join!(post_saver.start(), profile_classifier.start())?;
 | 
					    tokio::try_join!(
 | 
				
			||||||
 | 
					        post_saver.start(),
 | 
				
			||||||
 | 
					        profile_classifier.start(),
 | 
				
			||||||
 | 
					        feed_server.serve(),
 | 
				
			||||||
 | 
					    )?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    Ok(())
 | 
					    Ok(())
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,5 @@
 | 
				
			||||||
 | 
					mod endpoints;
 | 
				
			||||||
 | 
					mod server;
 | 
				
			||||||
 | 
					mod state;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub use server::FeedServer;
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,9 @@
 | 
				
			||||||
 | 
					mod describe_feed_generator;
 | 
				
			||||||
 | 
					mod did_json;
 | 
				
			||||||
 | 
					mod get_feed_skeleton;
 | 
				
			||||||
 | 
					mod root;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub use describe_feed_generator::describe_feed_generator;
 | 
				
			||||||
 | 
					pub use did_json::did_json;
 | 
				
			||||||
 | 
					pub use get_feed_skeleton::get_feed_skeleton;
 | 
				
			||||||
 | 
					pub use root::root;
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,18 @@
 | 
				
			||||||
 | 
					use atrium_api::app::bsky::feed::describe_feed_generator::{
 | 
				
			||||||
 | 
					    Feed, Output as FeedGeneratorDescription,
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					use axum::{extract::State, Json};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use crate::processes::feed_server::state::FeedServerState;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub async fn describe_feed_generator(
 | 
				
			||||||
 | 
					    State(state): State<FeedServerState>,
 | 
				
			||||||
 | 
					) -> Json<FeedGeneratorDescription> {
 | 
				
			||||||
 | 
					    Json(FeedGeneratorDescription {
 | 
				
			||||||
 | 
					        did: state.config.service_did.clone(),
 | 
				
			||||||
 | 
					        feeds: vec![Feed {
 | 
				
			||||||
 | 
					            uri: format!("at://{}/app.bsky.feed.generator/{}", state.config.publisher_did, "nederlandskie"),
 | 
				
			||||||
 | 
					        }],
 | 
				
			||||||
 | 
					        links: None,
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,32 @@
 | 
				
			||||||
 | 
					use axum::{extract::State, Json};
 | 
				
			||||||
 | 
					use serde::Serialize;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use crate::processes::feed_server::state::FeedServerState;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[derive(Serialize)]
 | 
				
			||||||
 | 
					pub struct Did {
 | 
				
			||||||
 | 
					    #[serde(rename = "@context")]
 | 
				
			||||||
 | 
					    context: Vec<String>,
 | 
				
			||||||
 | 
					    id: String,
 | 
				
			||||||
 | 
					    service: Vec<Service>,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[derive(Serialize)]
 | 
				
			||||||
 | 
					pub struct Service {
 | 
				
			||||||
 | 
					    id: String,
 | 
				
			||||||
 | 
					    #[serde(rename = "type")]
 | 
				
			||||||
 | 
					    type_: String,
 | 
				
			||||||
 | 
					    service_endpoint: String,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub async fn did_json(State(state): State<FeedServerState>) -> Json<Did> {
 | 
				
			||||||
 | 
					    Json(Did {
 | 
				
			||||||
 | 
					        context: vec!["https://www.w3.org/ns/did/v1".to_owned()],
 | 
				
			||||||
 | 
					        id: state.config.service_did.clone(),
 | 
				
			||||||
 | 
					        service: vec![Service {
 | 
				
			||||||
 | 
					            id: "#bsky_fg".to_owned(),
 | 
				
			||||||
 | 
					            type_: "BskyFeedGenerator".to_owned(),
 | 
				
			||||||
 | 
					            service_endpoint: format!("https://{}", state.config.hostname),
 | 
				
			||||||
 | 
					        }],
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,61 @@
 | 
				
			||||||
 | 
					use anyhow::{anyhow, Result};
 | 
				
			||||||
 | 
					use atrium_api::app::bsky::feed::defs::SkeletonFeedPost;
 | 
				
			||||||
 | 
					use atrium_api::app::bsky::feed::get_feed_skeleton::{
 | 
				
			||||||
 | 
					    Output as FeedSkeleton, Parameters as FeedSkeletonQuery,
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					use axum::extract::{Query, State};
 | 
				
			||||||
 | 
					use axum::Json;
 | 
				
			||||||
 | 
					use chrono::{DateTime, TimeZone, Utc};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use crate::processes::feed_server::state::FeedServerState;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub async fn get_feed_skeleton(
 | 
				
			||||||
 | 
					    State(state): State<FeedServerState>,
 | 
				
			||||||
 | 
					    query: Query<FeedSkeletonQuery>,
 | 
				
			||||||
 | 
					) -> Json<FeedSkeleton> {
 | 
				
			||||||
 | 
					    let limit = query.limit.unwrap_or(20) as usize;
 | 
				
			||||||
 | 
					    let earlier_than = query
 | 
				
			||||||
 | 
					        .cursor
 | 
				
			||||||
 | 
					        .as_deref()
 | 
				
			||||||
 | 
					        .map(parse_cursor)
 | 
				
			||||||
 | 
					        .transpose()
 | 
				
			||||||
 | 
					        .unwrap(); // TODO: handle error
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let posts = state
 | 
				
			||||||
 | 
					        .database
 | 
				
			||||||
 | 
					        .fetch_posts_by_authors_country("ru", limit, earlier_than)
 | 
				
			||||||
 | 
					        .await
 | 
				
			||||||
 | 
					        .unwrap();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let feed = posts
 | 
				
			||||||
 | 
					        .iter()
 | 
				
			||||||
 | 
					        .map(|p| SkeletonFeedPost {
 | 
				
			||||||
 | 
					            post: p.uri.clone(),
 | 
				
			||||||
 | 
					            reason: None,
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					        .collect();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let cursor = posts.last().map(|p| make_cursor(&p.indexed_at, &p.cid));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Json(FeedSkeleton { cursor, feed })
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					fn make_cursor(date: &DateTime<Utc>, cid: &str) -> String {
 | 
				
			||||||
 | 
					    format!("{}::{}", date.timestamp() * 1000, cid)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					fn parse_cursor(cursor: &str) -> Result<(DateTime<Utc>, &str)> {
 | 
				
			||||||
 | 
					    let mut parts = cursor.split("::");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let indexed_at = parts.next().ok_or_else(|| anyhow!("Malformed cursor"))?;
 | 
				
			||||||
 | 
					    let cid = parts.next().ok_or_else(|| anyhow!("Malformed cursor"))?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if parts.next().is_some() {
 | 
				
			||||||
 | 
					        return Err(anyhow!("Malformed cursor"));
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let indexed_at: i64 = indexed_at.parse()?;
 | 
				
			||||||
 | 
					    let indexed_at = Utc.timestamp_opt(indexed_at / 1000, 0).unwrap(); // TODO: handle error
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Ok((indexed_at, cid))
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,3 @@
 | 
				
			||||||
 | 
					pub async fn root() -> &'static str {
 | 
				
			||||||
 | 
					    "Hello, World!"
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,44 @@
 | 
				
			||||||
 | 
					use std::net::SocketAddr;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use anyhow::Result;
 | 
				
			||||||
 | 
					use axum::routing::get;
 | 
				
			||||||
 | 
					use axum::{Router, Server};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use crate::config::Config;
 | 
				
			||||||
 | 
					use crate::services::database::Database;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use super::endpoints::{describe_feed_generator, did_json, get_feed_skeleton, root};
 | 
				
			||||||
 | 
					use super::state::FeedServerState;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub struct FeedServer<'a> {
 | 
				
			||||||
 | 
					    database: &'a Database,
 | 
				
			||||||
 | 
					    config: &'a Config,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl<'a> FeedServer<'a> {
 | 
				
			||||||
 | 
					    pub fn new(database: &'a Database, config: &'a Config) -> Self {
 | 
				
			||||||
 | 
					        Self { database, config }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    pub async fn serve(self) -> Result<()> {
 | 
				
			||||||
 | 
					        let app = Router::new()
 | 
				
			||||||
 | 
					            .route("/", get(root))
 | 
				
			||||||
 | 
					            .route("/.well-known/did.json", get(did_json))
 | 
				
			||||||
 | 
					            .route(
 | 
				
			||||||
 | 
					                "/xrpc/app.bsky.feed.describeFeedGenerator",
 | 
				
			||||||
 | 
					                get(describe_feed_generator),
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					            .route(
 | 
				
			||||||
 | 
					                "/xrpc/app.bsky.feed.getFeedSkeleton",
 | 
				
			||||||
 | 
					                get(get_feed_skeleton),
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					            .with_state(FeedServerState {
 | 
				
			||||||
 | 
					                database: self.database.clone(),
 | 
				
			||||||
 | 
					                config: self.config.clone(),
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let addr = SocketAddr::from(([127, 0, 0, 1], 3000));
 | 
				
			||||||
 | 
					        Server::bind(&addr).serve(app.into_make_service()).await?;
 | 
				
			||||||
 | 
					        Ok(())
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,8 @@
 | 
				
			||||||
 | 
					use crate::config::Config;
 | 
				
			||||||
 | 
					use crate::services::database::Database;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[derive(Clone)]
 | 
				
			||||||
 | 
					pub struct FeedServerState {
 | 
				
			||||||
 | 
					    pub database: Database,
 | 
				
			||||||
 | 
					    pub config: Config,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -1,2 +1,3 @@
 | 
				
			||||||
 | 
					pub mod feed_server;
 | 
				
			||||||
pub mod post_saver;
 | 
					pub mod post_saver;
 | 
				
			||||||
pub mod profile_classifier;
 | 
					pub mod profile_classifier;
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -23,7 +23,7 @@ impl AI {
 | 
				
			||||||
                Message {
 | 
					                Message {
 | 
				
			||||||
                    role: Role::System,
 | 
					                    role: Role::System,
 | 
				
			||||||
                    // TODO: Lol, prompt injection much?
 | 
					                    // TODO: Lol, prompt injection much?
 | 
				
			||||||
                    content: "You are a tool that attempts to guess where a person is likely to be from based on their name and short bio. Please respond with two-letter country code only. Use lowercase letters.".to_string(),
 | 
					                    content: "You are a tool that attempts to guess where a person is likely to be from based on their name and short bio. Please respond with two-letter country code only. If unable to determine, say xx.".to_string(),
 | 
				
			||||||
                },
 | 
					                },
 | 
				
			||||||
                Message {
 | 
					                Message {
 | 
				
			||||||
                    role: Role::User,
 | 
					                    role: Role::User,
 | 
				
			||||||
| 
						 | 
					@ -36,6 +36,6 @@ impl AI {
 | 
				
			||||||
        let response = self.chat_gpt_client.chat(chat_input).await?;
 | 
					        let response = self.chat_gpt_client.chat(chat_input).await?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        // TODO: Error handling?
 | 
					        // TODO: Error handling?
 | 
				
			||||||
        return Ok(response.choices[0].message.content.clone());
 | 
					        return Ok(response.choices[0].message.content.to_lowercase());
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,15 +1,15 @@
 | 
				
			||||||
use anyhow::Result;
 | 
					use anyhow::Result;
 | 
				
			||||||
use chrono::{DateTime, Utc};
 | 
					use chrono::{DateTime, Utc};
 | 
				
			||||||
use scooby::postgres::{insert_into, select, update, Parameters};
 | 
					use scooby::postgres::{insert_into, select, update, Joinable, Orderable, Parameters, Aliasable};
 | 
				
			||||||
use sqlx::postgres::{PgPool, PgPoolOptions, PgRow};
 | 
					use sqlx::postgres::{PgPool, PgPoolOptions, PgRow};
 | 
				
			||||||
use sqlx::query;
 | 
					use sqlx::query;
 | 
				
			||||||
use sqlx::Row;
 | 
					use sqlx::Row;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
pub struct Post {
 | 
					pub struct Post {
 | 
				
			||||||
    indexed_at: DateTime<Utc>,
 | 
					    pub indexed_at: DateTime<Utc>,
 | 
				
			||||||
    author_did: String,
 | 
					    pub author_did: String,
 | 
				
			||||||
    cid: String,
 | 
					    pub cid: String,
 | 
				
			||||||
    uri: String,
 | 
					    pub uri: String,
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
pub struct Profile {
 | 
					pub struct Profile {
 | 
				
			||||||
| 
						 | 
					@ -24,6 +24,7 @@ pub struct SubscriptionState {
 | 
				
			||||||
    cursor: i64,
 | 
					    cursor: i64,
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[derive(Clone)]
 | 
				
			||||||
pub struct Database {
 | 
					pub struct Database {
 | 
				
			||||||
    connection_pool: PgPool,
 | 
					    connection_pool: PgPool,
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -52,6 +53,49 @@ impl Database {
 | 
				
			||||||
        .map(|_| ())?)
 | 
					        .map(|_| ())?)
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    pub async fn fetch_posts_by_authors_country(
 | 
				
			||||||
 | 
					        &self,
 | 
				
			||||||
 | 
					        author_country: &str,
 | 
				
			||||||
 | 
					        limit: usize,
 | 
				
			||||||
 | 
					        earlier_than: Option<(DateTime<Utc>, &str)>,
 | 
				
			||||||
 | 
					    ) -> Result<Vec<Post>> {
 | 
				
			||||||
 | 
					        let mut params = Parameters::new();
 | 
				
			||||||
 | 
					        let mut sql_builder = select(("p.indexed_at", "p.author_did", "p.cid", "p.uri"))
 | 
				
			||||||
 | 
					            .from(
 | 
				
			||||||
 | 
					                "Post".as_("p")
 | 
				
			||||||
 | 
					                    .inner_join("Profile".as_("pr"))
 | 
				
			||||||
 | 
					                    .on("pr.did = p.author_did"),
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					            .where_(format!("pr.likely_country_of_living = {}", params.next()))
 | 
				
			||||||
 | 
					            .order_by(("p.indexed_at".desc(), "p.cid".desc()))
 | 
				
			||||||
 | 
					            .limit(limit);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if earlier_than.is_some() {
 | 
				
			||||||
 | 
					            sql_builder = sql_builder
 | 
				
			||||||
 | 
					                .where_(format!("p.indexed_at <= {}", params.next()))
 | 
				
			||||||
 | 
					                .where_(format!("p.cid < {}", params.next()));
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let sql_string = sql_builder.to_string();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let mut query_object = query(&sql_string)
 | 
				
			||||||
 | 
					            .bind(author_country);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if let Some((last_indexed_at, last_cid)) = earlier_than {
 | 
				
			||||||
 | 
					            query_object = query_object.bind(last_indexed_at).bind(last_cid);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Ok(query_object
 | 
				
			||||||
 | 
					            .map(|r: PgRow| Post {
 | 
				
			||||||
 | 
					                indexed_at: r.get("indexed_at"),
 | 
				
			||||||
 | 
					                author_did: r.get("author_did"),
 | 
				
			||||||
 | 
					                cid: r.get("cid"),
 | 
				
			||||||
 | 
					                uri: r.get("uri"),
 | 
				
			||||||
 | 
					            })
 | 
				
			||||||
 | 
					            .fetch_all(&self.connection_pool)
 | 
				
			||||||
 | 
					            .await?)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    pub async fn insert_profile_if_it_doesnt_exist(&self, did: &str) -> Result<bool> {
 | 
					    pub async fn insert_profile_if_it_doesnt_exist(&self, did: &str) -> Result<bool> {
 | 
				
			||||||
        let mut params = Parameters::new();
 | 
					        let mut params = Parameters::new();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
		Reference in New Issue