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:
parent
5128bf9d4a
commit
e95c4923d6
|
@ -1,3 +1,6 @@
|
|||
PUBLISHER_BLUESKY_HANDLE="..."
|
||||
PUBLISHER_BLUESKY_PASSWORD="..."
|
||||
PUBLISHER_DID="..."
|
||||
CHAT_GPT_API_KEY="fake-chat-gpt-key"
|
||||
DATABASE_URL="postgres://postgres:password@localhost/nederlandskie"
|
||||
HOSTNAME="..."
|
||||
FEED_GENERATOR_HOSTNAME="..."
|
||||
|
|
|
@ -75,6 +75,54 @@ dependencies = [
|
|||
"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]]
|
||||
name = "anyhow"
|
||||
version = "1.0.75"
|
||||
|
@ -430,6 +478,52 @@ dependencies = [
|
|||
"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]]
|
||||
name = "compact_str"
|
||||
version = "0.7.1"
|
||||
|
@ -2248,6 +2342,7 @@ dependencies = [
|
|||
"chat-gpt-lib-rs",
|
||||
"chrono",
|
||||
"ciborium",
|
||||
"clap",
|
||||
"dotenv",
|
||||
"env_logger",
|
||||
"futures",
|
||||
|
@ -3284,6 +3379,12 @@ dependencies = [
|
|||
"unicode-normalization",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "strsim"
|
||||
version = "0.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
|
||||
|
||||
[[package]]
|
||||
name = "strum"
|
||||
version = "0.24.1"
|
||||
|
@ -3658,6 +3759,12 @@ version = "0.7.6"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9"
|
||||
|
||||
[[package]]
|
||||
name = "utf8parse"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a"
|
||||
|
||||
[[package]]
|
||||
name = "vcpkg"
|
||||
version = "0.2.15"
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
name = "nederlandskie"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
default-run = "nederlandskie"
|
||||
|
||||
# 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"
|
||||
chrono = "0.4.31"
|
||||
ciborium = "0.2.1"
|
||||
clap = { version = "4.4.4", features = ["derive"] }
|
||||
dotenv = "0.15.0"
|
||||
env_logger = "0.10.0"
|
||||
futures = "0.3.28"
|
||||
|
|
24
README.md
24
README.md
|
@ -15,14 +15,32 @@ Heavily WIP. Doesn't work yet at all, but does read the stream of posts as they
|
|||
- [ ] Publish the feed
|
||||
- [ ] 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
|
||||
- `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
|
||||
|
||||
### Populate and serve the feed
|
||||
|
||||
`cargo run`
|
||||
|
||||
### Determine your own did for publishing
|
||||
|
||||
`cargo run --bin who_am_i`
|
||||
|
||||
### Publish the feed
|
||||
|
||||
`cargo run --bin publish_feed -- --help`
|
||||
|
|
|
@ -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(())
|
||||
}
|
|
@ -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(())
|
||||
}
|
|
@ -5,9 +5,9 @@ use std::env;
|
|||
pub struct Config {
|
||||
pub chat_gpt_api_key: String,
|
||||
pub database_url: String,
|
||||
pub service_did: String,
|
||||
pub feed_generator_did: String,
|
||||
pub publisher_did: String,
|
||||
pub hostname: String,
|
||||
pub feed_generator_hostname: String,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
|
@ -17,9 +17,9 @@ impl Config {
|
|||
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
|
||||
feed_generator_hostname: env::var("FEED_GENERATOR_HOSTNAME")?,
|
||||
feed_generator_did: format!("did:web:{}", env::var("FEED_GENERATOR_HOSTNAME")?),
|
||||
publisher_did: env::var("PUBLISHER_DID")?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,7 +9,7 @@ pub async fn describe_feed_generator(
|
|||
State(state): State<FeedServerState>,
|
||||
) -> Json<FeedGeneratorDescription> {
|
||||
Json(FeedGeneratorDescription {
|
||||
did: state.config.service_did.clone(),
|
||||
did: state.config.feed_generator_did.clone(),
|
||||
feeds: state
|
||||
.algos
|
||||
.iter_names()
|
||||
|
|
|
@ -22,11 +22,11 @@ pub struct Service {
|
|||
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(),
|
||||
id: state.config.feed_generator_did.clone(),
|
||||
service: vec![Service {
|
||||
id: "#bsky_fg".to_owned(),
|
||||
type_: "BskyFeedGenerator".to_owned(),
|
||||
service_endpoint: format!("https://{}", state.config.hostname),
|
||||
service_endpoint: format!("https://{}", state.config.feed_generator_hostname),
|
||||
}],
|
||||
})
|
||||
}
|
||||
|
|
|
@ -38,12 +38,12 @@ impl PostIndexer {
|
|||
|
||||
let cursor = self
|
||||
.database
|
||||
.fetch_subscription_cursor(&self.config.service_did)
|
||||
.fetch_subscription_cursor(&self.config.feed_generator_did)
|
||||
.await?;
|
||||
|
||||
if cursor.is_none() {
|
||||
self.database
|
||||
.create_subscription_state(&self.config.service_did)
|
||||
.create_subscription_state(&self.config.feed_generator_did)
|
||||
.await?;
|
||||
}
|
||||
|
||||
|
@ -91,10 +91,10 @@ impl CommitProcessor for PostIndexer {
|
|||
if commit.seq % 20 == 0 {
|
||||
info!(
|
||||
"Updating cursor for {} to {}",
|
||||
self.config.service_did, commit.seq
|
||||
self.config.feed_generator_did, commit.seq
|
||||
);
|
||||
self.database
|
||||
.update_subscription_cursor(&self.config.service_did, commit.seq)
|
||||
.update_subscription_cursor(&self.config.feed_generator_did, commit.seq)
|
||||
.await?;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,7 +1,10 @@
|
|||
use anyhow::{anyhow, Result};
|
||||
use atrium_api::blob::BlobRef;
|
||||
use atrium_api::client::AtpServiceClient;
|
||||
use atrium_api::client::AtpServiceWrapper;
|
||||
use atrium_api::records::Record;
|
||||
use atrium_xrpc::client::reqwest::ReqwestClient;
|
||||
use chrono::Utc;
|
||||
use futures::StreamExt;
|
||||
use log::error;
|
||||
use tokio_tungstenite::{connect_async, tungstenite};
|
||||
|
@ -14,6 +17,11 @@ pub struct ProfileDetails {
|
|||
pub description: String,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct SessionDetails {
|
||||
pub did: String,
|
||||
}
|
||||
|
||||
pub struct Bluesky {
|
||||
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> {
|
||||
let result = self
|
||||
.client
|
||||
|
@ -41,7 +120,7 @@ impl Bluesky {
|
|||
.await?;
|
||||
|
||||
let profile = match result.value {
|
||||
atrium_api::records::Record::AppBskyActorProfile(profile) => profile,
|
||||
Record::AppBskyActorProfile(profile) => profile,
|
||||
_ => return Err(anyhow!("Big bad, no such profile")),
|
||||
};
|
||||
|
||||
|
|
Loading…
Reference in New Issue