Personal ATProto tools.
at main 31 kB view raw
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}