this repo has no description
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).