Files
atproto-cosocial-labeler/src/labeler.rs
T
govandClaude Opus 5.5 a7d410fd6d Canadian cities labeler over Bluesky DMs
Users DM @cities.pds.cosocial.ca their city and get a signed account label.
Serves com.atproto.label.queryLabels and subscribeLabels, built on jacquard.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 16:33:19 -05:00

62 lines
1.8 KiB
Rust

use std::sync::Arc;
use anyhow::Result;
use jacquard::types::string::Did;
use k256::ecdsa::SigningKey;
use tokio::sync::broadcast;
use crate::db::{Db, SeqLabel};
use crate::labels::sign_label;
pub struct Labeler {
pub db: Arc<Db>,
pub did: Did,
key: SigningKey,
published: broadcast::Sender<SeqLabel>,
}
impl Labeler {
pub fn new(db: Arc<Db>, did: Did, key: SigningKey) -> Self {
let (published, _) = broadcast::channel(1024);
Self {
db,
did,
key,
published,
}
}
/// Live labels for `subscribeLabels`.
pub fn subscribe(&self) -> broadcast::Receiver<SeqLabel> {
self.published.subscribe()
}
pub fn current_city(&self, subject: &Did<&str>) -> Result<Option<String>> {
Ok(self.db.active_vals(subject.as_str())?.into_iter().next())
}
/// Make `city` the account's only city label; `None` removes it.
pub fn set_city(&self, subject: &Did<&str>, city: Option<&str>) -> Result<()> {
let active = self.db.active_vals(subject.as_str())?;
for val in active.iter().filter(|v| Some(v.as_str()) != city) {
self.emit(subject, val, true)?;
}
if let Some(city) = city.filter(|c| !active.iter().any(|v| v == c)) {
self.emit(subject, city, false)?;
}
Ok(())
}
fn emit(&self, subject: &Did<&str>, val: &str, neg: bool) -> Result<()> {
let label = sign_label(&self.key, &self.did, subject, val, neg)?;
let seq = self.db.insert_label(&label)?;
// no receivers just means no one is subscribed right now
let _ = self.published.send(SeqLabel { seq, label });
println!(
"{} {val} on {subject} (seq {seq})",
if neg { "negated" } else { "applied" }
);
Ok(())
}
}