···1515 case getProfile(did: String)
1616 case getProfiles(dids: [String])
1717 case getSuggestions(limit: Int, cursor: String?)
1818- case searchActors(query: String, limit: Int)
1818+ case searchActors(query: String, limit: Int, cursor: String?)
1919 case searchActorsTypeahead(query: String, limit: Int)
20202121 // Feed endpoints
···6666 case getUnreadCount
6767 case updateSeen(seenAt: Date)
68686969+ // Chat endpoints (proxied through PDS)
7070+ case chatListConvos(limit: Int, cursor: String?)
7171+ case chatGetConvo(convoId: String)
7272+ case chatGetMessages(convoId: String, limit: Int, cursor: String?)
7373+ case chatSendMessage(convoId: String, message: Data)
7474+ case chatUpdateRead(convoId: String, messageId: String?)
7575+ case chatLeaveConvo(convoId: String)
7676+ case chatMuteConvo(convoId: String)
7777+ case chatUnmuteConvo(convoId: String)
7878+ case chatGetConvoForMembers(members: [String])
7979+8080+ // Moderation
8181+ case createReport(body: Data)
8282+6983 // Generic endpoints for newer/less-common lexicons.
7084 case xrpcQuery(id: String, parameters: Parameters)
7185 case xrpcProcedure(id: String, body: Parameters?)
···124138 case .listNotifications: "/xrpc/app.bsky.notification.listNotifications"
125139 case .getUnreadCount: "/xrpc/app.bsky.notification.getUnreadCount"
126140 case .updateSeen: "/xrpc/app.bsky.notification.updateSeen"
141141+ // Chat
142142+ case .chatListConvos: "/xrpc/chat.bsky.convo.listConvos"
143143+ case .chatGetConvo: "/xrpc/chat.bsky.convo.getConvo"
144144+ case .chatGetMessages: "/xrpc/chat.bsky.convo.getMessages"
145145+ case .chatSendMessage: "/xrpc/chat.bsky.convo.sendMessage"
146146+ case .chatUpdateRead: "/xrpc/chat.bsky.convo.updateRead"
147147+ case .chatLeaveConvo: "/xrpc/chat.bsky.convo.leaveConvo"
148148+ case .chatMuteConvo: "/xrpc/chat.bsky.convo.muteConvo"
149149+ case .chatUnmuteConvo: "/xrpc/chat.bsky.convo.unmuteConvo"
150150+ case .chatGetConvoForMembers: "/xrpc/chat.bsky.convo.getConvoForMembers"
151151+ // Moderation
152152+ case .createReport: "/xrpc/com.atproto.moderation.createReport"
127153 case .xrpcQuery(let id, _), .xrpcProcedure(let id, _), .xrpcDataProcedure(let id, _, _, _):
128154 "/xrpc/\(id)"
129155 }
···135161 .getFeed, .getActorFeeds, .getFeedGenerator, .getFeedGenerators, .getListFeed, .getQuotes, .getSuggestedFeeds,
136162 .getTimeline, .getAuthorFeed, .getPostThread, .getPosts, .searchPosts, .getActorLikes, .getLikes, .getRepostedBy,
137163 .getFollows, .getFollowers, .getBlocks, .getMutes, .getRelationships,
138138- .listNotifications, .getUnreadCount:
164164+ .listNotifications, .getUnreadCount,
165165+ .chatListConvos, .chatGetConvo, .chatGetMessages, .chatGetConvoForMembers:
139166 return .get
140167 case .updateSeen, .muteActor, .unmuteActor, .muteThread, .unmuteThread, .muteActorList, .unmuteActorList,
168168+ .chatSendMessage, .chatUpdateRead, .chatLeaveConvo, .chatMuteConvo, .chatUnmuteConvo,
169169+ .createReport,
141170 .xrpcProcedure, .xrpcDataProcedure:
142171 return .post
143172 case .xrpcQuery:
···162191 if let cursor { params["cursor"] = cursor }
163192 return .requestParameters(encoding: .urlEncoding(parameters: params))
164193165165- case .searchActors(let query, let limit):
166166- return .requestParameters(encoding: .urlEncoding(parameters: [
167167- "q": query,
168168- "limit": limit
169169- ]))
194194+ case .searchActors(let query, let limit, let cursor):
195195+ var params: Parameters = ["q": query, "limit": limit]
196196+ if let cursor { params["cursor"] = cursor }
197197+ return .requestParameters(encoding: .urlEncoding(parameters: params))
170198171199 case .searchActorsTypeahead(let query, let limit):
172200 return .requestParameters(encoding: .urlEncoding(parameters: [
···318346 "seenAt": formatter.string(from: seenAt)
319347 ]))
320348349349+ // Chat endpoints
350350+ case .chatListConvos(let limit, let cursor):
351351+ var params: Parameters = ["limit": limit]
352352+ if let cursor { params["cursor"] = cursor }
353353+ return .requestParameters(encoding: .urlEncoding(parameters: params))
354354+355355+ case .chatGetConvo(let convoId):
356356+ return .requestParameters(encoding: .urlEncoding(parameters: ["convoId": convoId]))
357357+358358+ case .chatGetMessages(let convoId, let limit, let cursor):
359359+ var params: Parameters = ["convoId": convoId, "limit": limit]
360360+ if let cursor { params["cursor"] = cursor }
361361+ return .requestParameters(encoding: .urlEncoding(parameters: params))
362362+363363+ case .chatSendMessage(_, let message):
364364+ return .requestParameters(encoding: .jsonDataEncoding(data: message))
365365+366366+ case .chatUpdateRead(let convoId, let messageId):
367367+ var params: Parameters = ["convoId": convoId]
368368+ if let messageId { params["messageId"] = messageId }
369369+ return .requestParameters(encoding: .jsonEncoding(parameters: params))
370370+371371+ case .chatLeaveConvo(let convoId):
372372+ return .requestParameters(encoding: .jsonEncoding(parameters: ["convoId": convoId]))
373373+374374+ case .chatMuteConvo(let convoId):
375375+ return .requestParameters(encoding: .jsonEncoding(parameters: ["convoId": convoId]))
376376+377377+ case .chatUnmuteConvo(let convoId):
378378+ return .requestParameters(encoding: .jsonEncoding(parameters: ["convoId": convoId]))
379379+380380+ case .chatGetConvoForMembers(let members):
381381+ return .requestParameters(encoding: .urlEncoding(parameters: ["members": members]))
382382+383383+ // Moderation
384384+ case .createReport(let body):
385385+ return .requestParameters(encoding: .jsonDataEncoding(data: body))
386386+321387 case .xrpcQuery(_, let parameters):
322388 if parameters.isEmpty {
323389 return .request
···339405 var headers: HTTPHeaders = ["Content-Type": contentType]
340406 if let accept { headers["Accept"] = accept }
341407 return headers
408408+ case .chatListConvos, .chatGetConvo, .chatGetMessages, .chatSendMessage,
409409+ .chatUpdateRead, .chatLeaveConvo, .chatMuteConvo, .chatUnmuteConvo,
410410+ .chatGetConvoForMembers:
411411+ return ["atproto-proxy": "did:web:api.bsky.chat#bsky_chat"]
342412 default:
343413 return nil
344414 }
+94-2
Sources/bskyKit/BskyService.swift
···138138 /// - limit: Maximum number of results to return (default: 25, max: 100).
139139 /// - Returns: Matching user profiles with optional cursor for pagination.
140140 /// - Throws: An error if the request fails.
141141- public func searchActors(query: String, limit: Int = 25) async throws -> SearchActorsResult {
142142- try await execute(.searchActors(query: query, limit: limit))
141141+ public func searchActors(query: String, limit: Int = 25, cursor: String? = nil) async throws -> SearchActorsResult {
142142+ try await execute(.searchActors(query: query, limit: limit, cursor: cursor))
143143 }
144144145145 /// Fast search for autocomplete functionality.
···405405 /// - Throws: An error if not authenticated or the request fails.
406406 public func updateSeen(at date: Date = Date()) async throws {
407407 let _: EmptyResponse = try await execute(.updateSeen(seenAt: date))
408408+ }
409409+410410+ // MARK: - Chat
411411+412412+ /// Lists the user's conversations.
413413+ public func listConvos(limit: Int = 50, cursor: String? = nil) async throws -> ConvoListResponse {
414414+ try await execute(.chatListConvos(limit: limit, cursor: cursor))
415415+ }
416416+417417+ /// Gets a single conversation by ID.
418418+ public func getConvo(convoId: String) async throws -> GetConvoForMembersResponse {
419419+ try await execute(.chatGetConvo(convoId: convoId))
420420+ }
421421+422422+ /// Gets messages in a conversation.
423423+ public func getMessages(convoId: String, limit: Int = 50, cursor: String? = nil) async throws -> ChatMessagesResponse {
424424+ try await execute(.chatGetMessages(convoId: convoId, limit: limit, cursor: cursor))
425425+ }
426426+427427+ /// Sends a message in a conversation.
428428+ public func sendMessage(convoId: String, text: String) async throws -> ChatSendMessageResponse {
429429+ let body: [String: Any] = [
430430+ "convoId": convoId,
431431+ "message": ["text": text]
432432+ ]
433433+ let data = try JSONSerialization.data(withJSONObject: body)
434434+ return try await execute(.chatSendMessage(convoId: convoId, message: data))
435435+ }
436436+437437+ /// Marks a conversation as read.
438438+ public func updateConvoRead(convoId: String, messageId: String? = nil) async throws -> UpdateReadResponse {
439439+ try await execute(.chatUpdateRead(convoId: convoId, messageId: messageId))
440440+ }
441441+442442+ /// Leaves a conversation.
443443+ public func leaveConvo(convoId: String) async throws -> LeaveConvoResponse {
444444+ try await execute(.chatLeaveConvo(convoId: convoId))
445445+ }
446446+447447+ /// Mutes a conversation.
448448+ public func muteConvo(convoId: String) async throws -> GetConvoForMembersResponse {
449449+ try await execute(.chatMuteConvo(convoId: convoId))
450450+ }
451451+452452+ /// Unmutes a conversation.
453453+ public func unmuteConvo(convoId: String) async throws -> GetConvoForMembersResponse {
454454+ try await execute(.chatUnmuteConvo(convoId: convoId))
455455+ }
456456+457457+ /// Gets or creates a conversation with the given members.
458458+ public func getConvoForMembers(members: [String]) async throws -> GetConvoForMembersResponse {
459459+ try await execute(.chatGetConvoForMembers(members: members))
460460+ }
461461+462462+ // MARK: - Moderation
463463+464464+ /// Reports a post or user.
465465+ public func createReport(reasonType: String, reason: String?, subjectDid: String?, subjectUri: String?, subjectCid: String?) async throws -> JSONValue {
466466+ var subject: [String: Any] = [:]
467467+ if let subjectUri, let subjectCid {
468468+ subject["$type"] = "com.atproto.repo.strongRef"
469469+ subject["uri"] = subjectUri
470470+ subject["cid"] = subjectCid
471471+ } else if let subjectDid {
472472+ subject["$type"] = "com.atproto.admin.defs#repoRef"
473473+ subject["did"] = subjectDid
474474+ }
475475+476476+ var body: [String: Any] = [
477477+ "reasonType": reasonType,
478478+ "subject": subject
479479+ ]
480480+ if let reason { body["reason"] = reason }
481481+482482+ let data = try JSONSerialization.data(withJSONObject: body)
483483+ return try await execute(.createReport(body: data))
484484+ }
485485+486486+ // MARK: - Typed List Methods
487487+488488+ /// Fetches lists created by an actor with typed response.
489489+ public func getListsTyped(for actor: String, limit: Int = 50, cursor: String? = nil) async throws -> GraphListsResponse {
490490+ var params: Parameters = ["actor": actor, "limit": limit]
491491+ if let cursor { params["cursor"] = cursor }
492492+ return try await executeQuery("app.bsky.graph.getLists", parameters: params)
493493+ }
494494+495495+ /// Fetches a list and its membership with typed response.
496496+ public func getListTyped(uri: String, limit: Int = 50, cursor: String? = nil) async throws -> GraphListResponse {
497497+ var params: Parameters = ["list": uri, "limit": limit]
498498+ if let cursor { params["cursor"] = cursor }
499499+ return try await executeQuery("app.bsky.graph.getList", parameters: params)
408500 }
409501410502 // MARK: - Priority 2: Account, Bookmark, and Graph Collections
+120
Sources/bskyKit/Models/Chat.swift
···11+import Foundation
22+33+/// Response from chat.bsky.convo.listConvos
44+public struct ConvoListResponse: Codable, Sendable {
55+ public let convos: [ConvoView]
66+ public let cursor: String?
77+}
88+99+/// A conversation view
1010+public struct ConvoView: Codable, Sendable, Identifiable {
1111+ public let id: String
1212+ public let rev: String
1313+ public let members: [ChatMember]
1414+ public let lastMessage: ChatMessageUnion?
1515+ public let muted: Bool
1616+ public let unreadCount: Int
1717+}
1818+1919+/// Union type for messages
2020+public enum ChatMessageUnion: Codable, Sendable {
2121+ case message(ChatMessageView)
2222+ case deleted(DeletedMessageView)
2323+ case unknown
2424+2525+ enum CodingKeys: String, CodingKey {
2626+ case type = "$type"
2727+ }
2828+2929+ public init(from decoder: Decoder) throws {
3030+ let container = try decoder.container(keyedBy: CodingKeys.self)
3131+ let type = try container.decodeIfPresent(String.self, forKey: .type)
3232+3333+ switch type {
3434+ case "chat.bsky.convo.defs#messageView":
3535+ self = .message(try ChatMessageView(from: decoder))
3636+ case "chat.bsky.convo.defs#deletedMessageView":
3737+ self = .deleted(try DeletedMessageView(from: decoder))
3838+ default:
3939+ self = .unknown
4040+ }
4141+ }
4242+4343+ public func encode(to encoder: Encoder) throws {
4444+ switch self {
4545+ case .message(let msg):
4646+ try msg.encode(to: encoder)
4747+ case .deleted(let msg):
4848+ try msg.encode(to: encoder)
4949+ case .unknown:
5050+ var container = encoder.singleValueContainer()
5151+ try container.encode([String: String]())
5252+ }
5353+ }
5454+}
5555+5656+/// A chat message
5757+public struct ChatMessageView: Codable, Sendable, Identifiable {
5858+ public let id: String
5959+ public let rev: String
6060+ public let text: String
6161+ public let facets: [Facet]?
6262+ public let sender: ChatSender
6363+ public let sentAt: Date
6464+}
6565+6666+/// Deleted message placeholder
6767+public struct DeletedMessageView: Codable, Sendable, Identifiable {
6868+ public let id: String
6969+ public let rev: String
7070+ public let sender: ChatSender
7171+ public let sentAt: Date
7272+}
7373+7474+/// Chat message sender
7575+public struct ChatSender: Codable, Sendable {
7676+ public let did: String
7777+}
7878+7979+/// Chat member profile
8080+public struct ChatMember: Codable, Sendable, Identifiable {
8181+ public let did: String
8282+ public let handle: String
8383+ public let displayName: String?
8484+ public let avatar: String?
8585+ public let chatDisabled: Bool?
8686+8787+ public var id: String { did }
8888+}
8989+9090+/// Response from chat.bsky.convo.getMessages
9191+public struct ChatMessagesResponse: Codable, Sendable {
9292+ public let messages: [ChatMessageUnion]
9393+ public let cursor: String?
9494+}
9595+9696+/// Response from chat.bsky.convo.sendMessage
9797+public struct ChatSendMessageResponse: Codable, Sendable {
9898+ public let id: String
9999+ public let rev: String
100100+ public let text: String
101101+ public let facets: [Facet]?
102102+ public let sender: ChatSender
103103+ public let sentAt: Date
104104+}
105105+106106+/// Response from chat.bsky.convo.getConvoForMembers
107107+public struct GetConvoForMembersResponse: Codable, Sendable {
108108+ public let convo: ConvoView
109109+}
110110+111111+/// Response from chat.bsky.convo.updateRead
112112+public struct UpdateReadResponse: Codable, Sendable {
113113+ public let convo: ConvoView
114114+}
115115+116116+/// Response from chat.bsky.convo.leaveConvo
117117+public struct LeaveConvoResponse: Codable, Sendable {
118118+ public let convoId: String
119119+ public let rev: String
120120+}
+67
Sources/bskyKit/Models/Lists.swift
···11+import Foundation
22+33+/// Response from app.bsky.graph.getLists
44+public struct GraphListsResponse: Codable, Sendable {
55+ public let lists: [GraphListView]
66+ public let cursor: String?
77+}
88+99+/// Response from app.bsky.graph.getList
1010+public struct GraphListResponse: Codable, Sendable {
1111+ public let list: GraphListView
1212+ public let items: [GraphListItemView]
1313+ public let cursor: String?
1414+}
1515+1616+/// A list view
1717+public struct GraphListView: Codable, Sendable, Identifiable {
1818+ public let uri: String
1919+ public let cid: String
2020+ public let creator: Author
2121+ public let name: String
2222+ public let purpose: String
2323+ public let description: String?
2424+ public let descriptionFacets: [Facet]?
2525+ public let avatar: String?
2626+ public let listItemCount: Int?
2727+ public let indexedAt: Date?
2828+ public let viewer: ListViewer?
2929+ public let labels: [AuthorLabels]?
3030+3131+ public var id: String { uri }
3232+3333+ public var purposeLabel: String {
3434+ switch purpose {
3535+ case "app.bsky.graph.defs#curatelist":
3636+ return "Curation"
3737+ case "app.bsky.graph.defs#modlist":
3838+ return "Moderation"
3939+ case "app.bsky.graph.defs#referencelist":
4040+ return "Reference"
4141+ default:
4242+ return "List"
4343+ }
4444+ }
4545+4646+ public var isCurationList: Bool {
4747+ purpose == "app.bsky.graph.defs#curatelist"
4848+ }
4949+5050+ public var isModList: Bool {
5151+ purpose == "app.bsky.graph.defs#modlist"
5252+ }
5353+}
5454+5555+/// Viewer state for a list
5656+public struct ListViewer: Codable, Sendable {
5757+ public let muted: Bool?
5858+ public let blocked: String?
5959+}
6060+6161+/// An item (member) in a list
6262+public struct GraphListItemView: Codable, Sendable, Identifiable {
6363+ public let uri: String
6464+ public let subject: ActorProfile
6565+6666+ public var id: String { uri }
6767+}
+7
Sources/bskyKit/Models/Preferences.swift
···2828 public let type: String
2929 public let value: String
3030 public let pinned: Bool
3131+3232+ public init(id: String, type: String, value: String, pinned: Bool) {
3333+ self.id = id
3434+ self.type = type
3535+ self.value = value
3636+ self.pinned = pinned
3737+ }
3138}
32393340// MARK: - Convenience
+39
Sources/bskyKit/RepoService.swift
···201201 try await deleteRecord(repo: repo, collection: "app.bsky.graph.follow", rkey: rkey)
202202 }
203203204204+ /// Creates a list
205205+ public func createList(repo: String, name: String, purpose: String, description: String?) async throws -> CreateRecordResponse {
206206+ var record: [String: Any] = [
207207+ "$type": "app.bsky.graph.list",
208208+ "name": name,
209209+ "purpose": purpose,
210210+ "createdAt": ISO8601DateFormatter().string(from: Date())
211211+ ]
212212+ if let description { record["description"] = description }
213213+ return try await createRecord(repo: repo, collection: "app.bsky.graph.list", record: record)
214214+ }
215215+216216+ /// Deletes a list
217217+ public func deleteList(uri: String, repo: String) async throws {
218218+ guard let rkey = extractRkey(from: uri) else {
219219+ throw RepoError.invalidUri(uri)
220220+ }
221221+ try await deleteRecord(repo: repo, collection: "app.bsky.graph.list", rkey: rkey)
222222+ }
223223+224224+ /// Adds a user to a list
225225+ public func addListItem(listUri: String, subjectDid: String, repo: String) async throws -> CreateRecordResponse {
226226+ let record: [String: Any] = [
227227+ "$type": "app.bsky.graph.listitem",
228228+ "subject": subjectDid,
229229+ "list": listUri,
230230+ "createdAt": ISO8601DateFormatter().string(from: Date())
231231+ ]
232232+ return try await createRecord(repo: repo, collection: "app.bsky.graph.listitem", record: record)
233233+ }
234234+235235+ /// Removes a user from a list
236236+ public func removeListItem(uri: String, repo: String) async throws {
237237+ guard let rkey = extractRkey(from: uri) else {
238238+ throw RepoError.invalidUri(uri)
239239+ }
240240+ try await deleteRecord(repo: repo, collection: "app.bsky.graph.listitem", rkey: rkey)
241241+ }
242242+204243 /// Blocks a user
205244 public func block(did: String, repo: String) async throws -> CreateRecordResponse {
206245 let record: [String: Any] = [