@@ -240,6 +240,72 @@ extension ATProtoBluesky {
240240 /// }
241241 /// ```
242242 ///
243+ /// # Adding Audience Controls
244+ /// You can control who can reply to your post and whether it can be embedded by other posts.
245+ ///
246+ /// ## Controlling Replies (Threadgate)
247+ /// Use the `replyControls` parameter to restrict who can reply:
248+ ///
249+ /// ```swift
250+ /// do {
251+ /// let postResult = try await atProtoBluesky.createPostRecord(
252+ /// text: "Only my followers can reply to this post!",
253+ /// replyControls: [.allowFollowers]
254+ /// )
255+ ///
256+ /// print(postResult)
257+ /// } catch {
258+ /// throw error
259+ /// }
260+ /// ```
261+ ///
262+ /// You can combine multiple rules:
263+ ///
264+ /// ```swift
265+ /// do {
266+ /// let postResult = try await atProtoBluesky.createPostRecord(
267+ /// text: "Only people I follow or who follow me can reply.",
268+ /// replyControls: [.allowFollowers, .allowFollowing]
269+ /// )
270+ ///
271+ /// print(postResult)
272+ /// } catch {
273+ /// throw error
274+ /// }
275+ /// ```
276+ ///
277+ /// ## Disabling Embedding (Postgate)
278+ /// Use the `embeddingRules` parameter to prevent others from quoting your post:
279+ ///
280+ /// ```swift
281+ /// do {
282+ /// let postResult = try await atProtoBluesky.createPostRecord(
283+ /// text: "This post cannot be quoted by others.",
284+ /// embeddingRules: [.disable]
285+ /// )
286+ ///
287+ /// print(postResult)
288+ /// } catch {
289+ /// throw error
290+ /// }
291+ /// ```
292+ ///
293+ /// You can combine both audience controls:
294+ ///
295+ /// ```swift
296+ /// do {
297+ /// let postResult = try await atProtoBluesky.createPostRecord(
298+ /// text: "Followers only replies, no quotes allowed.",
299+ /// replyControls: [.allowFollowers],
300+ /// embeddingRules: [.disable]
301+ /// )
302+ ///
303+ /// print(postResult)
304+ /// } catch {
305+ /// throw error
306+ /// }
307+ /// ```
308+ ///
243309 /// - Parameters:
244310 /// - text: The text that's directly displayed in the post record. Current limit is
245311 /// 300 characters.
@@ -254,6 +320,10 @@ extension ATProtoBluesky {
254320 /// - labels: An array of labels made by the user. Optional. Defaults to `nil`.
255321 /// - tags: An array of tags for the post record. Optional. Defaults to `nil`.
256322 /// - creationDate: The date of the post record. Defaults to `Date.now`.
323+ /// - replyControls: An array of rules to control who can reply to the post. Optional.
324+ /// Defaults to `nil`. When provided, a threadgate record is created for the post.
325+ /// - embeddingRules: An array of rules to control embedding of the post. Optional.
326+ /// Defaults to `nil`. When provided, a postgate record is created for the post.
257327 /// - recordKey: The record key of the collection. Optional. Defaults to `nil`.
258328 /// - shouldValidate: Indicates whether the record should be validated. Optional.
259329 /// Defaults to `true`.
@@ -268,6 +338,8 @@ extension ATProtoBluesky {
268338 labels: ComAtprotoLexicon . Label . SelfLabelsDefinition ? = nil ,
269339 tags: [ String ] ? = nil ,
270340 creationDate: Date = Date ( ) ,
341+ replyControls: [ ThreadgateAllowRule ] ? = nil ,
342+ embeddingRules: [ PostgateEmbeddingRule ] ? = nil ,
271343 recordKey: String ? = nil ,
272344 shouldValidate: Bool ? = true ,
273345 swapCommit: String ? = nil
@@ -432,6 +504,81 @@ extension ATProtoBluesky {
432504
433505 try await Task . sleep ( nanoseconds: 500_000_000 )
434506
507+ // Create threadgate record if reply controls are specified.
508+ // nil = no threadgate (everyone can reply)
509+ // [] = threadgate with empty allow (nobody can reply)
510+ // [rules] = threadgate with those rules
511+ if let replyControls = replyControls {
512+ let postRecordKey = try ATProtoTools ( ) . parseURI ( record. recordURI) . recordKey
513+
514+ var threadgateAllowArray : [ AppBskyLexicon . Feed . ThreadgateRecord . ThreadgateUnion ] = [ ]
515+ let cappedReplyControls = Array ( replyControls. prefix ( 5 ) )
516+
517+ for replyControl in cappedReplyControls {
518+ switch replyControl {
519+ case . allowMentions:
520+ threadgateAllowArray. append ( . mentionRule( AppBskyLexicon . Feed. ThreadgateRecord. MentionRule ( ) ) )
521+ case . allowFollowers:
522+ threadgateAllowArray. append ( . followerRule( AppBskyLexicon . Feed. ThreadgateRecord. FollowerRule ( ) ) )
523+ case . allowFollowing:
524+ threadgateAllowArray. append ( . followingRule( AppBskyLexicon . Feed. ThreadgateRecord. FollowingRule ( ) ) )
525+ case . allowList( listURI: let listURI) :
526+ threadgateAllowArray. append ( . listRule( AppBskyLexicon . Feed. ThreadgateRecord. ListRule ( listURI: listURI) ) )
527+ }
528+ }
529+
530+ // Empty allow array means nobody can reply
531+ let threadgateRecord = AppBskyLexicon . Feed. ThreadgateRecord (
532+ postURI: record. recordURI,
533+ allow: threadgateAllowArray,
534+ createdAt: creationDate,
535+ hiddenReplies: nil
536+ )
537+
538+ _ = try await atProtoKitInstance. createRecord (
539+ repositoryDID: session. sessionDID,
540+ collection: " app.bsky.feed.threadgate " ,
541+ recordKey: postRecordKey,
542+ shouldValidate: shouldValidate,
543+ record: UnknownType . record ( threadgateRecord) ,
544+ swapCommit: nil
545+ )
546+
547+ try await Task . sleep ( nanoseconds: 500_000_000 )
548+ }
549+
550+ // Create postgate record if embedding rules are specified.
551+ if let embeddingRules = embeddingRules, embeddingRules. isEmpty == false {
552+ let postRecordKey = try ATProtoTools ( ) . parseURI ( record. recordURI) . recordKey
553+
554+ var postgateEmbedRules : [ AppBskyLexicon . Feed . PostgateRecord . EmbeddingRulesUnion ] = [ ]
555+
556+ for rule in embeddingRules {
557+ switch rule {
558+ case . disable:
559+ postgateEmbedRules. append ( . disabledRule( AppBskyLexicon . Feed. PostgateRecord. DisableRule ( ) ) )
560+ }
561+ }
562+
563+ let postgateRecord = AppBskyLexicon . Feed. PostgateRecord (
564+ createdAt: creationDate,
565+ postURI: record. recordURI,
566+ detachedEmbeddingURIs: nil ,
567+ embeddingRules: postgateEmbedRules. isEmpty ? nil : postgateEmbedRules
568+ )
569+
570+ _ = try await atProtoKitInstance. createRecord (
571+ repositoryDID: session. sessionDID,
572+ collection: " app.bsky.feed.postgate " ,
573+ recordKey: postRecordKey,
574+ shouldValidate: shouldValidate,
575+ record: UnknownType . record ( postgateRecord) ,
576+ swapCommit: nil
577+ )
578+
579+ try await Task . sleep ( nanoseconds: 500_000_000 )
580+ }
581+
435582 return record
436583 } catch {
437584 throw error
@@ -468,7 +615,7 @@ extension ATProtoBluesky {
468615 )
469616
470617 let embedImage = AppBskyLexicon . Embed. ImagesDefinition. Image (
471- imageBlob: blobReference. blob ,
618+ imageBlob: blobReference,
472619 altText: image. altText ?? " " ,
473620 aspectRatio: image. aspectRatio
474621 )
@@ -634,7 +781,7 @@ extension ATProtoBluesky {
634781 imageData: caption. file
635782 )
636783
637- captionReferences. append ( AppBskyLexicon . Embed. VideoDefinition. Caption ( language: caption. language. identifier, fileBlob: blobReference. blob ) )
784+ captionReferences. append ( AppBskyLexicon . Embed. VideoDefinition. Caption ( language: caption. language. identifier, fileBlob: blobReference) )
638785 }
639786 }
640787
@@ -688,7 +835,7 @@ extension ATProtoBluesky {
688835 accessToken: accessToken,
689836 filename: " \( ATProtoTools ( ) . generateRandomString ( ) ) _thumbnail.jpg " ,
690837 imageData: imageData
691- ) . blob
838+ )
692839 } else {
693840 thumbnailImage = nil
694841 }
0 commit comments