Compare commits

..
2 Commits
Author SHA1 Message Date
govandClaude Opus 5.5 049a198b1c feat: add setup command to publish the labeler declaration
`cargo run --bin setup -- declare` writes app.bsky.labeler.service/self with
one inform-severity label per city and opts out of reports.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 16:39:19 -05:00
govandClaude Opus 5.5 d1b01e180d refactor: move modules into a library crate
Lets additional binaries share the labeler code. Adds a shared login()
helper and Cities::iter/is_empty for use outside the crate.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 16:39:17 -05:00
4 changed files with 115 additions and 21 deletions

No files matched your search

+80
View File
@@ -0,0 +1,80 @@
//! One-time labeler setup.
//!
//! `cargo run --bin setup -- declare` publishes the labeler declaration
//! (`app.bsky.labeler.service/self`) with one label per city. This is what
//! gives the account a Subscribe button. Safe to re-run after editing the list.
use anyhow::{Result, bail};
use cities_labeler::cities::Cities;
use cities_labeler::config::Config;
use cities_labeler::login;
use jacquard::api::app_bsky::labeler::LabelerPolicies;
use jacquard::api::app_bsky::labeler::service::Service;
use jacquard::api::com_atproto::label::{
LabelValue, LabelValueDefinition, LabelValueDefinitionBlurs,
LabelValueDefinitionDefaultSetting, LabelValueDefinitionSeverity, LabelValueDefinitionStrings,
};
use jacquard::prelude::*;
use jacquard::types::language::Language;
use jacquard::types::recordkey::RecordKey;
use jacquard::types::string::Datetime;
#[tokio::main]
async fn main() -> Result<()> {
let config = Config::from_env()?;
match std::env::args().nth(1).as_deref() {
Some("declare") => declare(&config).await,
_ => bail!("usage: setup declare"),
}
}
async fn declare(config: &Config) -> Result<()> {
let cities = Cities::load(&config.cities_path)?;
let (agent, did) = login(&config.handle, &config.app_password).await?;
let definitions: Vec<LabelValueDefinition> = cities
.iter()
.map(|city| LabelValueDefinition {
identifier: city.id.as_str().into(),
// a neutral badge, shown by default, never hiding content
severity: LabelValueDefinitionSeverity::Inform,
blurs: LabelValueDefinitionBlurs::None,
default_setting: Some(LabelValueDefinitionDefaultSetting::Warn),
adult_only: Some(false),
locales: vec![LabelValueDefinitionStrings {
lang: Language::new_static("en").expect("valid language tag"),
name: city.display().into(),
description: format!("Chose {} as their city.", city.display()).into(),
extra_data: None,
}],
extra_data: None,
})
.collect();
let service: Service = Service {
created_at: Datetime::now(),
labels: None,
policies: LabelerPolicies {
label_values: cities
.iter()
.map(|c| LabelValue::Other(c.id.as_str().into()))
.collect(),
label_value_definitions: Some(definitions),
extra_data: None,
},
// these declare which reports the labeler reviews: none
reason_types: Some(vec![]),
subject_types: Some(vec![]),
subject_collections: None,
extra_data: None,
};
let output = agent
.put_record(RecordKey::any_static("self")?, service)
.await?;
println!(
"published {} with {} labels for {did}",
output.uri,
cities.len()
);
Ok(())
}
+8
View File
@@ -75,6 +75,14 @@ impl Cities {
self.keyed.len()
}
pub fn is_empty(&self) -> bool {
self.keyed.is_empty()
}
pub fn iter(&self) -> impl Iterator<Item = &City> {
self.keyed.iter().map(|k| &k.city)
}
pub fn get(&self, id: &str) -> Option<&City> {
self.by_id.get(id).map(|&i| &self.keyed[i].city)
}
+19
View File
@@ -0,0 +1,19 @@
pub mod cities;
pub mod config;
pub mod db;
pub mod dms;
pub mod labeler;
pub mod labels;
pub mod server;
use anyhow::{Context, Result};
use jacquard::client::{Agent, MemoryCredentialSession};
use jacquard::types::string::Did;
/// Log in as the labeler account with an app password.
pub async fn login(handle: &str, password: &str) -> Result<(Agent<MemoryCredentialSession>, Did)> {
let (session, account) = MemoryCredentialSession::authenticated(handle, password, None, None)
.await
.with_context(|| format!("logging in as {handle}"))?;
Ok((Agent::from(session), account.did))
}
+8 -21
View File
@@ -1,38 +1,25 @@
mod cities;
mod config;
mod db;
mod dms;
mod labeler;
mod labels;
mod server;
use std::sync::Arc;
use anyhow::{Context, Result};
use jacquard::client::{Agent, MemoryCredentialSession};
use cities_labeler::cities::Cities;
use cities_labeler::config::Config;
use cities_labeler::db::Db;
use cities_labeler::dms::Dms;
use cities_labeler::labeler::Labeler;
use cities_labeler::{login, server};
use k256::ecdsa::SigningKey;
use crate::cities::Cities;
use crate::config::Config;
use crate::db::Db;
use crate::dms::Dms;
use crate::labeler::Labeler;
#[tokio::main]
async fn main() -> Result<()> {
let config = Config::from_env()?;
let (session, account) =
MemoryCredentialSession::authenticated(&config.handle, &config.app_password, None, None)
.await
.with_context(|| format!("logging in as {}", config.handle))?;
let agent = Agent::from(session);
let (agent, did) = login(&config.handle, &config.app_password).await?;
let key = SigningKey::from_slice(&hex::decode(&config.signing_key_hex)?)
.context("SIGNING_KEY_HEX")?;
let cities = Cities::load(&config.cities_path)?;
let db = Arc::new(Db::open(&config.db_path)?);
let labeler = Arc::new(Labeler::new(db, account.did, key));
let labeler = Arc::new(Labeler::new(db, did, key));
println!(
"labeling as {}, loaded {} cities",
labeler.did,