Personal ATProto tools.
1//! Structs, enums, and impls.
2use std::str::FromStr;
3use base64::Engine;
4use chrono::{DateTime as Datetime, Datelike};
5use jetstream_oxide::exports::Did;
6use serde::{ser::{Serialize, Serializer}, Deserialize};
7
8// /// How should a client visually convey this label?
9// enum LabelDefinitionSeverity {
10// /// 'inform' means neutral and informational
11// Inform,
12// /// 'alert' means negative and warning
13// Alert,
14// /// 'none' means show nothing.
15// None,
16// }
17// impl LabelDefinitionSeverity {
18// fn to_string(&self) -> String {
19// match self {
20// Self::Inform => "inform".to_owned(),
21// Self::Alert => "alert".to_owned(),
22// Self::None => "none".to_owned(),
23// }
24// }
25// }
26// /// What should this label hide in the UI, if applied?
27// enum LabelDefinitionBlurs {
28// /// 'content' hides all of the target
29// Content,
30// /// 'media' hides the images/video/audio
31// Media,
32// /// 'none' hides nothing.
33// None,
34// }
35// impl LabelDefinitionBlurs {
36// fn to_string(&self) -> String {
37// match self {
38// Self::Content => "content".to_owned(),
39// Self::Media => "media".to_owned(),
40// Self::None => "none".to_owned(),
41// }
42// }
43// }
44// /// The default setting for this label.
45// enum LabelDefinitionDefaultSetting {
46// Hide,
47// Warn,
48// Ignore,
49// }
50// impl LabelDefinitionDefaultSetting {
51// fn to_string(&self) -> String {
52// match self {
53// Self::Hide => "hide".to_owned(),
54// Self::Warn => "warn".to_owned(),
55// Self::Ignore => "ignore".to_owned(),
56// }
57// }
58// }
59// /// Strings which describe the label in the UI, localized into a specific language.
60// struct LabelValueDefinitionStrings {
61// /// The code of the language these strings are written in.
62// lang: String,
63// /// A short human-readable name for the label.
64// name: String,
65// /// A longer description of what the label means and why it might be applied.
66// description: String,
67// }
68// /// Labels.
69// struct LabelDefinition {
70// /// The value of the label being defined. Must only include lowercase ascii and the '-' character (a-z-+).
71// identifier: String,
72// /// How should a client visually convey this label? 'inform' means neutral and informational; 'alert' means negative and warning; 'none' means show nothing.
73// severity: LabelDefinitionSeverity,
74// /// What should this label hide in the UI, if applied? 'content' hides all of the target; 'media' hides the images/video/audio; 'none' hides nothing.
75// blurs: LabelDefinitionBlurs,
76// /// The default setting for this label.
77// default_setting: LabelDefinitionDefaultSetting,
78// /// Does the user need to have adult content enabled in order to configure this label?
79// adult_content: Option<bool>,
80// /// Strings which describe the label in the UI, localized into a specific language.
81// locales: Vec<LabelValueDefinitionStrings>,
82// }
83// impl LabelDefinition {
84// fn new(identifier: String) -> Self {
85// let locales = vec![LabelValueDefinitionStrings {
86// lang: "en".to_owned(),
87// name: identifier.replace("joined-", "Joined "),
88// description: format!("Profile created {}", identifier.replace("joined-", "").replace("-", " ")),
89// }];
90// Self {
91// identifier,
92// severity: LabelDefinitionSeverity::Inform,
93// blurs: LabelDefinitionBlurs::None,
94// default_setting: LabelDefinitionDefaultSetting::Warn,
95// adult_content: Some(false),
96// locales,
97// }
98// }
99// }
100
101
102#[derive(Debug)]
103/// Signature bytes.
104pub struct SignatureBytes([u8; 64]);
105impl FromStr for SignatureBytes {
106 type Err = std::io::Error;
107 fn from_str(s: &str) -> Result<Self, Self::Err> {
108 let bytes = base64::engine::GeneralPurpose::new(
109 &base64::alphabet::STANDARD,
110 base64::engine::general_purpose::NO_PAD).decode(s).expect("Expected to be able to decode the base64 string as bytes but failed.");
111 let mut array = [0; 64];
112 array.copy_from_slice(&bytes);
113 Ok(Self(array))
114 }
115}
116impl SignatureBytes {
117 /// Create a new signature from a vector of bytes.
118 pub fn from_vec(vec: Vec<u8>) -> Self {
119 let mut array = [0; 64];
120 array.copy_from_slice(&vec);
121 Self(array)
122 }
123 /// Create a new signature from a slice of bytes.
124 pub const fn from_bytes(bytes: [u8; 64]) -> Self {
125 Self(bytes)
126 }
127 /// Create a new signature from a JSON value in the format of a $bytes object.
128 pub fn from_json(json: serde_json::Value) -> Self {
129 let byte_string = json["$bytes"].as_str().expect("Expected to be able to get the $bytes field from the JSON object as a string but failed.");
130 let bytes = base64::engine::GeneralPurpose::new(
131 &base64::alphabet::STANDARD,
132 base64::engine::general_purpose::NO_PAD).decode(byte_string).expect("Expected to be able to decode the base64 string as bytes but failed.");
133 Self::from_vec(bytes)
134 }
135 /// Get the signature as a vector of bytes.
136 pub fn as_vec(&self) -> Vec<u8> {
137 self.0.to_vec()
138 }
139 /// Get the signature as a base64 string.
140 pub fn as_base64(&self) -> String {
141 base64::engine::GeneralPurpose::new(
142 &base64::alphabet::STANDARD,
143 base64::engine::general_purpose::NO_PAD).encode(self.0)
144 }
145 /// Get the signature as a JSON object in the format of a $bytes object.
146 pub fn as_json_object(&self) -> serde_json::Value {
147 serde_json::json!({
148 "$bytes": self.as_base64()
149 })
150 }
151}
152impl Serialize for SignatureBytes {
153 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
154 where
155 S: Serializer,
156 {
157 serializer.serialize_bytes(&self.0)
158 }
159}
160#[derive(Debug)]
161/// Signature bytes or JSON value.
162pub enum SignatureEnum {
163 /// Signature bytes.
164 Bytes(SignatureBytes),
165 /// Signature JSON value.
166 Json(serde_json::Value),
167}
168impl Serialize for SignatureEnum {
169 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
170 where
171 S: Serializer,
172 {
173 match self {
174 Self::Bytes(bytes) => bytes.serialize(serializer),
175 Self::Json(json) => json.serialize(serializer),
176 }
177 }
178}
179
180#[derive(serde::Serialize, Debug)]
181/// Label response content.
182pub struct AssignedLabelResponse {
183 // /// Timestamp at which this label expires (no longer applies, is no longer valid).
184 // exp: Option<DateTime<FixedOffset>>,
185 // /// Optionally, CID specifying the specific version of 'uri' resource this label applies to. \
186 // /// If provided, the label applies to a specific version of the subject uri
187 // cid: Option<String>,
188 /// Timestamp when this label was created.
189 /// Note that timestamps in a distributed system are not trustworthy or verified by default.
190 pub cts: String, // DateTime<Utc>,
191 /// If true, this is a negation label, indicates that this label "negates" an earlier label with the same src, uri, and val.
192 /// If the neg field is false, best practice is to simply not include the field at all.
193 #[serde(skip_serializing_if = "bool_is_false")]
194 pub neg: bool,
195 /// Signature of dag-cbor encoded label. \
196 /// cryptographic signature bytes. \
197 /// Uses the bytes type from the [Data Model](https://atproto.com/specs/data-model), which encodes in JSON as a $bytes object with base64 encoding
198 /// When labels are being transferred as full objects between services, the ver and sig fields are required.
199 pub sig: Option<SignatureEnum>,
200 /// DID of the actor authority (account) which generated this label. \
201 pub src: Did,
202 /// AT URI of the record, repository (account), or other resource that this label applies to. \
203 /// For a specific record, an `at://` URI. For an account, the `did:`.
204 pub uri: String,
205 /// The short (<=128 character) string name of the value or type of this label.
206 pub val: String,
207 /// The AT Protocol version of the label object schema version. \
208 /// Current version is always 1.
209 /// When labels are being transferred as full objects between services, the ver and sig fields are required.
210 pub ver: u64,
211}
212impl AssignedLabelResponse {
213 /// Create a new label.
214 pub fn generate(
215 src: Did,
216 uri: String,
217 val: String,
218 ) -> Self {
219 let sig = SignatureEnum::Bytes(SignatureBytes([0; 64]));
220 Self::reconstruct(src, uri, val, false, chrono::Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Millis, true), sig)
221 }
222 /// Reconstruct a label from parts.
223 pub const fn reconstruct(
224 src: Did,
225 uri: String,
226 val: String,
227 neg: bool,
228 cts: String,
229 sig: SignatureEnum,
230 ) -> Self {
231 Self {
232 ver: 1,
233 src,
234 uri,
235 // cid: None,
236 val,
237 neg,
238 sig: Some(sig),
239 cts,
240 // exp: None,
241 }
242 }
243 // The process to sign or verify a signature is to construct a complete version of the label, using only the specified schema fields, and not including the sig field.
244 // This means including the ver field, but not any $type field or other un-specified fields which may have been included in a Lexicon representation of the label. This data object is then encoded in CBOR, following the deterministic IPLD/DAG-CBOR normalization rules.
245 // The CBOR bytes are hashed with SHA-256, and then the direct hash bytes (not a hex-encoded string) are signed (or verified) using the appropriate cryptographic key. The signature bytes are stored in the sig field as bytes (see Data Model for details representing bytes).
246 /// Generate signature.
247 pub fn sign(mut self) -> Self {
248 crate::crypto::Crypto::new().sign(&mut self);
249 self
250 }
251
252}
253#[derive(serde::Serialize)]
254/// A label response wrapper.
255pub struct AssignedLabelResponseWrapper {
256 /// The cursor to find the sequence number of this label. \
257 /// Returned as a string.
258 pub cursor: String,
259 /// Vector of labels.
260 pub labels: Vec<AssignedLabelResponse>,
261}
262#[derive(serde::Serialize, Debug)]
263/// A label response wrapper.
264pub struct SubscribeLabelsLabels {
265 /// The sequence number of this label. \
266 /// The seq field is a monotonically increasing integer, starting at 1 for the first label.
267 /// Returned as a long.
268 pub seq: i64,
269 /// Vector of labels.
270 pub labels: Vec<AssignedLabelResponse>,
271}
272#[derive(Deserialize)]
273#[expect(non_snake_case, reason = "Name matches URI parameter literally.")]
274/// URI parameters.
275pub struct UriParams {
276 /// URI patterns.
277 pub uriPatterns: Option<String>,
278 /// The DID of sources.
279 pub sources: Option<String>,
280 /// The limit of labels to fetch. Default is (50?).
281 pub limit: Option<i64>,
282 /// The cursor to use for seq.
283 pub cursor: Option<String>,
284 /// The actor to lookup.
285 pub actor: Option<String>,
286}
287const fn neg_default() -> bool {
288 false
289}
290
291#[derive(serde::Serialize, serde::Deserialize, Debug)]
292/// A label retrieved from the atproto API.
293pub struct RetrievedLabelResponse {
294 /// The creation timestamp.
295 pub cts: String,
296 /// Whether the label is negative.
297 #[serde(skip_serializing_if = "bool_is_false", default = "neg_default")]
298 pub neg: bool,
299 /// The source DID.
300 pub src: Did,
301 /// The URI.
302 pub uri: String,
303 /// The value.
304 pub val: String,
305 /// The version.
306 pub ver: u64,
307}
308#[derive(serde::Serialize, serde::Deserialize, Debug)]
309/// A label retrieved from the atproto API.
310pub struct SignedRetrievedLabelResponse {
311 /// The creation timestamp.
312 pub cts: String,
313 /// Whether the label is negative.
314 #[serde(skip_serializing_if = "bool_is_false")]
315 pub neg: bool,
316 /// The source DID.
317 pub sig: serde_json::Value,
318 /// The source DID.
319 pub src: Did,
320 /// The URI.
321 pub uri: String,
322 /// The value.
323 pub val: String,
324 /// The version.
325 pub ver: u64,
326}
327fn bool_is_false(b: &bool) -> bool {
328 !b
329}
330#[derive(serde::Serialize, serde::Deserialize, Debug)]
331/// A label retrieved from the atproto API.
332pub struct SignedRetrievedLabelResponseWs {
333 /// The creation timestamp.
334 pub cts: String,
335 /// Whether the label is negative.
336 #[serde(skip_serializing_if = "bool_is_false")]
337 pub neg: bool,
338 /// The signature.
339 #[serde(with = "serde_bytes")]
340 pub sig: [u8; 64],
341 /// The source DID.
342 pub src: Did,
343 /// The URI.
344 pub uri: String,
345 /// The value.
346 pub val: String,
347 /// The version.
348 pub ver: u64,
349}
350#[derive(serde::Serialize, serde::Deserialize, Debug)]
351/// Labels with a sequence number.
352pub struct LabelsVecWithSeq {
353 /// The sequence number.
354 pub seq: u64,
355 /// The labels.
356 pub labels: Vec<SignedRetrievedLabelResponseWs>,
357}
358#[derive(Debug)]
359/// Profile stats.
360pub struct ProfileStats {
361 /// The number of followers.
362 pub follower_count: i32,
363 /// The number of posts.
364 pub post_count: i32,
365 /// The creation timestamp, as reported by actor.
366 pub created_at: Datetime<chrono::Utc>,
367 /// The timestamp at which the stats were checked.
368 pub checked_at: Datetime<chrono::Utc>,
369}
370impl ProfileStats {
371 fn new(
372 follower_count: i32,
373 post_count: i32,
374 created_at: Datetime<chrono::Utc>,
375 ) -> Self {
376 Self {
377 follower_count,
378 post_count,
379 created_at,
380 checked_at: chrono::Utc::now(),
381 }
382 }
383 /// Given a AT uri, lookup the profile and return the stats.
384 pub async fn from_at_url(
385 uri: String,
386 agent: &mut crate::webrequest::Agent,
387 ) -> Result<Self, Box<dyn std::error::Error>> {
388 let uri = uri.replace("at://","").replace("/app.bsky.actor.profile/self", "");
389 if let Ok(profile) = agent.get_profile(uri.as_str()).await {
390 tracing::debug!("{:?}", profile);
391
392 // Begin enforce reasonable limits on the number of follows.
393 // https://jazco.dev/2025/02/19/imperfection/
394 let follows_count = profile["followsCount"].as_i64().expect("Expected to be able to parse an integer, but failed") as i32;
395 const MAX_FOLLOWS: i32 = 4_000;
396 if follows_count > MAX_FOLLOWS {
397 tracing::warn!("Profile {:?} has a suspicious number of follows: {:?}", uri, follows_count);
398 return Err(Box::new(std::io::Error::new(
399 std::io::ErrorKind::Other,
400 "Profile has a suspicious number of follows",
401 )));
402 }
403 // End
404
405 let followers_count = profile["followersCount"].as_i64().expect("Expected to be able to parse an integer, but failed") as i32;
406 let posts_count = profile["postsCount"].as_i64().expect("Expected to be able to parse an integer, but failed") as i32;
407 let created_at = Datetime::parse_from_rfc3339(profile["createdAt"].as_str().expect("Expected to be able to parse a string, but failed"))?;
408 Ok(Self::new(followers_count, posts_count, created_at.into()))
409 } else {
410 Err(Box::new(std::io::Error::new(
411 std::io::ErrorKind::Other,
412 "Failed to get profile",
413 )))
414 }
415 }
416}
417#[derive(Debug, Clone, Copy, serde::Deserialize)]
418enum Year {
419 _2022,
420 _2023,
421 _2024,
422 _2025,
423}
424impl std::fmt::Display for Year {
425 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
426 write!(
427 f,
428 "{}",
429 match self {
430 Self::_2022 => "a",
431 Self::_2023 => "b",
432 Self::_2024 => "c",
433 Self::_2025 => "d",
434 }
435 )
436 }
437}
438impl Serialize for Year {
439 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
440 where
441 S: Serializer,
442 {
443 let val: char = self.to_string().chars().next().expect("Expected to be able to get the first character, but failed");
444 serializer.serialize_char(val)
445 }
446}
447#[derive(Debug, Clone, Copy, serde::Deserialize)]
448enum Month {
449 January,
450 February,
451 March,
452 April,
453 May,
454 June,
455 July,
456 August,
457 September,
458 October,
459 November,
460 December,
461}
462impl Serialize for Month {
463 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
464 where
465 S: Serializer,
466 {
467 let val = self.to_string().to_lowercase();
468 serializer.serialize_str(&val)
469 }
470}
471impl std::fmt::Display for Month {
472 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
473 write!(
474 f,
475 "{}",
476 match self {
477 Self::January => "jan",
478 Self::February => "feb",
479 Self::March => "mar",
480 Self::April => "apr",
481 Self::May => "may",
482 Self::June => "jun",
483 Self::July => "jul",
484 Self::August => "aug",
485 Self::September => "sep",
486 Self::October => "oct",
487 Self::November => "nov",
488 Self::December => "dec",
489 }
490 )
491 }
492}
493/// Profile labels for month+year.
494#[derive(Debug, Clone, Copy, serde::Deserialize, serde::Serialize)]
495pub struct ProfileLabel {
496 year: Year,
497 month: Month,
498}
499impl ProfileLabel {
500 /// Create a new profile label from a datetime.
501 #[allow(clippy::cognitive_complexity)]
502 pub fn from_datetime(datetime: Datetime<chrono::Utc>) -> Option<Self> {
503 Some(Self {
504 year: match datetime.year() {
505 2023 => Year::_2023,
506 2024 => Year::_2024,
507 2025 => Year::_2025,
508 _ => {
509 tracing::debug!("Invalid year");
510 return None;
511 }
512 },
513 month: match datetime.month() {
514 1 => Month::January,
515 2 => Month::February,
516 3 => Month::March,
517 4 => Month::April,
518 5 => Month::May,
519 6 => Month::June,
520 7 => Month::July,
521 8 => Month::August,
522 9 => Month::September,
523 10 => Month::October,
524 11 => Month::November,
525 12 => Month::December,
526 _ => {
527 tracing::debug!("Invalid month");
528 return None;
529 }
530 },
531 })
532 }
533 /// Convert a profile label to a string.
534 pub fn to_label_val(self) -> String {
535 format!("joined-{}-{}", self.month, self.year)
536 .to_lowercase()
537 }
538}
539/// Profile with optional stats and label.
540#[derive(Debug)]
541pub struct Profile {
542 did: String,
543 /// Stats for a profile.
544 pub stats: Option<ProfileStats>,
545 label: Option<String>,
546}
547impl Profile {
548 /// Create a new profile, given a DID.
549 pub fn new(did: &str) -> Self {
550 Self {
551 did: {
552 // if did.starts_with("did:") {
553 // format!("at://{}{}", did, "/app.bsky.actor.profile/self")
554 // } else {
555 did.to_owned()
556 // }
557 },
558 stats: None,
559 label: None,
560 }
561 }
562 /// Fetch stats for a profile.
563 pub async fn determine_stats(&mut self, agent: &mut crate::webrequest::Agent, pool: &sqlx::sqlite::SqlitePool) -> &mut Self {
564 if let Ok(stats) = ProfileStats::from_at_url(self.did.clone(), agent).await {
565 self.stats = Some(stats);
566 } else {
567 tracing::warn!("Failed to get stats for profile {}", self.did);
568 }
569 self.insert_profile_stats(pool).await.expect("Expected to be able to insert profile stats, but failed");
570 self
571 }
572 /// Determine if stats exist for a profile.
573 pub async fn determine_stats_exist(&mut self, pool: &sqlx::Pool<sqlx::Sqlite>) -> Result<Option<&mut Self>, Box<dyn std::error::Error + Sync + Send>> {
574 if self.stats.is_some() {
575 return Ok(Some(self));
576 }
577 let profile_stats = sqlx::query!(
578 r#"
579 SELECT created_at "created_at: String", follower_count, post_count, checked_at "checked_at: String" FROM profile_stats WHERE did = ?
580 "#,
581 self.did
582 )
583 .fetch_one(pool)
584 .await;
585 if profile_stats.is_ok() {
586 let profile_stats = profile_stats.expect("Expected to be able to unwrap a profile_checked_at, but failed");
587 let created_at = Datetime::parse_from_rfc3339(profile_stats.created_at.as_str()).expect("Expected to be able to parse a string as a datetime, but failed").to_utc();
588 let follower_count = profile_stats.follower_count as i32;
589 let post_count = profile_stats.post_count as i32;
590 let checked_at = Datetime::parse_from_rfc3339(profile_stats.checked_at.as_str()).expect("Expected to be able to parse a string as a datetime, but failed").to_utc();
591 const TIMEPERIOD: i64 = 60 * 60 * 24 * 7 * 1000 * 4; // 4 weeks in milliseconds
592 if chrono::Utc::now().timestamp_millis() - checked_at.timestamp_millis() < TIMEPERIOD {
593 tracing::debug!("Stats exist for: {:?}", self.did);
594 self.stats = Some(ProfileStats {
595 follower_count,
596 post_count,
597 created_at,
598 checked_at,
599 });
600 return Ok(Some(self));
601 }
602 tracing::info!("Refetching stats for: {:?}", self.did);
603 return Ok(None);
604 }
605 tracing::info!("Stats do not exist for: {:?}", self.did);
606 Ok(None)
607 }
608 /// Determine the label of a profile.
609 pub async fn determine_label(&mut self, pool: &sqlx::sqlite::SqlitePool) -> &mut Self {
610 if self.stats.is_none() {
611 return self;
612 }
613 const MIN_POSTS: i32 = 30;
614 const SOME_POSTS: i32 = 200;
615 const MIN_FOLLOWERS: i32 = 400;
616 const SOME_FOLLOWERS: i32 = 2_500;
617 let post_count = self.stats.as_ref().expect("Expected stats to exist, but failed").post_count;
618 let follower_count = self.stats.as_ref().expect("Expected stats to exist, but failed").follower_count;
619 if (post_count >= MIN_POSTS && follower_count >= MIN_FOLLOWERS) && (post_count >= SOME_POSTS || follower_count >= SOME_FOLLOWERS)
620 {
621 match ProfileLabel::from_datetime(self.stats.as_ref().expect("Expected stats to exist, but failed").created_at) {
622 Some(label) => self.label = Some(label.to_label_val()),
623 None => {
624 tracing::debug!("Invalid datetime");
625 }
626 }
627 }
628 self.insert_profile_labels(pool).await.expect("Expected to be able to insert profile labels, but failed");
629 self
630 }
631 /// Determine the label of a profile, and insert it without checking stats reqs.
632 pub async fn determine_label_agnostic(&mut self, pool: &sqlx::sqlite::SqlitePool) -> &mut Self {
633 if self.stats.is_none() {
634 return self;
635 }
636 match ProfileLabel::from_datetime(self.stats.as_ref().expect("Expected stats to exist, but failed").created_at) {
637 Some(label) => self.label = Some(label.to_label_val()),
638 None => {
639 tracing::debug!("Invalid datetime");
640 }
641 }
642 self.insert_profile_labels(pool).await.expect("Expected to be able to insert profile labels, but failed");
643 self
644 }
645 /// Insert a profile into the database.
646 pub async fn insert_profile(self, pool: &sqlx::sqlite::SqlitePool) -> Result<Self, sqlx::Error> {
647 if (sqlx::query(&format!(
648 "INSERT INTO profile (did) VALUES ('{}')",
649 self.did
650 ))
651 .execute(pool)
652 .await).is_ok() {
653 tracing::debug!("Inserted profile {:?}", self.did);
654 } else {
655 tracing::debug!("Duplicate profile: {:?}", self.did);
656 }
657 Ok(self)
658 }
659 /// Insert profile stats into the database.
660 pub async fn insert_profile_stats(
661 &self,
662 pool: &sqlx::sqlite::SqlitePool,
663 ) -> Result<(), sqlx::Error> {
664 if self.stats.is_none() {
665 return Ok(());
666 }
667 // if sqlx::query!(
668 // r#"SELECT did "did: String" FROM profile_stats WHERE did = ? LIMIT 1"#,
669 // self.did
670 // )
671 // .fetch_one(pool)
672 // .await.is_ok() {
673 // tracing::debug!("Stats already exist for {:?}", self.did);
674 // return Ok(());
675 // }
676 let created_at = self.stats.as_ref().expect("Expected stats to exist, but failed").created_at.to_rfc3339_opts(chrono::SecondsFormat::Millis, true);
677 let follower_count = self.stats.as_ref().expect("Expected stats to exist, but failed").follower_count;
678 let post_count = self.stats.as_ref().expect("Expected stats to exist, but failed").post_count;
679 let checked_at = self.stats.as_ref().expect("Expected stats to exist, but failed").checked_at.to_rfc3339_opts(chrono::SecondsFormat::Millis, true);
680 _ = sqlx::query!(r#"
681 INSERT INTO profile_stats (did, created_at, follower_count, post_count, checked_at)
682 VALUES (?, ?, ?, ?, ?)
683 ON CONFLICT(did) DO UPDATE SET
684 created_at = ?,
685 follower_count = ?,
686 post_count = ?,
687 checked_at = ?
688 "#,
689 self.did,
690 created_at,
691 follower_count,
692 post_count,
693 checked_at,
694 created_at,
695 follower_count,
696 post_count,
697 checked_at,
698 )
699 .execute(pool).await.expect("Expected to be able to insert profile stats, but failed");
700 tracing::info!("Inserted profile stats for {:?} with {:?} followers", self.did, self.stats.as_ref().expect("Expected stats to exist, but failed").follower_count);
701 Ok(())
702 }
703 /// Negate a profile label.
704 pub async fn negate_label(
705 &mut self,
706 pool: &sqlx::sqlite::SqlitePool,
707 ) -> Result<(), sqlx::Error> {
708 if self.stats.is_none() {
709 tracing::warn!("No stats for {:?}", self.did);
710 return Ok(());
711 }
712 match ProfileLabel::from_datetime(self.stats.as_ref().expect("Expected stats to exist, but failed").created_at) {
713 Some(label) => self.label = Some(label.to_label_val()),
714 None => {
715 tracing::debug!("Invalid datetime");
716 }
717 }
718 let label = self.label.as_ref().expect("Expected label to exist, but failed");
719 let uri = self.did.as_str();
720 let val: &str = label.as_str();
721 drop(dotenvy::dotenv().expect("Failed to load .env file"));
722 let self_did = dotenvy::var("SELF_DID").expect("Expected to be able to get the SELF_DID from the environment, but failed");
723 let src = Did::new(self_did).expect("Expected to be able to create a valid DID but failed");
724 let mut label_response: AssignedLabelResponse = AssignedLabelResponse::generate(src, self.did.clone(), val.to_owned());
725 label_response.neg = true;
726 label_response = label_response.sign();
727 let sig_enum = label_response.sig.expect("Expected a signature, but failed");
728 if let SignatureEnum::Bytes(sig) = sig_enum {
729 let sig = sig.as_vec();
730 _ = sqlx::query!(
731 r#"INSERT INTO profile_labels (uri, val, neg, cts, sig) VALUES (?, ?, ?, ?, ?)"#,
732 uri,
733 val,
734 label_response.neg,
735 label_response.cts,
736 sig,
737 )
738 .execute(pool)
739 .await?;
740 }
741 tracing::info!("Negated profile label for {:?} with {:?}", self.did, self.label.as_ref().expect("Expected label to exist, but failed"));
742 Ok(())
743 }
744 /// Insert profile labels into the database.
745 async fn insert_profile_labels(
746 &self,
747 pool: &sqlx::sqlite::SqlitePool,
748 ) -> Result<(), sqlx::Error> {
749 if self.label.is_none() {
750 return Ok(());
751 }
752 if sqlx::query!(
753 r#"SELECT seq FROM profile_labels WHERE uri = ? LIMIT 1"#,
754 self.did
755 )
756 .fetch_one(pool)
757 .await.is_ok() {
758 tracing::debug!("Label already exists for {:?}", self.did);
759 return Ok(());
760 }
761 let label = self.label.as_ref().expect("Expected label to exist, but failed");
762 let uri = self.did.as_str();
763 let val: &str = label.as_str();
764 drop(dotenvy::dotenv().expect("Failed to load .env file"));
765 let self_did = dotenvy::var("SELF_DID").expect("Expected to be able to get the SELF_DID from the environment, but failed");
766 let src = Did::new(self_did).expect("Expected to be able to create a valid DID but failed");
767 let mut label_response: AssignedLabelResponse = AssignedLabelResponse::generate(src, self.did.clone(), val.to_owned());
768 label_response = label_response.sign();
769 let sig_enum = label_response.sig.expect("Expected a signature, but failed");
770 if let SignatureEnum::Bytes(sig) = sig_enum {
771 let sig = sig.as_vec();
772 let result = sqlx::query!(
773 r#"INSERT INTO profile_labels (uri, val, cts, sig) VALUES (?, ?, ?, ?)"#,
774 uri,
775 val,
776 label_response.cts,
777 sig,
778 )
779 .execute(pool)
780 .await;
781 if result.is_ok() {
782 tracing::info!("Inserted profile label for {:?} with {:?}", self.did, self.label.as_ref().expect("Expected label to exist, but failed"));
783 } else {
784 tracing::debug!("Duplicate profile label for {:?}", self.did);
785 }
786 return Ok(());
787 }
788 tracing::warn!("Failed to insert profile label for {:?}", self.did);
789 Ok(())
790 }
791 /// Remove label from profile_labels.
792 /// Used when a label needs to be regenerated.
793 pub async fn remove_label(
794 pool: &sqlx::sqlite::SqlitePool,
795 seq: i64,
796 ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
797 tracing::debug!("Removing label with seq: {:?}", seq);
798 _ = sqlx::query!(
799 r#"DELETE FROM profile_labels WHERE seq = ?"#,
800 seq,
801 )
802 .execute(pool)
803 .await.expect("Expected to be able to delete a label, but failed.");
804 Ok(())
805 }
806}