feat(feed): implement feed service #8

open
opened by tsiry-sandratraina.com targeting main from feat/feed-generator
Changed files
+151
crates
+35
crates/feed/Cargo.toml
···
+
[package]
+
name = "rocksky-feed"
+
version = "0.1.0"
+
authors.workspace = true
+
edition.workspace = true
+
license.workspace = true
+
repository.workspace = true
+
+
[dependencies]
+
warp = "0.3.7"
+
owo-colors = "4.1.0"
+
anyhow = "1.0.100"
+
atrium-api = "0.25.5"
+
atrium-xrpc-client = "0.5.14"
+
chrono = { version = "= 0.4.39", features = ["serde"] }
+
duckdb = { version = "1.2.0", features = ["chrono"] }
+
jetstream-oxide = "=0.1.2"
+
reqwest = { version = "0.12.12", features = [
+
"rustls-tls",
+
"json",
+
"multipart",
+
], default-features = false }
+
serde = "1.0.227"
+
serde_json = "1.0.145"
+
tokio = { version = "1.43.0", features = ["full"] }
+
tracing = "0.1.41"
+
dotenv = "0.15.0"
+
sqlx = { version = "0.8.3", features = [
+
"runtime-tokio",
+
"tls-rustls",
+
"postgres",
+
"chrono",
+
"derive",
+
"macros",
+
] }
+35
crates/feed/src/config.rs
···
+
use std::env;
+
+
use dotenv::dotenv;
+
+
#[derive(Debug, Clone)]
+
/// Configuration values for a Feed service
+
pub struct Config {
+
/// Your account's decentralized identifier (DID)
+
/// A DID is a persistent, long-term identifier for every account. Usually look like did:plc:ewvi7nxzyoun6zhxrhs64oiz.
+
pub publisher_did: String,
+
/// The host name for your feed generator.
+
///
+
/// For example: if github were to host a feed generator service at their domain they would set this value to `github.com`
+
///
+
/// You can develop your feed locally without setting this to a real value. However, when publishing, this value must be a domain that:
+
/// - Points to your service.
+
/// - Is secured with SSL (HTTPS).
+
/// - Is accessible on the public internet.
+
pub feed_generator_hostname: String,
+
}
+
+
impl Config {
+
/// Loads the config from a local .env file containing these variables
+
/// PUBLISHER_DID
+
/// FEED_GENERATOR_HOSTNAME
+
pub fn load_env_config() -> Self {
+
dotenv().expect("Missing .env");
+
Config {
+
publisher_did: env::var("PUBLISHER_DID")
+
.expect(".env file is missing an entry for PUBLISHER_DID"),
+
feed_generator_hostname: env::var("FEED_GENERATOR_HOSTNAME")
+
.expect(".env file is missing an entry for FEED_GENERATOR_HOSTNAME"),
+
}
+
}
+
}
+10
crates/feed/src/feed_handler.rs
···
+
use crate::types::{FeedResult, Request, Scrobble, Uri};
+
+
/// A feed handler is responsible for
+
/// - Storing and managing firehose input.
+
/// - Serving responses to feed requests with `serve_feed`
+
pub trait FeedHandler {
+
fn insert_scrobble(&self, scrobble: Scrobble) -> impl std::future::Future<Output = ()> + Send;
+
fn delete_scrobble(&self, uri: Uri) -> impl std::future::Future<Output = ()> + Send;
+
fn serve_feed(&self, request: Request) -> impl std::future::Future<Output = FeedResult> + Send;
+
}
+54
crates/feed/src/types.rs
···
+
#[derive(Debug, Clone)]
+
pub struct Request {
+
pub cursor: Option<String>,
+
pub feed: String,
+
pub limit: Option<u8>,
+
}
+
+
#[derive(Debug, Clone)]
+
pub struct Cid(pub String);
+
+
#[derive(Debug, Clone)]
+
pub struct Did(pub String);
+
+
#[derive(Debug, Clone, Hash, PartialEq, Eq)]
+
pub struct Uri(pub String);
+
+
#[derive(Debug, Clone)]
+
pub struct FeedResult {
+
pub cursor: Option<String>,
+
pub feed: Vec<Uri>,
+
}
+
+
pub struct FeedSkeletonQuery {}
+
+
#[derive(Deserialize)]
+
pub struct FeedSkeletonParameters {}
+
+
impl Into<FeedSkeletonQuery> for FeedSkeletonParameters {
+
fn into(self) -> FeedSkeletonQuery {
+
FeedSkeletonQuery {}
+
}
+
}
+
+
#[derive(Debug, Clone)]
+
pub struct Scrobble {}
+
+
use serde::{Deserialize, Serialize};
+
+
#[derive(Serialize)]
+
pub(crate) struct DidDocument {
+
#[serde(rename = "@context")]
+
pub(crate) context: Vec<String>,
+
pub(crate) id: String,
+
pub(crate) service: Vec<Service>,
+
}
+
+
#[derive(Serialize)]
+
pub(crate) struct Service {
+
pub(crate) id: String,
+
#[serde(rename = "type")]
+
pub(crate) type_: String,
+
#[serde(rename = "serviceEndpoint")]
+
pub(crate) service_endpoint: String,
+
}
+1
crates/rockskyd/Cargo.toml
···
rocksky-spotify = { path = "../spotify" }
rocksky-tracklist = { path = "../tracklist" }
rocksky-webscrobbler = { path = "../webscrobbler" }
+
rocksky-feed = { path = "../feed" }
tracing = "0.1.41"
tracing-subscriber = "0.3.20"
tracing-log = "0.2.0"
+6
crates/rockskyd/src/cmd/feed.rs
···
+
use anyhow::Error;
+
+
pub async fn serve() -> Result<(), Error> {
+
rocksky_feed::run().await;
+
Ok(())
+
}
+1
crates/rockskyd/src/cmd/mod.rs
···
pub mod analytics;
pub mod dropbox;
+
pub mod feed;
pub mod googledrive;
pub mod jetstream;
pub mod playlist;
+9
crates/rockskyd/src/main.rs
···
.subcommand(Command::new("spotify").about("Start Spotify Listener Service"))
.subcommand(Command::new("tracklist").about("Start User Current Track Queue Service"))
.subcommand(Command::new("webscrobbler").about("Start Webscrobbler API"))
+
.subcommand(
+
Command::new("feed")
+
.about("Feed related commands")
+
.subcommand(Command::new("serve").about("Serve the Rocksky Feed API")),
+
)
}
#[tokio::main]
···
Some(("webscrobbler", _)) => {
cmd::webscrobbler::start_webscrobbler_service().await?;
}
+
Some(("feed", sub_m)) => match sub_m.subcommand() {
+
Some(("serve", _)) => cmd::feed::serve().await?,
+
_ => println!("Unknown feed command"),
+
},
_ => {
println!("No valid subcommand was used. Use --help to see available commands.");
}