diff --git a/Sources/ATProtoKit/APIReference/AppBskyAPI/GetQuotes.swift b/Sources/ATProtoKit/APIReference/AppBskyAPI/GetQuotes.swift new file mode 100644 index 0000000000..03393481b4 --- /dev/null +++ b/Sources/ATProtoKit/APIReference/AppBskyAPI/GetQuotes.swift @@ -0,0 +1,75 @@ +// +// GetQuotes.swift +// +// +// Created by Christopher Jr Riley on 2024-08-23. +// + +import Foundation + +extension ATProtoKit { + + /// Gets an array of quuote posts that has embeded a given post. + /// + /// - Note: According to the AT Protocol specifications: "Get a list of quotes for a + /// given post." + /// + /// - SeeAlso: This is based on the [`app.bsky.feed.getQuotes`][github] lexicon. + /// + /// [github]: https://github.com/bluesky-social/atproto/blob/main/lexicons/app/bsky/feed/getQuotes.json + /// + /// + public func getQuotes( + from postURI: String, + postCID: String?, + limit: Int? = 50 + ) async throws -> AppBskyLexicon.Feed.GetQuotesOutput { + guard session != nil, + let accessToken = session?.accessToken else { + throw ATRequestPrepareError.missingActiveSession + } + + guard let sessionURL = session?.pdsURL, + let requestURL = URL(string: "\(sessionURL)/xrpc/app.bsky.feed.getQuotes") else { + throw ATRequestPrepareError.invalidRequestURL + } + + var queryItems = [(String, String)]() + + queryItems.append(("uri", postURI)) + + if let postCID { + queryItems.append(("cid", postCID)) + } + + if let limit { + let finalLimit = max(1, min(limit, 100)) + queryItems.append(("limit", "\(finalLimit)")) + } + + let queryURL: URL + + do { + queryURL = try APIClientService.setQueryItems( + for: requestURL, + with: queryItems + ) + + let request = APIClientService.createRequest( + forRequest: queryURL, + andMethod: .get, + acceptValue: "application/json", + contentTypeValue: nil, + authorizationValue: "Bearer \(accessToken)" + ) + let response = try await APIClientService.shared.sendRequest( + request, + decodeTo: AppBskyLexicon.Feed.GetQuotesOutput.self + ) + + return response + } catch { + throw error + } + } +} diff --git a/Sources/ATProtoKit/Models/Lexicons/ATUnion.swift b/Sources/ATProtoKit/Models/Lexicons/ATUnion.swift index 1f8ef5f724..bd8384e283 100644 --- a/Sources/ATProtoKit/Models/Lexicons/ATUnion.swift +++ b/Sources/ATProtoKit/Models/Lexicons/ATUnion.swift @@ -131,6 +131,9 @@ public struct ATUnion { /// A record that may have been blocked. case viewBlocked(AppBskyLexicon.Embed.RecordDefinition.ViewBlocked) + /// A record that may have been detached. + case viewDetached(AppBskyLexicon.Embed.RecordDefinition.ViewDetached) + /// A generator view. case generatorView(AppBskyLexicon.Feed.GeneratorViewDefinition) @@ -152,6 +155,8 @@ public struct ATUnion { self = .viewNotFound(value) } else if let value = try? container.decode(AppBskyLexicon.Embed.RecordDefinition.ViewBlocked.self) { self = .viewBlocked(value) + } else if let value = try? container.decode(AppBskyLexicon.Embed.RecordDefinition.ViewDetached.self) { + self = .viewDetached(value) } else if let value = try? container.decode(AppBskyLexicon.Feed.GeneratorViewDefinition.self) { self = .generatorView(value) } else if let value = try? container.decode(AppBskyLexicon.Graph.ListViewDefinition.self) { @@ -177,6 +182,8 @@ public struct ATUnion { try container.encode(viewNotFound) case .viewBlocked(let viewBlocked): try container.encode(viewBlocked) + case .viewDetached(let viewDetached): + try container.encode(viewDetached) case .generatorView(let generatorView): try container.encode(generatorView) case .listView(let listView): @@ -670,6 +677,32 @@ public struct ATUnion { } } + public enum EmbeddingRulesUnion: Codable { + + case disabledRule(AppBskyLexicon.Feed.PostgateRecord) + + public init(from decoder: any Decoder) throws { + let container = try decoder.singleValueContainer() + + if let value = try? container.decode(AppBskyLexicon.Feed.PostgateRecord.self) { + self = .disabledRule(value) + } else { + throw DecodingError.typeMismatch( + EmbeddingRulesUnion.self, DecodingError.Context( + codingPath: decoder.codingPath, debugDescription: "Unknown EmbeddingRulesUnion type")) + } + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + + switch self { + case .disabledRule(let disabledRule): + try container.encode(disabledRule) + } + } + } + /// A reference containing the list of thread rules for a post. public enum ThreadgateUnion: Codable { diff --git a/Sources/ATProtoKit/Models/Lexicons/app.bsky/Embed/AppBskyEmbedRecord.swift b/Sources/ATProtoKit/Models/Lexicons/app.bsky/Embed/AppBskyEmbedRecord.swift index 0a83050a07..5102e4a7f9 100644 --- a/Sources/ATProtoKit/Models/Lexicons/app.bsky/Embed/AppBskyEmbedRecord.swift +++ b/Sources/ATProtoKit/Models/Lexicons/app.bsky/Embed/AppBskyEmbedRecord.swift @@ -93,57 +93,15 @@ extension AppBskyLexicon.Embed { /// The number of likes for the record. Optional. public let likeCount: Int? + /// The number of quotes for the record. Optional. + public let quoteCount: Int? + /// An array of embed views of various types. public let embeds: [ATUnion.EmbedViewUnion]? /// The date the record was last indexed. @DateFormatting public var indexedAt: Date - public init(recordURI: String, cidHash: String, author: AppBskyLexicon.Actor.ProfileViewBasicDefinition, value: UnknownType, - labels: [ComAtprotoLexicon.Label.LabelDefinition]?, replyCount: Int?, repostCount: Int?, likeCount: Int?, - embeds: [ATUnion.EmbedViewUnion]?, indexedAt: Date) { - self.recordURI = recordURI - self.cidHash = cidHash - self.author = author - self.value = value - self.labels = labels - self.replyCount = replyCount - self.repostCount = repostCount - self.likeCount = likeCount - self.embeds = embeds - self._indexedAt = DateFormatting(wrappedValue: indexedAt) - } - - public init(from decoder: any Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - - self.recordURI = try container.decode(String.self, forKey: .recordURI) - self.cidHash = try container.decode(String.self, forKey: .cidHash) - self.author = try container.decode(AppBskyLexicon.Actor.ProfileViewBasicDefinition.self, forKey: .author) - self.value = try container.decode(UnknownType.self, forKey: .value) - self.labels = try container.decodeIfPresent([ComAtprotoLexicon.Label.LabelDefinition].self, forKey: .labels) - self.replyCount = try container.decodeIfPresent(Int.self, forKey: .replyCount) - self.repostCount = try container.decodeIfPresent(Int.self, forKey: .repostCount) - self.likeCount = try container.decodeIfPresent(Int.self, forKey: .likeCount) - self.embeds = try container.decodeIfPresent([ATUnion.EmbedViewUnion].self, forKey: .embeds) - self.indexedAt = try container.decode(DateFormatting.self, forKey: .indexedAt).wrappedValue - } - - public func encode(to encoder: any Encoder) throws { - var container = encoder.container(keyedBy: CodingKeys.self) - - try container.encode(self.recordURI, forKey: .recordURI) - try container.encode(self.cidHash, forKey: .cidHash) - try container.encode(self.author, forKey: .author) - try container.encode(self.value, forKey: .value) - try container.encodeIfPresent(self.labels, forKey: .labels) - try container.encodeIfPresent(self.replyCount, forKey: .replyCount) - try container.encodeIfPresent(self.repostCount, forKey: .repostCount) - try container.encodeIfPresent(self.likeCount, forKey: .likeCount) - try container.encodeIfPresent(self.embeds, forKey: .embeds) - try container.encode(self._indexedAt, forKey: .indexedAt) - } - enum CodingKeys: String, CodingKey { case type = "$type" case recordURI = "uri" @@ -154,6 +112,7 @@ extension AppBskyLexicon.Embed { case replyCount case repostCount case likeCount + case quoteCount case embeds = "embeds" case indexedAt } @@ -200,5 +159,24 @@ extension AppBskyLexicon.Embed { case recordAuthor = "author" } } + + /// A data model for a definition of a record that has been detached. + /// + /// - SeeAlso: This is based on the [`app.bsky.embed.record`][github] lexicon. + /// + /// [github]: https://github.com/bluesky-social/atproto/blob/main/lexicons/app/bsky/embed/record.json + public struct ViewDetached: Codable { + + /// The URI of the record. + public let postURI: String + + /// Indicates whether the record was detached. + public let isRecordDetached: Bool + + enum CodingKeys: String, CodingKey { + case postURI = "uri" + case isRecordDetached = "detached" + } + } } } diff --git a/Sources/ATProtoKit/Models/Lexicons/app.bsky/Feed/AppBskyFeedDefs.swift b/Sources/ATProtoKit/Models/Lexicons/app.bsky/Feed/AppBskyFeedDefs.swift index 98fc4ca425..ae1c78ec1d 100644 --- a/Sources/ATProtoKit/Models/Lexicons/app.bsky/Feed/AppBskyFeedDefs.swift +++ b/Sources/ATProtoKit/Models/Lexicons/app.bsky/Feed/AppBskyFeedDefs.swift @@ -38,7 +38,10 @@ extension AppBskyLexicon.Feed { public let repostCount: Int? /// The number of likes in the post. Optional. - public var likeCount: Int? + public let likeCount: Int? + + /// The number of quote posts in the post. Optional. + public let quoteCount: Int? /// The last time the post has been indexed. @DateFormatting public var indexedAt: Date @@ -61,6 +64,7 @@ extension AppBskyLexicon.Feed { case replyCount case repostCount case likeCount + case quoteCount case indexedAt case viewer case labels @@ -91,11 +95,15 @@ extension AppBskyLexicon.Feed { /// Indicates whether the requesting account can reply to the account's post. Optional. public let areRepliesDisabled: Bool? + /// Indicates whether the post can be embedded. + public let isEmbeddingDisabled: Bool? + enum CodingKeys: String, CodingKey { case repostURI = "repost" case likeURI = "like" case isThreadMuted = "threadMuted" case areRepliesDisabled = "replyDisabled" + case isEmbeddingDisabled = "embeddingDisabled" } } @@ -506,7 +514,7 @@ extension AppBskyLexicon.Feed { } } - /// A definition model for a feed threadgate view. + /// A definition model for a feed threadgate view. /// /// - SeeAlso: This is based on the [`app.bsky.feed.defs`][github] lexicon. /// diff --git a/Sources/ATProtoKit/Models/Lexicons/app.bsky/Feed/AppBskyFeedGetPostThread.swift b/Sources/ATProtoKit/Models/Lexicons/app.bsky/Feed/AppBskyFeedGetPostThread.swift index ab659aa927..c1908100ad 100644 --- a/Sources/ATProtoKit/Models/Lexicons/app.bsky/Feed/AppBskyFeedGetPostThread.swift +++ b/Sources/ATProtoKit/Models/Lexicons/app.bsky/Feed/AppBskyFeedGetPostThread.swift @@ -21,5 +21,8 @@ extension AppBskyLexicon.Feed { /// The post thread itself. public let thread: ATUnion.GetPostThreadOutputThreadUnion + + /// A feed's threadgate view. Optional. + public let threadgate: AppBskyLexicon.Feed.ThreadgateViewDefinition? } } diff --git a/Sources/ATProtoKit/Models/Lexicons/app.bsky/Feed/AppBskyFeedGetQuotes.swift b/Sources/ATProtoKit/Models/Lexicons/app.bsky/Feed/AppBskyFeedGetQuotes.swift new file mode 100644 index 0000000000..1007a40895 --- /dev/null +++ b/Sources/ATProtoKit/Models/Lexicons/app.bsky/Feed/AppBskyFeedGetQuotes.swift @@ -0,0 +1,34 @@ +// +// AppBskyFeedGetQuotes.swift +// +// +// Created by Christopher Jr Riley on 2024-08-23. +// + +import Foundation + +extension AppBskyLexicon.Feed { + + /// An output model for getting the quote posts of a given post. + /// + /// - Note: According to the AT Protocol specifications: "Get posts in a thread. Does not require + /// auth, but additional metadata and filtering will be applied for authed requests." + /// + /// - SeeAlso: This is based on the [`app.bsky.feed.getPostThread`][github] lexicon. + /// + /// [github]: https://github.com/bluesky-social/atproto/blob/main/lexicons/app/bsky/feed/getPostThread.json + public struct GetQuotesOutput: Codable { + + /// The URI of the given post. + public let postURI: String + + /// The CID hash of the given post. + public let postCID: String? + + /// The mark used to indicate the starting point for the next set of results. Optional. + public let cursor: String? + + /// An array of quote posts. + public let posts: [AppBskyLexicon.Feed.PostViewDefinition] + } +} diff --git a/Sources/ATProtoKit/Models/Lexicons/app.bsky/Feed/AppBskyFeedPostgate.swift b/Sources/ATProtoKit/Models/Lexicons/app.bsky/Feed/AppBskyFeedPostgate.swift new file mode 100644 index 0000000000..ba88146ca5 --- /dev/null +++ b/Sources/ATProtoKit/Models/Lexicons/app.bsky/Feed/AppBskyFeedPostgate.swift @@ -0,0 +1,61 @@ +// +// AppBskyFeedPostgate.swift +// +// +// Created by Christopher Jr Riley on 2024-08-23. +// + +import Foundation + +extension AppBskyLexicon.Feed { + + /// A record model for the rules of a post's interaction. + /// + /// - Note: According to the AT Protocol specifications: "Record defining interaction rules for + /// a post. The record key (rkey) of the postgate record must match the record key of the post, + /// and that record must be in the same repository." + /// + /// - SeeAlso: This is based on the [`app.bsky.feed.postgate`][github] lexicon. + /// + /// [github]: https://github.com/bluesky-social/atproto/blob/main/lexicons/app/bsky/feed/postgate.json + public struct PostgateRecord: ATRecordProtocol { + + /// The identifier of the lexicon. + /// + /// - Warning: The value must not change. + public static var type: String = "app.bsky.feed.postgate" + + /// The date and time the post was created. + @DateFormatting public var createdAt: Date + + /// The URI of the post. + public let postURI: String + + /// An array of URIs belonging to posts that the `postURI`'s auther has detached. Optional. + /// + /// - Note: According to the AT Protocol specifications: "List of AT-URIs embedding this + /// post that the author has detached from." + public let detachedEmbeddingURIs: [String]? + + /// An array of rules for embedding the post. Optional. + public let embeddingRules: [ATUnion.EmbeddingRulesUnion]? + + public func encode(to encoder: any Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + + try container.encode(self._createdAt, forKey: .createdAt) + try container.encode(self.postURI, forKey: .postURI) + + try truncatedEncodeIfPresent(self.detachedEmbeddingURIs, withContainer: &container, forKey: .detachedEmbeddingURIs, upToArrayLength: 50) + + try truncatedEncodeIfPresent(self.embeddingRules, withContainer: &container, forKey: .embeddingRules, upToArrayLength: 5) + } + + enum CodingKeys: String, CodingKey { + case createdAt + case postURI = "post" + case detachedEmbeddingURIs = "detachedEmbeddingUris" + case embeddingRules + } + } +} diff --git a/Sources/ATProtoKit/Models/Lexicons/app.bsky/Feed/AppBskyFeedThreadgate.swift b/Sources/ATProtoKit/Models/Lexicons/app.bsky/Feed/AppBskyFeedThreadgate.swift index a2042fd95a..258113feaa 100644 --- a/Sources/ATProtoKit/Models/Lexicons/app.bsky/Feed/AppBskyFeedThreadgate.swift +++ b/Sources/ATProtoKit/Models/Lexicons/app.bsky/Feed/AppBskyFeedThreadgate.swift @@ -37,11 +37,8 @@ extension AppBskyLexicon.Feed { /// The date and time of the creation of the threadgate. @DateFormatting public var createdAt: Date - public init(post: String, allow: [ATUnion.ThreadgateUnion], createdAt: Date) { - self.post = post - self.allow = allow - self._createdAt = DateFormatting(wrappedValue: createdAt) - } + /// An array of hidden replies in the form of URIs. Optional. + public let hiddenReplies: [String]? public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) @@ -49,6 +46,7 @@ extension AppBskyLexicon.Feed { self.post = try container.decode(String.self, forKey: .post) self.allow = try container.decode([ATUnion.ThreadgateUnion].self, forKey: .allow) self.createdAt = try container.decode(DateFormatting.self, forKey: .createdAt).wrappedValue + self.hiddenReplies = try container.decodeIfPresent([String].self, forKey: .hiddenReplies) } public func encode(to encoder: Encoder) throws { @@ -57,6 +55,8 @@ extension AppBskyLexicon.Feed { try container.encode(self.post, forKey: .post) try container.encode(self.allow, forKey: .allow) try container.encode(self._createdAt, forKey: .createdAt) + + try truncatedEncodeIfPresent(self.hiddenReplies, withContainer: &container, forKey: .hiddenReplies, upToArrayLength: 50) } enum CodingKeys: String, CodingKey { @@ -64,6 +64,7 @@ extension AppBskyLexicon.Feed { case post case allow case createdAt + case hiddenReplies } /// A rule that indicates whether users that the post author mentions can reply to the post.