···1919)]
2020#[serde(rename_all = "camelCase")]
2121pub struct FollowerRule<'a> {}
2222+fn lexicon_doc_app_bsky_feed_threadgate() -> ::jacquard_lexicon::lexicon::LexiconDoc<
2323+ 'static,
2424+> {
2525+ ::jacquard_lexicon::lexicon::LexiconDoc {
2626+ lexicon: ::jacquard_lexicon::lexicon::Lexicon::Lexicon1,
2727+ id: ::jacquard_common::CowStr::new_static("app.bsky.feed.threadgate"),
2828+ revision: None,
2929+ description: None,
3030+ defs: {
3131+ let mut map = ::alloc::collections::BTreeMap::new();
3232+ map.insert(
3333+ ::jacquard_common::smol_str::SmolStr::new_static("followerRule"),
3434+ ::jacquard_lexicon::lexicon::LexUserType::Object(::jacquard_lexicon::lexicon::LexObject {
3535+ description: Some(
3636+ ::jacquard_common::CowStr::new_static(
3737+ "Allow replies from actors who follow you.",
3838+ ),
3939+ ),
4040+ required: None,
4141+ nullable: None,
4242+ properties: {
4343+ #[allow(unused_mut)]
4444+ let mut map = ::alloc::collections::BTreeMap::new();
4545+ map
4646+ },
4747+ }),
4848+ );
4949+ map.insert(
5050+ ::jacquard_common::smol_str::SmolStr::new_static("followingRule"),
5151+ ::jacquard_lexicon::lexicon::LexUserType::Object(::jacquard_lexicon::lexicon::LexObject {
5252+ description: Some(
5353+ ::jacquard_common::CowStr::new_static(
5454+ "Allow replies from actors you follow.",
5555+ ),
5656+ ),
5757+ required: None,
5858+ nullable: None,
5959+ properties: {
6060+ #[allow(unused_mut)]
6161+ let mut map = ::alloc::collections::BTreeMap::new();
6262+ map
6363+ },
6464+ }),
6565+ );
6666+ map.insert(
6767+ ::jacquard_common::smol_str::SmolStr::new_static("listRule"),
6868+ ::jacquard_lexicon::lexicon::LexUserType::Object(::jacquard_lexicon::lexicon::LexObject {
6969+ description: Some(
7070+ ::jacquard_common::CowStr::new_static(
7171+ "Allow replies from actors on a list.",
7272+ ),
7373+ ),
7474+ required: Some(
7575+ vec![::jacquard_common::smol_str::SmolStr::new_static("list")],
7676+ ),
7777+ nullable: None,
7878+ properties: {
7979+ #[allow(unused_mut)]
8080+ let mut map = ::alloc::collections::BTreeMap::new();
8181+ map.insert(
8282+ ::jacquard_common::smol_str::SmolStr::new_static("list"),
8383+ ::jacquard_lexicon::lexicon::LexObjectProperty::String(::jacquard_lexicon::lexicon::LexString {
8484+ description: None,
8585+ format: Some(
8686+ ::jacquard_lexicon::lexicon::LexStringFormat::AtUri,
8787+ ),
8888+ default: None,
8989+ min_length: None,
9090+ max_length: None,
9191+ min_graphemes: None,
9292+ max_graphemes: None,
9393+ r#enum: None,
9494+ r#const: None,
9595+ known_values: None,
9696+ }),
9797+ );
9898+ map
9999+ },
100100+ }),
101101+ );
102102+ map.insert(
103103+ ::jacquard_common::smol_str::SmolStr::new_static("main"),
104104+ ::jacquard_lexicon::lexicon::LexUserType::Record(::jacquard_lexicon::lexicon::LexRecord {
105105+ description: Some(
106106+ ::jacquard_common::CowStr::new_static(
107107+ "Record defining interaction gating rules for a thread (aka, reply controls). The record key (rkey) of the threadgate record must match the record key of the thread's root post, and that record must be in the same repository.",
108108+ ),
109109+ ),
110110+ key: Some(::jacquard_common::CowStr::new_static("tid")),
111111+ record: ::jacquard_lexicon::lexicon::LexRecordRecord::Object(::jacquard_lexicon::lexicon::LexObject {
112112+ description: None,
113113+ required: Some(
114114+ vec![
115115+ ::jacquard_common::smol_str::SmolStr::new_static("post"),
116116+ ::jacquard_common::smol_str::SmolStr::new_static("createdAt")
117117+ ],
118118+ ),
119119+ nullable: None,
120120+ properties: {
121121+ #[allow(unused_mut)]
122122+ let mut map = ::alloc::collections::BTreeMap::new();
123123+ map.insert(
124124+ ::jacquard_common::smol_str::SmolStr::new_static("allow"),
125125+ ::jacquard_lexicon::lexicon::LexObjectProperty::Array(::jacquard_lexicon::lexicon::LexArray {
126126+ description: Some(
127127+ ::jacquard_common::CowStr::new_static(
128128+ "List of rules defining who can reply to this post. If value is an empty array, no one can reply. If value is undefined, anyone can reply.",
129129+ ),
130130+ ),
131131+ items: ::jacquard_lexicon::lexicon::LexArrayItem::Union(::jacquard_lexicon::lexicon::LexRefUnion {
132132+ description: None,
133133+ refs: vec![
134134+ ::jacquard_common::CowStr::new_static("#mentionRule"),
135135+ ::jacquard_common::CowStr::new_static("#followerRule"),
136136+ ::jacquard_common::CowStr::new_static("#followingRule"),
137137+ ::jacquard_common::CowStr::new_static("#listRule")
138138+ ],
139139+ closed: None,
140140+ }),
141141+ min_length: None,
142142+ max_length: Some(5usize),
143143+ }),
144144+ );
145145+ map.insert(
146146+ ::jacquard_common::smol_str::SmolStr::new_static(
147147+ "createdAt",
148148+ ),
149149+ ::jacquard_lexicon::lexicon::LexObjectProperty::String(::jacquard_lexicon::lexicon::LexString {
150150+ description: None,
151151+ format: Some(
152152+ ::jacquard_lexicon::lexicon::LexStringFormat::Datetime,
153153+ ),
154154+ default: None,
155155+ min_length: None,
156156+ max_length: None,
157157+ min_graphemes: None,
158158+ max_graphemes: None,
159159+ r#enum: None,
160160+ r#const: None,
161161+ known_values: None,
162162+ }),
163163+ );
164164+ map.insert(
165165+ ::jacquard_common::smol_str::SmolStr::new_static(
166166+ "hiddenReplies",
167167+ ),
168168+ ::jacquard_lexicon::lexicon::LexObjectProperty::Array(::jacquard_lexicon::lexicon::LexArray {
169169+ description: Some(
170170+ ::jacquard_common::CowStr::new_static(
171171+ "List of hidden reply URIs.",
172172+ ),
173173+ ),
174174+ items: ::jacquard_lexicon::lexicon::LexArrayItem::String(::jacquard_lexicon::lexicon::LexString {
175175+ description: None,
176176+ format: Some(
177177+ ::jacquard_lexicon::lexicon::LexStringFormat::AtUri,
178178+ ),
179179+ default: None,
180180+ min_length: None,
181181+ max_length: None,
182182+ min_graphemes: None,
183183+ max_graphemes: None,
184184+ r#enum: None,
185185+ r#const: None,
186186+ known_values: None,
187187+ }),
188188+ min_length: None,
189189+ max_length: Some(300usize),
190190+ }),
191191+ );
192192+ map.insert(
193193+ ::jacquard_common::smol_str::SmolStr::new_static("post"),
194194+ ::jacquard_lexicon::lexicon::LexObjectProperty::String(::jacquard_lexicon::lexicon::LexString {
195195+ description: Some(
196196+ ::jacquard_common::CowStr::new_static(
197197+ "Reference (AT-URI) to the post record.",
198198+ ),
199199+ ),
200200+ format: Some(
201201+ ::jacquard_lexicon::lexicon::LexStringFormat::AtUri,
202202+ ),
203203+ default: None,
204204+ min_length: None,
205205+ max_length: None,
206206+ min_graphemes: None,
207207+ max_graphemes: None,
208208+ r#enum: None,
209209+ r#const: None,
210210+ known_values: None,
211211+ }),
212212+ );
213213+ map
214214+ },
215215+ }),
216216+ }),
217217+ );
218218+ map.insert(
219219+ ::jacquard_common::smol_str::SmolStr::new_static("mentionRule"),
220220+ ::jacquard_lexicon::lexicon::LexUserType::Object(::jacquard_lexicon::lexicon::LexObject {
221221+ description: Some(
222222+ ::jacquard_common::CowStr::new_static(
223223+ "Allow replies from actors mentioned in your post.",
224224+ ),
225225+ ),
226226+ required: None,
227227+ nullable: None,
228228+ properties: {
229229+ #[allow(unused_mut)]
230230+ let mut map = ::alloc::collections::BTreeMap::new();
231231+ map
232232+ },
233233+ }),
234234+ );
235235+ map
236236+ },
237237+ }
238238+}
239239+240240+impl<'a> ::jacquard_lexicon::schema::LexiconSchema for FollowerRule<'a> {
241241+ fn nsid() -> &'static str {
242242+ "app.bsky.feed.threadgate"
243243+ }
244244+ fn def_name() -> &'static str {
245245+ "followerRule"
246246+ }
247247+ fn lexicon_doc() -> ::jacquard_lexicon::lexicon::LexiconDoc<'static> {
248248+ lexicon_doc_app_bsky_feed_threadgate()
249249+ }
250250+ fn validate(
251251+ &self,
252252+ ) -> ::core::result::Result<(), ::jacquard_lexicon::validation::ConstraintError> {
253253+ Ok(())
254254+ }
255255+}
256256+22257/// Allow replies from actors you follow.
23258#[jacquard_derive::lexicon]
24259#[derive(
···33268)]
34269#[serde(rename_all = "camelCase")]
35270pub struct FollowingRule<'a> {}
271271+impl<'a> ::jacquard_lexicon::schema::LexiconSchema for FollowingRule<'a> {
272272+ fn nsid() -> &'static str {
273273+ "app.bsky.feed.threadgate"
274274+ }
275275+ fn def_name() -> &'static str {
276276+ "followingRule"
277277+ }
278278+ fn lexicon_doc() -> ::jacquard_lexicon::lexicon::LexiconDoc<'static> {
279279+ lexicon_doc_app_bsky_feed_threadgate()
280280+ }
281281+ fn validate(
282282+ &self,
283283+ ) -> ::core::result::Result<(), ::jacquard_lexicon::validation::ConstraintError> {
284284+ Ok(())
285285+ }
286286+}
287287+36288/// Allow replies from actors on a list.
37289#[jacquard_derive::lexicon]
38290#[derive(
···42294 Clone,
43295 PartialEq,
44296 Eq,
4545- jacquard_derive::IntoStatic,
4646- bon::Builder
297297+ jacquard_derive::IntoStatic
47298)]
48299#[serde(rename_all = "camelCase")]
49300pub struct ListRule<'a> {
···51302 pub list: jacquard_common::types::string::AtUri<'a>,
52303}
53304305305+pub mod list_rule_state {
306306+307307+ pub use crate::builder_types::{Set, Unset, IsSet, IsUnset};
308308+ #[allow(unused)]
309309+ use ::core::marker::PhantomData;
310310+ mod sealed {
311311+ pub trait Sealed {}
312312+ }
313313+ /// State trait tracking which required fields have been set
314314+ pub trait State: sealed::Sealed {
315315+ type List;
316316+ }
317317+ /// Empty state - all required fields are unset
318318+ pub struct Empty(());
319319+ impl sealed::Sealed for Empty {}
320320+ impl State for Empty {
321321+ type List = Unset;
322322+ }
323323+ ///State transition - sets the `list` field to Set
324324+ pub struct SetList<S: State = Empty>(PhantomData<fn() -> S>);
325325+ impl<S: State> sealed::Sealed for SetList<S> {}
326326+ impl<S: State> State for SetList<S> {
327327+ type List = Set<members::list>;
328328+ }
329329+ /// Marker types for field names
330330+ #[allow(non_camel_case_types)]
331331+ pub mod members {
332332+ ///Marker type for the `list` field
333333+ pub struct list(());
334334+ }
335335+}
336336+337337+/// Builder for constructing an instance of this type
338338+pub struct ListRuleBuilder<'a, S: list_rule_state::State> {
339339+ _phantom_state: ::core::marker::PhantomData<fn() -> S>,
340340+ __unsafe_private_named: (
341341+ ::core::option::Option<jacquard_common::types::string::AtUri<'a>>,
342342+ ),
343343+ _phantom: ::core::marker::PhantomData<&'a ()>,
344344+}
345345+346346+impl<'a> ListRule<'a> {
347347+ /// Create a new builder for this type
348348+ pub fn new() -> ListRuleBuilder<'a, list_rule_state::Empty> {
349349+ ListRuleBuilder::new()
350350+ }
351351+}
352352+353353+impl<'a> ListRuleBuilder<'a, list_rule_state::Empty> {
354354+ /// Create a new builder with all fields unset
355355+ pub fn new() -> Self {
356356+ ListRuleBuilder {
357357+ _phantom_state: ::core::marker::PhantomData,
358358+ __unsafe_private_named: (None,),
359359+ _phantom: ::core::marker::PhantomData,
360360+ }
361361+ }
362362+}
363363+364364+impl<'a, S> ListRuleBuilder<'a, S>
365365+where
366366+ S: list_rule_state::State,
367367+ S::List: list_rule_state::IsUnset,
368368+{
369369+ /// Set the `list` field (required)
370370+ pub fn list(
371371+ mut self,
372372+ value: impl Into<jacquard_common::types::string::AtUri<'a>>,
373373+ ) -> ListRuleBuilder<'a, list_rule_state::SetList<S>> {
374374+ self.__unsafe_private_named.0 = ::core::option::Option::Some(value.into());
375375+ ListRuleBuilder {
376376+ _phantom_state: ::core::marker::PhantomData,
377377+ __unsafe_private_named: self.__unsafe_private_named,
378378+ _phantom: ::core::marker::PhantomData,
379379+ }
380380+ }
381381+}
382382+383383+impl<'a, S> ListRuleBuilder<'a, S>
384384+where
385385+ S: list_rule_state::State,
386386+ S::List: list_rule_state::IsSet,
387387+{
388388+ /// Build the final struct
389389+ pub fn build(self) -> ListRule<'a> {
390390+ ListRule {
391391+ list: self.__unsafe_private_named.0.unwrap(),
392392+ extra_data: Default::default(),
393393+ }
394394+ }
395395+ /// Build the final struct with custom extra_data
396396+ pub fn build_with_data(
397397+ self,
398398+ extra_data: std::collections::BTreeMap<
399399+ jacquard_common::smol_str::SmolStr,
400400+ jacquard_common::types::value::Data<'a>,
401401+ >,
402402+ ) -> ListRule<'a> {
403403+ ListRule {
404404+ list: self.__unsafe_private_named.0.unwrap(),
405405+ extra_data: Some(extra_data),
406406+ }
407407+ }
408408+}
409409+410410+impl<'a> ::jacquard_lexicon::schema::LexiconSchema for ListRule<'a> {
411411+ fn nsid() -> &'static str {
412412+ "app.bsky.feed.threadgate"
413413+ }
414414+ fn def_name() -> &'static str {
415415+ "listRule"
416416+ }
417417+ fn lexicon_doc() -> ::jacquard_lexicon::lexicon::LexiconDoc<'static> {
418418+ lexicon_doc_app_bsky_feed_threadgate()
419419+ }
420420+ fn validate(
421421+ &self,
422422+ ) -> ::core::result::Result<(), ::jacquard_lexicon::validation::ConstraintError> {
423423+ Ok(())
424424+ }
425425+}
426426+54427/// Record defining interaction gating rules for a thread (aka, reply controls). The record key (rkey) of the threadgate record must match the record key of the thread's root post, and that record must be in the same repository.
55428#[jacquard_derive::lexicon]
56429#[derive(
···60433 Clone,
61434 PartialEq,
62435 Eq,
6363- jacquard_derive::IntoStatic,
6464- bon::Builder
436436+ jacquard_derive::IntoStatic
65437)]
66438#[serde(rename_all = "camelCase")]
67439pub struct Threadgate<'a> {
68440 /// List of rules defining who can reply to this post. If value is an empty array, no one can reply. If value is undefined, anyone can reply.
69441 #[serde(skip_serializing_if = "std::option::Option::is_none")]
7070- #[builder(into)]
71442 #[serde(borrow)]
7272- pub allow: Option<Vec<ThreadgateAllowItem<'a>>>,
443443+ pub allow: std::option::Option<Vec<ThreadgateAllowItem<'a>>>,
73444 pub created_at: jacquard_common::types::string::Datetime,
74445 /// List of hidden reply URIs.
75446 #[serde(skip_serializing_if = "std::option::Option::is_none")]
7676- #[builder(into)]
77447 #[serde(borrow)]
7878- pub hidden_replies: Option<Vec<jacquard_common::types::string::AtUri<'a>>>,
448448+ pub hidden_replies: std::option::Option<
449449+ Vec<jacquard_common::types::string::AtUri<'a>>,
450450+ >,
79451 /// Reference (AT-URI) to the post record.
80452 #[serde(borrow)]
81453 pub post: jacquard_common::types::string::AtUri<'a>,
82454}
83455456456+pub mod threadgate_state {
457457+458458+ pub use crate::builder_types::{Set, Unset, IsSet, IsUnset};
459459+ #[allow(unused)]
460460+ use ::core::marker::PhantomData;
461461+ mod sealed {
462462+ pub trait Sealed {}
463463+ }
464464+ /// State trait tracking which required fields have been set
465465+ pub trait State: sealed::Sealed {
466466+ type Post;
467467+ type CreatedAt;
468468+ }
469469+ /// Empty state - all required fields are unset
470470+ pub struct Empty(());
471471+ impl sealed::Sealed for Empty {}
472472+ impl State for Empty {
473473+ type Post = Unset;
474474+ type CreatedAt = Unset;
475475+ }
476476+ ///State transition - sets the `post` field to Set
477477+ pub struct SetPost<S: State = Empty>(PhantomData<fn() -> S>);
478478+ impl<S: State> sealed::Sealed for SetPost<S> {}
479479+ impl<S: State> State for SetPost<S> {
480480+ type Post = Set<members::post>;
481481+ type CreatedAt = S::CreatedAt;
482482+ }
483483+ ///State transition - sets the `created_at` field to Set
484484+ pub struct SetCreatedAt<S: State = Empty>(PhantomData<fn() -> S>);
485485+ impl<S: State> sealed::Sealed for SetCreatedAt<S> {}
486486+ impl<S: State> State for SetCreatedAt<S> {
487487+ type Post = S::Post;
488488+ type CreatedAt = Set<members::created_at>;
489489+ }
490490+ /// Marker types for field names
491491+ #[allow(non_camel_case_types)]
492492+ pub mod members {
493493+ ///Marker type for the `post` field
494494+ pub struct post(());
495495+ ///Marker type for the `created_at` field
496496+ pub struct created_at(());
497497+ }
498498+}
499499+500500+/// Builder for constructing an instance of this type
501501+pub struct ThreadgateBuilder<'a, S: threadgate_state::State> {
502502+ _phantom_state: ::core::marker::PhantomData<fn() -> S>,
503503+ __unsafe_private_named: (
504504+ ::core::option::Option<Vec<ThreadgateAllowItem<'a>>>,
505505+ ::core::option::Option<jacquard_common::types::string::Datetime>,
506506+ ::core::option::Option<Vec<jacquard_common::types::string::AtUri<'a>>>,
507507+ ::core::option::Option<jacquard_common::types::string::AtUri<'a>>,
508508+ ),
509509+ _phantom: ::core::marker::PhantomData<&'a ()>,
510510+}
511511+512512+impl<'a> Threadgate<'a> {
513513+ /// Create a new builder for this type
514514+ pub fn new() -> ThreadgateBuilder<'a, threadgate_state::Empty> {
515515+ ThreadgateBuilder::new()
516516+ }
517517+}
518518+519519+impl<'a> ThreadgateBuilder<'a, threadgate_state::Empty> {
520520+ /// Create a new builder with all fields unset
521521+ pub fn new() -> Self {
522522+ ThreadgateBuilder {
523523+ _phantom_state: ::core::marker::PhantomData,
524524+ __unsafe_private_named: (None, None, None, None),
525525+ _phantom: ::core::marker::PhantomData,
526526+ }
527527+ }
528528+}
529529+530530+impl<'a, S: threadgate_state::State> ThreadgateBuilder<'a, S> {
531531+ /// Set the `allow` field (optional)
532532+ pub fn allow(
533533+ mut self,
534534+ value: impl Into<Option<Vec<ThreadgateAllowItem<'a>>>>,
535535+ ) -> Self {
536536+ self.__unsafe_private_named.0 = value.into();
537537+ self
538538+ }
539539+ /// Set the `allow` field to an Option value (optional)
540540+ pub fn maybe_allow(mut self, value: Option<Vec<ThreadgateAllowItem<'a>>>) -> Self {
541541+ self.__unsafe_private_named.0 = value;
542542+ self
543543+ }
544544+}
545545+546546+impl<'a, S> ThreadgateBuilder<'a, S>
547547+where
548548+ S: threadgate_state::State,
549549+ S::CreatedAt: threadgate_state::IsUnset,
550550+{
551551+ /// Set the `createdAt` field (required)
552552+ pub fn created_at(
553553+ mut self,
554554+ value: impl Into<jacquard_common::types::string::Datetime>,
555555+ ) -> ThreadgateBuilder<'a, threadgate_state::SetCreatedAt<S>> {
556556+ self.__unsafe_private_named.1 = ::core::option::Option::Some(value.into());
557557+ ThreadgateBuilder {
558558+ _phantom_state: ::core::marker::PhantomData,
559559+ __unsafe_private_named: self.__unsafe_private_named,
560560+ _phantom: ::core::marker::PhantomData,
561561+ }
562562+ }
563563+}
564564+565565+impl<'a, S: threadgate_state::State> ThreadgateBuilder<'a, S> {
566566+ /// Set the `hiddenReplies` field (optional)
567567+ pub fn hidden_replies(
568568+ mut self,
569569+ value: impl Into<Option<Vec<jacquard_common::types::string::AtUri<'a>>>>,
570570+ ) -> Self {
571571+ self.__unsafe_private_named.2 = value.into();
572572+ self
573573+ }
574574+ /// Set the `hiddenReplies` field to an Option value (optional)
575575+ pub fn maybe_hidden_replies(
576576+ mut self,
577577+ value: Option<Vec<jacquard_common::types::string::AtUri<'a>>>,
578578+ ) -> Self {
579579+ self.__unsafe_private_named.2 = value;
580580+ self
581581+ }
582582+}
583583+584584+impl<'a, S> ThreadgateBuilder<'a, S>
585585+where
586586+ S: threadgate_state::State,
587587+ S::Post: threadgate_state::IsUnset,
588588+{
589589+ /// Set the `post` field (required)
590590+ pub fn post(
591591+ mut self,
592592+ value: impl Into<jacquard_common::types::string::AtUri<'a>>,
593593+ ) -> ThreadgateBuilder<'a, threadgate_state::SetPost<S>> {
594594+ self.__unsafe_private_named.3 = ::core::option::Option::Some(value.into());
595595+ ThreadgateBuilder {
596596+ _phantom_state: ::core::marker::PhantomData,
597597+ __unsafe_private_named: self.__unsafe_private_named,
598598+ _phantom: ::core::marker::PhantomData,
599599+ }
600600+ }
601601+}
602602+603603+impl<'a, S> ThreadgateBuilder<'a, S>
604604+where
605605+ S: threadgate_state::State,
606606+ S::Post: threadgate_state::IsSet,
607607+ S::CreatedAt: threadgate_state::IsSet,
608608+{
609609+ /// Build the final struct
610610+ pub fn build(self) -> Threadgate<'a> {
611611+ Threadgate {
612612+ allow: self.__unsafe_private_named.0,
613613+ created_at: self.__unsafe_private_named.1.unwrap(),
614614+ hidden_replies: self.__unsafe_private_named.2,
615615+ post: self.__unsafe_private_named.3.unwrap(),
616616+ extra_data: Default::default(),
617617+ }
618618+ }
619619+ /// Build the final struct with custom extra_data
620620+ pub fn build_with_data(
621621+ self,
622622+ extra_data: std::collections::BTreeMap<
623623+ jacquard_common::smol_str::SmolStr,
624624+ jacquard_common::types::value::Data<'a>,
625625+ >,
626626+ ) -> Threadgate<'a> {
627627+ Threadgate {
628628+ allow: self.__unsafe_private_named.0,
629629+ created_at: self.__unsafe_private_named.1.unwrap(),
630630+ hidden_replies: self.__unsafe_private_named.2,
631631+ post: self.__unsafe_private_named.3.unwrap(),
632632+ extra_data: Some(extra_data),
633633+ }
634634+ }
635635+}
636636+637637+impl<'a> Threadgate<'a> {
638638+ pub fn uri(
639639+ uri: impl Into<jacquard_common::CowStr<'a>>,
640640+ ) -> Result<
641641+ jacquard_common::types::uri::RecordUri<'a, ThreadgateRecord>,
642642+ jacquard_common::types::uri::UriError,
643643+ > {
644644+ jacquard_common::types::uri::RecordUri::try_from_uri(
645645+ jacquard_common::types::string::AtUri::new_cow(uri.into())?,
646646+ )
647647+ }
648648+}
649649+84650#[jacquard_derive::open_union]
85651#[derive(
86652 serde::Serialize,
···152718 type Record = ThreadgateRecord;
153719}
154720721721+impl<'a> ::jacquard_lexicon::schema::LexiconSchema for Threadgate<'a> {
722722+ fn nsid() -> &'static str {
723723+ "app.bsky.feed.threadgate"
724724+ }
725725+ fn def_name() -> &'static str {
726726+ "main"
727727+ }
728728+ fn lexicon_doc() -> ::jacquard_lexicon::lexicon::LexiconDoc<'static> {
729729+ lexicon_doc_app_bsky_feed_threadgate()
730730+ }
731731+ fn validate(
732732+ &self,
733733+ ) -> ::core::result::Result<(), ::jacquard_lexicon::validation::ConstraintError> {
734734+ if let Some(ref value) = self.allow {
735735+ #[allow(unused_comparisons)]
736736+ if value.len() > 5usize {
737737+ return Err(::jacquard_lexicon::validation::ConstraintError::MaxLength {
738738+ path: ::jacquard_lexicon::validation::ValidationPath::from_field(
739739+ "allow",
740740+ ),
741741+ max: 5usize,
742742+ actual: value.len(),
743743+ });
744744+ }
745745+ }
746746+ if let Some(ref value) = self.hidden_replies {
747747+ #[allow(unused_comparisons)]
748748+ if value.len() > 300usize {
749749+ return Err(::jacquard_lexicon::validation::ConstraintError::MaxLength {
750750+ path: ::jacquard_lexicon::validation::ValidationPath::from_field(
751751+ "hidden_replies",
752752+ ),
753753+ max: 300usize,
754754+ actual: value.len(),
755755+ });
756756+ }
757757+ }
758758+ Ok(())
759759+ }
760760+}
761761+155762/// Allow replies from actors mentioned in your post.
156763#[jacquard_derive::lexicon]
157764#[derive(
···165772 Default
166773)]
167774#[serde(rename_all = "camelCase")]
168168-pub struct MentionRule<'a> {}775775+pub struct MentionRule<'a> {}
776776+impl<'a> ::jacquard_lexicon::schema::LexiconSchema for MentionRule<'a> {
777777+ fn nsid() -> &'static str {
778778+ "app.bsky.feed.threadgate"
779779+ }
780780+ fn def_name() -> &'static str {
781781+ "mentionRule"
782782+ }
783783+ fn lexicon_doc() -> ::jacquard_lexicon::lexicon::LexiconDoc<'static> {
784784+ lexicon_doc_app_bsky_feed_threadgate()
785785+ }
786786+ fn validate(
787787+ &self,
788788+ ) -> ::core::result::Result<(), ::jacquard_lexicon::validation::ConstraintError> {
789789+ Ok(())
790790+ }
791791+}
+45
lexicon_types_crate/src/builder_types.rs
···11+// @generated by jacquard-lexicon. DO NOT EDIT.
22+//
33+// This file was automatically generated from Lexicon schemas.
44+// Any manual changes will be overwritten on the next regeneration.
55+66+use bon::__::rustversion;
77+88+/// Marker type indicating a builder field has been set
99+pub struct Set<T>(pub T);
1010+impl<T> Set<T> {
1111+ /// Extract the inner value
1212+ #[inline]
1313+ pub fn into_inner(self) -> T {
1414+ self.0
1515+ }
1616+}
1717+1818+/// Marker type indicating a builder field has not been set
1919+pub struct Unset;
2020+/// Trait indicating a builder field is set (has a value)
2121+#[rustversion::attr(
2222+ since(1.78.0),
2323+ diagnostic::on_unimplemented(
2424+ message = "the field `{Self}` was not set, but this method requires it to be set",
2525+ label = "the field `{Self}` was not set"
2626+ )
2727+)]
2828+pub trait IsSet: private::Sealed {}
2929+/// Trait indicating a builder field is unset (no value yet)
3030+#[rustversion::attr(
3131+ since(1.78.0),
3232+ diagnostic::on_unimplemented(
3333+ message = "the field `{Self}` was already set, but this method requires it to be unset",
3434+ label = "the field `{Self}` was already set"
3535+ )
3636+)]
3737+pub trait IsUnset: private::Sealed {}
3838+impl<T> IsSet for Set<T> {}
3939+impl IsUnset for Unset {}
4040+mod private {
4141+ /// Sealed trait to prevent external implementations
4242+ pub trait Sealed {}
4343+ impl<T> Sealed for super::Set<T> {}
4444+ impl Sealed for super::Unset {}
4545+}
+2-1
lexicon_types_crate/src/com_atproto/sync.rs
···44// Any manual changes will be overwritten on the next regeneration.
5566pub mod get_blob;
77-pub mod get_repo;77+pub mod get_repo;
88+pub mod list_blobs;
···2121#[serde(bound(deserialize = "'de: 'a"))]
2222pub enum SignUpError<'a> {
2323 #[serde(rename = "AlreadyRegistered")]
2424- AlreadyRegistered(std::option::Option<String>),
2424+ AlreadyRegistered(std::option::Option<jacquard_common::CowStr<'a>>),
2525 #[serde(rename = "NotAuthorized")]
2626- NotAuthorized(std::option::Option<String>),
2626+ NotAuthorized(std::option::Option<jacquard_common::CowStr<'a>>),
2727}
28282929-impl std::fmt::Display for SignUpError<'_> {
3030- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2929+impl core::fmt::Display for SignUpError<'_> {
3030+ fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
3131 match self {
3232 Self::AlreadyRegistered(msg) => {
3333 write!(f, "AlreadyRegistered")?;
···6060 jacquard_derive::IntoStatic
6161)]
6262pub struct SignUp;
6363-///Response type for
6363+/// Response type for
6464///com.pdsmoover.backup.signUp
6565pub struct SignUpResponse;
6666impl jacquard_common::xrpc::XrpcResp for SignUpResponse {
···7878 type Response = SignUpResponse;
7979}
80808181-///Endpoint type for
8181+/// Endpoint type for
8282///com.pdsmoover.backup.signUp
8383pub struct SignUpRequest;
8484impl jacquard_common::xrpc::XrpcEndpoint for SignUpRequest {
+2
lexicon_types_crate/src/lib.rs
···33// This file was automatically generated from Lexicon schemas.
44// Any manual changes will be overwritten on the next regeneration.
5566+extern crate alloc;
67#[cfg(feature = "app_bsky")]
78pub mod app_bsky;
99+pub mod builder_types;
810911#[cfg(feature = "com_atproto")]
1012pub mod com_atproto;
···11+-- Drop the existing unique constraint on cid_or_rev alone so that
22+-- different accounts can have the same CID.
33+ALTER TABLE blobs DROP CONSTRAINT blobs_cid_or_rev_key;
44+55+-- Add a composite unique constraint: the same account cannot have
66+-- duplicate cid_or_rev values, but different accounts can.
77+ALTER TABLE blobs ADD CONSTRAINT blobs_account_did_cid_or_rev_key UNIQUE (account_did, cid_or_rev);
+53-7
shared/src/db/mod.rs
···233233234234 Ok(ManualBackupStartOutcome::Started)
235235 }
236236+237237+ pub async fn list_blobs(
238238+ &self,
239239+ did: &str,
240240+ cursor: Option<&str>,
241241+ limit: i64,
242242+ ) -> Result<(Vec<String>, Option<String>)> {
243243+ let cids: Vec<String> = if let Some(cursor) = cursor {
244244+ sqlx::query!(
245245+ r#"
246246+ SELECT DISTINCT cid_or_rev
247247+ FROM blobs
248248+ WHERE account_did = $1 AND type = 'blob' AND cid_or_rev > $2
249249+ ORDER BY cid_or_rev
250250+ LIMIT $3
251251+ "#,
252252+ did,
253253+ cursor,
254254+ limit
255255+ )
256256+ .fetch_all(&self.pool)
257257+ .await?
258258+ .into_iter()
259259+ .map(|row| row.cid_or_rev)
260260+ .collect()
261261+ } else {
262262+ sqlx::query!(
263263+ r#"
264264+ SELECT DISTINCT cid_or_rev
265265+ FROM blobs
266266+ WHERE account_did = $1 AND type = 'blob'
267267+ ORDER BY cid_or_rev
268268+ LIMIT $2
269269+ "#,
270270+ did,
271271+ limit
272272+ )
273273+ .fetch_all(&self.pool)
274274+ .await?
275275+ .into_iter()
276276+ .map(|row| row.cid_or_rev)
277277+ .collect()
278278+ };
279279+280280+ // Return cursor as the last CID if we hit the limit
281281+ let next_cursor = if cids.len() == limit as usize {
282282+ cids.last().cloned()
283283+ } else {
284284+ None
285285+ };
286286+287287+ Ok((cids, next_cursor))
288288+ }
236289}
237290238291#[derive(Debug, Clone)]
···240293 NotFound,
241294 TooSoon { last_backup_started: DateTime<Utc> },
242295 Started,
243243-}
244244-245245-#[derive(Debug, Serialize, Deserialize, FromRow)]
246246-pub struct DemoItem {
247247- pub id: i64,
248248- pub text: String,
249249- pub created_at: DateTime<Utc>,
250296}
251297252298/// Row returned by get_repo_status, containing account details and
+4-6
shared/src/jobs/mod.rs
···142142 .await?)
143143 }
144144 }
145145- //on blob we upsert on cid (shouldnt happen ideally)
145145+ //on blob we upsert on (account_did, cid_or_rev)
146146 models::BlobType::Blob | _ => Ok(sqlx::query_as::<_, BlobModel>(
147147 r#"
148148 INSERT INTO blobs (account_did, size, type, cid_or_rev)
149149 VALUES ($1, $2, $3, $4)
150150- ON CONFLICT (cid_or_rev) DO UPDATE
151151- SET account_did = EXCLUDED.account_did,
152152- size = EXCLUDED.size,
153153- type = EXCLUDED.type,
154154- cid_or_rev = EXCLUDED.cid_or_rev
150150+ ON CONFLICT (account_did, cid_or_rev) DO UPDATE
151151+ SET size = EXCLUDED.size,
152152+ type = EXCLUDED.type
155153 RETURNING id, created_at, account_did, size, type, cid_or_rev
156154 "#,
157155 )
+1
shared/src/jobs/pds_backup.rs
···88888989 if !active_repos.is_empty() {
9090 // Batch upsert accounts using UNNEST; preserve created_at and pds_sign_up on conflict.
9191+ //TODO may filter ones not signed up on the PDS? Be a new SQL query to get those turned on via the PDS?
9192 let dids_with_revs: Vec<(String, String)> = active_repos
9293 .iter()
9394 .map(|(d, rev)| (d.clone(), rev.clone()))
-1
shared/src/jobs/scheduled_back_up_start.rs
···120120 let rev: Option<Tid> = match agent.send(request).await {
121121 Ok(response) => match response.parse() {
122122 Ok(output) => {
123123- log::info!("{output:?}");
124123 if output.active {
125124 output.rev
126125 } else {
+5-3
shared/src/jobs/upload_blob.rs
···198198 log::warn!("Blob: {cid} not found for: {did}");
199199 }
200200 DownloadCompressAndUploadError::BlobDownloadError => {
201201- //Saliently ignoring atm not to mess with the chunk
201201+ //Silently ignoring atm not to mess with the chunk
202202 }
203203 },
204204 }
···235235 };
236236237237 let response = atproto_client
238238- .get(repo_url)
238238+ .get(repo_url.clone())
239239 .header(ACCEPT, accept_type)
240240 .send()
241241 .await
···259259 // DownloadCompressAndUploadError::AnyError(Error::Failed(Arc::new(Box::new(e))))
260260 // })?;
261261 return Err(DownloadCompressAndUploadError::AnyError(Error::Failed(
262262- Arc::new(anyhow::anyhow!("Error downloading the blob: {response_status}").into()),
262262+ Arc::new(
263263+ anyhow::anyhow!("Error downloading the blob: {response_status} {repo_url}").into(),
264264+ ),
263265 )));
264266 }
265267
+4-4
web-ui/src/routes/terms/+page.svelte
···1313 <section class="section">
14141515 <h1>Privacy Policy</h1>
1616- <p>Last updated: 2025-10-20</p>
1616+ <p>Last updated: 2026-01-16</p>
17171818 <h2>Overview</h2>
1919 <p>PDS MOOver performs migrations in your browser. Where possible, operations are client side to minimize
···6464 policy.</p>
65656666 <h2>Service Provider and Location</h2>
6767- <p>All backup data is currently being stored on <a href="https://upcloud.com/">UpCloud's</a> <a
6868- href="https://upcloud.com/products/object-storage/">object store</a> in a data center located in the US.
6969- UpCloud is also our service provider for the VPS running the services. We may change service providers in
6767+ <p>All backup data is currently being stored on <a href="https://railway.com/">Railway's</a> <a
6868+ href="https://docs.railway.com/storage-buckets">Storage Buckets</a> in a data center located in the US.
6969+ Railway is also our service provider for the VPS running the services. We may change service providers in
7070 the future, if we do this terms of service will be updated to reflect that.</p>
71717272 </section>
+84-7
web/src/handlers/xrpc/com_atproto_sync.rs
···11use crate::AppState;
22use crate::handlers::xrpc::{XrpcError, XrpcErrorResponse};
33use async_compression::tokio::bufread::ZstdDecoder;
44-use axum::Router;
55-use axum::body::Body;
66-use axum::extract::State;
77-use axum::http::{StatusCode, header};
88-use axum::response::Response;
44+use axum::{
55+ Router,
66+ body::Body,
77+ extract::State,
88+ http::{StatusCode, header},
99+ response::Response,
1010+};
911use jacquard_axum::{ExtractXrpc, IntoRouter};
1010-use lexicon_types_crate::com_atproto::sync::get_blob::GetBlobRequest;
1111-use lexicon_types_crate::com_atproto::sync::get_repo::GetRepoRequest;
1212+use lexicon_types_crate::{
1313+ com_atproto::sync::get_blob::GetBlobRequest, com_atproto::sync::get_repo::GetRepoRequest,
1414+ com_atproto::sync::list_blobs::ListBlobsRequest,
1515+};
1216use s3::error::S3Error;
1317use shared::storage::{blob_backup_path, repo_backup_path};
1418use tokio::io::BufReader;
···140144 Ok(response)
141145}
142146147147+#[axum_macros::debug_handler]
148148+async fn list_blobs(
149149+ State(state): State<AppState>,
150150+ ExtractXrpc(args): ExtractXrpc<ListBlobsRequest>,
151151+) -> Result<
152152+ axum::Json<lexicon_types_crate::com_atproto::sync::list_blobs::ListBlobsOutput<'static>>,
153153+ XrpcErrorResponse,
154154+> {
155155+ //Since is not supported sadly since we do not record individual records and keep track of those tids
156156+ let did = args.did.to_string();
157157+ let limit = args.limit.unwrap_or(500).min(1000).max(1);
158158+ let cursor = args.cursor.as_ref().map(|c| c.as_ref());
159159+160160+ // Check if account exists
161161+ let account_exists = state
162162+ .db
163163+ .is_user_already_registered(&did)
164164+ .await
165165+ .map_err(|e| {
166166+ tracing::error!(%e, "failed to check if user exists");
167167+ XrpcErrorResponse::internal_server_error()
168168+ })?;
169169+170170+ if !account_exists {
171171+ return Err(XrpcErrorResponse {
172172+ error: XrpcError {
173173+ error: "RepoNotFound".to_string(),
174174+ message: Some(format!("Could not find repo for DID: {did}")),
175175+ },
176176+ status: StatusCode::NOT_FOUND,
177177+ });
178178+ }
179179+180180+ // Fetch blobs from database
181181+ let (cids, next_cursor) = state
182182+ .db
183183+ .list_blobs(&did, cursor, limit)
184184+ .await
185185+ .map_err(|e| {
186186+ tracing::error!(%e, "failed to list blobs");
187187+ XrpcErrorResponse::internal_server_error()
188188+ })?;
189189+190190+ // Convert to the response type
191191+ use lexicon_types_crate::com_atproto::sync::list_blobs::ListBlobsOutput;
192192+193193+ // Parse and validate CIDs, converting them to owned 'static lifetimes
194194+ let parsed_cids: Result<Vec<jacquard_common::types::string::Cid<'static>>, XrpcErrorResponse> =
195195+ cids.into_iter()
196196+ .map(|cid| {
197197+ // Validate CID format
198198+ jacquard_common::types::string::Cid::new(cid.as_bytes()).map_err(|e| {
199199+ tracing::error!(%e, cid = %cid, "failed to parse CID");
200200+ XrpcErrorResponse::internal_server_error()
201201+ })?;
202202+ // Convert to owned/static by parsing the validated string
203203+ Ok(jacquard_common::types::string::Cid::new(
204204+ cid.into_bytes().leak() as &'static [u8]
205205+ )
206206+ .expect("already validated"))
207207+ })
208208+ .collect();
209209+210210+ let output = ListBlobsOutput {
211211+ cids: parsed_cids?,
212212+ cursor: next_cursor.map(|c| c.into()),
213213+ extra_data: Default::default(),
214214+ };
215215+216216+ Ok(axum::Json(output))
217217+}
218218+143219pub fn atproto_routes(_state: AppState) -> Router<AppState> {
144220 Router::new()
145221 .merge(GetRepoRequest::into_router(get_repo))
146222 .merge(GetBlobRequest::into_router(get_blob))
223223+ .merge(ListBlobsRequest::into_router(list_blobs))
147224}