74e03c4e10
Two-target Xcode project (xcodegen spec). The GiteaIssue container app holds the base URL + personal access token in a shared keychain group; the GiteaIssueShare extension reads them, surfaces a repo picker (with recents) and a title/notes form, then creates the issue, uploads the screenshot as an asset, and patches the body to embed it inline. Min iOS 26.0, signing team V3PF3M6B6U. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
100 lines
4.4 KiB
Swift
100 lines
4.4 KiB
Swift
import Foundation
|
|
|
|
struct GiteaClient {
|
|
let baseURL: URL
|
|
let token: String
|
|
|
|
init() throws {
|
|
guard AppSettings.isConfigured else { throw GiteaError.notConfigured }
|
|
guard let url = URL(string: AppSettings.baseURL) else { throw GiteaError.invalidBaseURL }
|
|
self.baseURL = url
|
|
self.token = AppSettings.token
|
|
}
|
|
|
|
func currentUser() async throws -> GiteaUser {
|
|
let req = makeRequest(path: "/api/v1/user")
|
|
let (data, resp) = try await URLSession.shared.data(for: req)
|
|
try ensureOK(resp, data: data)
|
|
return try decode(data)
|
|
}
|
|
|
|
func searchRepos() async throws -> [GiteaRepo] {
|
|
var components = URLComponents(url: baseURL.appendingPathComponent("/api/v1/repos/search"), resolvingAgainstBaseURL: false)!
|
|
components.queryItems = [
|
|
URLQueryItem(name: "limit", value: "50"),
|
|
URLQueryItem(name: "sort", value: "updated"),
|
|
URLQueryItem(name: "order", value: "desc"),
|
|
]
|
|
var req = URLRequest(url: components.url!)
|
|
req.setValue("token \(token)", forHTTPHeaderField: "Authorization")
|
|
let (data, resp) = try await URLSession.shared.data(for: req)
|
|
try ensureOK(resp, data: data)
|
|
let decoder = JSONDecoder()
|
|
decoder.dateDecodingStrategy = .iso8601
|
|
let result = try decoder.decode(GiteaRepoSearch.self, from: data)
|
|
return result.data
|
|
}
|
|
|
|
func createIssue(owner: String, repo: String, title: String, body: String) async throws -> GiteaIssue {
|
|
let payload = ["title": title, "body": body]
|
|
var req = makeRequest(path: "/api/v1/repos/\(owner)/\(repo)/issues", method: "POST")
|
|
req.setValue("application/json", forHTTPHeaderField: "Content-Type")
|
|
req.httpBody = try JSONSerialization.data(withJSONObject: payload)
|
|
let (data, resp) = try await URLSession.shared.data(for: req)
|
|
try ensureOK(resp, data: data)
|
|
return try decode(data)
|
|
}
|
|
|
|
func uploadAsset(owner: String, repo: String, issueNumber: Int, image: Data, filename: String) async throws -> GiteaIssueAsset {
|
|
let boundary = "Boundary-\(UUID().uuidString)"
|
|
var req = makeRequest(path: "/api/v1/repos/\(owner)/\(repo)/issues/\(issueNumber)/assets", method: "POST")
|
|
req.setValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type")
|
|
|
|
var body = Data()
|
|
body.append("--\(boundary)\r\n".data(using: .utf8)!)
|
|
body.append("Content-Disposition: form-data; name=\"attachment\"; filename=\"\(filename)\"\r\n".data(using: .utf8)!)
|
|
body.append("Content-Type: image/png\r\n\r\n".data(using: .utf8)!)
|
|
body.append(image)
|
|
body.append("\r\n--\(boundary)--\r\n".data(using: .utf8)!)
|
|
req.httpBody = body
|
|
|
|
let (data, resp) = try await URLSession.shared.data(for: req)
|
|
try ensureOK(resp, data: data)
|
|
return try decode(data)
|
|
}
|
|
|
|
func updateIssueBody(owner: String, repo: String, issueNumber: Int, body: String) async throws {
|
|
let payload = ["body": body]
|
|
var req = makeRequest(path: "/api/v1/repos/\(owner)/\(repo)/issues/\(issueNumber)", method: "PATCH")
|
|
req.setValue("application/json", forHTTPHeaderField: "Content-Type")
|
|
req.httpBody = try JSONSerialization.data(withJSONObject: payload)
|
|
let (data, resp) = try await URLSession.shared.data(for: req)
|
|
try ensureOK(resp, data: data)
|
|
}
|
|
|
|
private func makeRequest(path: String, method: String = "GET") -> URLRequest {
|
|
var req = URLRequest(url: baseURL.appendingPathComponent(path))
|
|
req.httpMethod = method
|
|
req.setValue("token \(token)", forHTTPHeaderField: "Authorization")
|
|
req.setValue("application/json", forHTTPHeaderField: "Accept")
|
|
return req
|
|
}
|
|
|
|
private func ensureOK(_ resp: URLResponse, data: Data) throws {
|
|
guard let http = resp as? HTTPURLResponse else { return }
|
|
if (200..<300).contains(http.statusCode) { return }
|
|
let body = String(data: data, encoding: .utf8) ?? ""
|
|
throw GiteaError.http(http.statusCode, body)
|
|
}
|
|
|
|
private func decode<T: Decodable>(_ data: Data) throws -> T {
|
|
do {
|
|
let decoder = JSONDecoder()
|
|
decoder.dateDecodingStrategy = .iso8601
|
|
return try decoder.decode(T.self, from: data)
|
|
} catch {
|
|
throw GiteaError.decoding(String(describing: error))
|
|
}
|
|
}
|
|
}
|