Model+TalDir.swift (17402B)
1 /* 2 * This file is part of GNU Taler, ©2022-26 Taler Systems S.A. 3 * See LICENSE.md 4 */ 5 /** 6 * @author Marc Stibane 7 */ 8 import Foundation 9 import AnyCodable 10 import taler_swift 11 import SymLog 12 13 let TALDIR = "https://taldir.gnunet.org" 14 let MAILBOX = "https://mailbox.gnunet.org" 15 16 // MARK: - Contacts 17 struct ContactEntry: Codable, Hashable { 18 var alias: String 19 var aliasType: String // TaldirAliasType.name 20 var mailboxBaseUrl: String // MAILBOX 21 var mailboxAddress: String // MailboxConfiguration.hAddress 22 var source: String // source of this contact, may be a URI 23 var petname: String // local nickname 24 var expiration: Timestamp? 25 var isSelf: Bool? // is this my own alias? TODO: show user if mailboxAddress is not mine 26 } 27 extension ContactEntry: Identifiable { 28 var id: String { alias } 29 } 30 extension ContactEntry { 31 init(alias: String, // convenience init 32 aliasType: String, 33 mailboxAddress: String, 34 source: String = TALDIR, 35 petname: String = EMPTYSTRING, 36 expiration: Timestamp? = nil, 37 isSelf: Bool = false 38 ) { 39 self.alias = alias 40 self.aliasType = aliasType 41 self.mailboxBaseUrl = MAILBOX 42 self.mailboxAddress = mailboxAddress 43 self.source = source 44 self.petname = petname 45 self.expiration = expiration 46 self.isSelf = isSelf 47 } 48 } 49 fileprivate struct ContactListResponse: Codable, Hashable { 50 var contacts: [ContactEntry] 51 } 52 /// A request to get a list of contacts for TalDir. 53 fileprivate struct GetContacts: WalletBackendFormattedRequest { 54 var operation: String { "getContacts" } 55 struct Args: Encodable { } 56 func args() -> Args { Args() } 57 typealias Response = ContactListResponse 58 } 59 /// A request to add a contact 60 fileprivate struct AddContact: WalletBackendFormattedRequest { 61 var contact: ContactEntry 62 var operation: String { "addContact" } 63 struct Args: Encodable { 64 var contact: ContactEntry 65 } 66 func args() -> Args { Args(contact: contact) } 67 struct Response: Decodable {} 68 } 69 // MARK: - Contact calls 70 extension WalletModel { 71 nonisolated func getContacts(viewHandles: Bool = false) 72 async throws -> [ContactEntry] { // M for MainActor 73 let request = GetContacts() 74 // let controller = Controller.shared 75 // controller.progressOperation = request.operation 76 // controller.progressToken = request.operation 77 let (response, _) = try await sendRequest(request, viewHandles: viewHandles) 78 return response.contacts 79 } 80 nonisolated func addContact(_ contact: ContactEntry, viewHandles: Bool = false) 81 async throws { // M for MainActor 82 let request = AddContact(contact: contact) 83 // let controller = Controller.shared 84 // controller.progressOperation = request.operation 85 // controller.progressToken = request.operation 86 let _ = try await sendRequest(request, viewHandles: viewHandles) 87 } 88 } 89 // MARK: - Alias Types 90 struct TaldirAliasType: Codable, Hashable { 91 var name: String // use this for API calls 92 var display: String 93 var challengeFee: Amount? 94 95 var localized: String { // show this in UI (localize Phone & Email) 96 if name.lowercased() == "phone" { 97 return String(localized: "Phone", comment: "Alias type") 98 } 99 if name.lowercased() == "email" { 100 return String(localized: "E-mail", comment: "Alias type") 101 } 102 return display 103 } 104 105 enum CodingKeys: String, CodingKey { 106 case name, display 107 case challengeFee = "challenge_fee" 108 } 109 } 110 extension TaldirAliasType: Identifiable { 111 var id: String { name } 112 } 113 fileprivate struct TaldirAliasTypesResponse: Codable, Hashable { 114 // var version: String 115 // var name: String 116 var supportedAliases: [TaldirAliasType] 117 // var monthlyFee: Amount 118 } 119 120 /// A request to get a list of supported alias types for TalDir. 121 fileprivate struct GetSupportedAliasTypes: WalletBackendFormattedRequest { 122 var operation: String { "getSupportedAliasTypes" } 123 struct Args: Encodable { 124 let taldirBaseUrl: String 125 let progressToken: String? 126 } 127 func args() -> Args { Args(taldirBaseUrl: TALDIR, 128 progressToken: operation) } // + baseUrl 129 typealias Response = TaldirAliasTypesResponse 130 } 131 // MARK: - Alias Lookup 132 struct TaldirLookup: Codable, Hashable { 133 var alias: String 134 var aliasType: String // TaldirAliasType.name 135 // var taldirBaseUrl: String // TALDIR 136 } 137 struct TaldirLookupResponse: Codable { 138 var targetUri: String? // only if successful 139 var validFor: Duration? // only if known 140 } 141 142 /// A request to lookup an alias using TalDir. 143 fileprivate struct LookupAlias: WalletBackendFormattedRequest { 144 let lookup: TaldirLookup 145 var operation: String { "lookupAlias" } 146 struct Args: Encodable { 147 let alias: String 148 let aliasType: String 149 let taldirBaseUrl: String 150 } 151 func args() -> Args { Args(alias: lookup.alias, 152 aliasType: lookup.aliasType, 153 taldirBaseUrl: TALDIR) } 154 typealias Response = TaldirLookupResponse 155 } 156 // MARK: - Alias Registration 157 struct TaldirRegistration: Codable, Hashable { 158 var alias: String 159 var aliasType: String // TaldirAliasType.name 160 var taldirBaseUrl: String // TALDIR 161 var mailboxBaseUrl: String? // MailboxConfiguration.mailboxBaseUrl - use nil to de-register 162 var mailboxAddress: String? // MailboxConfiguration.hAddress - nil to de-register 163 var duration: Duration? // only for registerAlias (or renew). Set to 0 to update other data 164 var challenge: String? // only for completeRegisterAlias 165 166 init(alias: String, aliasType: String, mailboxAddress: String? = nil, 167 duration: Duration? = nil, challenge: String? = nil 168 ) { 169 self.alias = alias 170 self.aliasType = aliasType 171 self.taldirBaseUrl = TALDIR 172 self.mailboxBaseUrl = mailboxAddress != nil ? MAILBOX : nil 173 self.mailboxAddress = mailboxAddress 174 self.duration = duration 175 self.challenge = challenge 176 } 177 } 178 fileprivate struct TaldirRegistrationResponse: Codable { 179 var validFor: Duration? // only if called with duration of 0 180 181 enum CodingKeys: String, CodingKey { 182 case validFor = "valid_for" 183 } 184 } 185 /// A request to register an alias for TalDir. 186 fileprivate struct RegisterAlias: WalletBackendFormattedRequest { 187 let register: TaldirRegistration 188 var operation: String { "registerAlias" } 189 struct Args: Encodable { 190 let alias: String 191 let aliasType: String 192 let taldirBaseUrl: String // TALDIR 193 let mailboxBaseUrl: String? // MAILBOX 194 let mailboxAddress: String? 195 let duration: Duration 196 } 197 func args() -> Args { Args(alias: register.alias, 198 aliasType: register.aliasType, 199 taldirBaseUrl: TALDIR, 200 mailboxBaseUrl: register.mailboxBaseUrl, 201 mailboxAddress: register.mailboxAddress, 202 duration: register.duration!) } 203 typealias Response = TaldirRegistrationResponse 204 } 205 206 /// A request to confirm the registration with the alias-challenge 207 fileprivate struct CompleteRegisterAlias: WalletBackendFormattedRequest { 208 let register: TaldirRegistration 209 var operation: String { "completeRegisterAlias" } 210 struct Args: Encodable { 211 let alias: String 212 let aliasType: String 213 let taldirBaseUrl: String // TALDIR 214 let mailboxBaseUrl: String? // MAILBOX 215 let mailboxAddress: String? 216 let challenge: String 217 } 218 func args() -> Args { Args(alias: register.alias, 219 aliasType: register.aliasType, 220 taldirBaseUrl: TALDIR, 221 mailboxBaseUrl: register.mailboxBaseUrl, 222 mailboxAddress: register.mailboxAddress, 223 challenge: register.challenge!) } 224 struct Response: Decodable {} 225 } 226 // MARK: - Alias calls 227 extension WalletModel { 228 nonisolated func getSupportedAliasTypes(viewHandles: Bool = false) 229 async throws -> [TaldirAliasType] { // M for MainActor 230 let request = GetSupportedAliasTypes() 231 let controller = Controller.shared 232 controller.progressOperation = request.operation 233 controller.progressToken = request.operation 234 let (response, _) = try await sendRequest(request, viewHandles: viewHandles) 235 return response.supportedAliases.sorted(by: { 236 if $0.name == "email" { return true } // first email 237 if $1.name == "email" { return false } 238 if $0.name == "phone" { return true } // then phone 239 if $1.name == "phone" { return false } 240 return $0.name < $1.name // the rest alphabetically 241 }) 242 } 243 244 nonisolated func lookupAlias(_ lookup: TaldirLookup, viewHandles: Bool = false) 245 async throws -> TaldirLookupResponse { // M for MainActor 246 let request = LookupAlias(lookup: lookup) 247 let (response, _) = try await sendRequest(request, viewHandles: viewHandles) 248 return response 249 } 250 251 nonisolated func registerAlias(_ register: TaldirRegistration, viewHandles: Bool = false) 252 async throws -> Duration? { // M for MainActor 253 if register.duration != nil { 254 let request = RegisterAlias(register: register) 255 let (response, _) = try await sendRequest(request, viewHandles: viewHandles) 256 return response.validFor 257 } 258 throw WalletBackendError.wrongParameters // no duration 259 } 260 261 nonisolated func completeRegisterAlias(_ register: TaldirRegistration, viewHandles: Bool = false) 262 async throws { // M for MainActor 263 if register.challenge != nil { 264 let request = CompleteRegisterAlias(register: register) 265 let (response, _) = try await sendRequest(request, viewHandles: viewHandles) 266 print("completeRegisterAlias: \(response)") 267 return 268 } 269 throw WalletBackendError.wrongParameters // no challenge 270 } 271 } 272 // MARK: - Mailbox 273 struct MailboxConfiguration: Codable, Hashable { 274 var mailboxBaseUrl: String // MAILBOX 275 var privateKey: String // Private EdDSA signing key of the mailbox 276 var privateEncryptionKey: String // Private HPKE encryption key of the mailbox 277 var expiration: Timestamp // Expiration of the current mailbox registration 278 var hAddress: String // Mailbox address (hash of the public signing key) 279 var payUri: URL? // only if mailbox service requires payment to complete the registration 280 } 281 fileprivate struct GetMailboxResponse: Codable, Hashable { 282 var mailboxConfiguration: MailboxConfiguration? 283 } 284 285 /// A request to get the mailbox configuration - if it exists. Otherwise initialize... 286 /// no network traffic => no progressToken 287 fileprivate struct GetMailbox: WalletBackendFormattedRequest { 288 let mailboxBaseUrl: String 289 var operation: String { "getMailbox" } 290 struct Args: Encodable { 291 let mailboxBaseUrl: String 292 } 293 func args() -> Args { Args(mailboxBaseUrl: mailboxBaseUrl) } 294 typealias Response = GetMailboxResponse 295 } 296 /// A request to initialize a mailbox for TalDir. 297 fileprivate struct InitializeMailbox: WalletBackendFormattedRequest { 298 let mailboxBaseUrl: String 299 var operation: String { "initializeMailbox" } 300 struct Args: Encodable { 301 let mailboxBaseUrl: String 302 let progressToken: String? 303 } 304 func args() -> Args { Args(mailboxBaseUrl: mailboxBaseUrl, 305 progressToken: operation + mailboxBaseUrl) } 306 typealias Response = GetMailboxResponse 307 } 308 // MARK: - Messages 309 struct MailboxMessage: Codable, Hashable { 310 var originMailboxBaseUrl: String // Origin mailbox 311 var downloadedAt: Timestamp // Time of download 312 var talerUri: String 313 var subjectHint: String? 314 } 315 extension MailboxMessage: Identifiable { 316 var id: String { talerUri } 317 } 318 struct MailboxMessagesResponse: Codable, Hashable { 319 var messages: [MailboxMessage] 320 var mailboxExpiration: Timestamp? 321 } 322 323 /// A request to refresh the mailbox / download messages 324 fileprivate struct RefreshMailbox: WalletBackendFormattedRequest { 325 let configuration: MailboxConfiguration 326 var operation: String { "refreshMailbox" } 327 struct Args: Encodable { 328 let mailboxConf: MailboxConfiguration 329 let progressToken: String? 330 } 331 func args() -> Args { Args(mailboxConf: configuration, 332 progressToken: operation + configuration.mailboxBaseUrl) } 333 typealias Response = MailboxMessagesResponse 334 } 335 /// A request to get the cached messages array of the mailbox. 336 fileprivate struct GetMailboxMessages: WalletBackendFormattedRequest { 337 var operation: String { "getMailboxMessages" } 338 struct Args: Encodable {} // no arguments needed 339 func args() -> Args { Args() } 340 typealias Response = MailboxMessagesResponse 341 } 342 343 /// A request to add a new message to the mailbox. 344 fileprivate struct UpsertMailboxMessage: WalletBackendFormattedRequest { 345 let message: MailboxMessage 346 var operation: String { "upsertMailboxMessage" } 347 struct Args: Encodable { 348 let message: MailboxMessage 349 } 350 func args() -> Args { Args(message: message) } 351 struct Response: Decodable {} 352 } 353 354 /// A request to delete a message from the mailbox. 355 fileprivate struct DeleteMailboxMessage: WalletBackendFormattedRequest { 356 let message: MailboxMessage 357 var operation: String { "deleteMailboxMessage" } 358 struct Args: Encodable { 359 let message: MailboxMessage 360 } 361 func args() -> Args { Args(message: message) } 362 struct Response: Decodable {} 363 } 364 365 /// A request to send a message from the mailbox. 366 fileprivate struct SendTalerUriMailboxMessage: WalletBackendFormattedRequest { 367 let contact: ContactEntry 368 let talerUri: String 369 var operation: String { "sendTalerUriMailboxMessage" } 370 struct Args: Encodable { 371 let contact: ContactEntry 372 let talerUri: String 373 } 374 func args() -> Args { Args(contact: contact, talerUri: talerUri) } 375 struct Response: Decodable {} 376 } 377 // MARK: - Mailbox calls 378 extension WalletModel { 379 nonisolated func getMailbox(_ mailboxBaseUrl: String = MAILBOX, viewHandles: Bool = false) 380 async throws -> MailboxConfiguration? { // M for MainActor 381 let request = GetMailbox(mailboxBaseUrl: mailboxBaseUrl) 382 let (response, _) = try await sendRequest(request, viewHandles: viewHandles) 383 return response.mailboxConfiguration 384 } 385 nonisolated func initializeMailbox(_ mailboxBaseUrl: String = MAILBOX, viewHandles: Bool = false) 386 async throws -> MailboxConfiguration? { // M for MainActor 387 let request = InitializeMailbox(mailboxBaseUrl: mailboxBaseUrl) 388 let controller = Controller.shared 389 controller.progressOperation = request.operation 390 controller.progressToken = request.operation + mailboxBaseUrl 391 let (response, _) = try await sendRequest(request, viewHandles: viewHandles) 392 return response.mailboxConfiguration 393 } 394 nonisolated func refreshMailbox(_ configuration: MailboxConfiguration, viewHandles: Bool = false) 395 async throws -> MailboxMessagesResponse { // M for MainActor 396 let request = RefreshMailbox(configuration: configuration) 397 let controller = Controller.shared 398 controller.progressOperation = request.operation 399 controller.progressToken = request.operation + configuration.mailboxBaseUrl 400 let (response, _) = try await sendRequest(request, viewHandles: viewHandles) 401 return response 402 } 403 nonisolated func getMailboxMessages(viewHandles: Bool = false) 404 async throws -> MailboxMessagesResponse { // M for MainActor 405 let request = GetMailboxMessages() 406 let (response, _) = try await sendRequest(request, viewHandles: viewHandles) 407 return response 408 } 409 nonisolated func upsertMailboxMessage(message: MailboxMessage, viewHandles: Bool = false) 410 async throws { // M for MainActor 411 let request = UpsertMailboxMessage(message: message) 412 let _ = try await sendRequest(request, viewHandles: viewHandles) 413 } 414 nonisolated func deleteMailboxMessage(_ message: MailboxMessage, viewHandles: Bool = false) 415 async throws { // M for MainActor 416 let request = DeleteMailboxMessage(message: message) 417 let _ = try await sendRequest(request, viewHandles: viewHandles) 418 } 419 nonisolated func sendTalerUriMailboxMessage(_ talerUri: String, 420 to contact: ContactEntry, viewHandles: Bool = false) 421 async throws { // M for MainActor 422 let request = SendTalerUriMailboxMessage(contact: contact, 423 talerUri: talerUri) 424 let _ = try await sendRequest(request, viewHandles: viewHandles) 425 } 426 }