~ knotless Search/
↑↓ move↵ open⇧↵ all results

johnandersen777.bsky.social/atlibs

0
0
main
124 lines · 4.2 KB Raw History
use abi::repo::{create, delete_record, list_records};
use abi::{AtUri, Call, Datetime, Did, Fan, Handle, LexiconType, On, Run, Step, entries};
use serde::{Deserialize, Serialize};
use serde_json::json;

const COLLECTION: &str = "xyz.statusphere.status";
const CLEAR_LIMIT: usize = 10;

#[derive(Deserialize)]
struct Args {
    did: Did,
    now: String,
    #[serde(default)]
    status: Option<String>,
}

#[derive(Serialize, PartialEq)]
struct Status {
    status: String,
    #[serde(rename = "createdAt")]
    created_at: Datetime,
}

impl LexiconType for Status {
    fn lexicon_type() -> &'static str {
        COLLECTION
    }
}

fn validate_status(status: Option<&str>) -> Result<String, String> {
    let status = status.unwrap_or_default();
    if status.is_empty() || status.len() > 32 {
        return Err("status must be 1 to 32 bytes".into());
    }
    if status.chars().any(char::is_control) {
        return Err("status must not contain control characters".into());
    }
    Ok(status.to_string())
}

abi::call_ids! {
    #[derive(Clone, Copy)]
    enum Id {
        List = "list",
        Create = "create",
        Delete(usize) = "delete",
    }
}

fn list_statuses(did: &Did, limit: usize) -> Call {
    list_records(did.as_str(), COLLECTION, limit, None)
}

#[derive(Default, Deserialize, Serialize)]
struct SetStatus {
    status: String,
}

// list the latest status -> create one unless it already matches -> report the uri.
fn set_status_handler(run: Run<Args>) -> Result<Handle, String> {
    let (mut step, on) = Step::<Args, SetStatus, Id>::new(run)?;
    let args = &step.args;
    match on {
        On::Start => {
            step.state.status = validate_status(args.status.as_deref())?;
            Datetime::new(args.now.clone()).map_err(|e| format!("invalid now: {e}"))?;
            step.out.call(Id::List, list_statuses(&args.did, 1));
        }
        On::Result(Id::List, result) => {
            let status = validate_status(Some(&step.state.status))?;
            let latest = &result?["records"][0];
            if latest["value"]["status"].as_str() == Some(status.as_str()) {
                step.out.finish(json!({ "status": status, "unchanged": true, "uri": latest["uri"] }));
                return step.handle();
            }
            let now = Datetime::new(args.now.clone()).map_err(|e| format!("invalid now: {e}"))?;
            let created_at = Datetime::try_from(now.to_utc()).map_err(|e| format!("invalid now: {e}"))?;
            step.out.call(Id::Create, create(args.did.as_str(), Status { status, created_at }));
        }
        On::Result(Id::Create, result) => step.out.finish(json!({ "status": step.state.status, "unchanged": false, "uri": result?["uri"] })),
        _ => {}
    }
    step.handle()
}

#[derive(Default, Deserialize, Serialize)]
struct ClearStatus {
    deletes: Fan,
    deleted: u64,
    failed: u64,
}

// list up to CLEAR_LIMIT statuses -> delete each (results arrive one by one) -> count.
fn clear_status_handler(run: Run<Args>) -> Result<Handle, String> {
    let (mut step, on) = Step::<Args, ClearStatus, Id>::new(run)?;
    let did = step.args.did.as_str();
    match on {
        On::Start => step.out.call(Id::List, list_statuses(&step.args.did, CLEAR_LIMIT)),
        On::Result(Id::List, result) => {
            let output = result?;
            let rkeys: Vec<String> = output["records"].as_array().into_iter().flatten().filter_map(|record| Some(record["uri"].as_str()?.parse::<AtUri>().ok()?.rkey)).take(CLEAR_LIMIT).collect();
            if rkeys.is_empty() {
                step.out.finish(json!({ "deleted": 0 }));
                return step.handle();
            }
            let deletes = rkeys.iter().enumerate().map(|(i, rkey)| (Id::Delete(i), delete_record(&AtUri::new(did, COLLECTION, rkey))));
            step.out.fan_out(&mut step.state.deletes, deletes);
        }
        On::Result(id @ Id::Delete(_), result) => {
            let s = &mut step.state;
            if result.is_ok() { s.deleted += 1 } else { s.failed += 1 }
            if s.deletes.land(&id, result).is_some() {
                step.out.finish(json!({ "deleted": s.deleted, "failed": s.failed }));
            }
        }
        _ => {}
    }
    step.handle()
}

entries!(
    set_status => set_status_handler,
    clear_status => clear_status_handler
);