this repo has no description
2
fork

Configure Feed

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

at main 412 lines 12 kB view raw view rendered
1# bskyKit 2 3A Swift SDK for building [Bluesky](https://bsky.app) clients on Apple platforms. bskyKit provides a complete, type-safe interface to the Bluesky API with modern Swift concurrency support. 4 5## Overview 6 7bskyKit implements the `app.bsky.*` lexicons for the AT Protocol, giving you everything needed to build a full-featured Bluesky client: 8 9- **Read Operations** - Fetch timelines, profiles, posts, threads, notifications, and social graphs 10- **Write Operations** - Create posts, likes, reposts, follows, blocks, and low-level repo writes 11- **Rich Text** - Automatic detection and creation of mentions, links, and hashtags with proper byte indexing 12- **Type Safety** - Typed models for stable endpoints plus `JSONValue` for rapidly evolving/unspecced payloads 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. 16 17Built on [CoreATProtocol](https://tangled.org/@sparrowtek.com/CoreATProtocol) for networking, authentication, and token management. 18 19## Requirements 20 21- Swift 6.2+ 22- iOS 26.0+ / macOS 26.0+ / watchOS 26.0+ / tvOS 26.0+ / Mac Catalyst 26.0+ 23 24## Installation 25 26### Swift Package Manager 27 28Add bskyKit to your `Package.swift` dependencies: 29 30```swift 31dependencies: [ 32 .package(url: "https://tangled.org/@sparrowtek.com/bskyKit", branch: "main"), 33] 34``` 35 36Then add it to your target: 37 38```swift 39.target( 40 name: "YourApp", 41 dependencies: ["bskyKit"] 42), 43``` 44 45Or in Xcode: **File > Add Package Dependencies** and enter: 46``` 47https://tangled.org/@sparrowtek.com/bskyKit 48``` 49 50## Quick Start 51 52### Setup 53 54Configure the environment before making any API calls: 55 56```swift 57import bskyKit 58import CoreATProtocol 59 60// Configure with your PDS host and authentication tokens 61await setup( 62 hostURL: "https://bsky.social", 63 accessJWT: "your-access-token", 64 refreshJWT: "your-refresh-token" 65) 66``` 67 68### Reading Data 69 70Use `BskyService` for all read operations: 71 72```swift 73let service = await BskyService() 74 75// Fetch a profile 76let profile = try await service.getProfile(for: "alice.bsky.social") 77print("\(profile.displayName ?? profile.handle) has \(profile.followersCount ?? 0) followers") 78 79// Get your timeline 80let timeline = try await service.getTimeline(limit: 50) 81for item in timeline.feed { 82 print("\(item.post.author.handle): \(item.post.record.text ?? "")") 83} 84 85// Search for users 86let results = try await service.searchActors(query: "swift developer", limit: 10) 87``` 88 89### Creating Content 90 91Use `RepoService` for write operations: 92 93```swift 94let repo = await RepoService() 95let myDID = "did:plc:your-did-here" 96 97// Create a simple post 98let post = PostRecord.create(text: "Hello from bskyKit!") 99let result = try await repo.createPost(post, repo: myDID) 100 101// Create a post with rich text (auto-detected) 102let richPost = PostRecord.create( 103 text: "Hey @alice.bsky.social check out https://example.com #swift" 104) 105// Mentions, links, and hashtags are automatically detected! 106try await repo.createPost(richPost, repo: myDID) 107 108// Like a post 109try await repo.like(uri: postURI, cid: postCID, repo: myDID) 110 111// Follow someone 112try await repo.follow(did: "did:plc:someone", repo: myDID) 113``` 114 115## API Reference 116 117### BskyService (Read Operations) 118 119#### Actor Operations 120 121| Method | Description | 122|--------|-------------| 123| `getProfile(for:)` | Fetch a user profile by handle or DID | 124| `getProfiles(for:)` | Fetch multiple profiles in one request | 125| `getSuggestions(limit:cursor:)` | Get actor suggestions | 126| `getPreferences()` | Get authenticated user's preferences | 127| `searchActors(query:limit:)` | Search users by name/handle/bio | 128| `searchActorsTypeahead(query:limit:)` | Fast search for autocomplete | 129| `putPreferences(_:)` | Update actor preferences | 130 131#### Feed Operations 132 133| Method | Description | 134|--------|-------------| 135| `getTimeline(limit:cursor:)` | Get home timeline | 136| `getFeed(feed:limit:cursor:)` | Get posts from a feed generator | 137| `getAuthorFeed(for:limit:cursor:)` | Get a user's posts | 138| `getPostThread(uri:depth:)` | Get post with replies | 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 | 145| `getFeedGenerators(for:)` | Get custom feed info | 146| `getSuggestedFeeds(limit:cursor:)` | Get suggested feed generators | 147| `getLikes(uri:limit:cursor:)` | Get users who liked a post | 148| `getRepostedBy(uri:limit:cursor:)` | Get users who reposted | 149 150#### Graph Operations 151 152| Method | Description | 153|--------|-------------| 154| `getFollows(for:limit:cursor:)` | Get who a user follows | 155| `getFollowers(for:limit:cursor:)` | Get a user's followers | 156| `getBlocks(limit:cursor:)` | Get your blocked accounts | 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 | 162 163#### Notification Operations 164 165| Method | Description | 166|--------|-------------| 167| `listNotifications(limit:cursor:)` | Get notifications | 168| `getUnreadCount()` | Get unread notification count | 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 | 173 174### RepoService (Write Operations) 175 176#### High-Level Methods 177 178```swift 179// Posts 180createPost(_ post: PostRecord, repo: String) -> CreateRecordResponse 181 182// Interactions 183like(uri:cid:repo:) -> CreateRecordResponse 184unlike(uri:repo:) 185repost(uri:cid:repo:) -> CreateRecordResponse 186unrepost(uri:repo:) 187 188// Social Graph 189follow(did:repo:) -> CreateRecordResponse 190unfollow(uri:repo:) 191block(did:repo:) -> CreateRecordResponse 192unblock(uri:repo:) 193``` 194 195#### Low-Level Record Operations 196 197```swift 198createRecord(repo:collection:record:rkey:) -> CreateRecordResponse 199putRecord(repo:collection:rkey:record:validate:swapRecord:swapCommit:) -> PutRecordResponse 200applyWrites(repo:writes:validate:swapCommit:) -> ApplyWritesResponse 201deleteRecord(repo:collection:rkey:) 202describeRepo(repo:) -> DescribeRepoResponse 203getRecord(repo:collection:rkey:) -> GetRecordResponse 204listRecords(repo:collection:limit:cursor:) -> ListRecordsResponse 205listMissingBlobs(limit:cursor:) -> ListMissingBlobsResponse 206importRepo(car:) 207uploadBlob(data:mimeType:) -> BlobResponse 208``` 209 210## Rich Text 211 212bskyKit handles the complexity of AT Protocol rich text facets automatically. 213 214### Automatic Detection 215 216The easiest approach - facets are detected automatically: 217 218```swift 219let post = PostRecord.create( 220 text: "Hey @alice.bsky.social! Check https://swift.org #SwiftLang" 221) 222// post.facets contains 3 facets: mention, link, and hashtag 223``` 224 225### Manual Rich Text Processing 226 227For more control, use the `RichText` type directly: 228 229```swift 230let text = "Hello @bob.bsky.social!" 231let richText = RichText.detect(in: text) 232 233for facet in richText.facets { 234 switch facet.features[0] { 235 case .mention(let mention): 236 print("Mentioned: \(mention.handle ?? "unknown")") 237 // Resolve handle to DID before posting 238 case .link(let link): 239 print("Link to: \(link.uri)") 240 case .tag(let tag): 241 print("Hashtag: #\(tag.tag)") 242 case .unknown: 243 break 244 } 245} 246``` 247 248### Byte Index Handling 249 250AT Protocol uses UTF-8 byte indices, not character indices. bskyKit handles this automatically, but if you need manual conversion: 251 252```swift 253let text = "Hi 👋 there" // Emoji = 4 bytes 254let richText = RichText(text: text) 255 256// Convert character index to byte index 257let charIndex = text.index(text.startIndex, offsetBy: 4) 258let byteIndex = richText.byteIndex(from: charIndex) // Returns 7 259 260// Convert byte index to character index 261let charIdx = richText.characterIndex(from: 7) 262``` 263 264## Pagination 265 266All list endpoints support cursor-based pagination: 267 268```swift 269let service = await BskyService() 270 271// First page 272var timeline = try await service.getTimeline(limit: 50) 273displayPosts(timeline.feed) 274 275// Load more 276while let cursor = timeline.cursor { 277 timeline = try await service.getTimeline(limit: 50, cursor: cursor) 278 displayPosts(timeline.feed) 279} 280``` 281 282## Creating Posts with Embeds 283 284### Reply to a Post 285 286```swift 287let replyRef = ReplyRef( 288 root: PostRef(uri: rootPostURI, cid: rootPostCID), 289 parent: PostRef(uri: parentPostURI, cid: parentPostCID) 290) 291 292let post = PostRecord.create( 293 text: "This is my reply!", 294 reply: replyRef 295) 296try await repo.createPost(post, repo: myDID) 297``` 298 299### Quote Post 300 301```swift 302let post = PostRecord( 303 text: "Check out this post!", 304 embed: .record(RecordEmbed(uri: quotedPostURI, cid: quotedPostCID)) 305) 306try await repo.createPost(post, repo: myDID) 307``` 308 309### External Link Card 310 311```swift 312let post = PostRecord( 313 text: "Great article", 314 embed: .external(ExternalEmbed( 315 uri: "https://example.com/article", 316 title: "Article Title", 317 description: "A brief description of the article" 318 )) 319) 320try await repo.createPost(post, repo: myDID) 321``` 322 323## Models 324 325All models are `Codable`, `Sendable`, and most are `Identifiable` for SwiftUI compatibility. 326 327### Key Types 328 329| Type | Description | 330|------|-------------| 331| `Profile` | User profile with stats and viewer state | 332| `Timeline` | Home timeline with cursor | 333| `TimelineItem` | A post in the timeline (may include reply context) | 334| `Post` | Full post with author, record, embed, and stats | 335| `PostThread` | Thread view with replies | 336| `Notification` | Notification with reason and content | 337| `Feed` | Custom feed generator info | 338| `Follows` / `Followers` | Social graph lists | 339 340### Notification Reasons 341 342```swift 343public enum NotificationReason: Codable, Sendable, Equatable, Hashable { 344 case like 345 case repost 346 case follow 347 case mention 348 case reply 349 case quote 350 case starterpackJoined 351 case unknown(String) 352} 353``` 354 355## Thread Safety 356 357All services use `@APActor` for thread-safe access: 358 359```swift 360@APActor 361func loadTimeline() async throws { 362 let service = BskyService() // Safe to create on APActor 363 let timeline = try await service.getTimeline() 364 // Process timeline... 365} 366``` 367 368## Error Handling 369 370Errors are typed via `AtError` from CoreATProtocol: 371 372```swift 373do { 374 let profile = try await service.getProfile(for: "nonexistent.handle") 375} catch let error as AtError { 376 switch error { 377 case .message(let msg): 378 // API error (e.g., "ProfileNotFound") 379 print("Error: \(msg.error) - \(msg.message ?? "")") 380 case .network(let networkError): 381 // Network/HTTP error 382 print("Network error: \(networkError)") 383 } 384} 385``` 386 387## Testing 388 389bskyKit uses Swift Testing. Run tests with: 390 391```bash 392swift test 393``` 394 395The test suite includes: 396- Rich text detection (links, mentions, hashtags) 397- Byte index conversion 398- Model decoding 399 400## Related Packages 401 402- **[CoreATProtocol](https://tangled.org/@sparrowtek.com/CoreATProtocol)** - Core networking layer (dependency) 403 404## License 405 406This project is licensed under an [MIT license](https://tangled.org/sparrowtek.com/bskyKit/blob/main/LICENSE). 407 408## Contributing 409 410It is always a good idea to discuss before taking on a significant task. That said, I have a strong bias towards enthusiasm. If you are excited about doing something, I'll do my best to get out of your way. 411 412By participating in this project you agree to abide by the [Contributor Code of Conduct](https://tangled.org/sparrowtek.com/bskyKit/blob/main/CODE_OF_CONDUCT.md).