2022-01-29 11:10:04 +00:00
|
|
|
//
|
|
|
|
// ChatAPI.swift
|
|
|
|
// SimpleX
|
|
|
|
//
|
|
|
|
// Created by Evgeny Poberezkin on 27/01/2022.
|
|
|
|
// Copyright © 2022 SimpleX Chat. All rights reserved.
|
|
|
|
//
|
|
|
|
|
|
|
|
import Foundation
|
2022-01-29 23:37:02 +00:00
|
|
|
import UIKit
|
2022-01-29 11:10:04 +00:00
|
|
|
|
|
|
|
private var chatStore: chat_store?
|
|
|
|
private var chatController: chat_ctrl?
|
2022-01-29 23:37:02 +00:00
|
|
|
private let jsonDecoder = getJSONDecoder()
|
|
|
|
private let jsonEncoder = getJSONEncoder()
|
2022-01-29 11:10:04 +00:00
|
|
|
|
|
|
|
enum ChatCommand {
|
|
|
|
case apiGetChats
|
2022-01-30 18:27:20 +00:00
|
|
|
case apiGetChat(type: ChatType, id: Int64)
|
|
|
|
case apiSendMessage(type: ChatType, id: Int64, msg: MsgContent)
|
|
|
|
case addContact
|
|
|
|
case connect(connReq: String)
|
2022-01-31 21:28:07 +00:00
|
|
|
case apiDeleteChat(type: ChatType, id: Int64)
|
|
|
|
case apiUpdateProfile(profile: Profile)
|
2022-02-01 17:34:06 +00:00
|
|
|
case createMyAddress
|
|
|
|
case deleteMyAddress
|
|
|
|
case showMyAddress
|
|
|
|
case apiAcceptContact(contactReqId: Int64)
|
|
|
|
case apiRejectContact(contactReqId: Int64)
|
2022-01-29 11:10:04 +00:00
|
|
|
case string(String)
|
|
|
|
|
|
|
|
var cmdString: String {
|
|
|
|
get {
|
|
|
|
switch self {
|
|
|
|
case .apiGetChats:
|
2022-02-01 17:34:06 +00:00
|
|
|
return "/_get chats"
|
2022-01-30 18:27:20 +00:00
|
|
|
case let .apiGetChat(type, id):
|
2022-02-01 17:34:06 +00:00
|
|
|
return "/_get chat \(type.rawValue)\(id) count=500"
|
2022-01-30 18:27:20 +00:00
|
|
|
case let .apiSendMessage(type, id, mc):
|
2022-02-01 17:34:06 +00:00
|
|
|
return "/_send \(type.rawValue)\(id) \(mc.cmdString)"
|
2022-01-30 18:27:20 +00:00
|
|
|
case .addContact:
|
2022-02-01 17:34:06 +00:00
|
|
|
return "/connect"
|
2022-01-30 18:27:20 +00:00
|
|
|
case let .connect(connReq):
|
2022-02-01 17:34:06 +00:00
|
|
|
return "/connect \(connReq)"
|
2022-01-31 21:28:07 +00:00
|
|
|
case let .apiDeleteChat(type, id):
|
2022-02-01 17:34:06 +00:00
|
|
|
return "/_delete \(type.rawValue)\(id)"
|
2022-01-31 21:28:07 +00:00
|
|
|
case let .apiUpdateProfile(profile):
|
2022-02-01 17:34:06 +00:00
|
|
|
return "/profile \(profile.displayName) \(profile.fullName)"
|
|
|
|
case .createMyAddress:
|
|
|
|
return "/address"
|
|
|
|
case .deleteMyAddress:
|
|
|
|
return "/delete_address"
|
|
|
|
case .showMyAddress:
|
|
|
|
return "/show_address"
|
|
|
|
case let .apiAcceptContact(contactReqId):
|
|
|
|
return "/_accept \(contactReqId)"
|
|
|
|
case let .apiRejectContact(contactReqId):
|
|
|
|
return "/_reject \(contactReqId)"
|
2022-01-29 11:10:04 +00:00
|
|
|
case let .string(str):
|
|
|
|
return str
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-01-29 23:37:02 +00:00
|
|
|
struct APIResponse: Decodable {
|
2022-01-29 11:10:04 +00:00
|
|
|
var resp: ChatResponse
|
|
|
|
}
|
|
|
|
|
2022-01-29 23:37:02 +00:00
|
|
|
enum ChatResponse: Decodable, Error {
|
2022-01-29 11:10:04 +00:00
|
|
|
case response(type: String, json: String)
|
2022-02-02 12:51:39 +00:00
|
|
|
case apiChats(chats: [ChatData])
|
|
|
|
case apiChat(chat: ChatData)
|
2022-01-30 18:27:20 +00:00
|
|
|
case invitation(connReqInvitation: String)
|
|
|
|
case sentConfirmation
|
|
|
|
case sentInvitation
|
2022-01-31 21:28:07 +00:00
|
|
|
case contactDeleted(contact: Contact)
|
|
|
|
case userProfileNoChange
|
|
|
|
case userProfileUpdated(fromProfile: Profile, toProfile: Profile)
|
2022-02-01 17:34:06 +00:00
|
|
|
case userContactLink(connReqContact: String)
|
|
|
|
case userContactLinkCreated(connReqContact: String)
|
|
|
|
case userContactLinkDeleted
|
2022-01-29 11:10:04 +00:00
|
|
|
case contactConnected(contact: Contact)
|
2022-02-01 17:34:06 +00:00
|
|
|
case receivedContactRequest(contactRequest: UserContactRequest)
|
|
|
|
case acceptingContactRequest(contact: Contact)
|
|
|
|
case contactRequestRejected
|
2022-01-29 23:37:02 +00:00
|
|
|
case newChatItem(chatItem: AChatItem)
|
2022-02-01 17:34:06 +00:00
|
|
|
case chatCmdError(chatError: ChatError)
|
2022-01-29 11:10:04 +00:00
|
|
|
|
|
|
|
var responseType: String {
|
|
|
|
get {
|
|
|
|
switch self {
|
|
|
|
case let .response(type, _): return "* \(type)"
|
2022-01-29 23:37:02 +00:00
|
|
|
case .apiChats: return "apiChats"
|
2022-01-30 18:27:20 +00:00
|
|
|
case .apiChat: return "apiChat"
|
|
|
|
case .invitation: return "invitation"
|
|
|
|
case .sentConfirmation: return "sentConfirmation"
|
|
|
|
case .sentInvitation: return "sentInvitation"
|
2022-01-31 21:28:07 +00:00
|
|
|
case .contactDeleted: return "contactDeleted"
|
|
|
|
case .userProfileNoChange: return "userProfileNoChange"
|
|
|
|
case .userProfileUpdated: return "userProfileNoChange"
|
2022-02-01 17:34:06 +00:00
|
|
|
case .userContactLink: return "userContactLink"
|
|
|
|
case .userContactLinkCreated: return "userContactLinkCreated"
|
|
|
|
case .userContactLinkDeleted: return "userContactLinkDeleted"
|
2022-01-29 23:37:02 +00:00
|
|
|
case .contactConnected: return "contactConnected"
|
2022-02-01 17:34:06 +00:00
|
|
|
case .receivedContactRequest: return "receivedContactRequest"
|
|
|
|
case .acceptingContactRequest: return "acceptingContactRequest"
|
|
|
|
case .contactRequestRejected: return "contactRequestRejected"
|
2022-01-29 23:37:02 +00:00
|
|
|
case .newChatItem: return "newChatItem"
|
2022-02-01 17:34:06 +00:00
|
|
|
case .chatCmdError: return "chatCmdError"
|
2022-01-29 11:10:04 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
var details: String {
|
|
|
|
get {
|
|
|
|
switch self {
|
|
|
|
case let .response(_, json): return json
|
|
|
|
case let .apiChats(chats): return String(describing: chats)
|
2022-01-30 18:27:20 +00:00
|
|
|
case let .apiChat(chat): return String(describing: chat)
|
|
|
|
case let .invitation(connReqInvitation): return connReqInvitation
|
2022-02-01 17:34:06 +00:00
|
|
|
case .sentConfirmation: return noDetails
|
|
|
|
case .sentInvitation: return noDetails
|
2022-01-31 21:28:07 +00:00
|
|
|
case let .contactDeleted(contact): return String(describing: contact)
|
2022-02-01 17:34:06 +00:00
|
|
|
case .userProfileNoChange: return noDetails
|
2022-01-31 21:28:07 +00:00
|
|
|
case let .userProfileUpdated(_, toProfile): return String(describing: toProfile)
|
2022-02-01 17:34:06 +00:00
|
|
|
case let .userContactLink(connReq): return connReq
|
|
|
|
case let .userContactLinkCreated(connReq): return connReq
|
|
|
|
case .userContactLinkDeleted: return noDetails
|
2022-01-29 11:10:04 +00:00
|
|
|
case let .contactConnected(contact): return String(describing: contact)
|
2022-02-01 17:34:06 +00:00
|
|
|
case let .receivedContactRequest(contactRequest): return String(describing: contactRequest)
|
|
|
|
case let .acceptingContactRequest(contact): return String(describing: contact)
|
|
|
|
case .contactRequestRejected: return noDetails
|
2022-01-29 23:37:02 +00:00
|
|
|
case let .newChatItem(chatItem): return String(describing: chatItem)
|
2022-02-01 17:34:06 +00:00
|
|
|
case let .chatCmdError(chatError): return String(describing: chatError)
|
2022-01-29 23:37:02 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2022-02-01 17:34:06 +00:00
|
|
|
|
|
|
|
private var noDetails: String { get { "\(responseType): no details" } }
|
2022-01-29 23:37:02 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
enum TerminalItem: Identifiable {
|
|
|
|
case cmd(Date, ChatCommand)
|
|
|
|
case resp(Date, ChatResponse)
|
|
|
|
|
|
|
|
var id: Date {
|
|
|
|
get {
|
|
|
|
switch self {
|
|
|
|
case let .cmd(id, _): return id
|
|
|
|
case let .resp(id, _): return id
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
var label: String {
|
|
|
|
get {
|
|
|
|
switch self {
|
|
|
|
case let .cmd(_, cmd): return "> \(cmd.cmdString.prefix(30))"
|
|
|
|
case let .resp(_, resp): return "< \(resp.responseType)"
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
var details: String {
|
|
|
|
get {
|
|
|
|
switch self {
|
|
|
|
case let .cmd(_, cmd): return cmd.cmdString
|
|
|
|
case let .resp(_, resp): return resp.details
|
2022-01-29 11:10:04 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func chatGetUser() -> User? {
|
|
|
|
let store = getStore()
|
|
|
|
print("chatGetUser")
|
|
|
|
let r: UserResponse? = decodeCJSON(chat_get_user(store))
|
|
|
|
let user = r?.user
|
|
|
|
if user != nil { initChatCtrl(store) }
|
|
|
|
print("user", user as Any)
|
|
|
|
return user
|
|
|
|
}
|
|
|
|
|
|
|
|
func chatCreateUser(_ p: Profile) -> User? {
|
|
|
|
let store = getStore()
|
|
|
|
print("chatCreateUser")
|
|
|
|
var str = encodeCJSON(p)
|
|
|
|
chat_create_user(store, &str)
|
|
|
|
let user = chatGetUser()
|
|
|
|
if user != nil { initChatCtrl(store) }
|
|
|
|
print("user", user as Any)
|
|
|
|
return user
|
|
|
|
}
|
|
|
|
|
2022-01-29 23:37:02 +00:00
|
|
|
func chatSendCmd(_ cmd: ChatCommand) throws -> ChatResponse {
|
2022-01-29 11:10:04 +00:00
|
|
|
var c = cmd.cmdString.cString(using: .utf8)!
|
2022-01-30 18:27:20 +00:00
|
|
|
print("command", cmd.cmdString)
|
2022-01-29 23:37:02 +00:00
|
|
|
// TODO some mechanism to update model without passing it - maybe Publisher / Subscriber?
|
|
|
|
// DispatchQueue.main.async {
|
|
|
|
// termId += 1
|
|
|
|
// chatModel.terminalItems.append(.cmd(termId, cmd))
|
|
|
|
// }
|
|
|
|
return chatResponse(chat_send_cmd(getChatCtrl(), &c)!)
|
2022-01-29 11:10:04 +00:00
|
|
|
}
|
|
|
|
|
2022-01-29 23:37:02 +00:00
|
|
|
func chatRecvMsg() throws -> ChatResponse {
|
|
|
|
chatResponse(chat_recv_msg(getChatCtrl())!)
|
2022-01-29 11:10:04 +00:00
|
|
|
}
|
|
|
|
|
2022-01-31 21:28:07 +00:00
|
|
|
func apiGetChats() throws -> [Chat] {
|
2022-01-29 23:37:02 +00:00
|
|
|
let r = try chatSendCmd(.apiGetChats)
|
2022-02-02 12:51:39 +00:00
|
|
|
if case let .apiChats(chats) = r { return chats.map { Chat.init($0) } }
|
2022-01-30 18:27:20 +00:00
|
|
|
throw r
|
|
|
|
}
|
|
|
|
|
|
|
|
func apiGetChat(type: ChatType, id: Int64) throws -> Chat {
|
|
|
|
let r = try chatSendCmd(.apiGetChat(type: type, id: id))
|
2022-02-02 12:51:39 +00:00
|
|
|
if case let .apiChat(chat) = r { return Chat.init(chat) }
|
2022-01-30 18:27:20 +00:00
|
|
|
throw r
|
|
|
|
}
|
|
|
|
|
|
|
|
func apiSendMessage(type: ChatType, id: Int64, msg: MsgContent) throws -> ChatItem {
|
|
|
|
let r = try chatSendCmd(.apiSendMessage(type: type, id: id, msg: msg))
|
|
|
|
if case let .newChatItem(aChatItem) = r { return aChatItem.chatItem }
|
|
|
|
throw r
|
|
|
|
}
|
|
|
|
|
|
|
|
func apiAddContact() throws -> String {
|
|
|
|
let r = try chatSendCmd(.addContact)
|
|
|
|
if case let .invitation(connReqInvitation) = r { return connReqInvitation }
|
|
|
|
throw r
|
2022-01-29 23:37:02 +00:00
|
|
|
}
|
|
|
|
|
2022-01-30 18:27:20 +00:00
|
|
|
func apiConnect(connReq: String) throws {
|
|
|
|
let r = try chatSendCmd(.connect(connReq: connReq))
|
2022-01-29 23:37:02 +00:00
|
|
|
switch r {
|
2022-01-30 18:27:20 +00:00
|
|
|
case .sentConfirmation: return
|
|
|
|
case .sentInvitation: return
|
2022-01-29 23:37:02 +00:00
|
|
|
default: throw r
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-01-31 21:28:07 +00:00
|
|
|
func apiDeleteChat(type: ChatType, id: Int64) throws {
|
|
|
|
let r = try chatSendCmd(.apiDeleteChat(type: type, id: id))
|
|
|
|
if case .contactDeleted = r { return }
|
|
|
|
throw r
|
|
|
|
}
|
|
|
|
|
|
|
|
func apiUpdateProfile(profile: Profile) throws -> Profile? {
|
|
|
|
let r = try chatSendCmd(.apiUpdateProfile(profile: profile))
|
|
|
|
switch r {
|
|
|
|
case .userProfileNoChange: return nil
|
|
|
|
case let .userProfileUpdated(_, toProfile): return toProfile
|
|
|
|
default: throw r
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-02-01 17:34:06 +00:00
|
|
|
func apiCreateUserAddress() throws -> String {
|
|
|
|
let r = try chatSendCmd(.createMyAddress)
|
|
|
|
if case let .userContactLinkCreated(connReq) = r { return connReq }
|
|
|
|
throw r
|
|
|
|
}
|
|
|
|
|
|
|
|
func apiDeleteUserAddress() throws {
|
|
|
|
let r = try chatSendCmd(.deleteMyAddress)
|
|
|
|
if case .userContactLinkDeleted = r { return }
|
|
|
|
throw r
|
|
|
|
}
|
|
|
|
|
|
|
|
func apiGetUserAddress() throws -> String? {
|
|
|
|
let r = try chatSendCmd(.showMyAddress)
|
|
|
|
switch r {
|
|
|
|
case let .userContactLink(connReq):
|
|
|
|
return connReq
|
|
|
|
case .chatCmdError(chatError: .errorStore(storeError: .userContactLinkNotFound)):
|
|
|
|
return nil
|
|
|
|
default: throw r
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func apiAcceptContactRequest(contactReqId: Int64) throws -> Contact {
|
|
|
|
let r = try chatSendCmd(.apiAcceptContact(contactReqId: contactReqId))
|
|
|
|
if case let .acceptingContactRequest(contact) = r { return contact }
|
|
|
|
throw r
|
|
|
|
}
|
|
|
|
|
|
|
|
func apiRejectContactRequest(contactReqId: Int64) throws {
|
|
|
|
let r = try chatSendCmd(.apiRejectContact(contactReqId: contactReqId))
|
|
|
|
if case .contactRequestRejected = r { return }
|
|
|
|
throw r
|
|
|
|
}
|
|
|
|
|
2022-01-29 23:37:02 +00:00
|
|
|
func processReceivedMsg(_ chatModel: ChatModel, _ res: ChatResponse) {
|
|
|
|
DispatchQueue.main.async {
|
|
|
|
chatModel.terminalItems.append(.resp(Date.now, res))
|
|
|
|
switch res {
|
|
|
|
case let .contactConnected(contact):
|
2022-02-02 12:51:39 +00:00
|
|
|
let cInfo = ChatInfo.direct(contact: contact)
|
|
|
|
if chatModel.hasChat(contact.id) {
|
|
|
|
chatModel.updateChatInfo(cInfo)
|
2022-02-01 17:34:06 +00:00
|
|
|
} else {
|
2022-02-02 12:51:39 +00:00
|
|
|
chatModel.addChat(Chat(chatInfo: cInfo, chatItems: []))
|
2022-02-01 17:34:06 +00:00
|
|
|
}
|
|
|
|
case let .receivedContactRequest(contactRequest):
|
2022-02-02 12:51:39 +00:00
|
|
|
chatModel.addChat(Chat(
|
|
|
|
chatInfo: ChatInfo.contactRequest(contactRequest: contactRequest),
|
|
|
|
chatItems: []
|
|
|
|
))
|
2022-01-29 23:37:02 +00:00
|
|
|
case let .newChatItem(aChatItem):
|
2022-02-02 12:51:39 +00:00
|
|
|
chatModel.addChatItem(aChatItem.chatInfo, aChatItem.chatItem)
|
2022-01-29 23:37:02 +00:00
|
|
|
default:
|
2022-01-31 21:28:07 +00:00
|
|
|
print("unsupported response: ", res.responseType)
|
2022-01-29 11:10:04 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private struct UserResponse: Decodable {
|
|
|
|
var user: User?
|
|
|
|
var error: String?
|
|
|
|
}
|
|
|
|
|
2022-01-29 23:37:02 +00:00
|
|
|
private func chatResponse(_ cjson: UnsafePointer<CChar>) -> ChatResponse {
|
2022-01-29 11:10:04 +00:00
|
|
|
let s = String.init(cString: cjson)
|
|
|
|
let d = s.data(using: .utf8)!
|
2022-01-29 23:37:02 +00:00
|
|
|
// TODO is there a way to do it without copying the data? e.g:
|
2022-01-29 11:10:04 +00:00
|
|
|
// let p = UnsafeMutableRawPointer.init(mutating: UnsafeRawPointer(cjson))
|
|
|
|
// let d = Data.init(bytesNoCopy: p, count: strlen(cjson), deallocator: .free)
|
|
|
|
|
2022-01-29 23:37:02 +00:00
|
|
|
// TODO some mechanism to update model without passing it - maybe Publisher / Subscriber?
|
|
|
|
|
2022-01-29 11:10:04 +00:00
|
|
|
do {
|
2022-01-29 23:37:02 +00:00
|
|
|
let r = try jsonDecoder.decode(APIResponse.self, from: d)
|
|
|
|
return r.resp
|
2022-01-29 11:10:04 +00:00
|
|
|
} catch {
|
|
|
|
print (error)
|
|
|
|
}
|
|
|
|
|
|
|
|
var type: String?
|
|
|
|
var json: String?
|
|
|
|
if let j = try? JSONSerialization.jsonObject(with: d) as? NSDictionary {
|
|
|
|
if let j1 = j["resp"] as? NSDictionary, j1.count == 1 {
|
|
|
|
type = j1.allKeys[0] as? String
|
|
|
|
}
|
|
|
|
json = prettyJSON(j)
|
|
|
|
}
|
2022-01-29 23:37:02 +00:00
|
|
|
return ChatResponse.response(type: type ?? "invalid", json: json ?? s)
|
2022-01-29 11:10:04 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func prettyJSON(_ obj: NSDictionary) -> String? {
|
|
|
|
if let d = try? JSONSerialization.data(withJSONObject: obj, options: .prettyPrinted) {
|
|
|
|
return String(decoding: d, as: UTF8.self)
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
private func getStore() -> chat_store {
|
|
|
|
if let store = chatStore { return store }
|
|
|
|
let dataDir = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!.path + "/mobile_v1"
|
|
|
|
var cstr = dataDir.cString(using: .utf8)!
|
|
|
|
chatStore = chat_init_store(&cstr)
|
|
|
|
return chatStore!
|
|
|
|
}
|
|
|
|
|
|
|
|
private func initChatCtrl(_ store: chat_store) {
|
|
|
|
if chatController == nil {
|
|
|
|
chatController = chat_start(store)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private func getChatCtrl() -> chat_ctrl {
|
|
|
|
if let controller = chatController { return controller }
|
|
|
|
fatalError("Chat controller was not started!")
|
|
|
|
}
|
|
|
|
|
|
|
|
private func decodeCJSON<T: Decodable>(_ cjson: UnsafePointer<CChar>) -> T? {
|
|
|
|
let s = String.init(cString: cjson)
|
|
|
|
let d = s.data(using: .utf8)!
|
|
|
|
// let p = UnsafeMutableRawPointer.init(mutating: UnsafeRawPointer(cjson))
|
|
|
|
// let d = Data.init(bytesNoCopy: p, count: strlen(cjson), deallocator: .free)
|
|
|
|
return try? jsonDecoder.decode(T.self, from: d)
|
|
|
|
}
|
|
|
|
|
|
|
|
private func getJSONObject(_ cjson: UnsafePointer<CChar>) -> NSDictionary? {
|
|
|
|
let s = String.init(cString: cjson)
|
|
|
|
let d = s.data(using: .utf8)!
|
|
|
|
return try? JSONSerialization.jsonObject(with: d) as? NSDictionary
|
|
|
|
}
|
|
|
|
|
|
|
|
private func encodeCJSON<T: Encodable>(_ value: T) -> [CChar] {
|
|
|
|
let data = try! jsonEncoder.encode(value)
|
|
|
|
let str = String(decoding: data, as: UTF8.self)
|
|
|
|
return str.cString(using: .utf8)!
|
|
|
|
}
|
2022-02-01 17:34:06 +00:00
|
|
|
|
|
|
|
enum ChatError: Decodable {
|
2022-02-01 20:30:33 +00:00
|
|
|
case error(errorType: ChatErrorType)
|
2022-02-01 17:34:06 +00:00
|
|
|
case errorStore(storeError: StoreError)
|
|
|
|
// TODO other error cases
|
|
|
|
}
|
|
|
|
|
2022-02-01 20:30:33 +00:00
|
|
|
enum ChatErrorType: Decodable {
|
|
|
|
case invalidConnReq
|
|
|
|
}
|
|
|
|
|
2022-02-01 17:34:06 +00:00
|
|
|
enum StoreError: Decodable {
|
|
|
|
case userContactLinkNotFound
|
|
|
|
// TODO other error cases
|
|
|
|
}
|