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

johnandersen777.bsky.social/atlibs-market-status

socialweb.computer bidders' market state as an atlibs module

0
0
main
265 lines · 12.4 KB Raw History
use abi::repo::{get_record, put_record};
use abi::{AtUri, Call, Event, Handle, Run, entries};
use serde::Deserialize;
use serde_json::{Value, json};

// A bidder's market state, all by getRecord at computed keys (no listRecords):
//   offering/self and heartbeat/self in the bidder's repo (and the offering the beat names, if another);
//   badgeBlueKeys/association: the operator it claims; the operator's market.association
//   bidder_associate-<bidder DID, ':' -> '-'> confirms it.
// The bidders an account follows here: one computer.socialweb.bidders/self record {bidders: [DID]}.
const OFFERING: &str = "com.publicdomainrelay.temp.market.offering";
const HEARTBEAT: &str = "com.publicdomainrelay.temp.market.heartbeat";
const BADGE: &str = "com.publicdomainrelay.temp.badgeBlueKeys";
const ASSOCIATION: &str = "com.publicdomainrelay.temp.market.association";
const INDEX: &str = "computer.socialweb.bidders";
const MAX: usize = 50;

#[derive(Deserialize)]
struct Args {
    #[serde(default)]
    did: String,
    #[serde(default)]
    now: String,
    #[serde(default)]
    dids: Vec<String>,
    #[serde(default)]
    of: Option<String>,
    #[serde(default)]
    bidder: Option<String>,
}

fn read(id: String, repo: &str, collection: &str, rkey: &str) -> Call {
    get_record(&AtUri::new(repo, collection, rkey)).id(id).service(repo)
}

// A record, or null when it is not there (any failed read: the row says what it could see).
fn found(result: &abi::CallResult) -> Value {
    result.output().ok().filter(|o| o["value"].is_object()).map(|o| json!({ "uri": o["uri"], "cid": o["cid"], "value": o["value"] })).unwrap_or(Value::Null)
}

fn endpoints(offering: &Value) -> Value {
    let list = offering["value"]["endpoints"].as_array().cloned().unwrap_or_default();
    let mut out: Vec<Value> = list.iter().filter_map(|e| Some(json!({ "kind": e["kind"].as_str()?, "address": e["address"] }))).collect();
    if let Some(url) = offering["value"]["endpointUrl"].as_str() {
        out.push(json!({ "kind": "https", "address": url }));
    }
    json!(out)
}

// bidders {dids}: one row per DID: {did, offering, heartbeat, beatAt, endpoints, appliesTo, withdrawn, operator, claimedOperator}.
fn bidders_handler(run: Run<Args>) -> Result<Handle, String> {
    match run.event {
        Event::Start => {
            let mut dids: Vec<String> = Vec::new();
            for d in run.args.dids.iter().map(|d| d.trim()) {
                if !d.starts_with("did:") {
                    return Err(format!("not a DID: {d}"));
                }
                if !dids.iter().any(|x| x == d) {
                    dids.push(d.to_string());
                }
            }
            if dids.len() > MAX {
                return Err(format!("at most {MAX} DIDs"));
            }
            if dids.is_empty() {
                return Ok(Handle::new().result(json!({ "bidders": [] })).done());
            }
            let mut h = Handle::new();
            for (i, d) in dids.iter().enumerate() {
                h = h.call(read(format!("o:{i}"), d, OFFERING, "self")).call(read(format!("h:{i}"), d, HEARTBEAT, "self")).call(read(format!("b:{i}"), d, BADGE, "association"));
            }
            let rows: Vec<Value> = dids.iter().map(|d| json!({ "did": d, "offering": null, "heartbeat": null, "operator": null, "claimedOperator": null })).collect();
            Ok(h.state(json!({ "rows": rows, "pending": dids.len() * 3 })))
        }
        Event::Result { id, result, .. } => {
            let mut s = run.state;
            let (kind, i) = id.split_once(':').ok_or("bad id")?;
            let i: usize = i.parse().map_err(|_| "bad id")?;
            let did = s["rows"][i]["did"].as_str().unwrap_or_default().to_string();
            let mut h = Handle::new();
            let mut more = 0;
            let record = found(&result);
            match kind {
                "o" | "n" => {
                    if !record.is_null() {
                        s["rows"][i]["offering"] = record;
                    }
                }
                "h" => {
                    let named = record["value"]["offering"].as_str().unwrap_or_default().to_string();
                    s["rows"][i]["heartbeat"] = record;
                    let own = format!("at://{did}/{OFFERING}/");
                    if let Some(rkey) = named.strip_prefix(&own).filter(|r| *r != "self" && !r.is_empty()) {
                        h = h.call(read(format!("n:{i}"), &did, OFFERING, rkey));
                        more += 1;
                    }
                }
                "b" => {
                    let v = &record["value"];
                    let op = v["challenge"].as_str().unwrap_or_default();
                    if op.starts_with("did:") && op != did && v["keyId"] == did.as_str() && v["service"] == "bidder_associate" {
                        s["rows"][i]["claimedOperator"] = json!(op);
                        let rkey = format!("bidder_associate-{}", did.replace(':', "-"));
                        h = h.call(read(format!("a:{i}"), op, ASSOCIATION, &rkey)).call(read(format!("c:{i}"), op, BADGE, &rkey));
                        more += 2;
                    }
                }
                // the operator's side: a standing association, or the same claim in its own badgeBlueKeys
                "a" | "c" => {
                    let op = s["rows"][i]["claimedOperator"].clone();
                    let v = &record["value"];
                    let held = if kind == "a" {
                        v["subject"] == did.as_str() && v["service"] == "bidder_associate" && v.get("contract").is_none() && v.get("expiresAt").is_none()
                    } else {
                        v["challenge"] == op && v["keyId"] == did.as_str() && v["service"] == "bidder_associate"
                    };
                    if held {
                        s["rows"][i]["operator"] = op;
                    }
                }
                _ => {}
            }
            let pending = s["pending"].as_u64().unwrap_or(1) - 1 + more;
            s["pending"] = json!(pending);
            if pending > 0 {
                return Ok(h.state(s));
            }
            let rows: Vec<Value> = s["rows"].as_array().cloned().unwrap_or_default().into_iter().map(finish).collect();
            Ok(Handle::new().result(json!({ "bidders": rows, "at": run.args.now })).done())
        }
        _ => Ok(Handle::new()),
    }
}

fn finish(mut row: Value) -> Value {
    let offering = row["offering"].clone();
    row["endpoints"] = endpoints(&offering);
    row["appliesTo"] = offering["value"]["appliesTo"].clone();
    row["withdrawn"] = json!(offering["value"]["withdrawnAt"].is_string());
    row["beatAt"] = row["heartbeat"]["value"]["createdAt"].clone();
    row
}

fn index_of(run: &Run<Args>) -> Result<String, String> {
    let of = run.args.of.clone().filter(|s| !s.is_empty()).unwrap_or(run.args.did.clone());
    of.starts_with("did:").then_some(of).ok_or("of: a DID (or sign in)".into())
}

fn listed(result: &abi::CallResult) -> Vec<String> {
    found(result)["value"]["bidders"].as_array().into_iter().flatten().filter_map(|d| d.as_str().map(str::to_string)).collect()
}

// known {of?}: the bidders an account follows here (default: you).
fn known_handler(run: Run<Args>) -> Result<Handle, String> {
    let of = index_of(&run)?;
    Ok(match run.event {
        Event::Start => Handle::new().call(read("index".into(), &of, INDEX, "self")),
        Event::Result { id, result, .. } if id == "index" => Handle::new().result(json!({ "of": of, "bidders": listed(&result) })).done(),
        _ => Handle::new(),
    })
}

fn edit(run: Run<Args>, add: bool) -> Result<Handle, String> {
    if !run.args.did.starts_with("did:") {
        return Err("sign in first".into());
    }
    let bidder = run.args.bidder.clone().map(|b| b.trim().to_string()).filter(|b| b.starts_with("did:")).ok_or("bidder: a DID")?;
    let me = run.args.did.clone();
    Ok(match run.event {
        Event::Start => Handle::new().call(read("index".into(), &me, INDEX, "self")),
        Event::Result { id, result, .. } if id == "index" => {
            let mut list = listed(&result);
            list.retain(|d| *d != bidder);
            if add {
                if list.len() >= MAX {
                    return Err(format!("at most {MAX} bidders"));
                }
                list.push(bidder.clone());
            }
            let record = json!({ "$type": INDEX, "bidders": list, "updatedAt": run.args.now });
            Handle::new().call(put_record(&AtUri::new(&me, INDEX, "self"), record).id("put")).state(json!({ "bidders": list }))
        }
        Event::Result { id, result, .. } if id == "put" => {
            result.output()?;
            Handle::new().result(json!({ "bidders": run.state["bidders"] })).done()
        }
        _ => Handle::new(),
    })
}

fn track_handler(run: Run<Args>) -> Result<Handle, String> {
    edit(run, true)
}

fn untrack_handler(run: Run<Args>) -> Result<Handle, String> {
    edit(run, false)
}

entries!(
    bidders => bidders_handler,
    known => known_handler,
    track => track_handler,
    untrack => untrack_handler
);

#[cfg(test)]
mod tests {
    use serde_json::{Value, json};

    type H = fn(abi::Run<super::Args>) -> Result<abi::Handle, String>;
    fn step(f: H, args: &Value, event: Value, state: &Value) -> Value {
        let mut input = args.clone();
        input["event"] = event;
        input["state"] = state.clone();
        serde_json::to_value(f(serde_json::from_value(input).unwrap()).unwrap()).unwrap()
    }
    fn res(id: &str, output: Value) -> Value {
        json!({ "type": "result", "id": id, "ok": true, "output": output })
    }
    fn miss(id: &str) -> Value {
        json!({ "type": "result", "id": id, "ok": false, "error": "RecordNotFound" })
    }
    const B: &str = "did:plc:bidder";
    const OP: &str = "did:plc:op";

    #[test]
    fn a_bidder_with_a_named_offering_and_a_confirmed_operator() {
        let args = json!({ "did": "did:plc:me", "now": "2026-11-10T00:00:00Z", "dids": [B, B] });
        let s = step(super::bidders_handler, &args, json!({ "type": "start" }), &Value::Null);
        assert_eq!(s["calls"].as_array().unwrap().len(), 3, "a repeated DID is read once");
        let mut state = s["state"].clone();
        let s = step(super::bidders_handler, &args, miss("o:0"), &state);
        state = s["state"].clone();
        let named = format!("at://{B}/com.publicdomainrelay.temp.market.offering/3abc");
        let s = step(super::bidders_handler, &args, res("h:0", json!({ "uri": "h", "cid": "c", "value": { "offering": named, "createdAt": "2026-11-09T00:00:00Z" } })), &state);
        assert_eq!(s["calls"][0]["params"]["rkey"], "3abc");
        state = s["state"].clone();
        let s = step(super::bidders_handler, &args, res("b:0", json!({ "uri": "b", "cid": "c", "value": { "challenge": OP, "keyId": B, "service": "bidder_associate" } })), &state);
        assert_eq!(s["calls"][0]["params"]["rkey"], "bidder_associate-did-plc-bidder");
        assert_eq!(s["calls"][0]["service"], OP);
        state = s["state"].clone();
        let s = step(super::bidders_handler, &args, miss("c:0"), &state);
        state = s["state"].clone();
        let s = step(super::bidders_handler, &args, res("n:0", json!({ "uri": named, "cid": "c", "value": { "appliesTo": ["x"], "endpoints": [{ "kind": "iroh", "address": "t" }] } })), &state);
        state = s["state"].clone();
        let done = step(super::bidders_handler, &args, res("a:0", json!({ "uri": "a", "cid": "c", "value": { "subject": B, "service": "bidder_associate" } })), &state);
        let row = &done["result"]["bidders"][0];
        assert_eq!(row["operator"], OP);
        assert_eq!(row["beatAt"], "2026-11-09T00:00:00Z");
        assert_eq!(row["endpoints"], json!([{ "kind": "iroh", "address": "t" }]));
        assert_eq!(row["withdrawn"], false);
    }

    #[test]
    fn track_and_untrack_rewrite_the_one_index_record() {
        let args = json!({ "did": "did:plc:me", "now": "n", "bidder": B });
        let s = step(super::track_handler, &args, json!({ "type": "start" }), &Value::Null);
        assert_eq!(s["calls"][0]["params"]["collection"], "computer.socialweb.bidders");
        let s = step(super::track_handler, &args, res("index", json!({ "uri": "u", "cid": "c", "value": { "bidders": ["did:plc:x", B] } })), &Value::Null);
        assert_eq!(s["calls"][0]["input"]["record"]["bidders"], json!(["did:plc:x", B]));
        let s = step(super::untrack_handler, &args, miss("index"), &Value::Null);
        assert_eq!(s["calls"][0]["input"]["record"]["bidders"], json!([]));
    }
}