krz/hutch
an ios client for sourcehut
clone: git clone https://gitbay.org/krz/hutch.git
v2.9.1: Hutch/Networking/PasteService.swift · raw
1import Foundation
2
3struct PasteListPage: Decodable, Sendable {
4 let results: [Paste]
5 let cursor: String?
6}
7
8final class PasteService: Sendable {
9 private let client: SRHTClient
10
11 init(client: SRHTClient) {
12 self.client = client
13 }
14
15 private static let listQuery = """
16 query pastes($cursor: Cursor) {
17 pastes(cursor: $cursor) {
18 results {
19 id
20 created
21 visibility
22 files {
23 filename
24 hash
25 }
26 user {
27 canonicalName
28 }
29 }
30 cursor
31 }
32 }
33 """
34
35 private static let detailQuery = """
36 query paste($id: String!) {
37 paste(id: $id) {
38 id
39 created
40 visibility
41 files {
42 filename
43 hash
44 contents
45 }
46 user {
47 canonicalName
48 }
49 }
50 }
51 """
52
53 private static let createMutation = """
54 mutation createPaste($files: [Upload!]!, $visibility: Visibility!) {
55 create(files: $files, visibility: $visibility) {
56 id
57 created
58 visibility
59 files {
60 filename
61 hash
62 contents
63 }
64 user {
65 canonicalName
66 }
67 }
68 }
69 """
70
71 private static let updateMutation = """
72 mutation updatePaste($id: String!, $visibility: Visibility!) {
73 update(id: $id, visibility: $visibility) {
74 id
75 created
76 visibility
77 files {
78 filename
79 hash
80 contents
81 }
82 user {
83 canonicalName
84 }
85 }
86 }
87 """
88
89 private static let deleteMutation = """
90 mutation deletePaste($id: String!) {
91 delete(id: $id) {
92 id
93 created
94 visibility
95 files {
96 filename
97 hash
98 }
99 user {
100 canonicalName
101 }
102 }
103 }
104 """
105
106 private static let cacheKey = "paste.pastes"
107
108 func listPastes(cursor: String?, useCache: Bool) async throws -> PasteListPage {
109 let variables = cursor.map { ["cursor": $0 as any Sendable] }
110 let result: PasteListResponse
111 if useCache, cursor == nil {
112 result = try await client.executeAndCache(
113 service: .paste,
114 query: Self.listQuery,
115 variables: variables,
116 responseType: PasteListResponse.self,
117 cacheKey: Self.cacheKey
118 )
119 } else {
120 result = try await client.execute(
121 service: .paste,
122 query: Self.listQuery,
123 variables: variables,
124 responseType: PasteListResponse.self
125 )
126 }
127 return result.pastes ?? PasteListPage(results: [], cursor: nil)
128 }
129
130 func loadCachedPastes() -> PasteListPage? {
131 guard let data = client.responseCache.get(forKey: Self.cacheKey) else {
132 return nil
133 }
134
135 let decoder = JSONDecoder()
136 decoder.dateDecodingStrategy = .srhtFlexible
137 guard let response = try? decoder.decode(GraphQLResponse<PasteListResponse>.self, from: data) else {
138 return nil
139 }
140 return response.data?.pastes
141 }
142
143 func loadPaste(id: String) async throws -> Paste? {
144 let result = try await client.execute(
145 service: .paste,
146 query: Self.detailQuery,
147 variables: ["id": id],
148 responseType: PasteDetailResponse.self
149 )
150 return result.paste
151 }
152
153 func createPaste(files: [PasteUploadDraft], visibility: Visibility) async throws -> Paste {
154 let uploadFiles = normalizedUploadFiles(from: files)
155 let variables: [String: any Sendable] = [
156 "files": [String?](repeating: nil, count: uploadFiles.count),
157 "visibility": visibility.rawValue
158 ]
159
160 let result = try await client.executeMultipartFiles(
161 service: .paste,
162 query: Self.createMutation,
163 variables: variables,
164 files: uploadFiles.enumerated().map { index, file in
165 MultipartUploadFile(
166 variablePath: "files.\(index)",
167 fileData: file.data,
168 fileName: file.fileName,
169 mimeType: "text/plain"
170 )
171 },
172 responseType: CreatePasteResponse.self
173 )
174 return result.create
175 }
176
177 func updateVisibility(id: String, visibility: Visibility) async throws -> Paste? {
178 let result = try await client.execute(
179 service: .paste,
180 query: Self.updateMutation,
181 variables: ["id": id, "visibility": visibility.rawValue],
182 responseType: UpdatePasteResponse.self
183 )
184 return result.update
185 }
186
187 func deletePaste(id: String) async throws -> Paste? {
188 let result = try await client.execute(
189 service: .paste,
190 query: Self.deleteMutation,
191 variables: ["id": id],
192 responseType: DeletePasteResponse.self
193 )
194 return result.delete
195 }
196
197 func loadContents(from url: URL) async throws -> String {
198 try await client.fetchText(url: url)
199 }
200
201 private func normalizedUploadFiles(from files: [PasteUploadDraft]) -> [(fileName: String, data: Data)] {
202 files.compactMap { draft in
203 let text = draft.contents
204 guard let data = text.data(using: .utf8) else {
205 return nil
206 }
207
208 return (draft.filename, data)
209 }
210 }
211}
212
213private struct PasteListResponse: Decodable, Sendable {
214 let pastes: PasteListPage?
215}
216
217private struct PasteDetailResponse: Decodable, Sendable {
218 let paste: Paste?
219}
220
221private struct CreatePasteResponse: Decodable, Sendable {
222 let create: Paste
223}
224
225private struct UpdatePasteResponse: Decodable, Sendable {
226 let update: Paste?
227}
228
229private struct DeletePasteResponse: Decodable, Sendable {
230 let delete: Paste?
231}