taler-ios

iOS apps for GNU Taler (wallet)
Log | Files | Refs | README | LICENSE

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 }