0
2
mcp: grain-mcp moderates, and never shows a model child safety
Only what this pull adds. It is built on grain runs on a Rust server modelled on knotless, without hatk.
crates/grain/src/moderation.rs+16−0
| @@ -24,6 +24,7 @@ pub const UPDATE_STATUS: &str = "com.atproto.admin.updateSubjectStatus"; | ||
pub const GET_STATUS: &str = "com.atproto.admin.getSubjectStatus"; | ||
pub const LIST_REPORTS: &str = "social.grain.admin.listReports"; | ||
pub const RESOLVE_REPORT: &str = "social.grain.admin.resolveReport"; | ||
pub const GET_REPORT: &str = "social.grain.admin.getReport"; | ||
pub const APPLY_LABEL: &str = "social.grain.admin.applyLabel"; | ||
pub const NEGATE_LABEL: &str = "social.grain.admin.negateLabel"; | ||
pub const LIST_DEFINITIONS: &str = "social.grain.admin.listLabelDefinitions"; | ||
| @@ -37,6 +38,7 @@ pub const CALLED: &[&str] = &[ | ||
GET_STATUS, | ||
LIST_REPORTS, | ||
RESOLVE_REPORT, | ||
GET_REPORT, | ||
APPLY_LABEL, | ||
NEGATE_LABEL, | ||
LIST_DEFINITIONS, | ||
| @@ -196,6 +198,13 @@ impl Appview { | ||
self.query(LIST_REPORTS, ¶ms).await | ||
} | ||
| ||
/// The report `id`, in any state. | ||
pub async fn report_by_id(&self, id: i64) -> Result<Value> { | ||
let id = id.to_string(); | ||
let found = self.query(GET_REPORT, &[("id", &id)]).await?; | ||
Ok(found["report"].clone()) | ||
} | ||
| ||
/// Closes the open report `id`: resolving it gives out the label it | ||
/// asked for, dismissing it does not. | ||
pub async fn close(&self, id: i64, resolve: bool) -> Result<()> { | ||
| @@ -243,6 +252,13 @@ impl Appview { | ||
let did = account(&self.ctx, who).await?; | ||
self.query(GET_SUBJECT, &[("did", &did)]).await | ||
} | ||
| ||
/// The labels standing on `patterns`, each a subject or a prefix | ||
/// ending in `*`. Anyone may ask. | ||
pub async fn query_labels(&self, patterns: &[&str]) -> Result<Value> { | ||
let params: Vec<(&str, &str)> = patterns.iter().map(|p| ("uriPatterns", *p)).collect(); | ||
self.xrpc.query(QUERY_LABELS, ¶ms, None).await | ||
} | ||
} | ||
| ||
#[cfg(test)] | ||
crates/grain-mcp/src/lib.rsnew+498−0
498 lines changed: not loaded until asked for.
crates/grain-mcp/src/main.rsnew+18−0
| @@ -0,0 +1,18 @@ | ||
//! `grain-mcp`: grain's moderation over MCP on stdin and stdout | ||
//! ([`grain_mcp`]). | ||
| ||
fn main() -> std::process::ExitCode { | ||
if std::env::args().len() > 1 { | ||
eprintln!("usage: grain-mcp"); | ||
return std::process::ExitCode::from(2); | ||
} | ||
let ctx = grain::env::Ctx::new(grain::env::Env::from_process()); | ||
let rt = tokio::runtime::Runtime::new().expect("a runtime builds"); | ||
match rt.block_on(grain_mcp::serve_stdio(ctx)) { | ||
Ok(()) => std::process::ExitCode::SUCCESS, | ||
Err(err) => { | ||
eprintln!("grain-mcp: {err:#}"); | ||
std::process::ExitCode::FAILURE | ||
} | ||
} | ||
} | ||
crates/grain-mcp/tests/mcp.rsnew+352−0
| @@ -0,0 +1,352 @@ | ||
//! grain-mcp driven by an MCP client, as an admin of grain-serve on a port | ||
//! of its own, with service tokens from the reference PDS: everything it | ||
//! does, and that nothing a child-safety report or label is about reaches | ||
//! the client. | ||
| ||
#[path = "../../grain/tests/common/mod.rs"] | ||
mod net; | ||
| ||
use grain::env::Ctx; | ||
use grain_serve::App; | ||
use grain_serve::index::moderation::NewReport; | ||
use grain_serve::index::{Index, Label}; | ||
use rmcp::ServiceExt; | ||
use rmcp::model::CallToolRequestParams; | ||
use serde_json::{Value, json}; | ||
use std::sync::{Arc, Mutex}; | ||
| ||
const NOW: &str = "2026-10-31T12:00:00.000Z"; | ||
/// The appview's DID: one of where it serves would name its port, which | ||
/// no atproto DID does. | ||
const AUD: &str = "did:web:grain.test"; | ||
/// An account a child-safety report is about, and one with a `csae` label. | ||
const BOB: &str = "did:plc:bob"; | ||
const DAVE: &str = "did:plc:dave"; | ||
/// An account with nothing about the safety of a child. | ||
const CAROL: &str = "did:plc:carol"; | ||
/// What must never reach the client. | ||
const SECRETS: &[&str] = &[ | ||
"the words of the child-safety report", | ||
"a gallery a child-safety report is about", | ||
"a gallery of an account labeled csae", | ||
"a spam report on an account with a child-safety report", | ||
]; | ||
| ||
struct World { | ||
base: String, | ||
index: Arc<Index>, | ||
dir: tempfile::TempDir, | ||
} | ||
| ||
async fn world(admin: &str) -> World { | ||
let dir = tempfile::tempdir().unwrap(); | ||
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap(); | ||
let base = format!("http://{}", listener.local_addr().unwrap()); | ||
let index = Arc::new(Index::memory().unwrap()); | ||
let mut app = App::bare(net::ctx(&dir.path().join("serve"), &[])); | ||
app.index = Some(index.clone()); | ||
app.did = Some(AUD.into()); | ||
app.admins = vec![admin.to_string()]; | ||
tokio::spawn(grain_serve::serve(listener, Arc::new(app))); | ||
for (did, handle, title) in [ | ||
(BOB, "bob.test", SECRETS[1]), | ||
(DAVE, "dave.test", SECRETS[2]), | ||
(CAROL, "carol.test", "Carol's rocks"), | ||
] { | ||
index.track(did).unwrap(); | ||
index.set_handle(did, handle).unwrap(); | ||
let uri = format!("at://{did}/social.grain.gallery/3l3qo2vuowo2a"); | ||
let cid = grain::drisl::cid_text(&grain::drisl::cid(uri.as_bytes())); | ||
let record = json!({"$type": "social.grain.gallery", "title": title, "createdAt": NOW}); | ||
index.put_at(&uri, &cid, &record, NOW).unwrap(); | ||
} | ||
World { base, index, dir } | ||
} | ||
| ||
impl World { | ||
/// `account` on this machine, at this appview, logging in by password. | ||
fn ctx(&self, account: &net::Account) -> Ctx { | ||
net::ctx( | ||
&self.dir.path().join(&account.handle), | ||
&[ | ||
("GRAIN_APPVIEW", &self.base), | ||
("GRAIN_APPVIEW_DID", AUD), | ||
("GRAIN_IDENTIFIER", &account.did), | ||
("GRAIN_PASSWORD", &account.password), | ||
], | ||
) | ||
} | ||
} | ||
| ||
/// An MCP client of grain-mcp, keeping every text it is given. | ||
struct Client { | ||
peer: rmcp::service::RunningService<rmcp::RoleClient, ()>, | ||
seen: Mutex<Vec<String>>, | ||
} | ||
| ||
impl Client { | ||
async fn of(ctx: Ctx) -> Client { | ||
let (server_io, client_io) = tokio::io::duplex(1 << 20); | ||
tokio::spawn(async move { | ||
let running = grain_mcp::Server::new(ctx).serve(server_io).await.unwrap(); | ||
let _ = running.waiting().await; | ||
}); | ||
let peer = ().serve(client_io).await.unwrap(); | ||
Client { | ||
peer, | ||
seen: Mutex::default(), | ||
} | ||
} | ||
| ||
/// The tool's text, and whether it was an error. | ||
async fn call(&self, tool: &str, args: Value) -> (String, bool) { | ||
let params = CallToolRequestParams::new(tool.to_string()) | ||
.with_arguments(args.as_object().unwrap().clone()); | ||
let res = self.peer.call_tool(params).await.unwrap(); | ||
let text = res.content[0].as_text().unwrap().text.clone(); | ||
self.seen.lock().unwrap().push(text.clone()); | ||
(text, res.is_error == Some(true)) | ||
} | ||
| ||
async fn json(&self, tool: &str, args: Value) -> Value { | ||
let (text, err) = self.call(tool, args).await; | ||
assert!(!err, "{tool}: {text}"); | ||
serde_json::from_str(&text).unwrap() | ||
} | ||
| ||
/// The tool's error. | ||
async fn refused(&self, tool: &str, args: Value) -> String { | ||
let (text, err) = self.call(tool, args).await; | ||
assert!(err, "{tool} was not refused: {text}"); | ||
text | ||
} | ||
} | ||
| ||
#[tokio::test] | ||
async fn an_admin_moderates_and_never_sees_what_child_safety_is_about() { | ||
let (admin, someone) = (net::account().await, net::account().await); | ||
let w = world(&admin.did).await; | ||
w.index | ||
.label(&Label { | ||
src: AUD.into(), | ||
uri: DAVE.into(), | ||
val: "csae".into(), | ||
neg: false, | ||
cts: NOW.into(), | ||
exp: None, | ||
}) | ||
.unwrap(); | ||
| ||
// Someone reports, by MCP too. | ||
let theirs = Client::of(w.ctx(&someone)).await; | ||
let args = json!({"subject": BOB, "reason": "child-safety", "text": SECRETS[0]}); | ||
let csae = theirs.json("report", args).await["id"].as_i64().unwrap(); | ||
let args = json!({"subject": CAROL, "reason": "spam", "text": "ads"}); | ||
let spam = theirs.json("report", args).await["id"].as_i64().unwrap(); | ||
let args = json!({"subject": BOB, "reason": "spam", "text": SECRETS[3]}); | ||
let on_bob = theirs.json("report", args).await["id"].as_i64().unwrap(); | ||
let why = theirs.refused("list_reports", json!({})).await; | ||
assert!(why.contains("is not an admin of this server"), "{why}"); | ||
| ||
let c = Client::of(w.ctx(&admin)).await; | ||
| ||
// The tools say what they withhold. | ||
let tools = c.peer.list_all_tools().await.unwrap(); | ||
for name in [ | ||
"list_reports", | ||
"resolve_report", | ||
"takedown", | ||
"label", | ||
"account", | ||
] { | ||
let tool = tools.iter().find(|t| t.name == name).unwrap(); | ||
let about = tool.description.as_deref().unwrap_or_default(); | ||
assert!(about.contains("child-safety"), "{name}: {about}"); | ||
assert!(about.contains("grain command line"), "{name}: {about}"); | ||
} | ||
let info = c.peer.peer_info().unwrap(); | ||
let said = info.instructions.as_deref().unwrap(); | ||
assert!(said.contains("never shown here"), "{said}"); | ||
| ||
// A child-safety report is listed by its ids alone, and so is any | ||
// other report on its account. | ||
let listed = c.json("list_reports", json!({})).await; | ||
assert_eq!(listed["total"], 3); | ||
let reports = listed["reports"].as_array().unwrap(); | ||
let of = |id: i64| reports.iter().find(|r| r["id"] == id).unwrap().clone(); | ||
let (shown, plain) = (of(csae), of(spam)); | ||
assert_eq!(shown["subject"], BOB); | ||
assert_eq!(shown["label"], "csae"); | ||
assert_eq!(shown["status"], "open"); | ||
assert_eq!(shown["withheld"], grain_mcp::HUMAN); | ||
assert!(shown.get("reason").is_none(), "{shown}"); | ||
let beside = of(on_bob); | ||
assert_eq!( | ||
(&beside["label"], &beside["subjectDid"]), | ||
(&json!("spam"), &json!(BOB)) | ||
); | ||
assert_eq!(beside["withheld"], grain_mcp::HUMAN); | ||
assert!(beside.get("reason").is_none(), "{beside}"); | ||
assert_eq!(plain["reason"], "ads"); | ||
let only = c | ||
.json("list_reports", json!({"label": "spam", "limit": 1})) | ||
.await; | ||
assert_eq!(only["reports"].as_array().unwrap().len(), 1); | ||
| ||
// An account with a child-safety report, or a csae label, is shown | ||
// without its records; one with neither, whole. | ||
for actor in [BOB, DAVE] { | ||
let account = c.json("account", json!({"actor": actor})).await; | ||
assert_eq!(account["childSafety"], true, "{account}"); | ||
assert_eq!(account["withheld"], grain_mcp::HUMAN); | ||
assert!(account.get("records").is_none(), "{account}"); | ||
assert_eq!(account["counts"][0]["count"], 1, "{account}"); | ||
} | ||
let bob = c.json("account", json!({"actor": BOB})).await; | ||
let ids: Vec<&Value> = bob["reports"] | ||
.as_array() | ||
.unwrap() | ||
.iter() | ||
.map(|r| &r["id"]) | ||
.collect(); | ||
assert_eq!(ids.len(), 2, "{bob}"); | ||
assert!( | ||
ids.contains(&&json!(csae)) && ids.contains(&&json!(on_bob)), | ||
"{bob}" | ||
); | ||
let carol = c.json("account", json!({"actor": CAROL})).await; | ||
assert_eq!(carol["childSafety"], false); | ||
assert_eq!(carol["records"][0]["value"]["title"], "Carol's rocks"); | ||
assert_eq!(carol["reports"][0]["reason"], "ads"); | ||
| ||
// A report child safety by its reason type alone, whatever label it | ||
// was filed under, is as much a person's. | ||
let by_reason = w | ||
.index | ||
.file_report(&NewReport { | ||
subject: DAVE, | ||
subject_did: DAVE, | ||
label: "other", | ||
reason_type: Some("tools.ozone.report.defs#reasonChildSafetyGroom"), | ||
reason: Some(SECRETS[0]), | ||
reported_by: &someone.did, | ||
created_at: NOW, | ||
}) | ||
.unwrap() | ||
.id; | ||
let others = c.json("list_reports", json!({"label": "other"})).await; | ||
assert_eq!(others["reports"][0]["id"], by_reason); | ||
assert_eq!(others["reports"][0]["withheld"], grain_mcp::HUMAN); | ||
let args = json!({"id": by_reason, "action": "dismiss"}); | ||
assert_eq!(c.refused("resolve_report", args).await, grain_mcp::HUMAN); | ||
assert_eq!(w.index.report(by_reason).unwrap().unwrap().status, "open"); | ||
| ||
// Acting on child safety is for a person. | ||
for (tool, args) in [ | ||
("resolve_report", json!({"id": csae, "action": "resolve"})), | ||
("resolve_report", json!({"id": csae, "action": "dismiss"})), | ||
("resolve_report", json!({"id": on_bob, "action": "resolve"})), | ||
("takedown", json!({"subject": BOB})), | ||
("takedown", json!({"subject": DAVE, "applied": false})), | ||
( | ||
"takedown", | ||
json!({"subject": format!("at://{BOB}/social.grain.gallery/3l3qo2vuowo2a")}), | ||
), | ||
("label", json!({"subject": BOB, "val": "spam"})), | ||
("label", json!({"subject": CAROL, "val": "csae"})), | ||
( | ||
"label", | ||
json!({"subject": DAVE, "val": "csae", "negate": true}), | ||
), | ||
] { | ||
assert_eq!( | ||
c.refused(tool, args.clone()).await, | ||
grain_mcp::HUMAN, | ||
"{tool} {args}" | ||
); | ||
} | ||
assert_eq!(w.index.report(csae).unwrap().unwrap().status, "open"); | ||
| ||
// Everything else is the model's to do. | ||
let closed = c | ||
.json("resolve_report", json!({"id": spam, "action": "resolve"})) | ||
.await; | ||
assert_eq!(closed, json!({"id": spam, "status": "resolved"})); | ||
let why = c | ||
.refused("resolve_report", json!({"id": spam, "action": "maybe"})) | ||
.await; | ||
assert_eq!(why, "action is resolve or dismiss"); | ||
let why = c | ||
.refused("resolve_report", json!({"id": spam, "action": "dismiss"})) | ||
.await; | ||
assert!(why.contains("ReportNotFound"), "{why}"); | ||
// One it cannot read by its id is not closed: it fails closed. | ||
let why = c | ||
.refused("resolve_report", json!({"id": 999, "action": "dismiss"})) | ||
.await; | ||
assert!(why.starts_with("report 999 could not be read"), "{why}"); | ||
assert!(why.contains("ReportNotFound"), "{why}"); | ||
c.json("takedown", json!({"subject": CAROL})).await; | ||
let status = c.json("moderation_status", json!({"subject": CAROL})).await; | ||
assert_eq!(status["takedown"]["applied"], true); | ||
c.json("takedown", json!({"subject": CAROL, "applied": false})) | ||
.await; | ||
let labeled = c | ||
.json("label", json!({"subject": CAROL, "val": "nudity"})) | ||
.await; | ||
assert_eq!( | ||
labeled, | ||
json!({"subject": CAROL, "val": "nudity", "neg": false}) | ||
); | ||
let labels = c.json("query_labels", json!({"patterns": [CAROL]})).await; | ||
let vals: Vec<&str> = labels["labels"] | ||
.as_array() | ||
.unwrap() | ||
.iter() | ||
.map(|l| l["val"].as_str().unwrap()) | ||
.collect(); | ||
assert_eq!(vals, ["spam", "nudity"]); | ||
let defs = c.json("list_label_definitions", json!({})).await; | ||
assert_eq!( | ||
defs["definitions"].as_array().unwrap().len(), | ||
grain::labels::DEFINITIONS.len() | ||
); | ||
| ||
// Nothing a child-safety report or label is about was ever said. | ||
let seen = c.seen.lock().unwrap().join("\n") + &theirs.seen.lock().unwrap().join("\n"); | ||
for secret in SECRETS { | ||
assert!(!seen.contains(secret), "{secret} reached the client"); | ||
} | ||
} | ||
| ||
#[tokio::test] | ||
async fn with_no_login_a_tool_says_so() { | ||
let w = world("did:plc:nobody").await; | ||
let dir = w.dir.path().join("nobody"); | ||
let c = Client::of(net::ctx(&dir, &[("GRAIN_APPVIEW", &w.base)])).await; | ||
assert_eq!( | ||
c.refused("list_label_definitions", json!({})).await, | ||
"not logged in" | ||
); | ||
} | ||
| ||
#[test] | ||
fn the_binary_takes_no_arguments() { | ||
let bin = env!("CARGO_BIN_EXE_grain-mcp"); | ||
let out = std::process::Command::new(bin) | ||
.arg("--nope") | ||
.output() | ||
.unwrap(); | ||
assert_eq!(out.status.code(), Some(2)); | ||
assert_eq!( | ||
String::from_utf8_lossy(&out.stderr).trim(), | ||
"usage: grain-mcp" | ||
); | ||
// With no client on stdin, it ends. | ||
let out = std::process::Command::new(bin) | ||
.stdin(std::process::Stdio::null()) | ||
.env("GRAIN_CONFIG_DIR", tempfile::tempdir().unwrap().path()) | ||
.output() | ||
.unwrap(); | ||
assert_ne!(out.status.code(), Some(2), "{out:?}"); | ||
} | ||
crates/grain-mcp/Cargo.tomlnew+22−0
| @@ -0,0 +1,22 @@ | ||
[package] | ||
name = "grain-mcp" | ||
description = "An MCP server for grain's moderation, which never shows a model what a child-safety report is about" | ||
version.workspace = true | ||
edition.workspace = true | ||
license.workspace = true | ||
rust-version.workspace = true | ||
| ||
[dependencies] | ||
grain.workspace = true | ||
anyhow.workspace = true | ||
rmcp = { version = "3.6", features = ["server", "transport-io", "macros"] } | ||
schemars = "1" | ||
serde.workspace = true | ||
serde_json.workspace = true | ||
tokio.workspace = true | ||
| ||
[dev-dependencies] | ||
grain-serve.workspace = true | ||
reqwest.workspace = true | ||
rmcp = { version = "3.6", features = ["client"] } | ||
tempfile.workspace = true | ||
crates/grain-serve/src/moderation.rs+26−2
| @@ -42,8 +42,8 @@ use std::sync::Arc; | ||
use unicode_segmentation::UnicodeSegmentation; | ||
| ||
pub use grain::moderation::{ | ||
APPLY_LABEL, CREATE_REPORT, GET_STATUS, GET_SUBJECT, LIST_DEFINITIONS, LIST_REPORTS, | ||
NEGATE_LABEL, QUERY_LABELS, RESOLVE_REPORT, UPDATE_STATUS, | ||
APPLY_LABEL, CREATE_REPORT, GET_REPORT, GET_STATUS, GET_SUBJECT, LIST_DEFINITIONS, | ||
LIST_REPORTS, NEGATE_LABEL, QUERY_LABELS, RESOLVE_REPORT, UPDATE_STATUS, | ||
}; | ||
| ||
const REPO_REF: &str = "com.atproto.admin.defs#repoRef"; | ||
| @@ -65,6 +65,7 @@ pub fn routes(router: Router<Arc<App>>) -> Router<Arc<App>> { | ||
.route(&at(UPDATE_STATUS), post(update_subject_status)) | ||
.route(&at(GET_STATUS), get(get_subject_status)) | ||
.route(&at(LIST_REPORTS), get(list_reports)) | ||
.route(&at(GET_REPORT), get(get_report)) | ||
.route(&at(RESOLVE_REPORT), post(resolve_report)) | ||
.route(&at(APPLY_LABEL), post(apply_label)) | ||
.route(&at(NEGATE_LABEL), post(negate_label)) | ||
| @@ -374,6 +375,29 @@ pub async fn list_reports( | ||
Ok(Json(out)) | ||
} | ||
| ||
/// `social.grain.admin.getReport`: a report by its id, in any state. | ||
pub async fn get_report( | ||
State(app): State<Arc<App>>, | ||
headers: HeaderMap, | ||
uri: Uri, | ||
) -> Result<Json<Value>, Failure> { | ||
let index = index(&app)?; | ||
admin(&app, &headers, GET_REPORT).await?; | ||
let params = params(&uri); | ||
let id = params.get("id").map(str::parse::<i64>); | ||
let Some(Ok(id)) = id else { | ||
return Err(Failure::invalid("id is a report's id")); | ||
}; | ||
match index.report(id)? { | ||
Some(report) => Ok(Json(json!({ "report": report_view(&report) }))), | ||
None => Err(Failure::new( | ||
StatusCode::BAD_REQUEST, | ||
"ReportNotFound", | ||
format!("no report {id}"), | ||
)), | ||
} | ||
} | ||
| ||
#[derive(Deserialize)] | ||
struct Resolution { | ||
id: i64, | ||
crates/grain-serve/tests/moderation.rs+22−0
| @@ -26,6 +26,7 @@ const CAROL: &str = "did:plc:carol"; | ||
const REPORT: &str = "/xrpc/com.atproto.moderation.createReport"; | ||
const LIST: &str = "/xrpc/social.grain.admin.listReports"; | ||
const RESOLVE: &str = "/xrpc/social.grain.admin.resolveReport"; | ||
const GET: &str = "/xrpc/social.grain.admin.getReport"; | ||
const UPDATE: &str = "/xrpc/com.atproto.admin.updateSubjectStatus"; | ||
const STATUS: &str = "/xrpc/com.atproto.admin.getSubjectStatus"; | ||
const APPLY: &str = "/xrpc/social.grain.admin.applyLabel"; | ||
| @@ -257,6 +258,7 @@ async fn only_an_admin_with_a_token_for_the_method_moderates() { | ||
let w = world().await; | ||
let calls = [ | ||
(LIST, None), | ||
(GET, None), | ||
(STATUS, None), | ||
(DEFINITIONS, None), | ||
(SUBJECT, None), | ||
| @@ -332,6 +334,26 @@ async fn resolving_a_report_gives_out_its_label_and_dismissing_it_does_not() { | ||
); | ||
let dismiss = json!({"id": nudity["id"], "action": "dismiss"}); | ||
assert_eq!(w.post(&w.admin, RESOLVE, dismiss).await.0, 200); | ||
// A report is read by its id, in any state. | ||
let (status, got) = w.get(&w.admin, &format!("{GET}?id={}", spam["id"])).await; | ||
assert_eq!(status, 200); | ||
assert_eq!( | ||
(&got["report"]["id"], &got["report"]["status"]), | ||
(&spam["id"], &json!("resolved")) | ||
); | ||
let (status, out) = w.get(&w.admin, &format!("{GET}?id=999")).await; | ||
assert_eq!( | ||
(status, out["error"].as_str()), | ||
(400, Some("ReportNotFound")) | ||
); | ||
for wrong in ["", "?id=one"] { | ||
let (status, out) = w.get(&w.admin, &format!("{GET}{wrong}")).await; | ||
assert_eq!( | ||
(status, out["error"].as_str()), | ||
(400, Some("InvalidRequest")), | ||
"{wrong}" | ||
); | ||
} | ||
assert!(w.index.labels(CAROL).unwrap().is_empty()); | ||
let none = json!({"id": 999, "action": "dismiss"}); | ||
assert_eq!( | ||
lexicons/social/grain/admin/getReport.jsonnew+24−0
| @@ -0,0 +1,24 @@ | ||
{ | ||
"lexicon": 1, | ||
"id": "social.grain.admin.getReport", | ||
"defs": { | ||
"main": { | ||
"type": "query", | ||
"description": "A report by its id, in any state. Requires an admin session.", | ||
"parameters": { | ||
"type": "params", | ||
"required": ["id"], | ||
"properties": { "id": { "type": "integer" } } | ||
}, | ||
"output": { | ||
"encoding": "application/json", | ||
"schema": { | ||
"type": "object", | ||
"required": ["report"], | ||
"properties": { "report": { "type": "ref", "ref": "social.grain.admin.defs#reportView" } } | ||
} | ||
}, | ||
"errors": [{ "name": "ReportNotFound", "description": "No report by that id." }] | ||
} | ||
} | ||
} | ||
Cargo.lock+297−0
| @@ -52,6 +52,15 @@ dependencies = [ | ||
"memchr", | ||
] | ||
| ||
[[package]] | ||
name = "android_system_properties" | ||
version = "0.1.6" | ||
source = "registry+https://github.com/rust-lang/crates.io-index" | ||
checksum = "ae221649c9976a6f6c56ae1facf410f3ddb33cc661c4b7b61020a912d4237fbc" | ||
dependencies = [ | ||
"libc", | ||
] | ||
| ||
[[package]] | ||
name = "anyhow" | ||
version = "1.0.104" | ||
| @@ -267,6 +276,18 @@ dependencies = [ | ||
"rand_core 0.10.1", | ||
] | ||
| ||
[[package]] | ||
name = "chrono" | ||
version = "0.4.45" | ||
source = "registry+https://github.com/rust-lang/crates.io-index" | ||
checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" | ||
dependencies = [ | ||
"iana-time-zone", | ||
"num-traits", | ||
"serde", | ||
"windows-link", | ||
] | ||
| ||
[[package]] | ||
name = "ciborium" | ||
version = "0.2.2" | ||
| @@ -333,6 +354,12 @@ version = "0.9.6" | ||
source = "registry+https://github.com/rust-lang/crates.io-index" | ||
checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" | ||
| ||
[[package]] | ||
name = "core-foundation-sys" | ||
version = "0.8.7" | ||
source = "registry+https://github.com/rust-lang/crates.io-index" | ||
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" | ||
| ||
[[package]] | ||
name = "cpufeatures" | ||
version = "0.2.17" | ||
| @@ -398,6 +425,40 @@ dependencies = [ | ||
"cipher", | ||
] | ||
| ||
[[package]] | ||
name = "darling" | ||
version = "0.24.1" | ||
source = "registry+https://github.com/rust-lang/crates.io-index" | ||
checksum = "ed17f5901b6630b993ca003def43f2f8ef4014fc13b047b57aad617ff32bc2ec" | ||
dependencies = [ | ||
"darling_core", | ||
"darling_macro", | ||
] | ||
| ||
[[package]] | ||
name = "darling_core" | ||
version = "0.24.1" | ||
source = "registry+https://github.com/rust-lang/crates.io-index" | ||
checksum = "6837e2cf7485aaae18f86181d2f0e9a7ed297a025e220aeabf63fdebd3a2ddff" | ||
dependencies = [ | ||
"ident_case", | ||
"proc-macro2", | ||
"quote", | ||
"strsim", | ||
"syn 3.0.9", | ||
] | ||
| ||
[[package]] | ||
name = "darling_macro" | ||
version = "0.24.1" | ||
source = "registry+https://github.com/rust-lang/crates.io-index" | ||
checksum = "2ac7135c3ef02b2f7833bbeb1be5ba7f966dcde8a87c6b87f65a778d71a02785" | ||
dependencies = [ | ||
"darling_core", | ||
"quote", | ||
"syn 3.0.9", | ||
] | ||
| ||
[[package]] | ||
name = "data-encoding" | ||
version = "2.11.1" | ||
| @@ -444,6 +505,12 @@ dependencies = [ | ||
"syn 3.0.9", | ||
] | ||
| ||
[[package]] | ||
name = "dyn-clone" | ||
version = "1.0.20" | ||
source = "registry+https://github.com/rust-lang/crates.io-index" | ||
checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" | ||
| ||
[[package]] | ||
name = "ecdsa" | ||
version = "0.16.9" | ||
| @@ -804,6 +871,22 @@ dependencies = [ | ||
"tempfile", | ||
] | ||
| ||
[[package]] | ||
name = "grain-mcp" | ||
version = "0.1.0" | ||
dependencies = [ | ||
"anyhow", | ||
"grain", | ||
"grain-serve", | ||
"reqwest", | ||
"rmcp", | ||
"schemars", | ||
"serde", | ||
"serde_json", | ||
"tempfile", | ||
"tokio", | ||
] | ||
| ||
[[package]] | ||
name = "grain-serve" | ||
version = "0.1.0" | ||
| @@ -1040,6 +1123,30 @@ dependencies = [ | ||
"tracing", | ||
] | ||
| ||
[[package]] | ||
name = "iana-time-zone" | ||
version = "0.1.65" | ||
source = "registry+https://github.com/rust-lang/crates.io-index" | ||
checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" | ||
dependencies = [ | ||
"android_system_properties", | ||
"core-foundation-sys", | ||
"iana-time-zone-haiku", | ||
"js-sys", | ||
"log", | ||
"wasm-bindgen", | ||
"windows-core", | ||
] | ||
| ||
[[package]] | ||
name = "iana-time-zone-haiku" | ||
version = "0.1.2" | ||
source = "registry+https://github.com/rust-lang/crates.io-index" | ||
checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" | ||
dependencies = [ | ||
"cc", | ||
] | ||
| ||
[[package]] | ||
name = "icu_collections" | ||
version = "2.4.0" | ||
| @@ -1123,6 +1230,12 @@ dependencies = [ | ||
"zerovec", | ||
] | ||
| ||
[[package]] | ||
name = "ident_case" | ||
version = "1.0.1" | ||
source = "registry+https://github.com/rust-lang/crates.io-index" | ||
checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" | ||
| ||
[[package]] | ||
name = "idna" | ||
version = "1.1.0" | ||
| @@ -1184,6 +1297,8 @@ checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855" | ||
dependencies = [ | ||
"equivalent", | ||
"hashbrown 0.17.1", | ||
"serde", | ||
"serde_core", | ||
] | ||
| ||
[[package]] | ||
| @@ -1402,6 +1517,12 @@ dependencies = [ | ||
"sha2", | ||
] | ||
| ||
[[package]] | ||
name = "pastey" | ||
version = "0.2.3" | ||
source = "registry+https://github.com/rust-lang/crates.io-index" | ||
checksum = "2ee67f1008b1ba2321834326597b8e186293b049a023cdef258527550b9935b4" | ||
| ||
[[package]] | ||
name = "pem-rfc7468" | ||
version = "0.7.0" | ||
| @@ -1679,6 +1800,26 @@ dependencies = [ | ||
"once_cell", | ||
] | ||
| ||
[[package]] | ||
name = "ref-cast" | ||
version = "1.0.27" | ||
source = "registry+https://github.com/rust-lang/crates.io-index" | ||
checksum = "7e440fb4e4b4147295338efb76001ab9e4efc0e5839df2c47fc5ac2381d365c3" | ||
dependencies = [ | ||
"ref-cast-impl", | ||
] | ||
| ||
[[package]] | ||
name = "ref-cast-impl" | ||
version = "1.0.27" | ||
source = "registry+https://github.com/rust-lang/crates.io-index" | ||
checksum = "92ecd8964f8453721699a1ed72037b0db49ce2f5a5138486ee89bed6f67cdf3a" | ||
dependencies = [ | ||
"proc-macro2", | ||
"quote", | ||
"syn 3.0.9", | ||
] | ||
| ||
[[package]] | ||
name = "regex-automata" | ||
version = "0.4.18" | ||
| @@ -1786,6 +1927,44 @@ dependencies = [ | ||
"windows-sys 0.52.0", | ||
] | ||
| ||
[[package]] | ||
name = "rmcp" | ||
version = "3.8.0" | ||
source = "registry+https://github.com/rust-lang/crates.io-index" | ||
checksum = "f8be15815186873ea063bf005011d16c356804973489e40c39116ae3a2878d1f" | ||
dependencies = [ | ||
"base64 0.23.1", | ||
"chrono", | ||
"futures", | ||
"indexmap", | ||
"libc", | ||
"pastey", | ||
"pin-project-lite", | ||
"rmcp-macros", | ||
"schemars", | ||
"serde", | ||
"serde_json", | ||
"thiserror", | ||
"tokio", | ||
"tokio-stream", | ||
"tokio-util", | ||
"tracing", | ||
"uuid", | ||
] | ||
| ||
[[package]] | ||
name = "rmcp-macros" | ||
version = "3.8.0" | ||
source = "registry+https://github.com/rust-lang/crates.io-index" | ||
checksum = "8b0748e054a5f5598b3213cf72099ae6b2745ba4d996c59ccf455150a7dbb839" | ||
dependencies = [ | ||
"darling", | ||
"proc-macro2", | ||
"quote", | ||
"serde_json", | ||
"syn 3.0.9", | ||
] | ||
| ||
[[package]] | ||
name = "roxmltree" | ||
version = "0.21.2" | ||
| @@ -1886,6 +2065,32 @@ version = "1.0.23" | ||
source = "registry+https://github.com/rust-lang/crates.io-index" | ||
checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" | ||
| ||
[[package]] | ||
name = "schemars" | ||
version = "1.2.3" | ||
source = "registry+https://github.com/rust-lang/crates.io-index" | ||
checksum = "ab508826f74a77ca9d5aba6ff19b522583ee3eaf28a19384ff3d0e5835fadf6e" | ||
dependencies = [ | ||
"chrono", | ||
"dyn-clone", | ||
"ref-cast", | ||
"schemars_derive", | ||
"serde", | ||
"serde_json", | ||
] | ||
| ||
[[package]] | ||
name = "schemars_derive" | ||
version = "1.2.3" | ||
source = "registry+https://github.com/rust-lang/crates.io-index" | ||
checksum = "e1c3a92094fa7d61aa124645844facb6b554dfc797136d0f5fd1f890e2bffc69" | ||
dependencies = [ | ||
"proc-macro2", | ||
"quote", | ||
"serde_derive_internals", | ||
"syn 3.0.9", | ||
] | ||
| ||
[[package]] | ||
name = "sec1" | ||
version = "0.7.3" | ||
| @@ -1930,6 +2135,17 @@ dependencies = [ | ||
"syn 3.0.9", | ||
] | ||
| ||
[[package]] | ||
name = "serde_derive_internals" | ||
version = "0.30.0" | ||
source = "registry+https://github.com/rust-lang/crates.io-index" | ||
checksum = "f852137cce035d6a4df67ccce505ff6b3e9fd3a10e3e52b24dc71e650bb1a9bd" | ||
dependencies = [ | ||
"proc-macro2", | ||
"quote", | ||
"syn 3.0.9", | ||
] | ||
| ||
[[package]] | ||
name = "serde_json" | ||
version = "1.0.154" | ||
| @@ -2122,6 +2338,12 @@ dependencies = [ | ||
"float-cmp", | ||
] | ||
| ||
[[package]] | ||
name = "strsim" | ||
version = "0.11.1" | ||
source = "registry+https://github.com/rust-lang/crates.io-index" | ||
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" | ||
| ||
[[package]] | ||
name = "subtle" | ||
version = "2.6.1" | ||
| @@ -2301,6 +2523,17 @@ dependencies = [ | ||
"tokio", | ||
] | ||
| ||
[[package]] | ||
name = "tokio-stream" | ||
version = "0.1.19" | ||
source = "registry+https://github.com/rust-lang/crates.io-index" | ||
checksum = "a3d06f0b082ba57c26b79407372e57cf2a1e28124f78e9479fe80322cf53420b" | ||
dependencies = [ | ||
"futures-core", | ||
"pin-project-lite", | ||
"tokio", | ||
] | ||
| ||
[[package]] | ||
name = "tokio-tungstenite" | ||
version = "0.29.0" | ||
| @@ -2573,6 +2806,17 @@ version = "1.0.0" | ||
source = "registry+https://github.com/rust-lang/crates.io-index" | ||
checksum = "3a3b425286980a367278453f3b6d28cdcb134b574e5a35824a6ff5eebad50bb6" | ||
| ||
[[package]] | ||
name = "uuid" | ||
version = "1.29.0" | ||
source = "registry+https://github.com/rust-lang/crates.io-index" | ||
checksum = "4a0e2d11620c50695fe12bbbfde96b3477fe606e7284c22d505c12ce51214514" | ||
dependencies = [ | ||
"getrandom 0.4.3", | ||
"js-sys", | ||
"wasm-bindgen", | ||
] | ||
| ||
[[package]] | ||
name = "valuable" | ||
version = "0.1.1" | ||
| @@ -2714,12 +2958,65 @@ version = "0.1.12" | ||
source = "registry+https://github.com/rust-lang/crates.io-index" | ||
checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88" | ||
| ||
[[package]] | ||
name = "windows-core" | ||
version = "0.62.2" | ||
source = "registry+https://github.com/rust-lang/crates.io-index" | ||
checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" | ||
dependencies = [ | ||
"windows-implement", | ||
"windows-interface", | ||
"windows-link", | ||
"windows-result", | ||
"windows-strings", | ||
] | ||
| ||
[[package]] | ||
name = "windows-implement" | ||
version = "0.60.2" | ||
source = "registry+https://github.com/rust-lang/crates.io-index" | ||
checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" | ||
dependencies = [ | ||
"proc-macro2", | ||
"quote", | ||
"syn 2.0.119", | ||
] | ||
| ||
[[package]] | ||
name = "windows-interface" | ||
version = "0.59.3" | ||
source = "registry+https://github.com/rust-lang/crates.io-index" | ||
checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" | ||
dependencies = [ | ||
"proc-macro2", | ||
"quote", | ||
"syn 2.0.119", | ||
] | ||
| ||
[[package]] | ||
name = "windows-link" | ||
version = "0.2.1" | ||
source = "registry+https://github.com/rust-lang/crates.io-index" | ||
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" | ||
| ||
[[package]] | ||
name = "windows-result" | ||
version = "0.4.1" | ||
source = "registry+https://github.com/rust-lang/crates.io-index" | ||
checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" | ||
dependencies = [ | ||
"windows-link", | ||
] | ||
| ||
[[package]] | ||
name = "windows-strings" | ||
version = "0.5.1" | ||
source = "registry+https://github.com/rust-lang/crates.io-index" | ||
checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" | ||
dependencies = [ | ||
"windows-link", | ||
] | ||
| ||
[[package]] | ||
name = "windows-sys" | ||
version = "0.52.0" | ||
Cargo.toml+1−1
| @@ -1,5 +1,5 @@ | ||
[workspace] | ||
members = ["crates/grain", "crates/grain-import", "crates/grain-serve"] | ||
members = ["crates/grain", "crates/grain-import", "crates/grain-mcp", "crates/grain-serve"] | ||
resolver = "3" | ||
| ||
[workspace.package] | ||
hatk.generated.client.ts+1−1
| @@ -3,7 +3,7 @@ | ||
// to avoid pulling in server-only dependencies. | ||
export type { XrpcSchema } from './hatk.generated.ts' | ||
import type { XrpcSchema } from './hatk.generated.ts' | ||
export type { BskyActorProfile, Post, Postgate, Threadgate, BskyGraphFollow, GetSubjectStatus, UpdateSubjectStatus, QueryLabels, AtprotoModerationCreateReport, GermnetworkDeclaration, HatkApplyWrites, HatkCreateReport, DescribeCollections, HatkDescribeFeeds, DescribeLabels, HatkGetFeed, HatkGetPreferences, GetRecord, GetRecords, PutPreference, HatkSearchRecords, HatkUploadBlob, OpensocialDeclaration, Search, GrainActorGetFavorites, GrainActorGetPreferences, GetProfile, GrainActorGetSuggestedFollows, GrainActorProfile, PutPreferences, SearchActors, GrainActorSearchActorsTypeahead, ActOnReview, AddRepos, ApplyLabel, GetInfo, GetRepo, GetReviewQueue, GetReviewStats, GetReviewSubject, ListLabelDefinitions, ListReports, ListRepos, NegateLabel, RemoveRepos, ResolveReport, ResyncRepos, GrainAdminSearchRecords, StartScan, Comment, GetThread, Favorite, GrainFeedDescribeFeeds, GrainFeedGetFeed, Gallery, GrainGalleryDeleteGallery, GrainGalleryGetCameras, GrainGalleryGetFavorites, GrainGalleryGetGallery, GrainGalleryGetLocations, GrainGalleryItem, GrainGallerySearchGalleries, Block, GrainGraphFollow, GrainGraphGetBlocks, GrainGraphGetFollowers, GrainGraphGetFollowing, GrainGraphGetKnownFollowers, GrainGraphGetMutes, MuteActor, UnmuteActor, GrainGroupCreateGroup, Decline, GrainGroupGetGroup, GrainGroupGetGroupHost, GetSubmissions, GrainGroupItem, GrainGroupJoinGroup, GrainGroupLeaveGroup, GrainGroupListGroups, CreateComment, CreateGallery, DeleteComment, GrainGroupPoolDeleteGallery, GrainGroupPoolGetGallery, ListFeed, ListGalleries, PutFavorite, Submission, GetUnreadCount, ListNotifications, RegisterPush, UnregisterPush, UpdateSeen, Photo, Exif, AddMember, GrainPrivateGalleryCreatePrivateGallery, GrainPrivateGalleryGetPrivateGallery, GrainPrivateGalleryGetSpaceSupport, ListMembers, GrainPrivateGalleryListPrivateGalleries, ListSharedPrivateGalleries, GrainRepoApplyWrites, GrainRepoCreateRecord, GrainRepoDeleteRecord, GrainRepoPutRecord, GrainRepoUploadBlob, GrainServerDeleteAccount, GrainServerGetSitemap, Story, GetArchive, GetAuthors, GrainStoryGetStories, GrainStoryGetStory, MarkViewed, IndexRecords, AddSpaceMember, GrainUnspeccedCreateGroup, CreatePoolComment, CreatePoolGallery, GrainUnspeccedCreatePrivateGallery, GrainUnspeccedDeleteAccount, GrainUnspeccedDeleteGallery, DeletePoolComment, DeletePoolGallery, GetActorFavorites, GetActorProfile, GrainUnspeccedGetBlocks, GrainUnspeccedGetCameras, GetCommentThread, GrainUnspeccedGetFollowers, GrainUnspeccedGetFollowing, GrainUnspeccedGetGallery, GetGalleryFavorites, GrainUnspeccedGetGroup, GrainUnspeccedGetGroupHost, GetGroupSubmissions, GrainUnspeccedGetKnownFollowers, GrainUnspeccedGetLocations, GrainUnspeccedGetMutes, GetNotifications, GetPoolGallery, GrainUnspeccedGetPrivateGallery, GrainUnspeccedGetSitemap, GetSpaceMembers, GrainUnspeccedGetSpaceSupport, GrainUnspeccedGetStories, GrainUnspeccedGetStory, GetStoryArchive, GetStoryAuthors, GrainUnspeccedGetSuggestedFollows, GrainUnspeccedJoinGroup, GrainUnspeccedLeaveGroup, GrainUnspeccedListGroups, ListPoolFeed, ListPoolGalleries, GrainUnspeccedListPrivateGalleries, ListSharedGalleries, MarkStoriesViewed, PutPoolFavorite, GrainUnspeccedSearchActorsTypeahead, GrainUnspeccedSearchGalleries, SearchProfiles, RecordRegistry, CreateRecord, DeleteRecord, PutRecord, Nux, MutedWord, SavedFeed, StatusView, BskyActorDefsProfileView, BskyActorDefsViewerState, FeedViewPref, LabelersPref, InterestsPref, KnownFollowers, MutedWordsPref, SavedFeedsPref, ThreadViewPref, DeclaredAgePref, HiddenPostsPref, LabelerPrefItem, AdultContentPref, BskyAppStatePref, ContentLabelPref, ProfileViewBasic, SavedFeedsPrefV2, VerificationView, ProfileAssociated, VerificationPrefs, VerificationState, PersonalDetailsPref, BskyActorDefsProfileViewDetailed, BskyAppProgressGuide, LiveEventPreferences, ProfileAssociatedChat, ProfileAssociatedGerm, PostInteractionSettingsPref, ProfileAssociatedActivitySubscription, BskyEmbedDefsAspectRatio, ExternalView, External, ViewExternal, ImagesView, Image, ViewImage, RecordView, ViewRecord, ViewBlocked, ViewDetached, ViewNotFound, RecordWithMediaView, VideoView, Caption, PostView, BskyFeedDefsReplyRef, ReasonPin, BlockedPost, Interaction, BskyFeedDefsViewerState, FeedViewPost, NotFoundPost, ReasonRepost, BlockedAuthor, GeneratorView, ThreadContext, ThreadViewPost, ThreadgateView, SkeletonFeedPost, SkeletonReasonPin, GeneratorViewerState, SkeletonReasonRepost, Entity, PostReplyRef, TextSlice, DisableRule, ListRule, MentionRule, FollowerRule, FollowingRule, ListView, ListItemView, Relationship, ListViewBasic, NotFoundActor, ListViewerState, StarterPackView, StarterPackViewBasic, LabelerView, LabelerPolicies, LabelerViewerState, LabelerViewDetailed, Preference, Preferences, RecordDeleted, ChatPreference, ActivitySubscription, FilterablePreference, SubjectActivitySubscription, Tag, Link, Mention, ByteSlice, AtprotoAdminDefsRepoRef, RepoBlobRef, StatusAttr, Label, SelfLabels, SelfLabel, LabelValueDefinition, LabelValueDefinitionStrings, ModTool, GermnetworkDeclarationMessageMe, HatkApplyWritesCreate, HatkApplyWritesUpdate, HatkApplyWritesDelete, HatkApplyWritesCreateResult, HatkApplyWritesUpdateResult, HatkApplyWritesDeleteResult, HatkCreateReportRepoRef, LabelDefinition, LabelLocale, Result, MentionLabel, EmbedInfo, SearchAspectRatio, SubscopeInfo, GrainActorDefsProfileView, GrainActorDefsProfileViewDetailed, GrainActorDefsMessageMe, GrainActorDefsViewerState, GrainActorDefsProfileSearchResult, TypeaheadItem, SuggestedFollow, PinnedFeedsPref, PinnedFeed, PhotoMetadataPref, ThemePref, NotificationsPref, NotificationPref, ReportView, SignalView, ReviewItem, GrainAdminDefsRecordView, CollectionCount, ScanProgress, ClassifierCount, RepoView, StatusCount, CommentView, GrainCommentDefsViewerState, GrainDefsAspectRatio, FeedView, GalleryView, CrossPostInfo, GrainGalleryDefsViewerState, GrainGalleryDefsFavoriteItem, GrainGalleryDefsCameraItem, GrainGalleryDefsLocationItem, GrainGraphDefsFollowerItem, GrainGraphDefsViewerState, GrainGraphDefsFollowingItem, KnownFollowerItem, GrainGraphDefsBlockItem, GrainGraphDefsMuteItem, GroupView, GrainGroupDefsViewerState, SubmissionState, GroupRef, SubmissionView, Rule, GrainGroupPoolDefsPoolFeedItem, GrainGroupPoolDefsPoolGallery, GrainGroupPoolDefsPoolComment, GrainGroupPoolDefsPoolAuthor, GrainNotificationDefsNotificationItem, PhotoView, ExifView, GalleryState, GrainPrivateGalleryDefsPhotoInput, GrainPrivateGalleryDefsPrivateGallery, GrainPrivateGalleryDefsPrivateItem, GrainPrivateGalleryDefsPrivateGalleryRef, GrainPrivateGalleryDefsSharedGalleryRef, GrainPrivateGalleryDefsMemberView, GrainRepoApplyWritesCreate, GrainRepoApplyWritesUpdate, GrainRepoApplyWritesDelete, GrainRepoApplyWritesCreateResult, GrainRepoApplyWritesUpdateResult, GrainRepoApplyWritesDeleteResult, Written, GrainServerDefsSitemapEntry, StoryView, GrainStoryDefsViewerState, GrainStoryDefsStoryAuthor, CreatePoolGalleryPhotoInput, GrainUnspeccedCreatePrivateGalleryPhotoInput, GrainUnspeccedGetBlocksBlockItem, GrainUnspeccedGetCamerasCameraItem, GrainUnspeccedGetFollowersFollowerItem, GrainUnspeccedGetFollowersViewerState, GrainUnspeccedGetFollowingFollowingItem, GrainUnspeccedGetFollowingViewerState, GetGalleryFavoritesFavoriteItem, GrainUnspeccedGetKnownFollowersFollowerItem, GrainUnspeccedGetLocationsLocationItem, GrainUnspeccedGetMutesMuteItem, GetNotificationsNotificationItem, GetPoolGalleryPoolComment, GetPoolGalleryPoolAuthor, GrainUnspeccedGetPrivateGalleryPrivateGallery, GrainUnspeccedGetPrivateGalleryPrivateItem, GrainUnspeccedGetSitemapSitemapEntry, GetSpaceMembersMemberView, GetStoryAuthorsStoryAuthor, SuggestedItem, ListPoolFeedPoolFeedItem, ListPoolGalleriesPoolGallery, GrainUnspeccedListPrivateGalleriesPrivateGalleryRef, ListSharedGalleriesSharedGalleryRef, SearchProfilesProfileSearchResult } from './hatk.generated.ts' | ||
export type { BskyActorProfile, Post, Postgate, Threadgate, BskyGraphFollow, GetSubjectStatus, UpdateSubjectStatus, QueryLabels, AtprotoModerationCreateReport, GermnetworkDeclaration, HatkApplyWrites, HatkCreateReport, DescribeCollections, HatkDescribeFeeds, DescribeLabels, HatkGetFeed, HatkGetPreferences, GetRecord, GetRecords, PutPreference, HatkSearchRecords, HatkUploadBlob, OpensocialDeclaration, Search, GrainActorGetFavorites, GrainActorGetPreferences, GetProfile, GrainActorGetSuggestedFollows, GrainActorProfile, PutPreferences, SearchActors, GrainActorSearchActorsTypeahead, ActOnReview, AddRepos, ApplyLabel, GetInfo, GetRepo, GetReport, GetReviewQueue, GetReviewStats, GetReviewSubject, ListLabelDefinitions, ListReports, ListRepos, NegateLabel, RemoveRepos, ResolveReport, ResyncRepos, GrainAdminSearchRecords, StartScan, Comment, GetThread, Favorite, GrainFeedDescribeFeeds, GrainFeedGetFeed, Gallery, GrainGalleryDeleteGallery, GrainGalleryGetCameras, GrainGalleryGetFavorites, GrainGalleryGetGallery, GrainGalleryGetLocations, GrainGalleryItem, GrainGallerySearchGalleries, Block, GrainGraphFollow, GrainGraphGetBlocks, GrainGraphGetFollowers, GrainGraphGetFollowing, GrainGraphGetKnownFollowers, GrainGraphGetMutes, MuteActor, UnmuteActor, GrainGroupCreateGroup, Decline, GrainGroupGetGroup, GrainGroupGetGroupHost, GetSubmissions, GrainGroupItem, GrainGroupJoinGroup, GrainGroupLeaveGroup, GrainGroupListGroups, CreateComment, CreateGallery, DeleteComment, GrainGroupPoolDeleteGallery, GrainGroupPoolGetGallery, ListFeed, ListGalleries, PutFavorite, Submission, GetUnreadCount, ListNotifications, RegisterPush, UnregisterPush, UpdateSeen, Photo, Exif, AddMember, GrainPrivateGalleryCreatePrivateGallery, GrainPrivateGalleryGetPrivateGallery, GrainPrivateGalleryGetSpaceSupport, ListMembers, GrainPrivateGalleryListPrivateGalleries, ListSharedPrivateGalleries, GrainRepoApplyWrites, GrainRepoCreateRecord, GrainRepoDeleteRecord, GrainRepoPutRecord, GrainRepoUploadBlob, GrainServerDeleteAccount, GrainServerGetSitemap, Story, GetArchive, GetAuthors, GrainStoryGetStories, GrainStoryGetStory, MarkViewed, IndexRecords, AddSpaceMember, GrainUnspeccedCreateGroup, CreatePoolComment, CreatePoolGallery, GrainUnspeccedCreatePrivateGallery, GrainUnspeccedDeleteAccount, GrainUnspeccedDeleteGallery, DeletePoolComment, DeletePoolGallery, GetActorFavorites, GetActorProfile, GrainUnspeccedGetBlocks, GrainUnspeccedGetCameras, GetCommentThread, GrainUnspeccedGetFollowers, GrainUnspeccedGetFollowing, GrainUnspeccedGetGallery, GetGalleryFavorites, GrainUnspeccedGetGroup, GrainUnspeccedGetGroupHost, GetGroupSubmissions, GrainUnspeccedGetKnownFollowers, GrainUnspeccedGetLocations, GrainUnspeccedGetMutes, GetNotifications, GetPoolGallery, GrainUnspeccedGetPrivateGallery, GrainUnspeccedGetSitemap, GetSpaceMembers, GrainUnspeccedGetSpaceSupport, GrainUnspeccedGetStories, GrainUnspeccedGetStory, GetStoryArchive, GetStoryAuthors, GrainUnspeccedGetSuggestedFollows, GrainUnspeccedJoinGroup, GrainUnspeccedLeaveGroup, GrainUnspeccedListGroups, ListPoolFeed, ListPoolGalleries, GrainUnspeccedListPrivateGalleries, ListSharedGalleries, MarkStoriesViewed, PutPoolFavorite, GrainUnspeccedSearchActorsTypeahead, GrainUnspeccedSearchGalleries, SearchProfiles, RecordRegistry, CreateRecord, DeleteRecord, PutRecord, Nux, MutedWord, SavedFeed, StatusView, BskyActorDefsProfileView, BskyActorDefsViewerState, FeedViewPref, LabelersPref, InterestsPref, KnownFollowers, MutedWordsPref, SavedFeedsPref, ThreadViewPref, DeclaredAgePref, HiddenPostsPref, LabelerPrefItem, AdultContentPref, BskyAppStatePref, ContentLabelPref, ProfileViewBasic, SavedFeedsPrefV2, VerificationView, ProfileAssociated, VerificationPrefs, VerificationState, PersonalDetailsPref, BskyActorDefsProfileViewDetailed, BskyAppProgressGuide, LiveEventPreferences, ProfileAssociatedChat, ProfileAssociatedGerm, PostInteractionSettingsPref, ProfileAssociatedActivitySubscription, BskyEmbedDefsAspectRatio, ExternalView, External, ViewExternal, ImagesView, Image, ViewImage, RecordView, ViewRecord, ViewBlocked, ViewDetached, ViewNotFound, RecordWithMediaView, VideoView, Caption, PostView, BskyFeedDefsReplyRef, ReasonPin, BlockedPost, Interaction, BskyFeedDefsViewerState, FeedViewPost, NotFoundPost, ReasonRepost, BlockedAuthor, GeneratorView, ThreadContext, ThreadViewPost, ThreadgateView, SkeletonFeedPost, SkeletonReasonPin, GeneratorViewerState, SkeletonReasonRepost, Entity, PostReplyRef, TextSlice, DisableRule, ListRule, MentionRule, FollowerRule, FollowingRule, ListView, ListItemView, Relationship, ListViewBasic, NotFoundActor, ListViewerState, StarterPackView, StarterPackViewBasic, LabelerView, LabelerPolicies, LabelerViewerState, LabelerViewDetailed, Preference, Preferences, RecordDeleted, ChatPreference, ActivitySubscription, FilterablePreference, SubjectActivitySubscription, Tag, Link, Mention, ByteSlice, AtprotoAdminDefsRepoRef, RepoBlobRef, StatusAttr, Label, SelfLabels, SelfLabel, LabelValueDefinition, LabelValueDefinitionStrings, ModTool, GermnetworkDeclarationMessageMe, HatkApplyWritesCreate, HatkApplyWritesUpdate, HatkApplyWritesDelete, HatkApplyWritesCreateResult, HatkApplyWritesUpdateResult, HatkApplyWritesDeleteResult, HatkCreateReportRepoRef, LabelDefinition, LabelLocale, Result, MentionLabel, EmbedInfo, SearchAspectRatio, SubscopeInfo, GrainActorDefsProfileView, GrainActorDefsProfileViewDetailed, GrainActorDefsMessageMe, GrainActorDefsViewerState, GrainActorDefsProfileSearchResult, TypeaheadItem, SuggestedFollow, PinnedFeedsPref, PinnedFeed, PhotoMetadataPref, ThemePref, NotificationsPref, NotificationPref, ReportView, SignalView, ReviewItem, GrainAdminDefsRecordView, CollectionCount, ScanProgress, ClassifierCount, RepoView, StatusCount, CommentView, GrainCommentDefsViewerState, GrainDefsAspectRatio, FeedView, GalleryView, CrossPostInfo, GrainGalleryDefsViewerState, GrainGalleryDefsFavoriteItem, GrainGalleryDefsCameraItem, GrainGalleryDefsLocationItem, GrainGraphDefsFollowerItem, GrainGraphDefsViewerState, GrainGraphDefsFollowingItem, KnownFollowerItem, GrainGraphDefsBlockItem, GrainGraphDefsMuteItem, GroupView, GrainGroupDefsViewerState, SubmissionState, GroupRef, SubmissionView, Rule, GrainGroupPoolDefsPoolFeedItem, GrainGroupPoolDefsPoolGallery, GrainGroupPoolDefsPoolComment, GrainGroupPoolDefsPoolAuthor, GrainNotificationDefsNotificationItem, PhotoView, ExifView, GalleryState, GrainPrivateGalleryDefsPhotoInput, GrainPrivateGalleryDefsPrivateGallery, GrainPrivateGalleryDefsPrivateItem, GrainPrivateGalleryDefsPrivateGalleryRef, GrainPrivateGalleryDefsSharedGalleryRef, GrainPrivateGalleryDefsMemberView, GrainRepoApplyWritesCreate, GrainRepoApplyWritesUpdate, GrainRepoApplyWritesDelete, GrainRepoApplyWritesCreateResult, GrainRepoApplyWritesUpdateResult, GrainRepoApplyWritesDeleteResult, Written, GrainServerDefsSitemapEntry, StoryView, GrainStoryDefsViewerState, GrainStoryDefsStoryAuthor, CreatePoolGalleryPhotoInput, GrainUnspeccedCreatePrivateGalleryPhotoInput, GrainUnspeccedGetBlocksBlockItem, GrainUnspeccedGetCamerasCameraItem, GrainUnspeccedGetFollowersFollowerItem, GrainUnspeccedGetFollowersViewerState, GrainUnspeccedGetFollowingFollowingItem, GrainUnspeccedGetFollowingViewerState, GetGalleryFavoritesFavoriteItem, GrainUnspeccedGetKnownFollowersFollowerItem, GrainUnspeccedGetLocationsLocationItem, GrainUnspeccedGetMutesMuteItem, GetNotificationsNotificationItem, GetPoolGalleryPoolComment, GetPoolGalleryPoolAuthor, GrainUnspeccedGetPrivateGalleryPrivateGallery, GrainUnspeccedGetPrivateGalleryPrivateItem, GrainUnspeccedGetSitemapSitemapEntry, GetSpaceMembersMemberView, GetStoryAuthorsStoryAuthor, SuggestedItem, ListPoolFeedPoolFeedItem, ListPoolGalleriesPoolGallery, GrainUnspeccedListPrivateGalleriesPrivateGalleryRef, ListSharedGalleriesSharedGalleryRef, SearchProfilesProfileSearchResult } from './hatk.generated.ts' | ||
| ||
const _procedures = new Set(['com.atproto.admin.updateSubjectStatus', 'com.atproto.moderation.createReport', 'dev.hatk.applyWrites', 'dev.hatk.createRecord', 'dev.hatk.createReport', 'dev.hatk.deleteRecord', 'dev.hatk.putPreference', 'dev.hatk.putRecord', 'social.grain.actor.putPreferences', 'social.grain.admin.actOnReview', 'social.grain.admin.addRepos', 'social.grain.admin.applyLabel', 'social.grain.admin.negateLabel', 'social.grain.admin.removeRepos', 'social.grain.admin.resolveReport', 'social.grain.admin.resyncRepos', 'social.grain.admin.startScan', 'social.grain.gallery.deleteGallery', 'social.grain.graph.muteActor', 'social.grain.graph.unmuteActor', 'social.grain.group.createGroup', 'social.grain.group.joinGroup', 'social.grain.group.leaveGroup', 'social.grain.group.pool.createComment', 'social.grain.group.pool.createGallery', 'social.grain.group.pool.deleteComment', 'social.grain.group.pool.deleteGallery', 'social.grain.group.pool.putFavorite', 'social.grain.notification.registerPush', 'social.grain.notification.unregisterPush', 'social.grain.notification.updateSeen', 'social.grain.privateGallery.addMember', 'social.grain.privateGallery.createPrivateGallery', 'social.grain.repo.applyWrites', 'social.grain.repo.createRecord', 'social.grain.repo.deleteRecord', 'social.grain.repo.putRecord', 'social.grain.server.deleteAccount', 'social.grain.story.markViewed', 'social.grain.sync.indexRecords', 'social.grain.unspecced.addSpaceMember', 'social.grain.unspecced.createGroup', 'social.grain.unspecced.createPoolComment', 'social.grain.unspecced.createPoolGallery', 'social.grain.unspecced.createPrivateGallery', 'social.grain.unspecced.deleteAccount', 'social.grain.unspecced.deleteGallery', 'social.grain.unspecced.deletePoolComment', 'social.grain.unspecced.deletePoolGallery', 'social.grain.unspecced.joinGroup', 'social.grain.unspecced.leaveGroup', 'social.grain.unspecced.markStoriesViewed', 'social.grain.unspecced.putPoolFavorite']) | ||
const _blobInputs = new Set(['dev.hatk.uploadBlob', 'social.grain.repo.uploadBlob']) | ||