this repo has no description
2
fork

Configure Feed

Select the types of activity you want to include in your feed.

update readme and docs

+218 -35
+30 -3
README.md
··· 7 7 bskyKit implements the `app.bsky.*` lexicons for the AT Protocol, giving you everything needed to build a full-featured Bluesky client: 8 8 9 9 - **Read Operations** - Fetch timelines, profiles, posts, threads, notifications, and social graphs 10 - - **Write Operations** - Create posts, likes, reposts, follows, and blocks 10 + - **Write Operations** - Create posts, likes, reposts, follows, blocks, and low-level repo writes 11 11 - **Rich Text** - Automatic detection and creation of mentions, links, and hashtags with proper byte indexing 12 - - **Type Safety** - Fully typed models for all API responses with `Codable` and `Sendable` conformance 12 + - **Type Safety** - Typed models for stable endpoints plus `JSONValue` for rapidly evolving/unspecced payloads 13 13 - **SwiftUI Ready** - Models conform to `Identifiable` for seamless use in SwiftUI lists 14 + 15 + `BskyService` now covers all current `app.bsky` query/procedure lexicons, including actor/feed/graph/notification plus age-assurance, unspecced, bookmark, and video endpoints. 14 16 15 17 Built on [CoreATProtocol](https://tangled.org/@sparrowtek.com/CoreATProtocol) for networking, authentication, and token management. 16 18 ··· 120 122 |--------|-------------| 121 123 | `getProfile(for:)` | Fetch a user profile by handle or DID | 122 124 | `getProfiles(for:)` | Fetch multiple profiles in one request | 125 + | `getSuggestions(limit:cursor:)` | Get actor suggestions | 123 126 | `getPreferences()` | Get authenticated user's preferences | 124 127 | `searchActors(query:limit:)` | Search users by name/handle/bio | 125 128 | `searchActorsTypeahead(query:limit:)` | Fast search for autocomplete | 129 + | `putPreferences(_:)` | Update actor preferences | 126 130 127 131 #### Feed Operations 128 132 129 133 | Method | Description | 130 134 |--------|-------------| 131 135 | `getTimeline(limit:cursor:)` | Get home timeline | 136 + | `getFeed(feed:limit:cursor:)` | Get posts from a feed generator | 132 137 | `getAuthorFeed(for:limit:cursor:)` | Get a user's posts | 133 138 | `getPostThread(uri:depth:)` | Get post with replies | 134 139 | `getPosts(uris:)` | Fetch multiple posts by URI | 140 + | `searchPosts(...)` | Search posts | 141 + | `getQuotes(uri:cid:limit:cursor:)` | Get quotes of a post | 142 + | `getListFeed(list:limit:cursor:)` | Get posts from a list feed | 143 + | `getActorFeeds(for:limit:cursor:)` | Get feed generators by actor | 144 + | `getFeedGenerator(feed:)` | Get one feed generator | 135 145 | `getFeedGenerators(for:)` | Get custom feed info | 146 + | `getSuggestedFeeds(limit:cursor:)` | Get suggested feed generators | 136 147 | `getLikes(uri:limit:cursor:)` | Get users who liked a post | 137 148 | `getRepostedBy(uri:limit:cursor:)` | Get users who reposted | 138 149 ··· 144 155 | `getFollowers(for:limit:cursor:)` | Get a user's followers | 145 156 | `getBlocks(limit:cursor:)` | Get your blocked accounts | 146 157 | `getMutes(limit:cursor:)` | Get your muted accounts | 158 + | `getRelationships(for:others:)` | Get relationship state for actors | 159 + | `muteActor(_:)` / `unmuteActor(_:)` | Mute or unmute actor | 160 + | `muteThread(root:)` / `unmuteThread(root:)` | Mute or unmute thread | 161 + | `muteActorList(_:)` / `unmuteActorList(_:)` | Mute or unmute actor list | 147 162 148 163 #### Notification Operations 149 164 ··· 152 167 | `listNotifications(limit:cursor:)` | Get notifications | 153 168 | `getUnreadCount()` | Get unread notification count | 154 169 | `updateSeen(at:)` | Mark notifications as read | 170 + | `getNotificationPreferences()` | Read notification preferences | 171 + | `putNotificationPreferences(priority:)` | Update legacy notification preferences | 172 + | `putNotificationPreferencesV2(_:)` | Update v2 notification preferences | 155 173 156 174 ### RepoService (Write Operations) 157 175 ··· 178 196 179 197 ```swift 180 198 createRecord(repo:collection:record:rkey:) -> CreateRecordResponse 199 + putRecord(repo:collection:rkey:record:validate:swapRecord:swapCommit:) -> PutRecordResponse 200 + applyWrites(repo:writes:validate:swapCommit:) -> ApplyWritesResponse 181 201 deleteRecord(repo:collection:rkey:) 202 + describeRepo(repo:) -> DescribeRepoResponse 182 203 getRecord(repo:collection:rkey:) -> GetRecordResponse 183 204 listRecords(repo:collection:limit:cursor:) -> ListRecordsResponse 205 + listMissingBlobs(limit:cursor:) -> ListMissingBlobsResponse 206 + importRepo(car:) 207 + uploadBlob(data:mimeType:) -> BlobResponse 184 208 ``` 185 209 186 210 ## Rich Text ··· 215 239 print("Link to: \(link.uri)") 216 240 case .tag(let tag): 217 241 print("Hashtag: #\(tag.tag)") 242 + case .unknown: 243 + break 218 244 } 219 245 } 220 246 ``` ··· 314 340 ### Notification Reasons 315 341 316 342 ```swift 317 - public enum NotificationReason: String { 343 + public enum NotificationReason: Codable, Sendable, Equatable, Hashable { 318 344 case like 319 345 case repost 320 346 case follow ··· 322 348 case reply 323 349 case quote 324 350 case starterpackJoined 351 + case unknown(String) 325 352 } 326 353 ``` 327 354
+2 -2
Sources/bskyKit/Documentation.docc/GettingStarted.md
··· 116 116 } 117 117 118 118 // Use cursor for pagination 119 - if !timeline.cursor.isEmpty { 120 - let nextPage = try await service.getTimeline(limit: 20, cursor: timeline.cursor) 119 + if let cursor = timeline.cursor { 120 + let nextPage = try await service.getTimeline(limit: 20, cursor: cursor) 121 121 // Process next page... 122 122 } 123 123 }
+37 -10
Sources/bskyKit/Documentation.docc/Notifications.md
··· 16 16 let response = try await service.listNotifications(limit: 50) 17 17 18 18 for notification in response.notifications { 19 - print("[\(notification.reason.rawValue)] @\(notification.author.handle)") 19 + print("[\(notification.reason)] @\(notification.author.handle)") 20 20 21 21 switch notification.reason { 22 22 case .like: ··· 36 36 print(" quoted your post") 37 37 case .starterpackJoined: 38 38 print(" joined via your starter pack") 39 + case .unknown(let value): 40 + print(" \(value)") 39 41 } 40 42 41 43 print(" Read: \(notification.isRead)") ··· 84 86 try await service.updateSeen(at: Date()) 85 87 ``` 86 88 89 + ## Notification Preferences and Push 90 + 91 + ```swift 92 + // Read preferences 93 + let prefs = try await service.getNotificationPreferences() 94 + 95 + // Legacy preference toggle 96 + try await service.putNotificationPreferences(priority: true) 97 + 98 + // V2 preferences payload 99 + let updated = try await service.putNotificationPreferencesV2([ 100 + "like": ["allow": true], 101 + "mention": ["allow": true] 102 + ]) 103 + 104 + // Push registration 105 + try await service.registerPush( 106 + serviceDid: "did:web:push.example.com", 107 + token: "<apns-token>", 108 + platform: "ios", 109 + appID: "com.example.app" 110 + ) 111 + ``` 112 + 87 113 ## Filtering Notifications 88 114 89 115 Filter notifications by type: ··· 117 143 The ``NotificationReason`` enum defines all notification types: 118 144 119 145 ```swift 120 - public enum NotificationReason: String, Codable, Sendable { 121 - case like // Someone liked your post 122 - case repost // Someone reposted your post 123 - case follow // Someone followed you 124 - case mention // Someone mentioned you in a post 125 - case reply // Someone replied to your post 126 - case quote // Someone quoted your post 127 - case starterpackJoined = "starterpack-joined" // Someone joined via your starter pack 146 + public enum NotificationReason: Codable, Sendable, Equatable, Hashable { 147 + case like 148 + case repost 149 + case follow 150 + case mention 151 + case reply 152 + case quote 153 + case starterpackJoined 154 + case unknown(String) 128 155 } 129 156 ``` 130 157 ··· 234 261 ```swift 235 262 func groupNotifications(_ notifications: [Notification]) -> [String: [Notification]] { 236 263 Dictionary(grouping: notifications) { notification in 237 - notification.reason.rawValue 264 + String(describing: notification.reason) 238 265 } 239 266 } 240 267
+3
Sources/bskyKit/Documentation.docc/RichTextGuide.md
··· 43 43 print(" Link: \(link.uri)") 44 44 case .tag(let tag): 45 45 print(" Tag: #\(tag.tag)") 46 + case .unknown: 47 + break 46 48 } 47 49 } 48 50 } ··· 142 144 case link(RichTextLink) 143 145 case mention(RichTextMention) 144 146 case tag(RichTextTag) 147 + case unknown(String) 145 148 } 146 149 ``` 147 150
+40
Sources/bskyKit/Documentation.docc/SocialActions.md
··· 240 240 } 241 241 ``` 242 242 243 + ### Put Record 244 + 245 + ```swift 246 + let updated = try await repoService.putRecord( 247 + repo: myDID, 248 + collection: "app.bsky.feed.post", 249 + rkey: "abc123", 250 + record: [ 251 + "$type": "app.bsky.feed.post", 252 + "text": "Updated text", 253 + "createdAt": ISO8601DateFormatter().string(from: Date()) 254 + ] 255 + ) 256 + print(updated.uri) 257 + ``` 258 + 259 + ### Apply Batch Writes 260 + 261 + ```swift 262 + let response = try await repoService.applyWrites( 263 + repo: myDID, 264 + writes: [ 265 + .create(collection: "app.bsky.feed.post", value: [ 266 + "$type": "app.bsky.feed.post", 267 + "text": "Batch write post", 268 + "createdAt": ISO8601DateFormatter().string(from: Date()) 269 + ]) 270 + ] 271 + ) 272 + print(response.results?.count ?? 0) 273 + ``` 274 + 275 + ### Upload Blob 276 + 277 + ```swift 278 + let imageData: Data = ... 279 + let blob = try await repoService.uploadBlob(data: imageData, mimeType: "image/jpeg") 280 + print(blob.blob.ref.link) 281 + ``` 282 + 243 283 ## PostRecord Reference 244 284 245 285 ```swift
+45
Sources/bskyKit/Documentation.docc/SocialGraph.md
··· 138 138 139 139 > Note: Muting is handled differently from blocks - mute/unmute operations use dedicated endpoints rather than record creation. 140 140 141 + ### Mute and Unmute an Actor 142 + 143 + ```swift 144 + try await service.muteActor("did:plc:user-to-mute") 145 + try await service.unmuteActor("did:plc:user-to-mute") 146 + ``` 147 + 148 + ### Mute and Unmute a Thread 149 + 150 + ```swift 151 + let root = "at://did:plc:author/app.bsky.feed.post/abc123" 152 + try await service.muteThread(root: root) 153 + try await service.unmuteThread(root: root) 154 + ``` 155 + 156 + ### Mute and Unmute an Actor List 157 + 158 + ```swift 159 + let listURI = "at://did:plc:author/app.bsky.graph.list/xyz" 160 + try await service.muteActorList(listURI) 161 + try await service.unmuteActorList(listURI) 162 + ``` 163 + 164 + ## Relationship Queries 165 + 166 + Use ``BskyService/getRelationships(for:others:)`` to fetch relationship state for multiple actors in one request: 167 + 168 + ```swift 169 + let relationships = try await service.getRelationships( 170 + for: "did:plc:mydid", 171 + others: ["did:plc:alice", "did:plc:bob"] 172 + ) 173 + 174 + for relationship in relationships.relationships { 175 + switch relationship { 176 + case .relationship(let value): 177 + print("Actor: \(value.did), following: \(value.following != nil)") 178 + case .notFound(let missing): 179 + print("Not found: \(missing.actor)") 180 + case .unknown: 181 + break 182 + } 183 + } 184 + ``` 185 + 141 186 ## Checking Relationships 142 187 143 188 The ``Viewer`` struct on profiles indicates the relationship:
+42 -19
Sources/bskyKit/Documentation.docc/TimelineAndFeeds.md
··· 41 41 repeat { 42 42 let timeline = try await service.getTimeline(limit: 100, cursor: cursor) 43 43 allPosts.append(contentsOf: timeline.feed) 44 - cursor = timeline.cursor.isEmpty ? nil : timeline.cursor 44 + cursor = timeline.cursor 45 45 46 46 // Limit to 500 posts for this example 47 47 if allPosts.count >= 500 { break } ··· 157 157 } 158 158 ``` 159 159 160 + Fetch posts from a specific feed generator: 161 + 162 + ```swift 163 + let feedItems = try await service.getFeed( 164 + feed: "at://did:plc:xxx/app.bsky.feed.generator/whats-hot", 165 + limit: 25 166 + ) 167 + ``` 168 + 169 + Search posts: 170 + 171 + ```swift 172 + let results = try await service.searchPosts(query: "swift", limit: 25) 173 + for post in results.posts { 174 + print(post.record.text ?? "") 175 + } 176 + ``` 177 + 160 178 ## Understanding Timeline Structure 161 179 162 180 ### Timeline ··· 164 182 ```swift 165 183 public struct Timeline: Codable, Sendable { 166 184 public var feed: [TimelineItem] 167 - public var cursor: String 185 + public var cursor: String? 168 186 } 169 187 ``` 170 188 ··· 176 194 public struct TimelineItem: Codable, Sendable, Identifiable { 177 195 public let post: Post 178 196 public let reply: Reply? 197 + public let reason: TimelineReason? 198 + public let feedContext: String? 199 + public let reqId: String? 179 200 180 201 public var id: String { 181 - "\(post.uri ?? "")-\(post.cid ?? "")" 202 + "\(post.uri)-\(post.cid)" 182 203 } 183 204 } 184 205 ``` ··· 187 208 188 209 ```swift 189 210 public struct Post: Codable, Sendable { 190 - public let uri: String? 191 - public let cid: String? 211 + public let uri: String 212 + public let cid: String 192 213 public let author: Author 193 214 public let record: Record 194 - public let replyCount: Int 195 - public let repostCount: Int 196 - public let likeCount: Int 197 - public let indexedAt: String 198 - public let viewer: Viewer 199 - public let labels: [String] 215 + public let replyCount: Int? 216 + public let repostCount: Int? 217 + public let likeCount: Int? 218 + public let quoteCount: Int? 219 + public let bookmarkCount: Int? 220 + public let indexedAt: Date 221 + public let viewer: FeedViewer? 222 + public let labels: [AuthorLabels]? 200 223 public let embed: Embed? 201 224 } 202 225 ``` ··· 207 230 208 231 ```swift 209 232 public struct Record: Codable, Sendable { 210 - public let text: String 211 - public let type: String 233 + public let text: String? 234 + public let type: String? 212 235 public let langs: [String]? 213 236 public let reply: ReplyDetail? 214 - public let createdAt: String 237 + public let createdAt: Date? 215 238 public let embed: Embed? 216 239 public let facets: [Facet]? 217 240 } ··· 223 246 224 247 ```swift 225 248 if let embed = post.embed { 226 - switch EmbedType(rawValue: embed.type) { 227 - case .image: 249 + switch embed.type { 250 + case "app.bsky.embed.images#view": 228 251 // Image embed 229 252 if let images = embed.images { 230 253 for image in images { ··· 232 255 } 233 256 } 234 257 235 - case .external: 258 + case "app.bsky.embed.external#view": 236 259 // Link preview 237 260 if let external = embed.external { 238 261 print("Link: \(external.title)") 239 262 print("URL: \(external.uri ?? "")") 240 263 } 241 264 242 - case .record: 265 + case "app.bsky.embed.record#view": 243 266 // Quote post 244 267 if let record = embed.record { 245 268 print("Quote: \(record.value?.text ?? "")") 246 269 } 247 270 248 - case .recordWithMedia: 271 + case "app.bsky.embed.recordWithMedia#view": 249 272 // Quote post with images 250 273 print("Quote with media") 251 274
+16
Sources/bskyKit/Documentation.docc/WorkingWithProfiles.md
··· 91 91 92 92 > Note: Typeahead search is optimized for speed and returns fewer fields than full search. 93 93 94 + ### Suggestions 95 + 96 + Use ``BskyService/getSuggestions(limit:cursor:)`` for recommendation-style actor suggestions: 97 + 98 + ```swift 99 + let suggestions = try await service.getSuggestions(limit: 20) 100 + for actor in suggestions.actors { 101 + print("@\(actor.handle)") 102 + } 103 + ``` 104 + 94 105 ## Understanding Viewer State 95 106 96 107 The ``Viewer`` struct indicates the relationship between the authenticated user and a profile: ··· 127 138 for savedFeed in preferences.saved { 128 139 print("Saved: \(savedFeed)") 129 140 } 141 + 142 + // Update preferences payload when needed 143 + try await service.putPreferences([ 144 + "adultContentEnabled": false 145 + ]) 130 146 ``` 131 147 132 148 ## Profile Model Reference
+3 -1
Sources/bskyKit/Documentation.docc/bskyKit.md
··· 4 4 5 5 ## Overview 6 6 7 - bskyKit provides a type-safe, Swift-native interface to the Bluesky AT Protocol APIs. Built on top of CoreATProtocol, it offers comprehensive support for reading and writing social data including profiles, timelines, posts, follows, and notifications. 7 + bskyKit provides a Swift-native interface to Bluesky AT Protocol APIs. Built on top of CoreATProtocol, it supports the full `app.bsky` query/procedure surface, including stable typed models and `JSONValue` for endpoints with rapidly evolving schemas. 8 8 9 9 ### Key Features 10 10 ··· 14 14 - **Rich Text**: Auto-detect mentions, links, and hashtags with proper byte indexing 15 15 - **Write Operations**: Create posts, likes, reposts, follows, and blocks 16 16 - **Notifications**: List notifications and manage read state 17 + - **Complete app.bsky Coverage**: Actor, feed, graph, notification, bookmark, age-assurance, unspecced, and video endpoints 17 18 18 19 ### Quick Start 19 20 ··· 110 111 111 112 - ``Feed`` 112 113 - ``Feeds`` 114 + - ``JSONValue`` 113 115 - ``Creator`` 114 116 - ``Preferences`` 115 117 - ``Blocks``