tangled
alpha
login
or
join now
parakeet.at
/
parakeet
63
fork
atom
Parakeet is a Rust-based Bluesky AppServer aiming to implement most of the functionality required to support the Bluesky client
appview
atproto
bluesky
rust
appserver
63
fork
atom
overview
issues
12
pulls
pipelines
feat: app.bsky.actor.status
mia.omg.lol
8 months ago
6991c129
c1c7c5c2
+269
-27
12 changed files
expand all
collapse all
unified
split
consumer
src
db
record.rs
sql
status_upsert.sql
indexer
mod.rs
records.rs
types.rs
lexica
src
app_bsky
actor.rs
migrations
2025-06-11-192947_statuses
down.sql
up.sql
parakeet
src
hydration
profile.rs
loaders.rs
parakeet-db
src
models.rs
schema.rs
+33
consumer/src/db/record.rs
···
586
.await
587
}
588
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
589
pub async fn threadgate_upsert<C: GenericClient>(
590
conn: &mut C,
591
at_uri: &str,
···
586
.await
587
}
588
589
+
pub async fn status_upsert<C: GenericClient>(
590
+
conn: &mut C,
591
+
repo: &str,
592
+
rec: AppBskyActorStatus,
593
+
) -> PgExecResult {
594
+
let record = serde_json::to_value(&rec).unwrap();
595
+
let thumb = rec.embed.as_ref().and_then(|v| v.external.thumb.clone());
596
+
let thumb_mime = thumb.as_ref().map(|v| v.mime_type.clone());
597
+
let thumb_cid = thumb.as_ref().map(|v| v.r#ref.to_string());
598
+
599
+
conn.execute(
600
+
include_str!("sql/status_upsert.sql"),
601
+
&[
602
+
&repo,
603
+
&rec.status.to_string(),
604
+
&rec.duration_minutes,
605
+
&record,
606
+
&rec.embed.as_ref().map(|v| v.external.uri.clone()),
607
+
&rec.embed.as_ref().map(|v| v.external.title.clone()),
608
+
&rec.embed.as_ref().map(|v| v.external.description.clone()),
609
+
&thumb_mime,
610
+
&thumb_cid,
611
+
&rec.created_at,
612
+
],
613
+
)
614
+
.await
615
+
}
616
+
617
+
pub async fn status_delete<C: GenericClient>(conn: &mut C, did: &str) -> PgExecResult {
618
+
conn.execute("DELETE FROM statuses WHERE did=$1", &[&did])
619
+
.await
620
+
}
621
+
622
pub async fn threadgate_upsert<C: GenericClient>(
623
conn: &mut C,
624
at_uri: &str,
+12
consumer/src/db/sql/status_upsert.sql
···
0
0
0
0
0
0
0
0
0
0
0
0
···
1
+
INSERT INTO statuses (did, status, duration, record, embed_uri, embed_title, embed_description, thumb_mime_type,
2
+
thumb_cid, created_at)
3
+
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
4
+
ON CONFLICT (did) DO UPDATE SET status=EXCLUDED.status,
5
+
duration=EXCLUDED.duration,
6
+
record=EXCLUDED.record,
7
+
embed_uri=EXCLUDED.embed_uri,
8
+
embed_title=EXCLUDED.embed_title,
9
+
embed_description=EXCLUDED.embed_description,
10
+
thumb_mime_type=EXCLUDED.thumb_mime_type,
11
+
thumb_cid=EXCLUDED.thumb_cid,
12
+
indexed_at=NOW()
+6
consumer/src/indexer/mod.rs
···
518
}
519
}
520
}
0
0
0
0
0
521
RecordTypes::AppBskyFeedGenerator(record) => {
522
let labels = record.labels.clone();
523
let count = db::feedgen_upsert(conn, at_uri, repo, cid, record).await?;
···
691
) -> Result<(), tokio_postgres::Error> {
692
match collection {
693
CollectionType::BskyProfile => db::profile_delete(conn, repo).await?,
0
694
CollectionType::BskyBlock => db::block_delete(conn, at_uri).await?,
695
CollectionType::BskyFeedGen => {
696
let count = db::feedgen_delete(conn, at_uri).await?;
···
518
}
519
}
520
}
521
+
RecordTypes::AppBskyActorStatus(record) => {
522
+
if rkey == "self" {
523
+
db::status_upsert(conn, repo, record).await?;
524
+
}
525
+
}
526
RecordTypes::AppBskyFeedGenerator(record) => {
527
let labels = record.labels.clone();
528
let count = db::feedgen_upsert(conn, at_uri, repo, cid, record).await?;
···
696
) -> Result<(), tokio_postgres::Error> {
697
match collection {
698
CollectionType::BskyProfile => db::profile_delete(conn, repo).await?,
699
+
CollectionType::BskyStatus => db::status_delete(conn, repo).await?,
700
CollectionType::BskyBlock => db::block_delete(conn, at_uri).await?,
701
CollectionType::BskyFeedGen => {
702
let count = db::feedgen_delete(conn, at_uri).await?;
+13
-1
consumer/src/indexer/records.rs
···
1
use crate::utils;
2
use chrono::{DateTime, Utc};
3
use ipld_core::cid::Cid;
4
-
use lexica::app_bsky::actor::ChatAllowIncoming;
5
use lexica::app_bsky::embed::AspectRatio;
6
use lexica::app_bsky::labeler::LabelerPolicy;
7
use lexica::app_bsky::richtext::FacetMain;
···
41
pub joined_via_starter_pack: Option<StrongRef>,
42
pub pinned_post: Option<StrongRef>,
43
pub created_at: Option<DateTime<Utc>>,
0
0
0
0
0
0
0
0
0
0
44
}
45
46
#[derive(Clone, Debug, Deserialize, Serialize)]
···
151
}
152
153
#[derive(Clone, Debug, Deserialize, Serialize)]
0
0
154
pub struct AppBskyEmbedExternal {
155
pub external: EmbedExternal,
156
}
···
1
use crate::utils;
2
use chrono::{DateTime, Utc};
3
use ipld_core::cid::Cid;
4
+
use lexica::app_bsky::actor::{ChatAllowIncoming, Status};
5
use lexica::app_bsky::embed::AspectRatio;
6
use lexica::app_bsky::labeler::LabelerPolicy;
7
use lexica::app_bsky::richtext::FacetMain;
···
41
pub joined_via_starter_pack: Option<StrongRef>,
42
pub pinned_post: Option<StrongRef>,
43
pub created_at: Option<DateTime<Utc>>,
44
+
}
45
+
46
+
#[derive(Debug, Deserialize, Serialize)]
47
+
#[serde(rename_all = "camelCase")]
48
+
pub struct AppBskyActorStatus {
49
+
pub status: Status,
50
+
pub duration_minutes: Option<i32>,
51
+
pub embed: Option<AppBskyEmbedExternal>,
52
+
53
+
pub created_at: DateTime<Utc>,
54
}
55
56
#[derive(Clone, Debug, Deserialize, Serialize)]
···
161
}
162
163
#[derive(Clone, Debug, Deserialize, Serialize)]
164
+
#[serde(tag = "$type")]
165
+
#[serde(rename = "app.bsky.embed.external")]
166
pub struct AppBskyEmbedExternal {
167
pub external: EmbedExternal,
168
}
+5
consumer/src/indexer/types.rs
···
7
pub enum RecordTypes {
8
#[serde(rename = "app.bsky.actor.profile")]
9
AppBskyActorProfile(records::AppBskyActorProfile),
0
0
10
#[serde(rename = "app.bsky.feed.generator")]
11
AppBskyFeedGenerator(records::AppBskyFeedGenerator),
12
#[serde(rename = "app.bsky.feed.like")]
···
42
#[derive(Debug, PartialOrd, PartialEq, Deserialize, Serialize)]
43
pub enum CollectionType {
44
BskyProfile,
0
45
BskyFeedGen,
46
BskyFeedLike,
47
BskyFeedPost,
···
64
pub(crate) fn from_str(input: &str) -> CollectionType {
65
match input {
66
"app.bsky.actor.profile" => CollectionType::BskyProfile,
0
67
"app.bsky.feed.generator" => CollectionType::BskyFeedGen,
68
"app.bsky.feed.like" => CollectionType::BskyFeedLike,
69
"app.bsky.feed.post" => CollectionType::BskyFeedPost,
···
86
pub fn can_update(&self) -> bool {
87
match self {
88
CollectionType::BskyProfile => true,
0
89
CollectionType::BskyFeedGen => true,
90
CollectionType::BskyFeedLike => false,
91
CollectionType::BskyFeedPost => false,
···
7
pub enum RecordTypes {
8
#[serde(rename = "app.bsky.actor.profile")]
9
AppBskyActorProfile(records::AppBskyActorProfile),
10
+
#[serde(rename = "app.bsky.actor.status")]
11
+
AppBskyActorStatus(records::AppBskyActorStatus),
12
#[serde(rename = "app.bsky.feed.generator")]
13
AppBskyFeedGenerator(records::AppBskyFeedGenerator),
14
#[serde(rename = "app.bsky.feed.like")]
···
44
#[derive(Debug, PartialOrd, PartialEq, Deserialize, Serialize)]
45
pub enum CollectionType {
46
BskyProfile,
47
+
BskyStatus,
48
BskyFeedGen,
49
BskyFeedLike,
50
BskyFeedPost,
···
67
pub(crate) fn from_str(input: &str) -> CollectionType {
68
match input {
69
"app.bsky.actor.profile" => CollectionType::BskyProfile,
70
+
"app.bsky.actor.status" => CollectionType::BskyStatus,
71
"app.bsky.feed.generator" => CollectionType::BskyFeedGen,
72
"app.bsky.feed.like" => CollectionType::BskyFeedLike,
73
"app.bsky.feed.post" => CollectionType::BskyFeedPost,
···
90
pub fn can_update(&self) -> bool {
91
match self {
92
CollectionType::BskyProfile => true,
93
+
CollectionType::BskyStatus => true,
94
CollectionType::BskyFeedGen => true,
95
CollectionType::BskyFeedLike => false,
96
CollectionType::BskyFeedPost => false,
+53
lexica/src/app_bsky/actor.rs
···
0
1
use crate::com_atproto::label::Label;
2
use chrono::prelude::*;
3
use serde::{Deserialize, Serialize};
···
47
"all" => Ok(ChatAllowIncoming::All),
48
"none" => Ok(ChatAllowIncoming::None),
49
"following" => Ok(ChatAllowIncoming::Following),
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
50
x => Err(format!("Unrecognized variant {}", x)),
51
}
52
}
···
70
pub labels: Vec<Label>,
71
#[serde(skip_serializing_if = "Option::is_none")]
72
pub verification: Option<VerificationState>,
0
0
73
74
pub created_at: DateTime<Utc>,
75
}
···
94
pub labels: Vec<Label>,
95
#[serde(skip_serializing_if = "Option::is_none")]
96
pub verification: Option<VerificationState>,
0
0
97
98
pub created_at: DateTime<Utc>,
99
pub indexed_at: NaiveDateTime,
···
127
// pub pinned_post: Option<()>,
128
#[serde(skip_serializing_if = "Option::is_none")]
129
pub verification: Option<VerificationState>,
0
0
130
131
pub created_at: DateTime<Utc>,
132
pub indexed_at: NaiveDateTime,
···
164
Invalid,
165
None,
166
}
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
···
1
+
use crate::app_bsky::embed::External;
2
use crate::com_atproto::label::Label;
3
use chrono::prelude::*;
4
use serde::{Deserialize, Serialize};
···
48
"all" => Ok(ChatAllowIncoming::All),
49
"none" => Ok(ChatAllowIncoming::None),
50
"following" => Ok(ChatAllowIncoming::Following),
51
+
x => Err(format!("Unrecognized variant {}", x)),
52
+
}
53
+
}
54
+
}
55
+
56
+
#[derive(Copy, Clone, Debug, Deserialize, Serialize)]
57
+
pub enum Status {
58
+
/// Advertises an account as currently offering live content.
59
+
#[serde(rename = "app.bsky.actor.status#live")]
60
+
Live,
61
+
}
62
+
63
+
impl Display for Status {
64
+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
65
+
match self {
66
+
Status::Live => write!(f, "app.bsky.actor.status#live"),
67
+
}
68
+
}
69
+
}
70
+
71
+
impl FromStr for Status {
72
+
type Err = String;
73
+
74
+
fn from_str(s: &str) -> Result<Self, Self::Err> {
75
+
match s {
76
+
"app.bsky.actor.status#live" => Ok(Status::Live),
77
x => Err(format!("Unrecognized variant {}", x)),
78
}
79
}
···
97
pub labels: Vec<Label>,
98
#[serde(skip_serializing_if = "Option::is_none")]
99
pub verification: Option<VerificationState>,
100
+
#[serde(skip_serializing_if = "Option::is_none")]
101
+
pub status: Option<StatusView>,
102
103
pub created_at: DateTime<Utc>,
104
}
···
123
pub labels: Vec<Label>,
124
#[serde(skip_serializing_if = "Option::is_none")]
125
pub verification: Option<VerificationState>,
126
+
#[serde(skip_serializing_if = "Option::is_none")]
127
+
pub status: Option<StatusView>,
128
129
pub created_at: DateTime<Utc>,
130
pub indexed_at: NaiveDateTime,
···
158
// pub pinned_post: Option<()>,
159
#[serde(skip_serializing_if = "Option::is_none")]
160
pub verification: Option<VerificationState>,
161
+
#[serde(skip_serializing_if = "Option::is_none")]
162
+
pub status: Option<StatusView>,
163
164
pub created_at: DateTime<Utc>,
165
pub indexed_at: NaiveDateTime,
···
197
Invalid,
198
None,
199
}
200
+
201
+
#[derive(Clone, Debug, Serialize)]
202
+
#[serde(rename_all = "camelCase")]
203
+
pub struct StatusView {
204
+
pub status: Status,
205
+
pub record: serde_json::Value,
206
+
#[serde(skip_serializing_if = "Option::is_none")]
207
+
pub embed: Option<StatusViewEmbed>,
208
+
#[serde(skip_serializing_if = "Option::is_none")]
209
+
pub expires_at: Option<DateTime<Utc>>,
210
+
#[serde(skip_serializing_if = "Option::is_none")]
211
+
pub is_active: Option<bool>,
212
+
}
213
+
214
+
#[derive(Clone, Debug, Serialize)]
215
+
#[serde(tag = "$type")]
216
+
#[serde(rename = "app.bsky.embed.external#view")]
217
+
pub struct StatusViewEmbed {
218
+
pub external: External
219
+
}
+1
migrations/2025-06-11-192947_statuses/down.sql
···
0
···
1
+
drop table statuses;
+16
migrations/2025-06-11-192947_statuses/up.sql
···
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
···
1
+
create table statuses
2
+
(
3
+
did text primary key references actors (did),
4
+
status text not null,
5
+
duration int,
6
+
record jsonb not null,
7
+
8
+
embed_uri text,
9
+
embed_title text,
10
+
embed_description text,
11
+
thumb_mime_type text,
12
+
thumb_cid text,
13
+
14
+
created_at timestamptz not null default now(),
15
+
indexed_at timestamp not null default now()
16
+
);
+22
parakeet-db/src/models.rs
···
718
#[diesel(treat_none_as_default_value = true)]
719
pub indexed_at: Option<NaiveDateTime>,
720
}
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
···
718
#[diesel(treat_none_as_default_value = true)]
719
pub indexed_at: Option<NaiveDateTime>,
720
}
721
+
722
+
#[derive(Clone, Debug, Queryable, Selectable, Identifiable)]
723
+
#[diesel(table_name = crate::schema::statuses)]
724
+
#[diesel(primary_key(did))]
725
+
#[diesel(check_for_backend(diesel::pg::Pg))]
726
+
pub struct Status {
727
+
pub did: String,
728
+
pub status: String,
729
+
pub duration: Option<i32>,
730
+
731
+
pub record: serde_json::Value,
732
+
733
+
pub embed_uri: Option<String>,
734
+
pub embed_title: Option<String>,
735
+
pub embed_description: Option<String>,
736
+
pub thumb_mime_type: Option<String>,
737
+
pub thumb_cid: Option<String>,
738
+
739
+
pub created_at: NaiveDateTime,
740
+
pub indexed_at: NaiveDateTime,
741
+
}
742
+
+18
parakeet-db/src/schema.rs
···
303
}
304
305
diesel::table! {
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
306
threadgates (at_uri) {
307
at_uri -> Text,
308
cid -> Text,
···
349
diesel::joinable!(profiles -> actors (did));
350
diesel::joinable!(reposts -> actors (did));
351
diesel::joinable!(starterpacks -> actors (owner));
0
352
diesel::joinable!(threadgates -> posts (post_uri));
353
diesel::joinable!(verification -> actors (verifier));
354
···
378
records,
379
reposts,
380
starterpacks,
0
381
threadgates,
382
verification,
383
);
···
303
}
304
305
diesel::table! {
306
+
statuses (did) {
307
+
did -> Text,
308
+
status -> Text,
309
+
duration -> Nullable<Int4>,
310
+
record -> Jsonb,
311
+
embed_uri -> Nullable<Text>,
312
+
embed_title -> Nullable<Text>,
313
+
embed_description -> Nullable<Text>,
314
+
thumb_mime_type -> Nullable<Text>,
315
+
thumb_cid -> Nullable<Text>,
316
+
created_at -> Timestamptz,
317
+
indexed_at -> Timestamp,
318
+
}
319
+
}
320
+
321
+
diesel::table! {
322
threadgates (at_uri) {
323
at_uri -> Text,
324
cid -> Text,
···
365
diesel::joinable!(profiles -> actors (did));
366
diesel::joinable!(reposts -> actors (did));
367
diesel::joinable!(starterpacks -> actors (owner));
368
+
diesel::joinable!(statuses -> actors (did));
369
diesel::joinable!(threadgates -> posts (post_uri));
370
diesel::joinable!(verification -> actors (verifier));
371
···
395
records,
396
reposts,
397
starterpacks,
398
+
statuses,
399
threadgates,
400
verification,
401
);
+84
-24
parakeet/src/hydration/profile.rs
···
1
use crate::hydration::map_labels;
0
2
use lexica::app_bsky::actor::*;
0
3
use parakeet_db::models;
4
use parakeet_index::ProfileStats;
5
use std::collections::HashMap;
0
6
use std::sync::OnceLock;
0
7
8
pub static TRUSTED_VERIFIERS: OnceLock<Vec<String>> = OnceLock::new();
9
···
109
}
110
}
111
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
112
fn build_basic(
113
handle: Option<String>,
114
profile: models::Profile,
···
117
labels: Vec<models::Label>,
118
verifications: Option<Vec<models::VerificationEntry>>,
119
stats: Option<ProfileStats>,
0
120
) -> ProfileViewBasic {
121
let associated = build_associated(chat_decl, is_labeler, stats);
122
let verification = build_verification(&profile, &handle, verifications);
0
123
124
ProfileViewBasic {
125
did: profile.did,
···
131
associated,
132
labels: map_labels(labels),
133
verification,
0
134
created_at: profile.created_at.and_utc(),
135
}
136
}
···
143
labels: Vec<models::Label>,
144
verifications: Option<Vec<models::VerificationEntry>>,
145
stats: Option<ProfileStats>,
0
146
) -> ProfileView {
147
let associated = build_associated(chat_decl, is_labeler, stats);
148
let verification = build_verification(&profile, &handle, verifications);
0
149
150
ProfileView {
151
did: profile.did,
···
158
associated,
159
labels: map_labels(labels),
160
verification,
0
161
created_at: profile.created_at.and_utc(),
162
indexed_at: profile.indexed_at,
163
}
···
171
labels: Vec<models::Label>,
172
verifications: Option<Vec<models::VerificationEntry>>,
173
stats: Option<ProfileStats>,
0
174
) -> ProfileViewDetailed {
175
let associated = build_associated(chat_decl, is_labeler, stats);
176
let verification = build_verification(&profile, &handle, verifications);
0
177
178
ProfileViewDetailed {
179
did: profile.did,
···
191
associated,
192
labels: map_labels(labels),
193
verification,
0
194
created_at: profile.created_at.and_utc(),
195
indexed_at: profile.indexed_at,
196
}
···
200
pub async fn hydrate_profile_basic(&self, did: String) -> Option<ProfileViewBasic> {
201
let labels = self.get_profile_label(&did).await;
202
let verif = self.loaders.verification.load(did.clone()).await;
203
-
let (handle, profile, chat_decl, labeler, stats) = self.loaders.profile.load(did).await?;
0
204
205
Some(build_basic(
206
-
handle, profile, chat_decl, labeler, labels, verif, stats,
207
))
208
}
209
···
217
218
profiles
219
.into_iter()
220
-
.map(|(k, (handle, profile, chat_decl, labeler, stats))| {
221
-
let labels = labels.get(&k).cloned().unwrap_or_default();
222
-
let verif = verif.get(&k).cloned();
0
223
224
-
let v = build_basic(handle, profile, chat_decl, labeler, labels, verif, stats);
225
-
(k, v)
226
-
})
0
0
0
227
.collect()
228
}
229
···
231
let labels = self.get_profile_label(&did).await;
232
233
let verif = self.loaders.verification.load(did.clone()).await;
234
-
let (handle, profile, chat_decl, labeler, stats) = self.loaders.profile.load(did).await?;
0
235
236
Some(build_profile(
237
-
handle, profile, chat_decl, labeler, labels, verif, stats,
238
))
239
}
240
···
245
246
profiles
247
.into_iter()
248
-
.map(|(k, (handle, profile, chat_decl, labeler, stats))| {
249
-
let labels = labels.get(&k).cloned().unwrap_or_default();
250
-
let verif = verif.get(&k).cloned();
0
251
252
-
let v = build_profile(handle, profile, chat_decl, labeler, labels, verif, stats);
253
-
(k, v)
254
-
})
0
0
0
255
.collect()
256
}
257
···
259
let labels = self.get_profile_label(&did).await;
260
261
let verif = self.loaders.verification.load(did.clone()).await;
262
-
let (handle, profile, chat_decl, labeler, stats) = self.loaders.profile.load(did).await?;
0
263
264
Some(build_detailed(
265
-
handle, profile, chat_decl, labeler, labels, verif, stats,
266
))
267
}
268
···
276
277
profiles
278
.into_iter()
279
-
.map(|(k, (handle, profile, chat_decl, labeler, stats))| {
280
-
let labels = labels.get(&k).cloned().unwrap_or_default();
281
-
let verif = verif.get(&k).cloned();
0
282
283
-
let v = build_detailed(handle, profile, chat_decl, labeler, labels, verif, stats);
284
-
(k, v)
285
-
})
0
0
0
286
.collect()
287
}
288
}
···
1
use crate::hydration::map_labels;
2
+
use chrono::prelude::*;
3
use lexica::app_bsky::actor::*;
4
+
use lexica::app_bsky::embed::External;
5
use parakeet_db::models;
6
use parakeet_index::ProfileStats;
7
use std::collections::HashMap;
8
+
use std::str::FromStr;
9
use std::sync::OnceLock;
10
+
use chrono::TimeDelta;
11
12
pub static TRUSTED_VERIFIERS: OnceLock<Vec<String>> = OnceLock::new();
13
···
113
}
114
}
115
116
+
fn build_status(status: models::Status) -> Option<StatusView> {
117
+
let s = Status::from_str(&status.status).ok()?;
118
+
let embed = status
119
+
.embed_uri
120
+
.zip(status.embed_title)
121
+
.zip(status.embed_description)
122
+
.map(|((uri, title), description)| StatusViewEmbed {
123
+
external: External {
124
+
uri,
125
+
title,
126
+
description,
127
+
thumb: status
128
+
.thumb_cid
129
+
.map(|v| format!("https://localhost/embed/{v}")),
130
+
},
131
+
});
132
+
133
+
let expires_at = status
134
+
.duration
135
+
.map(|v| TimeDelta::seconds(v as i64))
136
+
.map(|v| (status.created_at + v).and_utc());
137
+
let is_active = expires_at.map(|v| Utc::now() < v);
138
+
139
+
Some(StatusView {
140
+
status: s,
141
+
record: status.record,
142
+
embed,
143
+
expires_at,
144
+
is_active,
145
+
})
146
+
}
147
+
148
fn build_basic(
149
handle: Option<String>,
150
profile: models::Profile,
···
153
labels: Vec<models::Label>,
154
verifications: Option<Vec<models::VerificationEntry>>,
155
stats: Option<ProfileStats>,
156
+
status: Option<models::Status>,
157
) -> ProfileViewBasic {
158
let associated = build_associated(chat_decl, is_labeler, stats);
159
let verification = build_verification(&profile, &handle, verifications);
160
+
let status = status.and_then(build_status);
161
162
ProfileViewBasic {
163
did: profile.did,
···
169
associated,
170
labels: map_labels(labels),
171
verification,
172
+
status,
173
created_at: profile.created_at.and_utc(),
174
}
175
}
···
182
labels: Vec<models::Label>,
183
verifications: Option<Vec<models::VerificationEntry>>,
184
stats: Option<ProfileStats>,
185
+
status: Option<models::Status>,
186
) -> ProfileView {
187
let associated = build_associated(chat_decl, is_labeler, stats);
188
let verification = build_verification(&profile, &handle, verifications);
189
+
let status = status.and_then(build_status);
190
191
ProfileView {
192
did: profile.did,
···
199
associated,
200
labels: map_labels(labels),
201
verification,
202
+
status,
203
created_at: profile.created_at.and_utc(),
204
indexed_at: profile.indexed_at,
205
}
···
213
labels: Vec<models::Label>,
214
verifications: Option<Vec<models::VerificationEntry>>,
215
stats: Option<ProfileStats>,
216
+
status: Option<models::Status>,
217
) -> ProfileViewDetailed {
218
let associated = build_associated(chat_decl, is_labeler, stats);
219
let verification = build_verification(&profile, &handle, verifications);
220
+
let status = status.and_then(build_status);
221
222
ProfileViewDetailed {
223
did: profile.did,
···
235
associated,
236
labels: map_labels(labels),
237
verification,
238
+
status,
239
created_at: profile.created_at.and_utc(),
240
indexed_at: profile.indexed_at,
241
}
···
245
pub async fn hydrate_profile_basic(&self, did: String) -> Option<ProfileViewBasic> {
246
let labels = self.get_profile_label(&did).await;
247
let verif = self.loaders.verification.load(did.clone()).await;
248
+
let (handle, profile, chat_decl, labeler, stats, status) =
249
+
self.loaders.profile.load(did).await?;
250
251
Some(build_basic(
252
+
handle, profile, chat_decl, labeler, labels, verif, stats, status,
253
))
254
}
255
···
263
264
profiles
265
.into_iter()
266
+
.map(
267
+
|(k, (handle, profile, chat_decl, labeler, stats, status))| {
268
+
let labels = labels.get(&k).cloned().unwrap_or_default();
269
+
let verif = verif.get(&k).cloned();
270
271
+
let v = build_basic(
272
+
handle, profile, chat_decl, labeler, labels, verif, stats, status,
273
+
);
274
+
(k, v)
275
+
},
276
+
)
277
.collect()
278
}
279
···
281
let labels = self.get_profile_label(&did).await;
282
283
let verif = self.loaders.verification.load(did.clone()).await;
284
+
let (handle, profile, chat_decl, labeler, stats, status) =
285
+
self.loaders.profile.load(did).await?;
286
287
Some(build_profile(
288
+
handle, profile, chat_decl, labeler, labels, verif, stats, status,
289
))
290
}
291
···
296
297
profiles
298
.into_iter()
299
+
.map(
300
+
|(k, (handle, profile, chat_decl, labeler, stats, status))| {
301
+
let labels = labels.get(&k).cloned().unwrap_or_default();
302
+
let verif = verif.get(&k).cloned();
303
304
+
let v = build_profile(
305
+
handle, profile, chat_decl, labeler, labels, verif, stats, status,
306
+
);
307
+
(k, v)
308
+
},
309
+
)
310
.collect()
311
}
312
···
314
let labels = self.get_profile_label(&did).await;
315
316
let verif = self.loaders.verification.load(did.clone()).await;
317
+
let (handle, profile, chat_decl, labeler, stats, status) =
318
+
self.loaders.profile.load(did).await?;
319
320
Some(build_detailed(
321
+
handle, profile, chat_decl, labeler, labels, verif, stats, status,
322
))
323
}
324
···
332
333
profiles
334
.into_iter()
335
+
.map(
336
+
|(k, (handle, profile, chat_decl, labeler, stats, status))| {
337
+
let labels = labels.get(&k).cloned().unwrap_or_default();
338
+
let verif = verif.get(&k).cloned();
339
340
+
let v = build_detailed(
341
+
handle, profile, chat_decl, labeler, labels, verif, stats, status,
342
+
);
343
+
(k, v)
344
+
},
345
+
)
346
.collect()
347
}
348
}
+6
-2
parakeet/src/loaders.rs
···
73
Option<ChatAllowIncoming>,
74
bool,
75
Option<parakeet_index::ProfileStats>,
0
76
);
77
impl BatchFn<String, ProfileLoaderRet> for ProfileLoader {
78
async fn load(&mut self, keys: &[String]) -> HashMap<String, ProfileLoaderRet> {
···
84
schema::chat_decls::table.on(schema::chat_decls::did.eq(schema::actors::did)),
85
)
86
.left_join(schema::labelers::table.on(schema::labelers::did.eq(schema::actors::did)))
0
87
.select((
88
schema::actors::did,
89
schema::actors::handle,
90
models::Profile::as_select(),
91
schema::chat_decls::allow_incoming.nullable(),
92
schema::labelers::cid.nullable(),
0
93
))
94
.filter(
95
schema::actors::did
···
102
models::Profile,
103
Option<String>,
104
Option<String>,
0
105
)>(&mut conn)
106
.await;
107
···
118
119
match res {
120
Ok(res) => HashMap::from_iter(res.into_iter().map(
121
-
|(did, handle, profile, chat_decl, labeler_cid)| {
122
let chat_decl = chat_decl.and_then(|v| ChatAllowIncoming::from_str(&v).ok());
123
let is_labeler = labeler_cid.is_some();
124
let maybe_stats = stats.remove(&did);
125
126
-
let val = (handle, profile, chat_decl, is_labeler, maybe_stats);
127
128
(did, val)
129
},
···
73
Option<ChatAllowIncoming>,
74
bool,
75
Option<parakeet_index::ProfileStats>,
76
+
Option<models::Status>,
77
);
78
impl BatchFn<String, ProfileLoaderRet> for ProfileLoader {
79
async fn load(&mut self, keys: &[String]) -> HashMap<String, ProfileLoaderRet> {
···
85
schema::chat_decls::table.on(schema::chat_decls::did.eq(schema::actors::did)),
86
)
87
.left_join(schema::labelers::table.on(schema::labelers::did.eq(schema::actors::did)))
88
+
.left_join(schema::statuses::table.on(schema::statuses::did.eq(schema::actors::did)))
89
.select((
90
schema::actors::did,
91
schema::actors::handle,
92
models::Profile::as_select(),
93
schema::chat_decls::allow_incoming.nullable(),
94
schema::labelers::cid.nullable(),
95
+
Option::<models::Status>::as_select(),
96
))
97
.filter(
98
schema::actors::did
···
105
models::Profile,
106
Option<String>,
107
Option<String>,
108
+
Option<models::Status>,
109
)>(&mut conn)
110
.await;
111
···
122
123
match res {
124
Ok(res) => HashMap::from_iter(res.into_iter().map(
125
+
|(did, handle, profile, chat_decl, labeler_cid, status)| {
126
let chat_decl = chat_decl.and_then(|v| ChatAllowIncoming::from_str(&v).ok());
127
let is_labeler = labeler_cid.is_some();
128
let maybe_stats = stats.remove(&did);
129
130
+
let val = (handle, profile, chat_decl, is_labeler, maybe_stats, status);
131
132
(did, val)
133
},