Add some untested version of publishing a feed

Also adjust names of different env vars, and also adjust setup instructions
This commit is contained in:
Aleksei Voronov 2023-09-22 13:33:13 +02:00
parent 5128bf9d4a
commit e95c4923d6
11 changed files with 321 additions and 19 deletions

View File

@ -1,3 +1,6 @@
PUBLISHER_BLUESKY_HANDLE="..."
PUBLISHER_BLUESKY_PASSWORD="..."
PUBLISHER_DID="..."
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="..." FEED_GENERATOR_HOSTNAME="..."

107
Cargo.lock generated
View File

@ -75,6 +75,54 @@ dependencies = [
"libc", "libc",
] ]
[[package]]
name = "anstream"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b1f58811cfac344940f1a400b6e6231ce35171f614f26439e80f8c1465c5cc0c"
dependencies = [
"anstyle",
"anstyle-parse",
"anstyle-query",
"anstyle-wincon",
"colorchoice",
"utf8parse",
]
[[package]]
name = "anstyle"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b84bf0a05bbb2a83e5eb6fa36bb6e87baa08193c35ff52bbf6b38d8af2890e46"
[[package]]
name = "anstyle-parse"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "938874ff5980b03a87c5524b3ae5b59cf99b1d6bc836848df7bc5ada9643c333"
dependencies = [
"utf8parse",
]
[[package]]
name = "anstyle-query"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5ca11d4be1bab0c8bc8734a9aa7bf4ee8316d462a08c6ac5052f888fef5b494b"
dependencies = [
"windows-sys",
]
[[package]]
name = "anstyle-wincon"
version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "58f54d10c6dfa51283a066ceab3ec1ab78d13fae00aa49243a45e4571fb79dfd"
dependencies = [
"anstyle",
"windows-sys",
]
[[package]] [[package]]
name = "anyhow" name = "anyhow"
version = "1.0.75" version = "1.0.75"
@ -430,6 +478,52 @@ dependencies = [
"unsigned-varint", "unsigned-varint",
] ]
[[package]]
name = "clap"
version = "4.4.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b1d7b8d5ec32af0fadc644bf1fd509a688c2103b185644bb1e29d164e0703136"
dependencies = [
"clap_builder",
"clap_derive",
]
[[package]]
name = "clap_builder"
version = "4.4.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5179bb514e4d7c2051749d8fcefa2ed6d06a9f4e6d69faf3805f5d80b8cf8d56"
dependencies = [
"anstream",
"anstyle",
"clap_lex",
"strsim",
]
[[package]]
name = "clap_derive"
version = "4.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0862016ff20d69b84ef8247369fabf5c008a7417002411897d40ee1f4532b873"
dependencies = [
"heck",
"proc-macro2",
"quote",
"syn 2.0.35",
]
[[package]]
name = "clap_lex"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cd7cc57abe963c6d3b9d8be5b06ba7c8957a930305ca90304f24ef040aa6f961"
[[package]]
name = "colorchoice"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7"
[[package]] [[package]]
name = "compact_str" name = "compact_str"
version = "0.7.1" version = "0.7.1"
@ -2248,6 +2342,7 @@ dependencies = [
"chat-gpt-lib-rs", "chat-gpt-lib-rs",
"chrono", "chrono",
"ciborium", "ciborium",
"clap",
"dotenv", "dotenv",
"env_logger", "env_logger",
"futures", "futures",
@ -3284,6 +3379,12 @@ dependencies = [
"unicode-normalization", "unicode-normalization",
] ]
[[package]]
name = "strsim"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
[[package]] [[package]]
name = "strum" name = "strum"
version = "0.24.1" version = "0.24.1"
@ -3658,6 +3759,12 @@ version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9"
[[package]]
name = "utf8parse"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a"
[[package]] [[package]]
name = "vcpkg" name = "vcpkg"
version = "0.2.15" version = "0.2.15"

View File

@ -2,6 +2,7 @@
name = "nederlandskie" name = "nederlandskie"
version = "0.1.0" version = "0.1.0"
edition = "2021" edition = "2021"
default-run = "nederlandskie"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
@ -14,6 +15,7 @@ axum = "0.6.20"
chat-gpt-lib-rs = "0.2.1" chat-gpt-lib-rs = "0.2.1"
chrono = "0.4.31" chrono = "0.4.31"
ciborium = "0.2.1" ciborium = "0.2.1"
clap = { version = "4.4.4", features = ["derive"] }
dotenv = "0.15.0" dotenv = "0.15.0"
env_logger = "0.10.0" env_logger = "0.10.0"
futures = "0.3.28" futures = "0.3.28"

View File

@ -15,14 +15,32 @@ Heavily WIP. Doesn't work yet at all, but does read the stream of posts as they
- [ ] Publish the feed - [ ] Publish the feed
- [ ] Handle deleting of posts - [ ] Handle deleting of posts
## Initial setup ## Configuration
Copy `.env.example` into `.env` and set up the environment variables within: 1. Copy `.env.example` into `.env` and set up the environment variables within:
- `PUBLISHER_BLUESKY_HANDLE` to your Bluesky handle
- `PUBLISHER_BLUESKY_PASSWORD` to Bluesky app password that you created in settings
- `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 - `FEED_GENERATOR_HOSTNAME` to the hostname of where you intend to host the feed
2. Determine your own DID and put it in `PUBLISHER_DID` env variable in `.env`:
```
cargo run --bin who_am_i
```
## Running ## Running
### Populate and serve the feed
`cargo run` `cargo run`
### Determine your own did for publishing
`cargo run --bin who_am_i`
### Publish the feed
`cargo run --bin publish_feed -- --help`

66
src/bin/publish_feed.rs Normal file
View File

@ -0,0 +1,66 @@
extern crate nederlandskie;
use std::env;
use anyhow::{Context, Result};
use clap::Parser;
use dotenv::dotenv;
use nederlandskie::services::Bluesky;
#[derive(Parser, Debug)]
struct Args {
/// Short name of the feed. Must match one of the defined algos.
#[arg(long)]
name: String,
/// Name that will be displayed in Bluesky interface
#[arg(long)]
display_name: String,
/// Description that will be displayed in Bluesky interface
#[arg(long)]
description: String,
/// Filename of the avatar that will be displayed
#[arg(long)]
avatar_filename: Option<String>,
}
#[tokio::main]
async fn main() -> Result<()> {
dotenv()?;
let args = Args::parse();
let handle = env::var("PUBLISHER_BLUESKY_HANDLE")
.context("PUBLISHER_BLUESKY_HANDLE environment variable must be set")?;
let password = env::var("PUBLISHER_BLUESKY_PASSWORD")
.context("PUBLISHER_BLUESKY_PASSWORD environment variable must be set")?;
let feed_generator_did = format!("did:web:{}", env::var("FEED_GENERATOR_HOSTNAME")?);
let bluesky = Bluesky::new("https://bsky.social");
let session = bluesky.login(&handle, &password).await?;
let mut avatar = None;
if let Some(path) = args.avatar_filename {
let bytes = std::fs::read(path)?;
avatar = Some(bluesky.upload_blob(bytes).await?);
}
bluesky
.publish_feed(
&session.did,
&feed_generator_did,
&args.name,
&args.display_name,
&args.description,
avatar,
)
.await?;
Ok(())
}

27
src/bin/who_am_i.rs Normal file
View File

@ -0,0 +1,27 @@
extern crate nederlandskie;
use std::env;
use anyhow::{Context, Result};
use dotenv::dotenv;
use nederlandskie::services::Bluesky;
#[tokio::main]
async fn main() -> Result<()> {
dotenv()?;
let bluesky = Bluesky::new("https://bsky.social");
let handle = env::var("PUBLISHER_BLUESKY_HANDLE")
.context("PUBLISHER_BLUESKY_HANDLE environment variable must be set")?;
let password = env::var("PUBLISHER_BLUESKY_PASSWORD")
.context("PUBLISHER_BLUESKY_PASSWORD environment variable must be set")?;
let session = bluesky.login(&handle, &password).await?;
println!("{}", session.did);
Ok(())
}

View File

@ -5,9 +5,9 @@ use std::env;
pub struct Config { pub struct Config {
pub chat_gpt_api_key: String, pub chat_gpt_api_key: String,
pub database_url: String, pub database_url: String,
pub service_did: String, pub feed_generator_did: String,
pub publisher_did: String, pub publisher_did: String,
pub hostname: String, pub feed_generator_hostname: String,
} }
impl Config { impl Config {
@ -17,9 +17,9 @@ impl Config {
Ok(Self { Ok(Self {
chat_gpt_api_key: env::var("CHAT_GPT_API_KEY")?, chat_gpt_api_key: env::var("CHAT_GPT_API_KEY")?,
database_url: env::var("DATABASE_URL")?, database_url: env::var("DATABASE_URL")?,
hostname: env::var("HOSTNAME")?, feed_generator_hostname: env::var("FEED_GENERATOR_HOSTNAME")?,
service_did: format!("did:web:{}", env::var("HOSTNAME")?), feed_generator_did: format!("did:web:{}", env::var("FEED_GENERATOR_HOSTNAME")?),
publisher_did: "".to_owned(), // TODO publisher_did: env::var("PUBLISHER_DID")?,
}) })
} }
} }

View File

@ -9,7 +9,7 @@ pub async fn describe_feed_generator(
State(state): State<FeedServerState>, State(state): State<FeedServerState>,
) -> Json<FeedGeneratorDescription> { ) -> Json<FeedGeneratorDescription> {
Json(FeedGeneratorDescription { Json(FeedGeneratorDescription {
did: state.config.service_did.clone(), did: state.config.feed_generator_did.clone(),
feeds: state feeds: state
.algos .algos
.iter_names() .iter_names()

View File

@ -22,11 +22,11 @@ pub struct Service {
pub async fn did_json(State(state): State<FeedServerState>) -> Json<Did> { pub async fn did_json(State(state): State<FeedServerState>) -> Json<Did> {
Json(Did { Json(Did {
context: vec!["https://www.w3.org/ns/did/v1".to_owned()], context: vec!["https://www.w3.org/ns/did/v1".to_owned()],
id: state.config.service_did.clone(), id: state.config.feed_generator_did.clone(),
service: vec![Service { service: vec![Service {
id: "#bsky_fg".to_owned(), id: "#bsky_fg".to_owned(),
type_: "BskyFeedGenerator".to_owned(), type_: "BskyFeedGenerator".to_owned(),
service_endpoint: format!("https://{}", state.config.hostname), service_endpoint: format!("https://{}", state.config.feed_generator_hostname),
}], }],
}) })
} }

View File

@ -38,12 +38,12 @@ impl PostIndexer {
let cursor = self let cursor = self
.database .database
.fetch_subscription_cursor(&self.config.service_did) .fetch_subscription_cursor(&self.config.feed_generator_did)
.await?; .await?;
if cursor.is_none() { if cursor.is_none() {
self.database self.database
.create_subscription_state(&self.config.service_did) .create_subscription_state(&self.config.feed_generator_did)
.await?; .await?;
} }
@ -91,10 +91,10 @@ impl CommitProcessor for PostIndexer {
if commit.seq % 20 == 0 { if commit.seq % 20 == 0 {
info!( info!(
"Updating cursor for {} to {}", "Updating cursor for {} to {}",
self.config.service_did, commit.seq self.config.feed_generator_did, commit.seq
); );
self.database self.database
.update_subscription_cursor(&self.config.service_did, commit.seq) .update_subscription_cursor(&self.config.feed_generator_did, commit.seq)
.await?; .await?;
} }

View File

@ -1,7 +1,10 @@
use anyhow::{anyhow, Result}; use anyhow::{anyhow, Result};
use atrium_api::blob::BlobRef;
use atrium_api::client::AtpServiceClient; use atrium_api::client::AtpServiceClient;
use atrium_api::client::AtpServiceWrapper; use atrium_api::client::AtpServiceWrapper;
use atrium_api::records::Record;
use atrium_xrpc::client::reqwest::ReqwestClient; use atrium_xrpc::client::reqwest::ReqwestClient;
use chrono::Utc;
use futures::StreamExt; use futures::StreamExt;
use log::error; use log::error;
use tokio_tungstenite::{connect_async, tungstenite}; use tokio_tungstenite::{connect_async, tungstenite};
@ -14,6 +17,11 @@ pub struct ProfileDetails {
pub description: String, pub description: String,
} }
#[derive(Debug)]
pub struct SessionDetails {
pub did: String,
}
pub struct Bluesky { pub struct Bluesky {
client: AtpServiceClient<AtpServiceWrapper<ReqwestClient>>, client: AtpServiceClient<AtpServiceWrapper<ReqwestClient>>,
} }
@ -25,6 +33,77 @@ impl Bluesky {
} }
} }
pub async fn login(&self, handle: &str, password: &str) -> Result<SessionDetails> {
use atrium_api::com::atproto::server::create_session::Input;
let result = self
.client
.service
.com
.atproto
.server
.create_session(Input {
identifier: handle.to_owned(),
password: password.to_owned(),
})
.await?;
Ok(SessionDetails { did: result.did })
}
pub async fn upload_blob(&self, blob: Vec<u8>) -> Result<BlobRef> {
let result = self
.client
.service
.com
.atproto
.repo
.upload_blob(blob)
.await?;
Ok(result.blob)
}
pub async fn publish_feed(
&self,
publisher_did: &str,
feed_generator_did: &str,
name: &str,
display_name: &str,
description: &str,
avatar: Option<BlobRef>,
) -> Result<()> {
use atrium_api::com::atproto::repo::put_record::Input;
self.client
.service
.com
.atproto
.repo
.put_record(Input {
collection: "app.bsky.feed.generator".to_owned(),
record: Record::AppBskyFeedGenerator(Box::new(
atrium_api::app::bsky::feed::generator::Record {
avatar,
created_at: Utc::now().to_string(),
description: Some(description.to_owned()),
description_facets: None,
did: feed_generator_did.to_owned(),
display_name: display_name.to_owned(),
labels: None,
},
)),
repo: publisher_did.to_owned(),
rkey: name.to_owned(),
swap_commit: None,
swap_record: None,
validate: None,
})
.await?;
Ok(())
}
pub async fn fetch_profile_details(&self, did: &str) -> Result<ProfileDetails> { pub async fn fetch_profile_details(&self, did: &str) -> Result<ProfileDetails> {
let result = self let result = self
.client .client
@ -41,7 +120,7 @@ impl Bluesky {
.await?; .await?;
let profile = match result.value { let profile = match result.value {
atrium_api::records::Record::AppBskyActorProfile(profile) => profile, Record::AppBskyActorProfile(profile) => profile,
_ => return Err(anyhow!("Big bad, no such profile")), _ => return Err(anyhow!("Big bad, no such profile")),
}; };