🕊️

[Swift]AT ProtocolでBlueskyにポストする

2023/08/16に公開

AT Protocol・Blueskyとは

AT Protocol: 分散型SNSのプロトコル
Bluesky: AT Protocolを使用したサービス
※現在、Blueskyはprivate betaとなっており、招待コードが必要です。
https://blueskyweb.xyz
https://atproto.com

投稿の流れ

1. セッションを作成

    func createSession(identifier: String, password: String, completion: @escaping (Bool) -> Void) {
        let url = URL(string: "https://bsky.social/xrpc/com.atproto.server.createSession")!
        var request = URLRequest(url: url)
        request.httpMethod = "POST"
        request.addValue("application/json", forHTTPHeaderField: "Content-Type")

        let body: [String: Any] = ["identifier": identifier, "password": password]
        request.httpBody = try? JSONSerialization.data(withJSONObject: body, options: [])

        let task = URLSession.shared.dataTask(with: request) { data, _, error in
            guard let data = data, error == nil else {
                completion(false)
                return
            }

            if let json = try? JSONSerialization.jsonObject(with: data, options: []) as? [String: Any],
               let jwt = json["accessJwt"] as? String
            {
                self.accessJwt = jwt
                completion(true)
            } else {
                completion(false)
            }
        }
        task.resume()
    }

2. 投稿の処理

https://atproto.com/lexicons/com-atproto-repo

    func createPost(text: String, completion: @escaping (Bool) -> Void) {
        guard let jwt = accessJwt else {
            completion(false)
            return
        }

        let url = URL(string: "https://bsky.social/xrpc/com.atproto.repo.createRecord")!
        var request = URLRequest(url: url)
        request.httpMethod = "POST"
        request.addValue("application/json", forHTTPHeaderField: "Content-Type")
        request.addValue("Bearer \(jwt)", forHTTPHeaderField: "Authorization")

        let now = ISO8601DateFormatter().string(from: Date())
        let post: [String: Any] = ["$type": "app.bsky.feed.post", "text": text, "createdAt": now]
        let body: [String: Any] = ["repo": identifier, "collection": "app.bsky.feed.post", "record": post]
        request.httpBody = try? JSONSerialization.data(withJSONObject: body, options: [])

        let task = URLSession.shared.dataTask(with: request) { data, _, error in
            guard data != nil, error == nil else {
                completion(false)
                return
            }
            completion(true)
        }
        task.resume()
    }

3. 投稿

import SwiftUI

struct ContentView: View {
    @EnvironmentObject var atProtocolManager: ATProtocolManager
    @State var identifier = "ryodeveloper.bsky.social"
    @State var password = "パスワード"
    @State var text = ""

    var body: some View {
        NavigationView {
            Form {
                Section("Settings") {
                    TextField("identifier", text: $identifier)
                    TextField("password", text: $password)
                }
                Section {
                    TextEditor(text: $text)
                        .frame(minHeight: 240)
                } header: {
                    Text("Content")
                } footer: {
                    Text("\(text.count) / 300")
                        .foregroundColor(text.count > 300 ? .red : Color(.secondaryLabel))
                        .frame(maxWidth: .infinity, alignment: .trailing)
                }
            }
            .navigationTitle("ATProtocolClient")
            .navigationBarTitleDisplayMode(.inline)
            .toolbar {
                ToolbarItem(placement: .navigationBarTrailing) {
                    Button("Post") {
                        atProtocolManager.createSession(identifier: identifier, password: password) { _ in
                            
                        }
                        atProtocolManager.createPost(text: text) { _ in
                            
                        }
                    }
                        .disabled(text.isEmpty || text.count > 300)
                }
            }
        }
    }
}

Discussion