···
···
.get_json("com.atproto.repo.listRecords", ¶ms, bearer)
172
-
let mut repos: Vec<Repository> = res.records.into_iter().map(|r| r.value).collect();
173
+
let mut repos: Vec<Repository> = res
177
+
let mut val = r.value;
178
+
if val.rkey.is_none() {
179
+
if let Some(k) = Self::uri_rkey(&r.uri) {
180
+
val.rkey = Some(k);
183
+
if val.did.is_none() {
184
+
if let Some(d) = Self::uri_did(&r.uri) {
// Apply optional filters client-side
repos.retain(|r| r.knot.as_deref().unwrap_or("") == k);
···
let _: serde_json::Value = self.post_json(REPO_CREATE, &req, Some(&sa.token)).await?;
299
+
pub async fn get_repo_info(
303
+
bearer: Option<&str>,
304
+
) -> Result<RepoRecord> {
305
+
let did = if owner.starts_with("did:") {
308
+
#[derive(Deserialize)]
312
+
let params = [("handle", owner.to_string())];
313
+
let res: Res = self
314
+
.get_json("com.atproto.identity.resolveHandle", ¶ms, bearer)
319
+
#[derive(Deserialize)]
320
+
struct RecordItem {
324
+
#[derive(Deserialize)]
327
+
records: Vec<RecordItem>,
330
+
("repo", did.clone()),
331
+
("collection", "sh.tangled.repo".to_string()),
332
+
("limit", "100".to_string()),
334
+
let res: ListRes = self
335
+
.get_json("com.atproto.repo.listRecords", ¶ms, bearer)
337
+
for item in res.records {
338
+
if item.value.name == name {
340
+
Self::uri_rkey(&item.uri).ok_or_else(|| anyhow!("missing rkey in uri"))?;
341
+
let knot = item.value.knot.unwrap_or_default();
342
+
return Ok(RepoRecord {
344
+
name: name.to_string(),
347
+
description: item.value.description,
351
+
Err(anyhow!("repo not found for owner/name"))
354
+
pub async fn delete_repo(
361
+
let pds_client = TangledClient::new(pds_base);
362
+
let info = pds_client
363
+
.get_repo_info(did, name, Some(access_jwt))
366
+
#[derive(Serialize)]
367
+
struct DeleteRecordReq<'a> {
369
+
collection: &'a str,
372
+
let del = DeleteRecordReq {
374
+
collection: "sh.tangled.repo",
377
+
let _: serde_json::Value = pds_client
378
+
.post_json("com.atproto.repo.deleteRecord", &del, Some(access_jwt))
383
+
.trim_end_matches('/')
384
+
.strip_prefix("https://")
385
+
.or_else(|| self.base_url.trim_end_matches('/').strip_prefix("http://"))
386
+
.ok_or_else(|| anyhow!("invalid base_url"))?;
387
+
let audience = format!("did:web:{}", host);
388
+
#[derive(Deserialize)]
394
+
("exp", (chrono::Utc::now().timestamp() + 600).to_string()),
396
+
let sa: GetSARes = pds_client
398
+
"com.atproto.server.getServiceAuth",
404
+
#[derive(Serialize)]
405
+
struct DeleteReq<'a> {
410
+
let body = DeleteReq {
415
+
let _: serde_json::Value = self
416
+
.post_json("sh.tangled.repo.delete", &body, Some(&sa.token))
421
+
pub async fn update_repo_knot(
429
+
let pds_client = TangledClient::new(pds_base);
430
+
#[derive(Deserialize, Serialize, Clone)]
434
+
#[serde(skip_serializing_if = "Option::is_none")]
435
+
description: Option<String>,
436
+
#[serde(rename = "createdAt")]
437
+
created_at: String,
439
+
#[derive(Deserialize)]
444
+
("repo", did.to_string()),
445
+
("collection", "sh.tangled.repo".to_string()),
446
+
("rkey", rkey.to_string()),
448
+
let got: GetRes = pds_client
449
+
.get_json("com.atproto.repo.getRecord", ¶ms, Some(access_jwt))
451
+
let mut rec = got.value;
452
+
rec.knot = new_knot.to_string();
453
+
#[derive(Serialize)]
454
+
struct PutReq<'a> {
456
+
collection: &'a str,
463
+
collection: "sh.tangled.repo",
468
+
let _: serde_json::Value = pds_client
469
+
.post_json("com.atproto.repo.putRecord", &req, Some(access_jwt))
474
+
pub async fn get_default_branch(
479
+
) -> Result<DefaultBranch> {
480
+
#[derive(Deserialize)]
484
+
#[serde(rename = "shortHash")]
485
+
short_hash: Option<String>,
487
+
message: Option<String>,
489
+
let knot_client = TangledClient::new(knot_host);
490
+
let repo_param = format!("{}/{}", did, name);
491
+
let params = [("repo", repo_param)];
492
+
let res: Res = knot_client
493
+
.get_json("sh.tangled.repo.getDefaultBranch", ¶ms, None)
498
+
short_hash: res.short_hash,
500
+
message: res.message,
504
+
pub async fn get_languages(&self, knot_host: &str, did: &str, name: &str) -> Result<Languages> {
505
+
let knot_client = TangledClient::new(knot_host);
506
+
let repo_param = format!("{}/{}", did, name);
507
+
let params = [("repo", repo_param)];
508
+
let res: serde_json::Value = knot_client
509
+
.get_json("sh.tangled.repo.languages", ¶ms, None)
514
+
.unwrap_or(serde_json::json!([]));
515
+
let languages: Vec<Language> = serde_json::from_value(langs)?;
516
+
let total_size = res.get("totalSize").and_then(|v| v.as_u64());
517
+
let total_files = res.get("totalFiles").and_then(|v| v.as_u64());
525
+
pub async fn star_repo(
529
+
subject_at_uri: &str,
531
+
) -> Result<String> {
532
+
#[derive(Serialize)]
535
+
#[serde(rename = "createdAt")]
536
+
created_at: String,
538
+
#[derive(Serialize)]
541
+
collection: &'a str,
545
+
#[derive(Deserialize)]
549
+
let now = chrono::Utc::now().to_rfc3339();
551
+
subject: subject_at_uri,
556
+
collection: "sh.tangled.feed.star",
560
+
let pds_client = TangledClient::new(pds_base);
561
+
let res: Res = pds_client
562
+
.post_json("com.atproto.repo.createRecord", &req, Some(access_jwt))
564
+
let rkey = Self::uri_rkey(&res.uri).ok_or_else(|| anyhow!("missing rkey in star uri"))?;
568
+
pub async fn unstar_repo(
572
+
subject_at_uri: &str,
575
+
#[derive(Deserialize)]
580
+
#[derive(Deserialize)]
583
+
records: Vec<Item>,
585
+
let pds_client = TangledClient::new(pds_base);
587
+
("repo", user_did.to_string()),
588
+
("collection", "sh.tangled.feed.star".to_string()),
589
+
("limit", "100".to_string()),
591
+
let res: ListRes = pds_client
592
+
.get_json("com.atproto.repo.listRecords", ¶ms, Some(access_jwt))
594
+
let mut rkey = None;
595
+
for item in res.records {
596
+
if item.value.subject == subject_at_uri {
597
+
rkey = Self::uri_rkey(&item.uri);
598
+
if rkey.is_some() {
603
+
let rkey = rkey.ok_or_else(|| anyhow!("star record not found"))?;
604
+
#[derive(Serialize)]
607
+
collection: &'a str,
612
+
collection: "sh.tangled.feed.star",
615
+
let _: serde_json::Value = pds_client
616
+
.post_json("com.atproto.repo.deleteRecord", &del, Some(access_jwt))
621
+
fn uri_rkey(uri: &str) -> Option<String> {
622
+
uri.rsplit('/').next().map(|s| s.to_string())
624
+
fn uri_did(uri: &str) -> Option<String> {
625
+
let parts: Vec<&str> = uri.split('/').collect();
626
+
if parts.len() >= 3 {
627
+
Some(parts[2].to_string())
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
···
pub description: Option<String>,
645
+
#[derive(Debug, Clone)]
646
+
pub struct RepoRecord {
651
+
pub description: Option<String>,
654
+
#[derive(Debug, Clone, Serialize, Deserialize)]
655
+
pub struct DefaultBranch {
658
+
#[serde(skip_serializing_if = "Option::is_none")]
659
+
pub short_hash: Option<String>,
661
+
#[serde(skip_serializing_if = "Option::is_none")]
662
+
pub message: Option<String>,
665
+
#[derive(Debug, Clone, Serialize, Deserialize)]
666
+
pub struct Language {
669
+
pub percentage: u64,
672
+
#[derive(Debug, Clone, Serialize, Deserialize)]
673
+
pub struct Languages {
674
+
pub languages: Vec<Language>,
675
+
#[serde(skip_serializing_if = "Option::is_none")]
676
+
pub total_size: Option<u64>,
677
+
#[serde(skip_serializing_if = "Option::is_none")]
678
+
pub total_files: Option<u64>,
681
+
#[derive(Debug, Clone, Serialize, Deserialize)]
682
+
pub struct StarRecord {
683
+
pub subject: String,
684
+
#[serde(rename = "createdAt")]
685
+
pub created_at: String,