+48
consumer/src/indexer/db.rs
+48
consumer/src/indexer/db.rs
···
693
.execute(conn)
694
.await
695
}
696
+
697
+
pub async fn upsert_starterpack(
698
+
conn: &mut AsyncPgConnection,
699
+
did: &str,
700
+
cid: Cid,
701
+
at_uri: &str,
702
+
rec: records::AppBskyGraphStarterPack,
703
+
) -> QueryResult<usize> {
704
+
let record = serde_json::to_value(&rec).unwrap();
705
+
706
+
let feeds = rec
707
+
.feeds
708
+
.map(|v| v.into_iter().map(|item| item.uri).collect());
709
+
710
+
let description_facets = rec
711
+
.description_facets
712
+
.and_then(|v| serde_json::to_value(v).ok());
713
+
714
+
715
+
let data = models::NewStarterPack {
716
+
at_uri,
717
+
cid: cid.to_string(),
718
+
owner: did,
719
+
record,
720
+
name: &rec.name,
721
+
description: rec.description,
722
+
description_facets,
723
+
list: &rec.list,
724
+
feeds,
725
+
created_at: rec.created_at.naive_utc(),
726
+
indexed_at: Utc::now().naive_utc(),
727
+
};
728
+
729
+
diesel::insert_into(schema::starterpacks::table)
730
+
.values(&data)
731
+
.on_conflict(schema::starterpacks::at_uri)
732
+
.do_update()
733
+
.set(&data)
734
+
.execute(conn)
735
+
.await
736
+
}
737
+
738
+
pub async fn delete_starterpack(conn: &mut AsyncPgConnection, at_uri: &str) -> QueryResult<usize> {
739
+
diesel::delete(schema::starterpacks::table)
740
+
.filter(schema::starterpacks::at_uri.eq(at_uri))
741
+
.execute(conn)
742
+
.await
743
+
}
+4
consumer/src/indexer/mod.rs
+4
consumer/src/indexer/mod.rs
···
420
421
db::insert_list_item(conn, at_uri, record).await?;
422
}
423
RecordTypes::ChatBskyActorDeclaration(record) => {
424
if rkey == "self" {
425
db::upsert_chat_decl(conn, repo, record).await?;
···
458
}
459
CollectionType::BskyListBlock => db::delete_list_block(conn, at_uri).await?,
460
CollectionType::BskyListItem => db::delete_list_item(conn, at_uri).await?,
461
CollectionType::ChatActorDecl => db::delete_chat_decl(conn, at_uri).await?,
462
_ => unreachable!(),
463
};
···
420
421
db::insert_list_item(conn, at_uri, record).await?;
422
}
423
+
RecordTypes::AppBskyGraphStarterPack(record) => {
424
+
db::upsert_starterpack(conn, repo, cid, at_uri, record).await?;
425
+
},
426
RecordTypes::ChatBskyActorDeclaration(record) => {
427
if rkey == "self" {
428
db::upsert_chat_decl(conn, repo, record).await?;
···
461
}
462
CollectionType::BskyListBlock => db::delete_list_block(conn, at_uri).await?,
463
CollectionType::BskyListItem => db::delete_list_item(conn, at_uri).await?,
464
+
CollectionType::BskyStarterPack => db::delete_starterpack(conn, at_uri).await?,
465
CollectionType::ChatActorDecl => db::delete_chat_decl(conn, at_uri).await?,
466
_ => unreachable!(),
467
};
+18
consumer/src/indexer/records.rs
+18
consumer/src/indexer/records.rs
···
294
}
295
296
#[derive(Debug, Deserialize, Serialize)]
297
+
#[serde(tag = "$type")]
298
+
#[serde(rename = "app.bsky.graph.starterpack")]
299
+
#[serde(rename_all = "camelCase")]
300
+
pub struct AppBskyGraphStarterPack {
301
+
pub name: String,
302
+
pub description: Option<String>,
303
+
pub description_facets: Option<Vec<FacetMain>>,
304
+
pub list: String,
305
+
pub feeds: Option<Vec<StarterPackFeedItem>>,
306
+
pub created_at: DateTime<Utc>,
307
+
}
308
+
309
+
#[derive(Debug, Deserialize, Serialize)]
310
+
pub struct StarterPackFeedItem {
311
+
pub uri: String,
312
+
}
313
+
314
+
#[derive(Debug, Deserialize, Serialize)]
315
#[serde(rename_all = "camelCase")]
316
pub struct ChatBskyActorDeclaration {
317
pub allow_incoming: ChatAllowIncoming,
+5
consumer/src/indexer/types.rs
+5
consumer/src/indexer/types.rs
···
29
AppBskyGraphListBlock(records::AppBskyGraphListBlock),
30
#[serde(rename = "app.bsky.graph.listitem")]
31
AppBskyGraphListItem(records::AppBskyGraphListItem),
32
#[serde(rename = "chat.bsky.actor.declaration")]
33
ChatBskyActorDeclaration(records::ChatBskyActorDeclaration),
34
}
···
47
BskyList,
48
BskyListBlock,
49
BskyListItem,
50
ChatActorDecl,
51
Unsupported,
52
}
···
66
"app.bsky.graph.list" => CollectionType::BskyList,
67
"app.bsky.graph.listblock" => CollectionType::BskyListBlock,
68
"app.bsky.graph.listitem" => CollectionType::BskyListItem,
69
"chat.bsky.actor.declaration" => CollectionType::ChatActorDecl,
70
_ => CollectionType::Unsupported,
71
}
···
86
CollectionType::BskyListBlock => false,
87
CollectionType::BskyListItem => false,
88
CollectionType::ChatActorDecl => true,
89
CollectionType::Unsupported => false,
90
}
91
}
···
29
AppBskyGraphListBlock(records::AppBskyGraphListBlock),
30
#[serde(rename = "app.bsky.graph.listitem")]
31
AppBskyGraphListItem(records::AppBskyGraphListItem),
32
+
#[serde(rename = "app.bsky.graph.starterpack")]
33
+
AppBskyGraphStarterPack(records::AppBskyGraphStarterPack),
34
#[serde(rename = "chat.bsky.actor.declaration")]
35
ChatBskyActorDeclaration(records::ChatBskyActorDeclaration),
36
}
···
49
BskyList,
50
BskyListBlock,
51
BskyListItem,
52
+
BskyStarterPack,
53
ChatActorDecl,
54
Unsupported,
55
}
···
69
"app.bsky.graph.list" => CollectionType::BskyList,
70
"app.bsky.graph.listblock" => CollectionType::BskyListBlock,
71
"app.bsky.graph.listitem" => CollectionType::BskyListItem,
72
+
"app.bsky.graph.starterpack" => CollectionType::BskyStarterPack,
73
"chat.bsky.actor.declaration" => CollectionType::ChatActorDecl,
74
_ => CollectionType::Unsupported,
75
}
···
90
CollectionType::BskyListBlock => false,
91
CollectionType::BskyListItem => false,
92
CollectionType::ChatActorDecl => true,
93
+
CollectionType::BskyStarterPack => true,
94
CollectionType::Unsupported => false,
95
}
96
}
+42
-2
lexica/src/app_bsky/graph.rs
+42
-2
lexica/src/app_bsky/graph.rs
···
1
-
use crate::app_bsky::actor::ProfileView;
2
use crate::app_bsky::richtext::FacetMain;
3
use chrono::prelude::*;
4
use serde::{Deserialize, Serialize};
···
48
pub indexed_at: NaiveDateTime,
49
}
50
51
-
#[derive(Debug, Serialize)]
52
pub struct ListItemView {
53
pub uri: String,
54
pub subject: ProfileView,
···
79
}
80
}
81
}
···
1
+
use crate::app_bsky::actor::{ProfileView, ProfileViewBasic};
2
+
use crate::app_bsky::feed::GeneratorView;
3
use crate::app_bsky::richtext::FacetMain;
4
use chrono::prelude::*;
5
use serde::{Deserialize, Serialize};
···
49
pub indexed_at: NaiveDateTime,
50
}
51
52
+
#[derive(Clone, Debug, Serialize)]
53
pub struct ListItemView {
54
pub uri: String,
55
pub subject: ProfileView,
···
80
}
81
}
82
}
83
+
84
+
#[derive(Clone, Debug, Serialize)]
85
+
#[serde(rename_all = "camelCase")]
86
+
pub struct StarterPackView {
87
+
pub uri: String,
88
+
pub cid: String,
89
+
pub record: serde_json::Value,
90
+
pub creator: ProfileViewBasic,
91
+
92
+
#[serde(skip_serializing_if = "Option::is_none")]
93
+
pub list: Option<ListViewBasic>,
94
+
#[serde(skip_serializing_if = "Vec::is_empty")]
95
+
pub list_items_sample: Vec<ListItemView>,
96
+
#[serde(skip_serializing_if = "Vec::is_empty")]
97
+
pub feeds: Vec<GeneratorView>,
98
+
99
+
pub list_item_count: i64,
100
+
pub joined_week_count: i64,
101
+
pub joined_all_time_count: i64,
102
+
103
+
// pub labels: Vec<()>,
104
+
pub indexed_at: NaiveDateTime,
105
+
}
106
+
107
+
#[derive(Clone, Debug, Serialize)]
108
+
#[serde(rename_all = "camelCase")]
109
+
pub struct StarterPackViewBasic {
110
+
pub uri: String,
111
+
pub cid: String,
112
+
pub record: serde_json::Value,
113
+
pub creator: ProfileViewBasic,
114
+
115
+
pub list_item_count: i64,
116
+
pub joined_week_count: i64,
117
+
pub joined_all_time_count: i64,
118
+
119
+
// pub labels: Vec<()>,
120
+
pub indexed_at: NaiveDateTime,
121
+
}
+1
migrations/2025-04-09-175735_starterpacks/down.sql
+1
migrations/2025-04-09-175735_starterpacks/down.sql
···
···
1
+
drop table starterpacks;
+18
migrations/2025-04-09-175735_starterpacks/up.sql
+18
migrations/2025-04-09-175735_starterpacks/up.sql
···
···
1
+
create table starterpacks
2
+
(
3
+
at_uri text primary key,
4
+
owner text not null references actors (did),
5
+
cid text not null,
6
+
record jsonb not null,
7
+
8
+
name text not null,
9
+
description text,
10
+
description_facets jsonb,
11
+
list text not null,
12
+
feeds text[],
13
+
14
+
created_at timestamptz not null default now(),
15
+
indexed_at timestamp not null default now()
16
+
);
17
+
18
+
create index starterpacks_owner_index on starterpacks using hash (owner);
+39
parakeet-db/src/models.rs
+39
parakeet-db/src/models.rs
···
554
pub did: &'a str,
555
pub allow_incoming: String,
556
}
557
+
558
+
#[derive(Clone, Debug, Queryable, Selectable, Identifiable)]
559
+
#[diesel(table_name = crate::schema::starterpacks)]
560
+
#[diesel(primary_key(at_uri))]
561
+
#[diesel(check_for_backend(diesel::pg::Pg))]
562
+
pub struct StaterPack {
563
+
pub at_uri: String,
564
+
pub cid: String,
565
+
pub owner: String,
566
+
pub record: serde_json::Value,
567
+
568
+
pub name: String,
569
+
pub description: Option<String>,
570
+
pub description_facets: Option<serde_json::Value>,
571
+
pub list: String,
572
+
pub feeds: Option<Vec<Option<String>>>,
573
+
574
+
pub created_at: NaiveDateTime,
575
+
pub indexed_at: NaiveDateTime,
576
+
}
577
+
578
+
#[derive(Insertable, AsChangeset)]
579
+
#[diesel(table_name = crate::schema::starterpacks)]
580
+
#[diesel(check_for_backend(diesel::pg::Pg))]
581
+
pub struct NewStarterPack<'a> {
582
+
pub at_uri: &'a str,
583
+
pub cid: String,
584
+
pub owner: &'a str,
585
+
pub record: serde_json::Value,
586
+
587
+
pub name: &'a str,
588
+
pub description: Option<String>,
589
+
pub description_facets: Option<serde_json::Value>,
590
+
pub list: &'a str,
591
+
pub feeds: Option<Vec<String>>,
592
+
593
+
pub created_at: NaiveDateTime,
594
+
pub indexed_at: NaiveDateTime,
595
+
}
+18
parakeet-db/src/schema.rs
+18
parakeet-db/src/schema.rs
···
234
}
235
236
diesel::table! {
237
threadgates (at_uri) {
238
at_uri -> Text,
239
cid -> Text,
···
264
diesel::joinable!(posts -> actors (did));
265
diesel::joinable!(profiles -> actors (did));
266
diesel::joinable!(reposts -> actors (did));
267
diesel::joinable!(threadgates -> posts (post_uri));
268
269
diesel::allow_tables_to_appear_in_same_query!(
···
287
posts,
288
profiles,
289
reposts,
290
threadgates,
291
);
···
234
}
235
236
diesel::table! {
237
+
starterpacks (at_uri) {
238
+
at_uri -> Text,
239
+
owner -> Text,
240
+
cid -> Text,
241
+
record -> Jsonb,
242
+
name -> Text,
243
+
description -> Nullable<Text>,
244
+
description_facets -> Nullable<Jsonb>,
245
+
list -> Text,
246
+
feeds -> Nullable<Array<Nullable<Text>>>,
247
+
created_at -> Timestamptz,
248
+
indexed_at -> Timestamp,
249
+
}
250
+
}
251
+
252
+
diesel::table! {
253
threadgates (at_uri) {
254
at_uri -> Text,
255
cid -> Text,
···
280
diesel::joinable!(posts -> actors (did));
281
diesel::joinable!(profiles -> actors (did));
282
diesel::joinable!(reposts -> actors (did));
283
+
diesel::joinable!(starterpacks -> actors (owner));
284
diesel::joinable!(threadgates -> posts (post_uri));
285
286
diesel::allow_tables_to_appear_in_same_query!(
···
304
posts,
305
profiles,
306
reposts,
307
+
starterpacks,
308
threadgates,
309
);
+3
-1
parakeet/src/hydration/mod.rs
+3
-1
parakeet/src/hydration/mod.rs
+145
parakeet/src/hydration/starter_packs.rs
+145
parakeet/src/hydration/starter_packs.rs
···
···
1
+
use crate::hydration::{
2
+
hydrate_feedgens, hydrate_list_basic, hydrate_lists_basic, hydrate_profile_basic,
3
+
hydrate_profiles_basic,
4
+
};
5
+
use crate::loaders::Dataloaders;
6
+
use lexica::app_bsky::actor::ProfileViewBasic;
7
+
use lexica::app_bsky::feed::GeneratorView;
8
+
use lexica::app_bsky::graph::{ListViewBasic, StarterPackView, StarterPackViewBasic};
9
+
use parakeet_db::models;
10
+
use std::collections::HashMap;
11
+
12
+
fn build_basic(
13
+
starter_pack: models::StaterPack,
14
+
creator: ProfileViewBasic,
15
+
list_item_count: i64,
16
+
) -> StarterPackViewBasic {
17
+
StarterPackViewBasic {
18
+
uri: starter_pack.at_uri,
19
+
cid: starter_pack.cid,
20
+
record: starter_pack.record,
21
+
creator,
22
+
list_item_count,
23
+
joined_week_count: 0,
24
+
joined_all_time_count: 0,
25
+
indexed_at: starter_pack.indexed_at,
26
+
}
27
+
}
28
+
29
+
fn build_spview(
30
+
starter_pack: models::StaterPack,
31
+
creator: ProfileViewBasic,
32
+
list: Option<ListViewBasic>,
33
+
feeds: Vec<GeneratorView>,
34
+
) -> StarterPackView {
35
+
let list_item_count = list.as_ref().map(|list| list.list_item_count).unwrap_or(0);
36
+
37
+
StarterPackView {
38
+
uri: starter_pack.at_uri,
39
+
cid: starter_pack.cid,
40
+
record: starter_pack.record,
41
+
creator,
42
+
list,
43
+
list_items_sample: vec![], // TODO: we should do this, but the app seems to be okay without?
44
+
feeds,
45
+
list_item_count,
46
+
joined_week_count: 0,
47
+
joined_all_time_count: 0,
48
+
indexed_at: starter_pack.indexed_at,
49
+
}
50
+
}
51
+
52
+
pub async fn hydrate_starterpack_basic(
53
+
loaders: &Dataloaders,
54
+
pack: String,
55
+
) -> Option<StarterPackViewBasic> {
56
+
let sp = loaders.starterpacks.load(pack).await?;
57
+
let creator = hydrate_profile_basic(loaders, sp.owner.clone()).await?;
58
+
let (_, list_item_count) = loaders.list.load(sp.list.clone()).await?;
59
+
60
+
Some(build_basic(sp, creator, list_item_count))
61
+
}
62
+
63
+
pub async fn hydrate_starterpacks_basic(
64
+
loaders: &Dataloaders,
65
+
packs: Vec<String>,
66
+
) -> HashMap<String, StarterPackViewBasic> {
67
+
let packs = loaders.starterpacks.load_many(packs).await;
68
+
69
+
let (creators, lists) = packs
70
+
.values()
71
+
.map(|pack| (pack.owner.clone(), pack.list.clone()))
72
+
.unzip();
73
+
74
+
let creators = hydrate_profiles_basic(loaders, creators).await;
75
+
let lists = loaders.list.load_many(lists).await;
76
+
77
+
packs
78
+
.into_iter()
79
+
.filter_map(|(at_uri, pack)| {
80
+
let creator = creators.get(&pack.owner).cloned()?;
81
+
let list_item_count = lists.get(&pack.list).map(|(_, v)| *v).unwrap_or(0);
82
+
83
+
Some((at_uri, build_basic(pack, creator, list_item_count)))
84
+
})
85
+
.collect()
86
+
}
87
+
88
+
pub async fn hydrate_starterpack(loaders: &Dataloaders, pack: String) -> Option<StarterPackView> {
89
+
let sp = loaders.starterpacks.load(pack).await?;
90
+
91
+
let creator = hydrate_profile_basic(loaders, sp.owner.clone()).await?;
92
+
let list = hydrate_list_basic(loaders, sp.list.clone()).await;
93
+
94
+
let feeds = sp
95
+
.feeds
96
+
.clone()
97
+
.unwrap_or_default()
98
+
.into_iter()
99
+
.flatten()
100
+
.collect();
101
+
let feeds = hydrate_feedgens(loaders, feeds)
102
+
.await
103
+
.into_values()
104
+
.collect();
105
+
106
+
Some(build_spview(sp, creator, list, feeds))
107
+
}
108
+
109
+
pub async fn hydrate_starterpacks(
110
+
loaders: &Dataloaders,
111
+
packs: Vec<String>,
112
+
) -> HashMap<String, StarterPackView> {
113
+
let packs = loaders.starterpacks.load_many(packs).await;
114
+
115
+
let (creators, lists) = packs
116
+
.values()
117
+
.map(|pack| (pack.owner.clone(), pack.list.clone()))
118
+
.unzip();
119
+
let feeds = packs
120
+
.values()
121
+
.filter_map(|pack| pack.feeds.clone())
122
+
.flat_map(|feeds| feeds.into_iter().flatten())
123
+
.collect();
124
+
125
+
let creators = hydrate_profiles_basic(loaders, creators).await;
126
+
let lists = hydrate_lists_basic(loaders, lists).await;
127
+
let feeds = hydrate_feedgens(loaders, feeds).await;
128
+
129
+
packs
130
+
.into_iter()
131
+
.filter_map(|(at_uri, pack)| {
132
+
let creator = creators.get(&pack.owner).cloned()?;
133
+
let list = lists.get(&pack.list).cloned();
134
+
let feeds = pack.feeds.as_ref().map(|v| {
135
+
v.iter()
136
+
.flatten()
137
+
.filter_map(|feed| feeds.get(feed).cloned())
138
+
.collect()
139
+
});
140
+
let feeds = feeds.unwrap_or_default();
141
+
142
+
Some((at_uri, build_spview(pack, creator, list, feeds)))
143
+
})
144
+
.collect()
145
+
}
+27
parakeet/src/loaders.rs
+27
parakeet/src/loaders.rs
···
16
pub list: Loader<String, ListLoaderRet, ListLoader>,
17
pub posts: Loader<String, PostLoaderRet, PostLoader>,
18
pub profile: Loader<String, ProfileLoaderRet, ProfileLoader>,
19
}
20
21
impl Dataloaders {
···
29
list: Loader::new(ListLoader(pool.clone())),
30
posts: Loader::new(PostLoader(pool.clone())),
31
profile: Loader::new(ProfileLoader(pool.clone())),
32
}
33
}
34
}
···
274
))
275
}
276
}
···
16
pub list: Loader<String, ListLoaderRet, ListLoader>,
17
pub posts: Loader<String, PostLoaderRet, PostLoader>,
18
pub profile: Loader<String, ProfileLoaderRet, ProfileLoader>,
19
+
pub starterpacks: Loader<String, StarterPackLoaderRet, StarterPackLoader>,
20
}
21
22
impl Dataloaders {
···
30
list: Loader::new(ListLoader(pool.clone())),
31
posts: Loader::new(PostLoader(pool.clone())),
32
profile: Loader::new(ProfileLoader(pool.clone())),
33
+
starterpacks: Loader::new(StarterPackLoader(pool.clone())),
34
}
35
}
36
}
···
276
))
277
}
278
}
279
+
280
+
pub struct StarterPackLoader(Pool<AsyncPgConnection>);
281
+
type StarterPackLoaderRet = models::StaterPack;
282
+
impl BatchFn<String, StarterPackLoaderRet> for StarterPackLoader {
283
+
async fn load(&mut self, keys: &[String]) -> HashMap<String, StarterPackLoaderRet> {
284
+
let mut conn = self.0.get().await.unwrap();
285
+
286
+
let res = schema::starterpacks::table
287
+
.select(models::StaterPack::as_select())
288
+
.filter(schema::starterpacks::at_uri.eq_any(keys))
289
+
.load(&mut conn)
290
+
.await;
291
+
292
+
match res {
293
+
Ok(res) => HashMap::from_iter(
294
+
res.into_iter()
295
+
.map(|starterpack| (starterpack.at_uri.clone(), (starterpack))),
296
+
),
297
+
Err(e) => {
298
+
tracing::error!("starterpack load failed: {e}");
299
+
HashMap::new()
300
+
}
301
+
}
302
+
}
303
+
}
+1
parakeet/src/xrpc/app_bsky/graph/mod.rs
+1
parakeet/src/xrpc/app_bsky/graph/mod.rs
+111
parakeet/src/xrpc/app_bsky/graph/starter_packs.rs
+111
parakeet/src/xrpc/app_bsky/graph/starter_packs.rs
···
···
1
+
use crate::xrpc::error::{Error, XrpcResult};
2
+
use crate::xrpc::{datetime_cursor, get_actor_did, ActorWithCursorQuery};
3
+
use crate::{hydration, GlobalState};
4
+
use axum::extract::{Query, State};
5
+
use axum::Json;
6
+
use axum_extra::extract::Query as ExtraQuery;
7
+
use diesel::prelude::*;
8
+
use diesel_async::RunQueryDsl;
9
+
use lexica::app_bsky::graph::{StarterPackView, StarterPackViewBasic};
10
+
use parakeet_db::schema;
11
+
use serde::{Deserialize, Serialize};
12
+
13
+
#[derive(Debug, Serialize)]
14
+
#[serde(rename_all = "camelCase")]
15
+
pub struct StarterPacksRes {
16
+
pub starter_packs: Vec<StarterPackViewBasic>,
17
+
#[serde(skip_serializing_if = "Option::is_none")]
18
+
cursor: Option<String>,
19
+
}
20
+
21
+
pub async fn get_actor_starter_packs(
22
+
State(state): State<GlobalState>,
23
+
Query(query): Query<ActorWithCursorQuery>,
24
+
) -> XrpcResult<Json<StarterPacksRes>> {
25
+
let mut conn = state.pool.get().await?;
26
+
27
+
let subj_did = get_actor_did(&state.dataloaders, query.actor).await?;
28
+
29
+
let limit = query.limit.unwrap_or(50).clamp(1, 100);
30
+
31
+
let mut sp_query = schema::starterpacks::table
32
+
.select((
33
+
schema::starterpacks::created_at,
34
+
schema::starterpacks::at_uri,
35
+
))
36
+
.filter(schema::starterpacks::owner.eq(subj_did))
37
+
.into_boxed();
38
+
39
+
if let Some(cursor) = datetime_cursor(query.cursor.as_ref()) {
40
+
sp_query = sp_query.filter(schema::starterpacks::created_at.lt(cursor));
41
+
}
42
+
43
+
let results = sp_query
44
+
.order(schema::starterpacks::created_at.desc())
45
+
.limit(limit as i64)
46
+
.load::<(chrono::DateTime<chrono::Utc>, String)>(&mut conn)
47
+
.await?;
48
+
49
+
let cursor = results
50
+
.last()
51
+
.map(|(last, _)| last.timestamp_millis().to_string());
52
+
53
+
let uris = results.iter().map(|(_, uri)| uri.clone()).collect();
54
+
55
+
let mut starter_packs = hydration::hydrate_starterpacks_basic(&state.dataloaders, uris).await;
56
+
57
+
let starter_packs = results
58
+
.into_iter()
59
+
.filter_map(|(_, uri)| starter_packs.remove(&uri))
60
+
.collect();
61
+
62
+
Ok(Json(StarterPacksRes {
63
+
starter_packs,
64
+
cursor,
65
+
}))
66
+
}
67
+
68
+
#[derive(Debug, Deserialize)]
69
+
#[serde(rename_all = "camelCase")]
70
+
pub struct GetStarterPackQuery {
71
+
pub starter_pack: String,
72
+
}
73
+
74
+
#[derive(Debug, Serialize)]
75
+
#[serde(rename_all = "camelCase")]
76
+
pub struct GetStarterPackRes {
77
+
pub starter_pack: StarterPackView,
78
+
}
79
+
80
+
pub async fn get_starter_pack(
81
+
State(state): State<GlobalState>,
82
+
Query(query): Query<GetStarterPackQuery>,
83
+
) -> XrpcResult<Json<GetStarterPackRes>> {
84
+
let Some(starter_pack) =
85
+
hydration::hydrate_starterpack(&state.dataloaders, query.starter_pack).await
86
+
else {
87
+
return Err(Error::not_found());
88
+
};
89
+
90
+
Ok(Json(GetStarterPackRes { starter_pack }))
91
+
}
92
+
93
+
#[derive(Debug, Deserialize)]
94
+
pub struct GetStarterPacksQuery {
95
+
pub uris: Vec<String>,
96
+
}
97
+
98
+
pub async fn get_starter_packs(
99
+
State(state): State<GlobalState>,
100
+
ExtraQuery(query): ExtraQuery<GetStarterPacksQuery>,
101
+
) -> XrpcResult<Json<StarterPacksRes>> {
102
+
let starter_packs = hydration::hydrate_starterpacks_basic(&state.dataloaders, query.uris)
103
+
.await
104
+
.into_values()
105
+
.collect();
106
+
107
+
Ok(Json(StarterPacksRes {
108
+
starter_packs,
109
+
cursor: None,
110
+
}))
111
+
}
+3
parakeet/src/xrpc/app_bsky/mod.rs
+3
parakeet/src/xrpc/app_bsky/mod.rs
···
19
.route("/app.bsky.feed.getRepostedBy", get(feed::posts::get_reposted_by))
20
.route("/app.bsky.feed.getFeedGenerator", get(feed::feedgen::get_feed_generator))
21
.route("/app.bsky.feed.getFeedGenerators", get(feed::feedgen::get_feed_generators))
22
.route("/app.bsky.graph.getFollowers", get(graph::relations::get_followers))
23
.route("/app.bsky.graph.getFollows", get(graph::relations::get_follows))
24
.route("/app.bsky.graph.getList", get(graph::lists::get_list))
25
.route("/app.bsky.graph.getLists", get(graph::lists::get_lists))
26
}
···
19
.route("/app.bsky.feed.getRepostedBy", get(feed::posts::get_reposted_by))
20
.route("/app.bsky.feed.getFeedGenerator", get(feed::feedgen::get_feed_generator))
21
.route("/app.bsky.feed.getFeedGenerators", get(feed::feedgen::get_feed_generators))
22
+
.route("/app.bsky.graph.getActorStarterPacks", get(graph::starter_packs::get_actor_starter_packs))
23
.route("/app.bsky.graph.getFollowers", get(graph::relations::get_followers))
24
.route("/app.bsky.graph.getFollows", get(graph::relations::get_follows))
25
.route("/app.bsky.graph.getList", get(graph::lists::get_list))
26
.route("/app.bsky.graph.getLists", get(graph::lists::get_lists))
27
+
.route("/app.bsky.graph.getStarterPack", get(graph::starter_packs::get_starter_pack))
28
+
.route("/app.bsky.graph.getStarterPacks", get(graph::starter_packs::get_starter_packs))
29
}