diff --git a/Sources/Chat/Group/CreateGroup.swift b/Sources/Chat/Group/CreateGroup.swift index eb0a543..0eddfe0 100644 --- a/Sources/Chat/Group/CreateGroup.swift +++ b/Sources/Chat/Group/CreateGroup.swift @@ -5,7 +5,6 @@ extension PushChat { public static func createGroup(options: CreateGroupOptions) async throws -> PushGroupInfoDTO { do { let payload = try CreateGroupPlayload(options: options) - print("got payload") return try await createGroupService(payload: payload, env: options.env) } catch { throw GroupChatError.RUNTIME_ERROR( diff --git a/Sources/Chat/Group/GetGroup.swift b/Sources/Chat/Group/GetGroup.swift index 7f171ec..c6c9b64 100644 --- a/Sources/Chat/Group/GetGroup.swift +++ b/Sources/Chat/Group/GetGroup.swift @@ -1,76 +1,219 @@ import Foundation extension PushChat { - public static func getGroup(chatId: String, env: ENV) async throws -> PushChat.PushGroup? { - let url = try PushEndpoint.getGroup(chatId: chatId, env: env).url - var request = URLRequest(url: url) - request.httpMethod = "GET" - request.setValue("application/json", forHTTPHeaderField: "Content-Type") - - let (data, res) = try await URLSession.shared.data(for: request) - guard let httpResponse = res as? HTTPURLResponse else { - throw URLError(.badServerResponse) + public static func getGroup(chatId: String, env: ENV) async throws -> PushChat.PushGroup? { + let url = try PushEndpoint.getGroup(chatId: chatId, env: env).url + var request = URLRequest(url: url) + request.httpMethod = "GET" + request.setValue("application/json", forHTTPHeaderField: "Content-Type") + + let (data, res) = try await URLSession.shared.data(for: request) + guard let httpResponse = res as? HTTPURLResponse else { + throw URLError(.badServerResponse) + } + + if httpResponse.statusCode == 400 { + return nil + } + + guard (200 ... 299).contains(httpResponse.statusCode) else { + throw URLError(.badServerResponse) + } + + let groupData = try JSONDecoder().decode(PushGroup.self, from: data) + return groupData } - if httpResponse.statusCode == 400 { - return nil - } + public static func getGroupInfoDTO(chatId: String, env: ENV) async throws -> PushChat + .PushGroupInfoDTO { + let url = try PushEndpoint.getGroup(chatId: chatId, apiVersion: "v2", env: env).url + var request = URLRequest(url: url) + request.httpMethod = "GET" + request.setValue("application/json", forHTTPHeaderField: "Content-Type") + + let (data, res) = try await URLSession.shared.data(for: request) + guard let httpResponse = res as? HTTPURLResponse else { + throw URLError(.badServerResponse) + } + + if httpResponse.statusCode == 400 { + throw URLError(.badServerResponse) + } + + guard (200 ... 299).contains(httpResponse.statusCode) else { + throw URLError(.badServerResponse) + } - guard (200...299).contains(httpResponse.statusCode) else { - throw URLError(.badServerResponse) + let groupData = try JSONDecoder().decode(PushGroupInfoDTO.self, from: data) + return groupData } - let groupData = try JSONDecoder().decode(PushGroup.self, from: data) - return groupData - } - - public static func getGroupInfoDTO(chatId: String, env: ENV) async throws -> PushChat - .PushGroupInfoDTO - { - let url = try PushEndpoint.getGroup(chatId: chatId, apiVersion: "v2", env: env).url - var request = URLRequest(url: url) - request.httpMethod = "GET" - request.setValue("application/json", forHTTPHeaderField: "Content-Type") - - let (data, res) = try await URLSession.shared.data(for: request) - guard let httpResponse = res as? HTTPURLResponse else { - throw URLError(.badServerResponse) + public static func getGroupSessionKey(sessionKey: String, env: ENV) async throws -> String { + let url = try PushEndpoint.getGroupSession(chatId: sessionKey, apiVersion: "v1", env: env).url + var request = URLRequest(url: url) + request.httpMethod = "GET" + request.setValue("application/json", forHTTPHeaderField: "Content-Type") + + let (data, res) = try await URLSession.shared.data(for: request) + guard let httpResponse = res as? HTTPURLResponse else { + throw URLError(.badServerResponse) + } + + guard (200 ... 299).contains(httpResponse.statusCode) else { + throw URLError(.badServerResponse) + } + + struct SecretSessionRes: Codable { + var encryptedSecret: String + } + + let groupData = try JSONDecoder().decode(SecretSessionRes.self, from: data) + + return groupData.encryptedSecret } - if httpResponse.statusCode == 400 { - throw URLError(.badServerResponse) + public static func getGroupAccess(chatId: String, did: String, env: ENV) async throws -> GroupAccess? { + let url = try PushEndpoint.getGroupAccess(chatId: chatId, did: did, env: env).url + var request = URLRequest(url: url) + request.httpMethod = "GET" + request.setValue("application/json", forHTTPHeaderField: "Content-Type") + + let (data, res) = try await URLSession.shared.data(for: request) + guard let httpResponse = res as? HTTPURLResponse else { + throw URLError(.badServerResponse) + } + + if httpResponse.statusCode == 400 { + return nil + } + + guard (200 ... 299).contains(httpResponse.statusCode) else { + throw URLError(.badServerResponse) + } + + return try JSONDecoder().decode(GroupAccess.self, from: data) } - guard (200...299).contains(httpResponse.statusCode) else { - throw URLError(.badServerResponse) + public static func getGroupMemberCount(chatId: String, env: ENV) async throws -> TotalMembersCount? { + let url = try PushEndpoint.getGroupMemberCount(chatId: chatId, env: env).url + var request = URLRequest(url: url) + request.httpMethod = "GET" + request.setValue("application/json", forHTTPHeaderField: "Content-Type") + + let (data, res) = try await URLSession.shared.data(for: request) + guard let httpResponse = res as? HTTPURLResponse else { + throw URLError(.badServerResponse) + } + + if httpResponse.statusCode == 400 { + return nil + } + + guard (200 ... 299).contains(httpResponse.statusCode) else { + throw URLError(.badServerResponse) + } + + let groupData = try JSONDecoder().decode(ChatMemberCounts.self, from: data) + return groupData.totalMembersCount } - let groupData = try JSONDecoder().decode(PushGroupInfoDTO.self, from: data) - return groupData - } + public static func getGroupMemberStatus(chatId: String, did: String, env: ENV) async throws -> GroupMemberStatus? { + let url = try PushEndpoint.getGroupMemberStatus(chatId: chatId, did: walletToPCAIP10(account: did), env: env).url + var request = URLRequest(url: url) + request.httpMethod = "GET" + request.setValue("application/json", forHTTPHeaderField: "Content-Type") + + let (data, res) = try await URLSession.shared.data(for: request) + guard let httpResponse = res as? HTTPURLResponse else { + throw URLError(.badServerResponse) + } - public static func getGroupSessionKey(sessionKey: String, env: ENV) async throws -> String { - let url = try PushEndpoint.getGroupSession(chatId: sessionKey, apiVersion: "v1", env: env).url - var request = URLRequest(url: url) - request.httpMethod = "GET" - request.setValue("application/json", forHTTPHeaderField: "Content-Type") + if httpResponse.statusCode == 400 { + return nil + } - let (data, res) = try await URLSession.shared.data(for: request) - guard let httpResponse = res as? HTTPURLResponse else { - throw URLError(.badServerResponse) + guard (200 ... 299).contains(httpResponse.statusCode) else { + throw URLError(.badServerResponse) + } + + let groupData = try JSONDecoder().decode(GroupMemberStatus.self, from: data) + return groupData } - guard (200...299).contains(httpResponse.statusCode) else { - throw URLError(.badServerResponse) + public struct FetchChatGroupInfo { + var chatId: String + var page: Int + var limit: Int + var pending: Bool? + var role: String? + + init(chatId: String, page: Int = 1, limit: Int = 20, pending: Bool? = nil, role: String? = nil) { + self.chatId = chatId + self.page = page + self.limit = limit + self.pending = pending + self.role = role + } } - struct SecretSessionRes: Codable { - var encryptedSecret: String + public static func getGroupMembers(options: FetchChatGroupInfo, env: ENV) async throws -> [ChatMemberProfile]? { + let url = try PushEndpoint.getGroupMembers(options: options, env: env).url + var request = URLRequest(url: url) + request.httpMethod = "GET" + request.setValue("application/json", forHTTPHeaderField: "Content-Type") + + let (data, res) = try await URLSession.shared.data(for: request) + guard let httpResponse = res as? HTTPURLResponse else { + throw URLError(.badServerResponse) + } + + if httpResponse.statusCode == 400 { + return nil + } + + guard (200 ... 299).contains(httpResponse.statusCode) else { + throw URLError(.badServerResponse) + } + + let groupData = try JSONDecoder().decode(GetMembersResponse.self, from: data) + return groupData.members } - let groupData = try JSONDecoder().decode(SecretSessionRes.self, from: data) + public static func getGroupMembersPublicKeys(chatId: String, page: Int = 1, limit: Int = 20, env: ENV) async throws -> [GroupMemberPublicKey]? { + let url = try PushEndpoint.getGroupMembersPublicKeys(chatId: chatId, page: page, limit: limit, env: env).url + var request = URLRequest(url: url) + request.httpMethod = "GET" + request.setValue("application/json", forHTTPHeaderField: "Content-Type") + + let (data, res) = try await URLSession.shared.data(for: request) + guard let httpResponse = res as? HTTPURLResponse else { + throw URLError(.badServerResponse) + } - return groupData.encryptedSecret - } + if httpResponse.statusCode == 400 { + return nil + } + guard (200 ... 299).contains(httpResponse.statusCode) else { + throw URLError(.badServerResponse) + } + + let groupData = try JSONDecoder().decode(GetMemberPublicKeysResponse.self, from: data) + return groupData.members + } + + public static func getAllGroupMembersPublicKeysV2(chatId: String, env: ENV) async throws -> [GroupMemberPublicKey]? { + let count = try await getGroupMemberCount(chatId: chatId, env: env) + let limit = 5000 + + let totalPages = Int(ceil(Double(count?.overallCount ?? 0) / Double(limit))) + var members = [GroupMemberPublicKey]() + + for i in 1 ..< totalPages { + let page = try await getGroupMembersPublicKeys(chatId: chatId, page: i, limit: limit, env: env) + members = members + (page ?? []) + } + + return members + } } diff --git a/Sources/Chat/Group/Group.swift b/Sources/Chat/Group/Group.swift index b86c713..5e12302 100644 --- a/Sources/Chat/Group/Group.swift +++ b/Sources/Chat/Group/Group.swift @@ -1,58 +1,158 @@ public enum GroupChatError: Error { - case ONE_OF_ACCOUNT_OR_SIGNER_REQUIRED - case INVALID_ETH_ADDRESS - case CHAT_ID_NOT_FOUND - case RUNTIME_ERROR(String) + case ONE_OF_ACCOUNT_OR_SIGNER_REQUIRED + case INVALID_ETH_ADDRESS + case CHAT_ID_NOT_FOUND + case RUNTIME_ERROR(String) } extension PushChat { - public struct PushGroup: Codable { - public var members: [Member] - public var pendingMembers: [Member] - public var contractAddressERC20: String? - public var numberOfERC20: Int - public var contractAddressNFT: String? - public var numberOfNFTTokens: Int - public var verificationProof: String - public var groupImage: String - public var groupName: String - public var groupDescription: String - public var isPublic: Bool - public var groupCreator: String - public var chatId: String - public var scheduleAt: String? - public var scheduleEnd: String? - public var groupType: String - public var status: String? - public var eventType: String? - - public struct Member: Codable { - public let wallet: String - public let publicKey: String? - public let isAdmin: Bool - public let image: String? - - public init(wallet: String, isAdmin: Bool, image: String, publicKey: String) { - self.wallet = wallet - self.isAdmin = isAdmin - self.image = image - self.publicKey = publicKey - } - } - } - - public struct PushGroupInfoDTO: Codable { - public var groupName: String - public var groupDescription: String - public var groupImage: String? - public var isPublic: Bool - public var groupCreator: String - public var chatId: String - public var groupType: String? - public var meta: String? - public var sessionKey: String? - public var encryptedSecret: String? - - } + public struct GroupMemberPublicKey: Codable { + let did: String + let publicKey: String + } + + public struct GetMemberPublicKeysResponse: Codable { + let members: [GroupMemberPublicKey] + } + + public struct GetMembersResponse: Codable { + let members: [ChatMemberProfile] + } + + public struct ChatMemberProfile: Codable { + let address: String + let intent: Bool + let role: String + let userInfo: UserData? + + init(address: String, intent: Bool, role: String, userInfo: UserData?) { + self.address = address + self.intent = intent + self.role = role + self.userInfo = userInfo + } + } + + public struct UserData: Codable { + let msgSent: Int + let maxMsgPersisted: Int + let did: String + let wallets: String + let profile: UserProfile + let encryptedPrivateKey: String? + let publicKey: String? + let verificationProof: String? + let origin: String? + } + + public struct UserProfile: Codable { + let verificationProof: String? + let profileVerificationProof: String? + let picture: String + let name: String? + let desc: String? + let blockedUsersList: [String]? + } + + public struct GroupMemberStatus: Codable { + let isMember: Bool + let isPending: Bool + let isAdmin: Bool + + init(isMember: Bool, isPending: Bool, isAdmin: Bool) { + self.isMember = isMember + self.isPending = isPending + self.isAdmin = isAdmin + } + } + + public struct ChatMemberCounts: Codable { + let totalMembersCount: TotalMembersCount + } + public struct TotalMembersCount: Codable { + let overallCount: Int + let adminsCount: Int + let membersCount: Int + let pendingCount: Int + let approvedCount: Int + let roles: MemberRoles + } + + public struct MemberRoles: Codable { + let admin: RoleCounts + let member: RoleCounts + + enum CodingKeys: String, CodingKey { + case admin = "ADMIN" + case member = "MEMBER" + } + } + + public struct RoleCounts: Codable { + let total: Int + let pending: Int + } + + + + public struct GroupAccess: Codable { + public var entry: Bool? + public var chat: Bool? + public var rules: [String: String]? + + public init(entry: Bool, chat: Bool, rules: [String: String]?) { + self.entry = entry + self.chat = chat + self.rules = rules + } + } + + public struct PushGroup: Codable { + public var members: [Member] + public var pendingMembers: [Member] + public var contractAddressERC20: String? + public var numberOfERC20: Int + public var contractAddressNFT: String? + public var numberOfNFTTokens: Int + public var verificationProof: String + public var groupImage: String + public var groupName: String + public var groupDescription: String + public var isPublic: Bool + public var groupCreator: String + public var chatId: String + public var scheduleAt: String? + public var scheduleEnd: String? + public var groupType: String + public var status: String? + public var eventType: String? + + public struct Member: Codable { + public let wallet: String + public let publicKey: String? + public let isAdmin: Bool + public let image: String? + + public init(wallet: String, isAdmin: Bool, image: String, publicKey: String) { + self.wallet = wallet + self.isAdmin = isAdmin + self.image = image + self.publicKey = publicKey + } + } + } + + public struct PushGroupInfoDTO: Codable { + public var groupName: String + public var groupDescription: String + public var groupImage: String? + public var isPublic: Bool + public var groupCreator: String + public var chatId: String + public var groupType: String? + public var meta: String? + public var sessionKey: String? + public var encryptedSecret: String? + } } diff --git a/Sources/Chat/Group/GroupMembers.swift b/Sources/Chat/Group/GroupMembers.swift new file mode 100644 index 0000000..c191667 --- /dev/null +++ b/Sources/Chat/Group/GroupMembers.swift @@ -0,0 +1,208 @@ +import Foundation + +extension PushChat { + public struct UpdateGroupMemberOptions { + var account: String + let chatId: String + var upsert: UpsertData + let remove: [String] + var pgpPrivateKey: String + + init(account: String, chatId: String, upsert: UpsertData = UpsertData(), remove: [String] = [], pgpPrivateKey: String) { + self.account = account + self.chatId = chatId + self.upsert = upsert + self.remove = remove + self.pgpPrivateKey = pgpPrivateKey + } + } + + public struct UpsertData { + let members: [String] + let admins: [String] + + init(members: [String] = [], admins: [String] = []) { + self.members = members + self.admins = admins + } + + func toJson() -> [String: [String]] { + return [ + "members": members, + "admins": admins, + ] + } + } + + public static func updateGroupMember(options: UpdateGroupMemberOptions, env: ENV) async throws -> PushChat.PushGroupInfoDTO { + do { + try validateGroupMemberUpdateOptions(chatId: options.chatId, upsert: options.upsert, remove: options.remove) + + var convertedUpsert = [String: [String]]() + for (key, value) in options.upsert.toJson() { + convertedUpsert[key] = value.map { walletToPCAIP10(account: $0) } + } + + let convertedRemove = options.remove.map { walletToPCAIP10(account: $0) } + + guard let connectedUser = try await PushUser.get(account: options.account, env: env) + + else { + fatalError("\(options.account) not found") + } + + let group = try await PushChat.getGroupInfoDTO(chatId: options.chatId, env: env) + + var encryptedSecret: String? + if !group.isPublic { + if group.encryptedSecret != nil && !(group.encryptedSecret?.isEmpty ?? true) { + guard let isMember = try await getGroupMemberStatus(chatId: options.chatId, did: connectedUser.did, env: env)?.isMember else { + fatalError("Failed to determine group membership") + } + + let removeParticipantSet = Set(convertedRemove.map { $0.lowercased() }) + + let groupMembers = try await getAllGroupMembersPublicKeysV2(chatId: options.chatId, env: env) + var sameMembers = true + + for member in groupMembers! { + if removeParticipantSet.contains(member.did.lowercased()) { + sameMembers = false + break + } + } + + if !sameMembers || !isMember { + let secretKey = AESGCMHelper.generateRandomSecret(length: 15) + var publicKeys = [String]() + + for member in groupMembers! { + if !removeParticipantSet.contains(member.did.lowercased()) { + publicKeys.append(member.publicKey) + } + } + + if !isMember { + publicKeys.append(connectedUser.publicKey) + } + + encryptedSecret = try Pgp.pgpEncryptV2(message: secretKey, pgpPublicKeys: publicKeys) + } + } + } + + let hash = try getGroupmemberUpdateHash(upsert: convertedUpsert, remove: convertedRemove, encryptedSecret: encryptedSecret) + + let signature = try Pgp.sign(message: hash, privateKey: options.pgpPrivateKey) + let sigType = "pgpv2" + let deltaVerificationProof = "\(sigType):\(signature):\(walletToPCAIP10(account: options.account))" + + return try await updateMemberService(chatId: options.chatId, payload: UpdateMembersPayload(upsert: convertedUpsert, remove: convertedRemove, encryptedSecret: encryptedSecret, deltaVerificationProof: deltaVerificationProof), env: env) + + } catch { + fatalError("Error: \(error.localizedDescription)") + } + } + + static func updateMemberService(chatId: String, payload: UpdateMembersPayload, env: ENV) async throws + -> PushChat.PushGroupInfoDTO { + let url = try PushEndpoint.updateGroupMembers(chatId: chatId, env: env).url + + var request = URLRequest(url: url) + request.httpMethod = "PUT" + request.setValue("application/json", forHTTPHeaderField: "Content-Type") + + let encoder = JSONEncoder() + encoder.outputFormatting = .prettyPrinted + let jsonData = try encoder.encode(payload) + + request.httpBody = jsonData + + let (data, res) = try await URLSession.shared.data(for: request) + + guard let httpResponse = res as? HTTPURLResponse else { + throw URLError(.badServerResponse) + } + + guard (200 ... 299).contains(httpResponse.statusCode) else { + throw URLError(.badServerResponse) + } + + let groupData = try JSONDecoder().decode(PushGroupInfoDTO.self, from: data) + return groupData + } + + struct UpdateMembersPayload: Codable { + let upsert: [String: [String]] + let remove: [String] + let encryptedSecret: String? + let deltaVerificationProof: String + + func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(upsert, forKey: .upsert) + try container.encode(remove, forKey: .remove) + try container.encode(encryptedSecret, forKey: .encryptedSecret) + try container.encode(deltaVerificationProof, forKey: .deltaVerificationProof) + } + } + + static func getGroupmemberUpdateHash( + upsert: [String: [String]] + , remove: [String] + , encryptedSecret: String?) throws -> String { + struct UpdateMemberStruct: Codable { + let upsert: [String: [String]] + let remove: [String] + let encryptedSecret: String? + + func toJson() throws -> String { + let output = "{\"upsert\":{\"members\":\(flatten_address_list(addresses: upsert["members"] ?? [])),\"admins\":\(flatten_address_list(addresses: upsert["admins"] ?? []))},\"remove\":\(flatten_address_list(addresses: remove)),\"encryptedSecret\":\(encryptedSecret == nil ? "null" : "\"\(encryptedSecret!)\"")}" + return output + } + } + + let updateMember = try UpdateMemberStruct(upsert: upsert, remove: remove, encryptedSecret: encryptedSecret).toJson() + + let hash = generateSHA256Hash(msg: updateMember) + + return hash + } + + static func validateGroupMemberUpdateOptions(chatId: String, upsert: UpsertData, remove: [String]) throws { + if chatId.isEmpty { + fatalError("chatId cannot be null or empty") + } + + // Validating upsert object + let allowedRoles = ["members", "admins"] + + let upsertJson = upsert.toJson() + for (role, value) in upsertJson { + if !allowedRoles.contains(role) { + fatalError("Invalid role: \(role). Allowed roles are \(allowedRoles.joined(separator: ", ")).") + } + + if value.count > 1000 { + fatalError("\(role) array cannot have more than 1000 addresses.") + } + + for address in value { + if !isValidETHAddress(address: address) { + fatalError("Invalid address found in \(role) list.") + } + } + } + + // Validating remove array + if remove.count > 1000 { + fatalError("Remove array cannot have more than 1000 addresses.") + } + + for address in remove { + if !isValidETHAddress(address: address) { + fatalError("Invalid address found in remove list.") + } + } + } +} diff --git a/Sources/Chat/Group/UpdateGroup.swift b/Sources/Chat/Group/UpdateGroup.swift index 06f0515..15a15cd 100644 --- a/Sources/Chat/Group/UpdateGroup.swift +++ b/Sources/Chat/Group/UpdateGroup.swift @@ -1,168 +1,162 @@ import Foundation extension PushChat { - public static func updateGroup( - updatedGroup: PushChat.PushGroup, adminAddress: String, adminPgpPrivateKey: String, env: ENV - ) async throws -> PushGroup { - do { - - let updatedGroupOptions = try UpdateGroupOptions( - group: updatedGroup, creatorPgpPrivateKey: adminPgpPrivateKey, - requesterAddress: walletToPCAIP10(account: adminAddress), env: env) - - let createGroupInfoHash = try getUpdateGroupHash(options: updatedGroupOptions) - let signature = try Pgp.sign( - message: createGroupInfoHash, privateKey: updatedGroupOptions.creatorPgpPrivateKey) - let sigType = "pgp" - let verificationProof = "\(sigType):\(signature):\(updatedGroupOptions.requesterAddress)" - - let payload = UpdateGroupPlayload( - options: updatedGroupOptions, verificationProof: verificationProof) - - return try await updateGroupService( - payload: payload, chatId: updatedGroup.chatId, env: updatedGroupOptions.env) - - } catch { - throw GroupChatError.RUNTIME_ERROR( - "[Push SDK] - API - Error - API update GroupChat -: \(error)") + public static func updateGroup( + updatedGroup: PushChat.PushGroup, adminAddress: String, adminPgpPrivateKey: String, env: ENV + ) async throws -> PushGroup { + do { + let updatedGroupOptions = try UpdateGroupOptions( + group: updatedGroup, creatorPgpPrivateKey: adminPgpPrivateKey, + requesterAddress: walletToPCAIP10(account: adminAddress), env: env) + + let createGroupInfoHash = try getUpdateGroupHash(options: updatedGroupOptions) + let signature = try Pgp.sign( + message: createGroupInfoHash, privateKey: updatedGroupOptions.creatorPgpPrivateKey) + let sigType = "pgp" + let verificationProof = "\(sigType):\(signature):\(updatedGroupOptions.requesterAddress)" + + let payload = UpdateGroupPlayload( + options: updatedGroupOptions, verificationProof: verificationProof) + + return try await updateGroupService( + payload: payload, chatId: updatedGroup.chatId, env: updatedGroupOptions.env) + + } catch { + throw GroupChatError.RUNTIME_ERROR( + "[Push SDK] - API - Error - API update GroupChat -: \(error)") + } } - } - public static func leaveGroup( - chatId: String, userAddress: String, userPgpPrivateKey: String, env: ENV - ) async throws { + public static func leaveGroup( + chatId: String, userAddress: String, userPgpPrivateKey: String, env: ENV + ) async throws { + guard var group = try await PushChat.getGroup(chatId: chatId, env: env) else { + throw PushChat.ChatError.chatError("Group with \(chatId) not found") + } - guard var group = try await PushChat.getGroup(chatId: chatId, env: env) else { - throw PushChat.ChatError.chatError("Group with \(chatId) not found") + group.members += group.pendingMembers + group.members = group.members.filter { $0.wallet != walletToPCAIP10(account: userAddress) } + + _ = try await PushChat.updateGroup( + updatedGroup: group, adminAddress: walletToPCAIP10(account: userAddress), + adminPgpPrivateKey: userPgpPrivateKey, + env: env) } - group.members += group.pendingMembers - group.members = group.members.filter { $0.wallet != walletToPCAIP10(account: userAddress) } - - _ = try await PushChat.updateGroup( - updatedGroup: group, adminAddress: walletToPCAIP10(account: userAddress), - adminPgpPrivateKey: userPgpPrivateKey, - env: env) - } - - struct UpdateGroupPlayload: Encodable { - var groupName: String - var groupDescription: String - var groupImage: String - var members: [String] - var admins: [String] - var address: String - var verificationProof: String - - public init(options: UpdateGroupOptions, verificationProof: String) { - groupName = options.name - groupDescription = options.description - members = options.members - groupImage = options.image - address = options.requesterAddress - admins = options.admins - self.verificationProof = verificationProof + struct UpdateGroupPlayload: Encodable { + var groupName: String + var groupDescription: String + var groupImage: String + var members: [String] + var admins: [String] + var address: String + var verificationProof: String + + public init(options: UpdateGroupOptions, verificationProof: String) { + groupName = options.name + groupDescription = options.description + members = options.members + groupImage = options.image + address = options.requesterAddress + admins = options.admins + self.verificationProof = verificationProof + } } - } - static func updateGroupService(payload: UpdateGroupPlayload, chatId: String, env: ENV) + static func updateGroupService(payload: UpdateGroupPlayload, chatId: String, env: ENV) async throws - -> PushChat.PushGroup - { - let url = try PushEndpoint.updatedChatGroup(chatId: chatId, env: env).url - - var request = URLRequest(url: url) - request.httpMethod = "PUT" - request.setValue("application/json", forHTTPHeaderField: "Content-Type") - request.httpBody = try JSONEncoder().encode(payload) + -> PushChat.PushGroup { + let url = try PushEndpoint.updatedChatGroup(chatId: chatId, env: env).url - let (data, res) = try await URLSession.shared.data(for: request) + var request = URLRequest(url: url) + request.httpMethod = "PUT" + request.setValue("application/json", forHTTPHeaderField: "Content-Type") + request.httpBody = try JSONEncoder().encode(payload) - guard let httpResponse = res as? HTTPURLResponse else { - throw URLError(.badServerResponse) - } + let (data, res) = try await URLSession.shared.data(for: request) - guard (200...299).contains(httpResponse.statusCode) else { - print(try data.toString()) - throw URLError(.badServerResponse) - } + guard let httpResponse = res as? HTTPURLResponse else { + throw URLError(.badServerResponse) + } - let groupData = try JSONDecoder().decode(PushGroup.self, from: data) - return groupData - - } + guard (200 ... 299).contains(httpResponse.statusCode) else { + print(try data.toString()) + throw URLError(.badServerResponse) + } + let groupData = try JSONDecoder().decode(PushGroup.self, from: data) + return groupData + } } public struct UpdateGroupOptions { - public var name: String - public var description: String - public var image: String - public var members: [String] - public var admins: [String] - public var chatId: String - - public var creatorAddress: String - public var creatorPgpPrivateKey: String - public var env: ENV = ENV.STAGING - public var requesterAddress: String - - public init( - group: PushChat.PushGroup, creatorPgpPrivateKey: String, requesterAddress: String, env: ENV - ) throws { - - let memebersAddresses = group.members.map { $0.wallet } - let adminsAddresses = [group.groupCreator] - - self.name = group.groupName - self.description = group.groupDescription - self.image = group.groupImage - self.members = memebersAddresses - self.admins = adminsAddresses - self.chatId = group.chatId - - self.requesterAddress = requesterAddress - - self.creatorAddress = group.groupCreator - self.creatorPgpPrivateKey = creatorPgpPrivateKey - self.env = env - - // validate the options - try updateGroupOptionValidator(self) - - // format the addresses - self.creatorAddress = walletToPCAIP10(account: creatorAddress) - self.members = walletsToPCAIP10(accounts: self.members) - } + public var name: String + public var description: String + public var image: String + public var members: [String] + public var admins: [String] + public var chatId: String + + public var creatorAddress: String + public var creatorPgpPrivateKey: String + public var env: ENV = ENV.STAGING + public var requesterAddress: String + + public init( + group: PushChat.PushGroup, creatorPgpPrivateKey: String, requesterAddress: String, env: ENV + ) throws { + let memebersAddresses = group.members.map { $0.wallet } + let adminsAddresses = [group.groupCreator] + + name = group.groupName + description = group.groupDescription + image = group.groupImage + members = memebersAddresses + admins = adminsAddresses + chatId = group.chatId + + self.requesterAddress = requesterAddress + + creatorAddress = group.groupCreator + self.creatorPgpPrivateKey = creatorPgpPrivateKey + self.env = env + + // validate the options + try updateGroupOptionValidator(self) + + // format the addresses + creatorAddress = walletToPCAIP10(account: creatorAddress) + members = walletsToPCAIP10(accounts: members) + } } func getUpdateGroupHash(options: UpdateGroupOptions) throws -> String { - struct CreateGroupStruct: Codable { - let groupName: String - let groupDescription: String - let members: [String] - let admins: [String] - let groupImage: String - let groupCreator: String - let chatId: String - - func toJSONString() throws -> String { - return - "{\"groupName\":\"\(groupName)\",\"groupDescription\":\"\(groupDescription)\",\"groupImage\":\"\(groupImage)\",\"members\":\(flatten_address_list(addresses:members)),\"admins\":\(flatten_address_list(addresses:(admins))),\"chatId\":\"\(chatId)\"}" + struct CreateGroupStruct: Codable { + let groupName: String + let groupDescription: String + let members: [String] + let admins: [String] + let groupImage: String + let groupCreator: String + let chatId: String + + func toJSONString() throws -> String { + return + "{\"groupName\":\"\(groupName)\",\"groupDescription\":\"\(groupDescription)\",\"groupImage\":\"\(groupImage)\",\"members\":\(flatten_address_list(addresses: members)),\"admins\":\(flatten_address_list(addresses: admins)),\"chatId\":\"\(chatId)\"}" + } } - } - let createGroupStruct = try CreateGroupStruct( - groupName: options.name, - groupDescription: options.description, - members: options.members, - admins: options.admins, - groupImage: options.image, - groupCreator: options.creatorAddress, - chatId: options.chatId - ).toJSONString() + let createGroupStruct = try CreateGroupStruct( + groupName: options.name, + groupDescription: options.description, + members: options.members, + admins: options.admins, + groupImage: options.image, + groupCreator: options.creatorAddress, + chatId: options.chatId + ).toJSONString() - let hash = generateSHA256Hash(msg: createGroupStruct) + let hash = generateSHA256Hash(msg: createGroupStruct) - return hash + return hash } diff --git a/Sources/Chat/Send.swift b/Sources/Chat/Send.swift index 874d59f..a391840 100644 --- a/Sources/Chat/Send.swift +++ b/Sources/Chat/Send.swift @@ -1,532 +1,590 @@ import Foundation extension PushChat { - struct SendIntentAPIOptions { - - } - - struct SendMessagePayload: Encodable { - var fromDID: String - var toDID: String - var fromCAIP10: String - var toCAIP10: String - var messageContent: String - var messageObj: String? - var messageType: String - var signature: String - var encType: String - var encryptedSecret: String? - var sigType: String - var verificationProof: String? - var sessionKey: String? - - private enum CodingKeys: String, CodingKey { - case fromDID, toDID, fromCAIP10, toCAIP10, messageContent, messageObj, messageType, signature, - encType, - encryptedSecret, sigType, verificationProof, sessionKey + struct SendIntentAPIOptions { } - func encode(to encoder: Encoder) throws { - var container = encoder.container(keyedBy: CodingKeys.self) + struct SendMessagePayload: Encodable { + var fromDID: String + var toDID: String + var fromCAIP10: String + var toCAIP10: String + var messageContent: String + var messageObj: String? + var messageType: String + var signature: String + var encType: String + var encryptedSecret: String? + var sigType: String + var verificationProof: String? + var sessionKey: String? + + private enum CodingKeys: String, CodingKey { + case fromDID, toDID, fromCAIP10, toCAIP10, messageContent, messageObj, messageType, signature, + encType, + encryptedSecret, sigType, verificationProof, sessionKey + } - try container.encode(fromDID, forKey: .fromDID) - try container.encode(toDID, forKey: .toDID) - try container.encode(fromCAIP10, forKey: .fromCAIP10) - try container.encode(toCAIP10, forKey: .toCAIP10) - try container.encode(messageContent, forKey: .messageContent) - try container.encode(messageObj, forKey: .messageObj) + func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) - try container.encode(messageType, forKey: .messageType) - try container.encode(signature, forKey: .signature) - try container.encode(encType, forKey: .encType) - try container.encode(encryptedSecret, forKey: .encryptedSecret) + try container.encode(fromDID, forKey: .fromDID) + try container.encode(toDID, forKey: .toDID) + try container.encode(fromCAIP10, forKey: .fromCAIP10) + try container.encode(toCAIP10, forKey: .toCAIP10) + try container.encode(messageContent, forKey: .messageContent) + try container.encode(messageObj, forKey: .messageObj) - try container.encode(sigType, forKey: .sigType) - try container.encode(verificationProof, forKey: .verificationProof) - try container.encode(sessionKey, forKey: .sessionKey) - } - } - - public enum MessageType: String { - case Text = "Text" - case Image = "Image" - case Reaction = "Reaction" - case Reply = "Reply" - case MediaEmbed = "MediaEmbed" - } - - public struct SendOptions { - public var messageContent = "" - public var messageType: MessageType - public var receiverAddress: String - public var account: String - public var pgpPrivateKey: String - public var senderPgpPubicKey: String? - public var receiverPgpPubicKey: String? - public var processMessage: String? - public var reference: String? - public var env: ENV = .STAGING - - public enum Reactions: String { - case THUMBSUP = "\u{1F44D}" - case THUMBSDOWN = "\u{1F44E}" - case HEART = "\u{2764}\u{FE0F}" - case CLAP = "\u{1F44F}" - case LAUGH = "\u{1F602}" - case SAD = "\u{1F622}" - case ANGRY = "\u{1F621}" - case SURPRISE = "\u{1F632}" - case FIRE = "\u{1F525}" - } + try container.encode(messageType, forKey: .messageType) + try container.encode(signature, forKey: .signature) + try container.encode(encType, forKey: .encType) + try container.encode(encryptedSecret, forKey: .encryptedSecret) - public init( - messageContent: String, messageType: String, receiverAddress: String, account: String, - pgpPrivateKey: String, refrence: String? = nil, env: ENV = .STAGING - ) { - self.messageContent = messageContent - self.messageType = MessageType(rawValue: messageType)! - self.receiverAddress = walletToPCAIP10(account: receiverAddress) - self.account = walletToPCAIP10(account: account) - self.pgpPrivateKey = pgpPrivateKey - self.reference = refrence - self.env = env + try container.encode(sigType, forKey: .sigType) + try container.encode(verificationProof, forKey: .verificationProof) + try container.encode(sessionKey, forKey: .sessionKey) + } } - public func getMessageObjJSON() throws -> String { - switch messageType { - case .Text, .Image, .MediaEmbed: - return try getJsonStringFromKV([ - ("content", self.messageContent) - ]) - case .Reaction: - return try getJsonStringFromKV([ - ("content", self.messageContent), - ("refrence", self.reference!), - ]) - case .Reply: - return """ - {"content":{"messageType":"Text","messageObj":{"content":"\(self.messageContent)"}},"reference":"\(self.reference!)"} - """.trimmingCharacters(in: .whitespaces) - } - } - } - - static func sendIntentService(payload: SendMessagePayload, env: ENV) async throws -> Message { - let url = try PushEndpoint.sendChatIntent(env: env).url - var request = URLRequest(url: url) - request.httpMethod = "POST" - request.setValue("application/json", forHTTPHeaderField: "Content-Type") - request.httpBody = try JSONEncoder().encode(payload) - - let (data, res) = try await URLSession.shared.data(for: request) - - guard let httpResponse = res as? HTTPURLResponse else { - throw URLError(.badServerResponse) + public enum MessageType: String { + case Text = "Text" + case Video = "Video" + case Audio = "Audio" + case Image = "Image" + case File = "File" + case MediaEmbed = "MediaEmbed" + case Meta = "Meta" + case Reaction = "Reaction" + case Composite = "Composite" + case Reply = "Reply" + case Receipt = "Receipt" + case UserActivity = "UserActivity" + case Intent = "Intent" + case Payment = "Payment" } - guard (200...299).contains(httpResponse.statusCode) else { - throw URLError(.badServerResponse) - } + public class SendMessage : Encodable { + + public var content: String + public var type: MessageType + public var replyContent: String? // Assuming this property exists in the SendMessage struct + public var compositeContent: [String]? + public var reference: String? + + public init(content: String, type: MessageType, replyContent: String? = nil, compositeContent: [String]? = nil) { + self.content = content + self.type = type + self.replyContent = replyContent + self.compositeContent = compositeContent + } - do { - return try JSONDecoder().decode(Message.self, from: data) - } catch { - print("[Push SDK] - API \(error.localizedDescription)") - throw error + public func toJson() throws -> String { + switch type { + case .Text, .Image, .MediaEmbed: + return try getJsonStringFromKV([ + ("content", content), + ]) + case .Reaction: + return try getJsonStringFromKV([ + ("content", content), + ("refrence", reference!), + ]) + case .Reply: + return """ + {"content":{"messageType":"Text","messageObj":{"content":"\(content)"}},"reference":"\(reference!)"} + """.trimmingCharacters(in: .whitespaces) + case .Video, .Audio, .File, .Meta, .Composite, .Receipt, .UserActivity, .Intent, .Payment: + return try getJsonStringFromKV([ + ("content", content), + ("refrence", reference!), + ]) + } + } + + private enum CodingKeys: String, CodingKey { + case content + } + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + + try container.encode(content, forKey: .content) + } } - } - static func sendMessageService(payload: SendMessagePayload, env: ENV) async throws -> Message { - let url = try PushEndpoint.sendChatMessage(env: env).url - var request = URLRequest(url: url) - request.httpMethod = "POST" - request.setValue("application/json", forHTTPHeaderField: "Content-Type") - request.httpBody = try JSONEncoder().encode(payload) + public struct SendOptions { + public var message: SendMessage? + +// @available(*, deprecated, message: "Use message.content instead") + public var messageContent = "" + +// @available(*, deprecated, message: "Use message.type instead") + public var messageType: MessageType + +// @available(*, deprecated, message: "Use to instead") + public var receiverAddress: String + + public var to: String? + public var account: String + public var pgpPrivateKey: String + public var senderPgpPubicKey: String? + public var receiverPgpPubicKey: String? + public var processMessage: String? + public var reference: String? + public var env: ENV = .STAGING + + public enum Reactions: String { + case THUMBSUP = "\u{1F44D}" + case THUMBSDOWN = "\u{1F44E}" + case HEART = "\u{2764}\u{FE0F}" + case CLAP = "\u{1F44F}" + case LAUGH = "\u{1F602}" + case SAD = "\u{1F622}" + case ANGRY = "\u{1F621}" + case SURPRISE = "\u{1F632}" + case FIRE = "\u{1F525}" + } - let (data, res) = try await URLSession.shared.data(for: request) + public init( + message: SendMessage? = nil, + messageContent: String, messageType: String, receiverAddress: String, account: String, + pgpPrivateKey: String, refrence: String? = nil, env: ENV = .STAGING, to: String? = nil + ) { + self.messageContent = messageContent + self.messageType = MessageType(rawValue: messageType)! + self.receiverAddress = walletToPCAIP10(account: receiverAddress) + self.account = walletToPCAIP10(account: account) + self.pgpPrivateKey = pgpPrivateKey + reference = refrence + self.env = env + self.to = to + self.message = message + } - guard let httpResponse = res as? HTTPURLResponse else { - throw URLError(.badServerResponse) + public func getMessageObjJSON() throws -> String { + switch messageType { + case .Text, .Image, .MediaEmbed: + return try getJsonStringFromKV([ + ("content", messageContent), + ]) + case .Reaction: + return try getJsonStringFromKV([ + ("content", messageContent), + ("refrence", reference!), + ]) + case .Reply: + return """ + {"content":{"messageType":"Text","messageObj":{"content":"\(messageContent)"}},"reference":"\(reference!)"} + """.trimmingCharacters(in: .whitespaces) + case .Video, .Audio, .File, .Meta, .Composite, .Receipt, .UserActivity, .Intent, .Payment: + return try getJsonStringFromKV([ + ("content", messageContent), + ("refrence", reference!), + ]) + } + } } - guard (200...299).contains(httpResponse.statusCode) else { - throw URLError(.badServerResponse) - } + static func sendIntentService(payload: SendMessagePayload, env: ENV) async throws -> Message { + let url = try PushEndpoint.sendChatIntent(env: env).url + var request = URLRequest(url: url) + request.httpMethod = "POST" + request.setValue("application/json", forHTTPHeaderField: "Content-Type") + request.httpBody = try JSONEncoder().encode(payload) - do { - return try JSONDecoder().decode(Message.self, from: data) - } catch { - print("[Push SDK] - API \(error.localizedDescription)") - throw error - } - } - - static func encryptAndSign( - messageContent: String, senderPgpPrivateKey: String, publicKeys: [String] - ) throws -> (String, String, String) { - - let aesKey = getRandomHexString(length: 15) - let cipherText = try AESCBCHelper.encrypt(messageText: messageContent, secretKey: aesKey) - let encryptedAES = try Pgp.pgpEncryptV2( - message: aesKey, pgpPublicKeys: publicKeys) - - let sig = try Pgp.sign(message: cipherText, privateKey: senderPgpPrivateKey) - - return ( - cipherText, - encryptedAES, - sig - ) - } - - static func signMessage( - messageContent: String, senderPgpPrivateKey: String - ) throws -> String { - - return try Pgp.sign(message: messageContent, privateKey: senderPgpPrivateKey) - } - - static func getPrivateGroupSendMessagePayload( - _ options: SendOptions, - groupInfo: PushChat - .PushGroupInfoDTO - ) async throws - -> SendMessagePayload - { - - var encType = "PlainText" - var (dep_signature, messageConent) = ("", options.messageContent) - var messageObj = try options.getMessageObjJSON() - - let secretKey = try Pgp.pgpDecrypt( - cipherText: groupInfo.encryptedSecret!, toPrivateKeyArmored: options.pgpPrivateKey) - - if groupInfo.encryptedSecret != nil { - // Enc message - encType = "pgpv1:group" - messageConent = try AESCBCHelper.encrypt(messageText: messageConent, secretKey: secretKey) - dep_signature = try Pgp.sign(message: messageConent, privateKey: options.pgpPrivateKey) - messageObj = try AESCBCHelper.encrypt(messageText: messageObj, secretKey: secretKey) - - } else { - dep_signature = try signMessage( - messageContent: messageConent, senderPgpPrivateKey: options.pgpPrivateKey) - } + let (data, res) = try await URLSession.shared.data(for: request) + + guard let httpResponse = res as? HTTPURLResponse else { + throw URLError(.badServerResponse) + } - let dataToHash = try getJsonStringFromKV([ - ("fromDID", options.account), - ("toDID", options.account), - ("fromCAIP10", options.account), - ("toCAIP10", options.receiverAddress), - ("messageObj", messageObj), - ("messageType", options.messageType.rawValue), - ("encType", encType), - ("sessionKey", groupInfo.sessionKey!), - ("encryptedSecret", "null"), - ]) - - let hash = generateSHA256Hash(msg: dataToHash) - let verificationProof = try Pgp.sign(message: hash, privateKey: options.pgpPrivateKey) - - return SendMessagePayload( - fromDID: options.account, toDID: options.receiverAddress, - fromCAIP10: options.account, toCAIP10: options.receiverAddress, - messageContent: messageConent, - messageObj: messageObj, - messageType: options.messageType.rawValue, - signature: dep_signature, encType: encType, encryptedSecret: nil, sigType: "pgpv3", - verificationProof: "pgpv3:\(verificationProof)", - sessionKey: groupInfo.sessionKey) - - } - - static func getSendMessagePayload( - _ options: SendOptions, publicKeys: [String], shouldEncrypt: Bool = true - ) async throws - -> SendMessagePayload - { - - var encType = "PlainText" - var (signature, encryptedSecret, messageConent) = ("", "", options.messageContent) - - if shouldEncrypt { - - encType = "pgp" - - (messageConent, encryptedSecret, signature) = try PushChat.encryptAndSign( - messageContent: options.messageContent, - senderPgpPrivateKey: options.pgpPrivateKey, - publicKeys: publicKeys - ) - - } else { - signature = try signMessage( - messageContent: messageConent, senderPgpPrivateKey: options.pgpPrivateKey) + guard (200 ... 299).contains(httpResponse.statusCode) else { + throw URLError(.badServerResponse) + } + + do { + return try JSONDecoder().decode(Message.self, from: data) + } catch { + print("[Push SDK] - API \(error.localizedDescription)") + throw error + } } - return SendMessagePayload( - fromDID: options.account, toDID: options.receiverAddress, - fromCAIP10: options.account, toCAIP10: options.receiverAddress, - messageContent: messageConent, messageType: options.messageType.rawValue, - signature: signature, encType: encType, encryptedSecret: encryptedSecret, sigType: "pgp") - } - - static func getP2PChatPublicKeys(_ options: SendOptions) async throws -> [String] { - guard - let anotherUser = try await PushUser.get(account: options.receiverAddress, env: options.env) - else { - throw PushChat.ChatError.userNotFound + static func sendMessageService(payload: SendMessagePayload, env: ENV) async throws -> Message { + let url = try PushEndpoint.sendChatMessage(env: env).url + var request = URLRequest(url: url) + request.httpMethod = "POST" + request.setValue("application/json", forHTTPHeaderField: "Content-Type") + request.httpBody = try JSONEncoder().encode(payload) + + let (data, res) = try await URLSession.shared.data(for: request) + + guard let httpResponse = res as? HTTPURLResponse else { + throw URLError(.badServerResponse) + } + + guard (200 ... 299).contains(httpResponse.statusCode) else { + throw URLError(.badServerResponse) + } + + do { + return try JSONDecoder().decode(Message.self, from: data) + } catch { + print("[Push SDK] - API \(error.localizedDescription)") + throw error + } } - guard let senderUser = try await PushUser.get(account: options.account, env: options.env) else { - throw PushChat.ChatError.userNotFound + + static func encryptAndSign( + messageContent: String, senderPgpPrivateKey: String, publicKeys: [String] + ) throws -> (String, String, String) { + let aesKey = getRandomHexString(length: 15) + let cipherText = try AESCBCHelper.encrypt(messageText: messageContent, secretKey: aesKey) + let encryptedAES = try Pgp.pgpEncryptV2( + message: aesKey, pgpPublicKeys: publicKeys) + + let sig = try Pgp.sign(message: cipherText, privateKey: senderPgpPrivateKey) + + return ( + cipherText, + encryptedAES, + sig + ) } - let publicKeys = [senderUser.getPGPPublickey(), anotherUser.getPGPPublickey()] - // validate the public keys else return empty - for pk in publicKeys { - if !pk.contains("-----BEGIN PGP") { - return [] - } + static func signMessage( + messageContent: String, senderPgpPrivateKey: String + ) throws -> String { + return try Pgp.sign(message: messageContent, privateKey: senderPgpPrivateKey) } - return publicKeys - - } - - static func getGroupChatPublicKeys(_ options: SendOptions) async throws -> [String] { - if let group = try await PushChat.getGroup(chatId: options.receiverAddress, env: options.env) { - let isGroupPublic = group.isPublic - if isGroupPublic { - return [] - } else { - let _publicKeys = group.members.compactMap { $0.publicKey } - return _publicKeys - } - } else { - return [] + static func getPrivateGroupSendMessagePayload( + _ options: SendOptions, + groupInfo: PushChat + .PushGroupInfoDTO + ) async throws + -> SendMessagePayload { + var encType = "PlainText" + var (dep_signature, messageConent) = ("", options.messageContent) + var messageObj = try options.getMessageObjJSON() + + let secretKey = try Pgp.pgpDecrypt( + cipherText: groupInfo.encryptedSecret!, toPrivateKeyArmored: options.pgpPrivateKey) + + if groupInfo.encryptedSecret != nil { + // Enc message + encType = "pgpv1:group" + messageConent = try AESCBCHelper.encrypt(messageText: messageConent, secretKey: secretKey) + dep_signature = try Pgp.sign(message: messageConent, privateKey: options.pgpPrivateKey) + messageObj = try AESCBCHelper.encrypt(messageText: messageObj, secretKey: secretKey) + + } else { + dep_signature = try signMessage( + messageContent: messageConent, senderPgpPrivateKey: options.pgpPrivateKey) + } + + let dataToHash = try getJsonStringFromKV([ + ("fromDID", options.account), + ("toDID", options.account), + ("fromCAIP10", options.account), + ("toCAIP10", options.receiverAddress), + ("messageObj", messageObj), + ("messageType", options.messageType.rawValue), + ("encType", encType), + ("sessionKey", groupInfo.sessionKey!), + ("encryptedSecret", "null"), + ]) + + let hash = generateSHA256Hash(msg: dataToHash) + let verificationProof = try Pgp.sign(message: hash, privateKey: options.pgpPrivateKey) + + return SendMessagePayload( + fromDID: options.account, toDID: options.receiverAddress, + fromCAIP10: options.account, toCAIP10: options.receiverAddress, + messageContent: messageConent, + messageObj: messageObj, + messageType: options.messageType.rawValue, + signature: dep_signature, encType: encType, encryptedSecret: nil, sigType: "pgpv3", + verificationProof: "pgpv3:\(verificationProof)", + sessionKey: groupInfo.sessionKey) } - } - static func getAllGroupMembersPublicKeys(_ groupId: String, _ env: ENV) async throws -> [String] { - if let group = try await PushChat.getGroup(chatId: groupId, env: env) { - let _publicKeys = group.members.compactMap { $0.publicKey } + static func getSendMessagePayload( + _ options: SendOptions, publicKeys: [String], shouldEncrypt: Bool = true + ) async throws + -> SendMessagePayload { + var encType = "PlainText" + var (signature, encryptedSecret, messageConent) = ("", "", options.messageContent) + + if shouldEncrypt { + encType = "pgp" + + (messageConent, encryptedSecret, signature) = try PushChat.encryptAndSign( + messageContent: options.messageContent, + senderPgpPrivateKey: options.pgpPrivateKey, + publicKeys: publicKeys + ) - return _publicKeys - } else { - return [] + } else { + signature = try signMessage( + messageContent: messageConent, senderPgpPrivateKey: options.pgpPrivateKey) + } + + return SendMessagePayload( + fromDID: options.account, toDID: options.receiverAddress, + fromCAIP10: options.account, toCAIP10: options.receiverAddress, + messageContent: messageConent, messageType: options.messageType.rawValue, + signature: signature, encType: encType, encryptedSecret: encryptedSecret, sigType: "pgp") } - } - public static func send(_ chatOptions: SendOptions) async throws -> Message { + static func getP2PChatPublicKeys(_ options: SendOptions) async throws -> [String] { + guard + let anotherUser = try await PushUser.get(account: options.receiverAddress, env: options.env) + else { + throw PushChat.ChatError.userNotFound + } + guard let senderUser = try await PushUser.get(account: options.account, env: options.env) else { + throw PushChat.ChatError.userNotFound + } + let publicKeys = [senderUser.getPGPPublickey(), anotherUser.getPGPPublickey()] - let senderAddress = walletToPCAIP10(account: chatOptions.account) - let receiverAddress = walletToPCAIP10(account: chatOptions.receiverAddress) + // validate the public keys else return empty + for pk in publicKeys { + if !pk.contains("-----BEGIN PGP") { + return [] + } + } - if isGroupChatId(receiverAddress) { - return try await PushChat.sendMessage(chatOptions) + return publicKeys } - let isConversationFirst = - try await ConversationHash(conversationId: receiverAddress, account: senderAddress) == nil + static func getGroupChatPublicKeys(_ options: SendOptions) async throws -> [String] { + if let group = try await PushChat.getGroup(chatId: options.receiverAddress, env: options.env) { + let isGroupPublic = group.isPublic + if isGroupPublic { + return [] + } else { + let _publicKeys = group.members.compactMap { $0.publicKey } + return _publicKeys + } + } else { + return [] + } + } - if isConversationFirst { - return try await PushChat.sendIntent(chatOptions) - } else { - // send regular message - return try await PushChat.sendMessage(chatOptions) + static func getAllGroupMembersPublicKeys(_ groupId: String, _ env: ENV) async throws -> [String] { + if let group = try await PushChat.getGroup(chatId: groupId, env: env) { + let _publicKeys = group.members.compactMap { $0.publicKey } + + return _publicKeys + } else { + return [] + } } - } - - public static func sendMessage(_ sendOptions: SendOptions, enctyptMessage: Bool = true) - async throws -> Message - { - let receiverAddress = walletToPCAIP10(account: sendOptions.receiverAddress) - - var publicKeys: [String] = [] - var shouldEncrypt = enctyptMessage - - if shouldEncrypt { - if isGroupChatId(receiverAddress) { - let groupInfo = try await PushChat.getGroupInfoDTO( - chatId: receiverAddress, env: sendOptions.env) - if groupInfo.isPublic { - publicKeys = try await getGroupChatPublicKeys(sendOptions) + + public static func send(_ chatOptions: SendOptions) async throws -> Message { + let senderAddress = walletToPCAIP10(account: chatOptions.account) + let receiverAddress = walletToPCAIP10(account: chatOptions.receiverAddress) + + if isGroupChatId(receiverAddress) { + return try await PushChat.sendMessage(chatOptions) + } + + let isConversationFirst = + try await ConversationHash(conversationId: receiverAddress, account: senderAddress) == nil + + if isConversationFirst { + return try await PushChat.sendIntent(chatOptions) } else { - let payload = try await getPrivateGroupSendMessagePayload( - sendOptions, groupInfo: groupInfo) - return try await sendMessageService(payload: payload, env: sendOptions.env) + // send regular message + return try await PushChat.sendMessage(chatOptions) + } + } + + public static func sendMessage(_ sendOptions: SendOptions, enctyptMessage: Bool = true) + async throws -> Message { + let receiverAddress = walletToPCAIP10(account: sendOptions.receiverAddress) + + var publicKeys: [String] = [] + var shouldEncrypt = enctyptMessage + + if shouldEncrypt { + if isGroupChatId(receiverAddress) { + let groupInfo = try await PushChat.getGroupInfoDTO( + chatId: receiverAddress, env: sendOptions.env) + if groupInfo.isPublic { + publicKeys = try await getGroupChatPublicKeys(sendOptions) + } else { + let payload = try await getPrivateGroupSendMessagePayload( + sendOptions, groupInfo: groupInfo) + return try await sendMessageService(payload: payload, env: sendOptions.env) + } + } else { + publicKeys = try await getP2PChatPublicKeys(sendOptions) + } + + shouldEncrypt = publicKeys.count > 0 ? true : false } - } else { - publicKeys = try await getP2PChatPublicKeys(sendOptions) - } - shouldEncrypt = publicKeys.count > 0 ? true : false + let sendMessagePayload = try await getSendMessagePayload( + sendOptions, publicKeys: publicKeys, shouldEncrypt: shouldEncrypt) + return try await sendMessageService(payload: sendMessagePayload, env: sendOptions.env) } - let sendMessagePayload = try await getSendMessagePayload( - sendOptions, publicKeys: publicKeys, shouldEncrypt: shouldEncrypt) - return try await sendMessageService(payload: sendMessagePayload, env: sendOptions.env) - } + public static func sendIntent(_ sendOptions: SendOptions) async throws -> Message { + // check if user exists + let anotherUser = try await PushUser.get( + account: sendOptions.receiverAddress, env: sendOptions.env) - public static func sendIntent(_ sendOptions: SendOptions) async throws -> Message { - // check if user exists - let anotherUser = try await PushUser.get( - account: sendOptions.receiverAddress, env: sendOptions.env) + var shouldEncrypt = true - var shouldEncrypt = true + // else create the user frist and send unencrypted intent message + if anotherUser == nil || anotherUser?.publicKey == nil { + let _ = try await PushUser.createUserEmpty( + userAddress: sendOptions.receiverAddress, env: sendOptions.env) + + shouldEncrypt = false + } - // else create the user frist and send unencrypted intent message - if anotherUser == nil || anotherUser?.publicKey == nil { - let _ = try await PushUser.createUserEmpty( - userAddress: sendOptions.receiverAddress, env: sendOptions.env) + let publicKeys = shouldEncrypt ? try await getP2PChatPublicKeys(sendOptions) : [] + let sendMessagePayload = try await getSendMessagePayload( + sendOptions, publicKeys: publicKeys, shouldEncrypt: shouldEncrypt) - shouldEncrypt = false + return try await sendIntentService(payload: sendMessagePayload, env: sendOptions.env) } - let publicKeys = shouldEncrypt ? try await getP2PChatPublicKeys(sendOptions) : [] - let sendMessagePayload = try await getSendMessagePayload( - sendOptions, publicKeys: publicKeys, shouldEncrypt: shouldEncrypt) - - return try await sendIntentService(payload: sendMessagePayload, env: sendOptions.env) - } - - static func getApprovePayloadCore(_ approveOptions: ApproveOptions) async throws - -> ApproveRequestPayload - { - if approveOptions.isGroupChat { - // TODO: remove unwrap - let groupInfo = try await PushChat.getGroupInfoDTO( - chatId: approveOptions.toDID, env: approveOptions.env) - if !groupInfo.isPublic { - return try await getApprovePayloadPrivateGroup(approveOptions, groupInfo) - } + static func getApprovePayloadCore(_ approveOptions: ApproveOptions) async throws + -> ApproveRequestPayload { + if approveOptions.isGroupChat { + // TODO: remove unwrap + let groupInfo = try await PushChat.getGroupInfoDTO( + chatId: approveOptions.toDID, env: approveOptions.env) + if !groupInfo.isPublic { + return try await getApprovePayloadPrivateGroup(approveOptions, groupInfo) + } + } + + let acceptIntentPayload = try await getApprovePayload(approveOptions) + return acceptIntentPayload } - let acceptIntentPayload = try await getApprovePayload(approveOptions) - return acceptIntentPayload - } + public static func approve(_ approveOptions: ApproveOptions) async throws -> String { + let acceptIntentPayload = try await getApprovePayloadCore(approveOptions) + let url = try PushEndpoint.acceptChatRequest(env: approveOptions.env).url - public static func approve(_ approveOptions: ApproveOptions) async throws -> String { + var request = URLRequest(url: url) + request.httpMethod = "PUT" + request.setValue("application/json", forHTTPHeaderField: "Content-Type") + request.httpBody = try JSONEncoder().encode(acceptIntentPayload) - let acceptIntentPayload = try await getApprovePayloadCore(approveOptions) - let url = try PushEndpoint.acceptChatRequest(env: approveOptions.env).url + let (data, res) = try await URLSession.shared.data(for: request) - var request = URLRequest(url: url) - request.httpMethod = "PUT" - request.setValue("application/json", forHTTPHeaderField: "Content-Type") - request.httpBody = try JSONEncoder().encode(acceptIntentPayload) + guard let httpResponse = res as? HTTPURLResponse else { + throw URLError(.badServerResponse) + } - let (data, res) = try await URLSession.shared.data(for: request) + guard (200 ... 299).contains(httpResponse.statusCode) else { + throw URLError(.badServerResponse) + } - guard let httpResponse = res as? HTTPURLResponse else { - throw URLError(.badServerResponse) + return try data.toString() } - guard (200...299).contains(httpResponse.statusCode) else { - throw URLError(.badServerResponse) - } + static func getApprovePayload(_ approveOptions: ApproveOptions) async throws + -> ApproveRequestPayload { + struct AcceptHashData: Encodable { + var fromDID: String + var toDID: String + var status: String + } + + let apiData = AcceptHashData( + fromDID: approveOptions.fromDID, + toDID: approveOptions.toDID, status: "Approved") + + let jsonString = + "{\"fromDID\":\"\(apiData.fromDID)\",\"toDID\":\"\(apiData.toDID)\",\"status\":\"\(apiData.status)\"}" + let hash = generateSHA256Hash( + msg: jsonString + ) - return try data.toString() - } + let sig = try Pgp.sign(message: hash, privateKey: approveOptions.privateKey) - static func getApprovePayload(_ approveOptions: ApproveOptions) async throws - -> ApproveRequestPayload - { - struct AcceptHashData: Encodable { - var fromDID: String - var toDID: String - var status: String + return ApproveRequestPayload( + fromDID: approveOptions.fromDID, toDID: approveOptions.toDID, signature: sig, + status: "Approved", sigType: "pgp", verificationProof: "pgp:\(sig)") } - let apiData = AcceptHashData( - fromDID: approveOptions.fromDID, - toDID: approveOptions.toDID, status: "Approved") - - let jsonString = - "{\"fromDID\":\"\(apiData.fromDID)\",\"toDID\":\"\(apiData.toDID)\",\"status\":\"\(apiData.status)\"}" - let hash = generateSHA256Hash( - msg: jsonString - ) - - let sig = try Pgp.sign(message: hash, privateKey: approveOptions.privateKey) - - return ApproveRequestPayload( - fromDID: approveOptions.fromDID, toDID: approveOptions.toDID, signature: sig, - status: "Approved", sigType: "pgp", verificationProof: "pgp:\(sig)") - } - - static func getApprovePayloadPrivateGroup( - _ approveOptions: ApproveOptions, _ groupInfo: PushChat.PushGroupInfoDTO - ) async throws -> ApproveRequestPayload { - let secretKey = getRandomHexString(length: 15) - let senderPublicKey = try await PushUser.get( - account: approveOptions.fromDID, env: approveOptions.env)!.getPGPPublickey() - - var groupMembersPublicKeys = try await getAllGroupMembersPublicKeys( - groupInfo.chatId, approveOptions.env) - groupMembersPublicKeys.append(senderPublicKey) - - let encryptedSecret = try Pgp.pgpEncryptV2( - message: secretKey, pgpPublicKeys: groupMembersPublicKeys) - - // let publicKeys = grou - let sigType = "pgpv2" - - let bodyToBeHashed = try getJsonStringFromKV([ - ("fromDID", approveOptions.fromDID), - ("toDID", approveOptions.toDID), - ("status", "Approved"), - ("encryptedSecret", encryptedSecret), - ]) - - let hash = generateSHA256Hash(msg: bodyToBeHashed) - - let signature = try Pgp.sign(message: hash, privateKey: approveOptions.privateKey) - let verificationProof = "\(sigType):\(signature)" - - let approvePayload = ApproveRequestPayload( - fromDID: approveOptions.fromDID, - toDID: approveOptions.toDID, - signature: signature, - // status: "Approved", - status: "Approved", - sigType: sigType, - verificationProof: verificationProof, encryptedSecret: encryptedSecret - ) - - return approvePayload - } - - public struct ApproveOptions { - var fromDID: String - var toDID: String - var privateKey: String - var env: ENV - var isGroupChat: Bool - - public init(requesterAddress: String, approverAddress: String, privateKey: String, env: ENV) { - self.fromDID = walletToPCAIP10(account: requesterAddress) - self.toDID = walletToPCAIP10(account: approverAddress) - self.privateKey = privateKey - self.env = env - - self.isGroupChat = isGroupChatId(requesterAddress) - - if isGroupChatId(requesterAddress) { - self.toDID = walletToPCAIP10(account: requesterAddress) - self.fromDID = walletToPCAIP10(account: approverAddress) - } + static func getApprovePayloadPrivateGroup( + _ approveOptions: ApproveOptions, _ groupInfo: PushChat.PushGroupInfoDTO + ) async throws -> ApproveRequestPayload { + let secretKey = getRandomHexString(length: 15) + let senderPublicKey = try await PushUser.get( + account: approveOptions.fromDID, env: approveOptions.env)!.getPGPPublickey() + + var groupMembersPublicKeys = try await getAllGroupMembersPublicKeys( + groupInfo.chatId, approveOptions.env) + groupMembersPublicKeys.append(senderPublicKey) + + let encryptedSecret = try Pgp.pgpEncryptV2( + message: secretKey, pgpPublicKeys: groupMembersPublicKeys) + + // let publicKeys = grou + let sigType = "pgpv2" + + let bodyToBeHashed = try getJsonStringFromKV([ + ("fromDID", approveOptions.fromDID), + ("toDID", approveOptions.toDID), + ("status", "Approved"), + ("encryptedSecret", encryptedSecret), + ]) + + let hash = generateSHA256Hash(msg: bodyToBeHashed) + + let signature = try Pgp.sign(message: hash, privateKey: approveOptions.privateKey) + let verificationProof = "\(sigType):\(signature)" + + let approvePayload = ApproveRequestPayload( + fromDID: approveOptions.fromDID, + toDID: approveOptions.toDID, + signature: signature, + // status: "Approved", + status: "Approved", + sigType: sigType, + verificationProof: verificationProof, encryptedSecret: encryptedSecret + ) + + return approvePayload } - } - - struct ApproveRequestPayload: Codable { - var fromDID: String - var toDID: String - var signature: String - var status: String = "Approved" - var sigType: String - var verificationProof: String - var encryptedSecret: String? - } + public struct ApproveOptions { + var fromDID: String + var toDID: String + var privateKey: String + var env: ENV + var isGroupChat: Bool + + public init(requesterAddress: String, approverAddress: String, privateKey: String, env: ENV) { + fromDID = walletToPCAIP10(account: requesterAddress) + toDID = walletToPCAIP10(account: approverAddress) + self.privateKey = privateKey + self.env = env + + isGroupChat = isGroupChatId(requesterAddress) + + if isGroupChatId(requesterAddress) { + toDID = walletToPCAIP10(account: requesterAddress) + fromDID = walletToPCAIP10(account: approverAddress) + } + } + } + + struct ApproveRequestPayload: Codable { + var fromDID: String + var toDID: String + var signature: String + var status: String = "Approved" + var sigType: String + var verificationProof: String + var encryptedSecret: String? + } } diff --git a/Sources/Chat/SendV2.swift b/Sources/Chat/SendV2.swift new file mode 100644 index 0000000..9e6493e --- /dev/null +++ b/Sources/Chat/SendV2.swift @@ -0,0 +1,436 @@ +import Foundation + +extension PushChat { + struct ComputedOptions { + public var messageType: MessageType + public var messageObj: SendMessage + public var account: String + public var to: String + public var pgpPrivateKey: String? + public var env: ENV + } + + static func computeOptions(_ options: SendOptions) throws -> ComputedOptions { + let messageType = options.message?.type ?? options.messageType + var messageObj: SendMessage? = options.message + + if messageObj == nil { + if ![.Text, .Image, .File, .MediaEmbed].contains(messageType) { + fatalError("Options.message is required") + } + // use messageContent for backwards compatibility + messageObj = SendMessage(content: options.messageContent, type: messageType) + } + + let to = options.to ?? options.receiverAddress + + if to.isEmpty { + fatalError("Options.to is required") + } + +// // Parse Reply Message +// if messageType == MessageType.Reply { +// if let replyContent = (messageObj as? SendMessage)?.replyContent { +// (messageObj as? SendMessage)?.replyContent = NestedContent.fromNestedContent(replyContent) +// } else { +// throw NSError(domain: "AppErrorDomain", code: -1, userInfo: [NSLocalizedDescriptionKey: "Options.message is not properly defined for Reply"]) +// } +// } + +// // Parse Composite Message +// if messageType == MessageType.COMPOSITE.rawValue { +// if let compositeContent = (messageObj as? SendMessage)?.compositeContent { +// (messageObj as? SendMessage)?.compositeContent = compositeContent.map { nestedContent in +// NestedContent.fromNestedContent(nestedContent) +// } +// } else { +// throw NSError(domain: "AppErrorDomain", code: -1, userInfo: [NSLocalizedDescriptionKey: "Options.message is not properly defined for Composite"]) +// } +// } + + return ComputedOptions( + messageType: messageType, + messageObj: messageObj!, + account: options.account, + to: to, + pgpPrivateKey: options.pgpPrivateKey, + env: options.env + ) + } + + static func validateSendOptions(options: ComputedOptions) throws { + guard isValidETHAddress(address: options.account) else { + fatalError("Invalid address \(options.account)") + } + + guard options.pgpPrivateKey != nil else { + fatalError("Please ensure that 'pgpPrivateKey' is properly defined.") + } + + if options.messageObj.content.isEmpty { + fatalError("Cannot send empty message") + } + } + + public struct SendOptionsV2 { + public var message: SendMessage? + +// @available(*, deprecated, message: "Use message.content instead") + public var messageContent = "" + +// @available(*, deprecated, message: "Use message.type instead") + public var messageType: MessageType + +// @available(*, deprecated, message: "Use to instead") + public var receiverAddress: String + + public var to: String? + public var account: String + public var pgpPrivateKey: String + public var senderPgpPubicKey: String? + public var receiverPgpPubicKey: String? + public var processMessage: String? + public var reference: String? + public var env: ENV = .STAGING + + public enum Reactions: String { + case THUMBSUP = "\u{1F44D}" + case THUMBSDOWN = "\u{1F44E}" + case HEART = "\u{2764}\u{FE0F}" + case CLAP = "\u{1F44F}" + case LAUGH = "\u{1F602}" + case SAD = "\u{1F622}" + case ANGRY = "\u{1F621}" + case SURPRISE = "\u{1F632}" + case FIRE = "\u{1F525}" + } + + public init( + message: SendMessage? = nil, + messageContent: String, messageType: String, receiverAddress: String, account: String, + pgpPrivateKey: String, refrence: String? = nil, env: ENV = .STAGING, to: String? = nil + ) { + self.messageContent = messageContent + self.messageType = MessageType(rawValue: messageType)! + self.receiverAddress = walletToPCAIP10(account: receiverAddress) + self.account = walletToPCAIP10(account: account) + self.pgpPrivateKey = pgpPrivateKey + reference = refrence + self.env = env + self.to = to + self.message = message + } + + public func getMessageObjJSON() throws -> String { + switch messageType { + case .Text, .Image, .MediaEmbed: + return try getJsonStringFromKV([ + ("content", messageContent), + ]) + case .Reaction: + return try getJsonStringFromKV([ + ("content", messageContent), + ("refrence", reference!), + ]) + case .Reply: + return """ + {"content":{"messageType":"Text","messageObj":{"content":"\(messageContent)"}},"reference":"\(reference!)"} + """.trimmingCharacters(in: .whitespaces) + case .Video, .Audio, .File, .Meta, .Composite, .Receipt, .UserActivity, .Intent, .Payment: + return try getJsonStringFromKV([ + ("content", messageContent), + ("refrence", reference!), + ]) + } + } + } + + public static func sendV2(chatOptions: SendOptions) async throws -> MessageV2 { + let computedOptions = try computeOptions(chatOptions) + + try validateSendOptions(options: computedOptions) + + let isGroup = isGroupChatId(computedOptions.to) + var groupInfo: PushGroupInfoDTO? + + if isGroup { + groupInfo = try await getGroupInfoDTO(chatId: computedOptions.to, env: computedOptions.env) + } + + let conversationHashResponse = + try await ConversationHash(conversationId: computedOptions.to, + account: computedOptions.account) + let isIntent = !isGroup && conversationHashResponse == nil + + let senderAccount = try? await PushUser.get(account: computedOptions.account, env: computedOptions.env) + + if senderAccount == nil { + fatalError("Cannot get sender account.") + } + + var messageContent: String + if computedOptions.messageType == .Reply || + computedOptions.messageType == .Composite { + messageContent = "MessageType Not Supported by this sdk version. Plz upgrade !!!" + } else { + messageContent = computedOptions.messageObj.content + } + + let payload = + try await sendMessagePayloadCore( + receiverAddress: computedOptions.to, + senderAddress: computedOptions.account, + senderPgpPrivateKey: computedOptions.pgpPrivateKey!, + messageType: computedOptions.messageType, + messageContent: messageContent, + messageObj: computedOptions.messageObj, + group: groupInfo, + isGroup: isGroup, + env: computedOptions.env) + +// if isIntent { +// return try await sendIntentService(payload: sendMessagePayload, env: computedOptions.env) +// } else { +// return try await sendMessageService(payload: sendMessagePayload, env: computedOptions.env) +// } + + let url = isIntent ? try PushEndpoint.sendChatIntent(env: computedOptions.env).url : try PushEndpoint.sendChatMessage(env: computedOptions.env).url + + var request = URLRequest(url: url) + request.httpMethod = "POST" + request.setValue("application/json", forHTTPHeaderField: "Content-Type") + request.httpBody = try JSONEncoder().encode(payload) + print("url: \(url)") + + let body = try JSONEncoder().encode(payload) + if let httpBodyString = String(data: body, encoding: .utf8) { + print(httpBodyString) + } + + let (data, res) = try await URLSession.shared.data(for: request) + + guard let httpResponse = res as? HTTPURLResponse else { + throw URLError(.badServerResponse) + } + + guard (200 ... 299).contains(httpResponse.statusCode) else { + throw URLError(.badServerResponse) + } + + do { + return try JSONDecoder().decode(MessageV2.self, from: data) + } catch { + print("[Push SDK] - API \(error.localizedDescription)") + throw error + } + } + + static func sendMessagePayloadCore( + receiverAddress: String, + senderAddress: String, + senderPgpPrivateKey: String, + messageType: MessageType, + messageContent: String, + messageObj: SendMessage, + group: PushGroupInfoDTO?, + isGroup: Bool, + env: ENV + ) async throws + -> SendMessagePayloadV2 { + var secretKey: String + + if isGroup, group != nil, group?.encryptedSecret != nil, group?.sessionKey != nil { + secretKey = try Pgp.pgpDecrypt(cipherText: group!.encryptedSecret!, toPrivateKeyArmored: senderPgpPrivateKey) + } else { + secretKey = AESGCMHelper.generateRandomSecret(length: 15) + } + + let encryptedMessageContentData = try await PayloadHelper.getEncryptedRequestCore( + receiverAddress: receiverAddress, + senderAddress: senderAddress, + senderPgpPrivateKey: senderPgpPrivateKey, + message: messageContent, + isGroup: isGroup, + group: group, + env: env, + secretKey: secretKey + ) + + let encryptedMessageContent = encryptedMessageContentData.message + let deprecatedSignature = encryptedMessageContentData.signature + + let encryptedMessageObjData = try await PayloadHelper.getEncryptedRequestCore( + receiverAddress: receiverAddress, + senderAddress: senderAddress, + senderPgpPrivateKey: senderPgpPrivateKey, + message: messageObj.toJson(), + isGroup: isGroup, + group: group, env: env, + secretKey: secretKey + ) + let encryptedMessageObj = encryptedMessageObjData.message + + let encryptionType = encryptedMessageObjData.encryptionType + + let encryptedMessageObjSecret = encryptedMessageObjData.aesEncryptedSecret + + var body = SendMessagePayloadV2( + fromDID: walletToPCAIP10(account: senderAddress), + toDID: isGroup ? receiverAddress : walletToPCAIP10(account: receiverAddress), + fromCAIP10: walletToPCAIP10(account: senderAddress), + toCAIP10: isGroup ? receiverAddress : walletToPCAIP10(account: receiverAddress), + messageContent: encryptedMessageContent, + messageObj: encryptionType == "PlainText" + ? messageObj + : encryptedMessageObj, + messageType: messageType.rawValue, + signature: deprecatedSignature, + encType: encryptionType, + encryptedSecret: encryptedMessageObjSecret, + sigType: "pgpv3", + sessionKey: + (group != nil && group?.isPublic == false && encryptionType == "pgpv1:group") + ? (group?.sessionKey ?? nil) : nil + ) + + let messageObjKey = "--body.messageObj--" + var bodyToBehashed = try getJsonStringFromKV([ + ("fromDID", body.fromDID), + ("toDID", body.fromDID), // TODO: correct this later + ("fromCAIP10", body.fromCAIP10), + ("toCAIP10", body.toCAIP10), + ("messageObj", messageObjKey), + ("messageType", body.messageType), + ("encType", body.encType), + ("sessionKey", body.sessionKey ?? "null"), + ("encryptedSecret", body.encryptedSecret ?? "null"), + ]) + bodyToBehashed = bodyToBehashed.replacingOccurrences(of: "\"\(messageObjKey)\"", + with:encryptionType == "PlainText" ? try messageObj.toJson(): "\"\(encryptedMessageObj)\"" ) + + let hash = generateSHA256Hash(msg: bodyToBehashed) + + let signature = try Pgp.sign(message: hash, privateKey: senderPgpPrivateKey) + body.verificationProof = "pgpv3:\(signature)" + + return body + } + + public static func removeVersionFromPublicKey(_ key: String) -> String { + var lines = key.components(separatedBy: "\n") + + lines.removeAll { line in + line.trimmingCharacters(in: .whitespacesAndNewlines).starts(with: "Version:") + } + + return lines.joined(separator: "\n") + } + + struct SendMessagePayloadV2: Encodable { + var fromDID: String + var toDID: String + var fromCAIP10: String + var toCAIP10: String + var messageContent: String + var messageObj: Any? + var messageType: String + var signature: String + var encType: String + var encryptedSecret: String? + var sigType: String + var verificationProof: String? + var sessionKey: String? + + private enum CodingKeys: String, CodingKey { + case fromDID, toDID, fromCAIP10, toCAIP10, messageContent, messageObj, messageType, signature, + encType, + encryptedSecret, sigType, verificationProof, sessionKey + } + + func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + + try container.encode(fromDID, forKey: .fromDID) + try container.encode(toDID, forKey: .toDID) + try container.encode(fromCAIP10, forKey: .fromCAIP10) + try container.encode(toCAIP10, forKey: .toCAIP10) + try container.encode(messageContent, forKey: .messageContent) + + + if let objString = messageObj as? String { + try container.encode(objString, forKey: .messageObj) + } else if let obj = messageObj as? SendMessage { + try container.encode(obj, forKey: .messageObj) + } + + try container.encode(messageType, forKey: .messageType) + try container.encode(signature, forKey: .signature) + try container.encode(encType, forKey: .encType) + try container.encode(encryptedSecret, forKey: .encryptedSecret) + + try container.encode(sigType, forKey: .sigType) + try container.encode(verificationProof, forKey: .verificationProof) + try container.encode(sessionKey, forKey: .sessionKey) + } + } + + + +} + + +public struct MessageV2: Codable { + public var fromCAIP10: String + public var toCAIP10: String + public var fromDID: String + public var toDID: String + public var messageType: String + public var messageContent: String + public var messageObj: MessageObj? // Define a type that can represent both String and JSON + public var signature: String + public var sigType: String + public var timestamp: Int? + public var encType: String + public var encryptedSecret: String? + public var link: String? + public var cid: String? + public var sessionKey: String? + + // Implement a custom init(from:) initializer for decoding + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + // Decode properties from the container + fromCAIP10 = try container.decode(String.self, forKey: .fromCAIP10) + toCAIP10 = try container.decode(String.self, forKey: .toCAIP10) + fromDID = try container.decode(String.self, forKey: .fromDID) + toDID = try container.decode(String.self, forKey: .toDID) + messageType = try container.decode(String.self, forKey: .messageType) + messageContent = try container.decode(String.self, forKey: .messageContent) + signature = try container.decode(String.self, forKey: .signature) + sigType = try container.decode(String.self, forKey: .sigType) + timestamp = try container.decodeIfPresent(Int.self, forKey: .timestamp) + encType = try container.decode(String.self, forKey: .encType) + encryptedSecret = try container.decodeIfPresent(String.self, forKey: .encryptedSecret) + link = try container.decodeIfPresent(String.self, forKey: .link) + cid = try container.decodeIfPresent(String.self, forKey: .cid) + sessionKey = try container.decodeIfPresent(String.self, forKey: .sessionKey) + + do { + self.messageObj = try container.decodeIfPresent( MessageObj.self, forKey: .messageObj); + }catch{ + do { + let stringValue = try container.decodeIfPresent(String?.self, forKey: .messageObj) + + self.messageObj = MessageObj(content: stringValue ?? nil) + } catch{ + self.messageObj = nil + } + } + } +} + + +public struct MessageObj: Codable { + let content: String? +} diff --git a/Sources/Chat/helpers/PayloadHelper.swift b/Sources/Chat/helpers/PayloadHelper.swift new file mode 100644 index 0000000..ec8eac6 --- /dev/null +++ b/Sources/Chat/helpers/PayloadHelper.swift @@ -0,0 +1,152 @@ +import Foundation + +enum PayloadHelper { + public struct IEncryptedRequest { + let message: String + let encryptionType: String + let aesEncryptedSecret: String + let signature: String + + init(message: String, encryptionType: String, aesEncryptedSecret: String, signature: String) { + self.message = message + self.encryptionType = encryptionType + self.aesEncryptedSecret = aesEncryptedSecret + self.signature = signature + } + } + + public static func getEncryptedRequestCore( + receiverAddress: String, + senderAddress: String, + senderPgpPrivateKey: String, + message: String, + isGroup: Bool, + group: PushChat.PushGroupInfoDTO?, + env: ENV, + secretKey: String + ) async throws -> IEncryptedRequest { + let senderCreatedUser = try await PushUser.get(account: senderAddress, env: env) + + if !isGroup { + if !isValidETHAddress(address: receiverAddress) { + fatalError("Invalid receiver address!") + } + + let receiverCreatedUser = try await PushUser.get(account: receiverAddress, env: env) + if receiverCreatedUser?.publicKey == nil { + let _ = try await PushUser.createUserEmpty(userAddress: receiverAddress, env: env) + // If the user is being created here, that means that user don't have a PGP keys. So this intent will be in plaintext + let signature = try await signMessageWithPGPCore(message: message, privateKeyArmored: senderPgpPrivateKey) + + return IEncryptedRequest( + message: message, + encryptionType: "PlainText", + aesEncryptedSecret: "", + signature: signature + ) + } else { + // It's possible for a user to be created but the PGP keys still not created + if !receiverCreatedUser!.publicKey + .contains("-----BEGIN PGP PUBLIC KEY BLOCK-----") + { + let signature = try await signMessageWithPGPCore(message: message, privateKeyArmored: senderPgpPrivateKey) + + return IEncryptedRequest( + message: message, + encryptionType: "PlainText", + aesEncryptedSecret: "", + signature: signature + ) + + } else { + let core = try await encryptAndSignCore( + plainText: message, + keys: [receiverCreatedUser!.getPGPPublickey(), senderCreatedUser!.getPGPPublickey()], + senderPgpPrivateKey: senderPgpPrivateKey, + secretKey: secretKey + ) + + return IEncryptedRequest( + message: core["cipherText"]!, + encryptionType: "pgp", + aesEncryptedSecret: core["encryptedSecret"]!, + signature: core["signature"]! + ) + } + } + } else if group != nil { + if group!.isPublic { + let signature = try await signMessageWithPGPCore(message: message, privateKeyArmored: senderPgpPrivateKey) + + return IEncryptedRequest( + message: message, + encryptionType: "PlainText", + aesEncryptedSecret: "", + signature: signature + ) + } else { + // Private Groups + + // 1. Private Groups with session keys + if group?.sessionKey != nil && group?.encryptedSecret != nil { + let cipherText = try AESCBCHelper.encrypt(messageText: message, secretKey: secretKey) + + let signature = try Pgp.sign(message: cipherText, privateKey: senderPgpPrivateKey) + + return IEncryptedRequest( + message: message, + encryptionType: "pgpv1:group", + aesEncryptedSecret: "", + signature: signature + ) + } else { + let members = try await PushChat.getAllGroupMembersPublicKeysV2(chatId: group!.chatId, env: env) + + let publicKeys = members!.map { $0.publicKey } + + let core = try await encryptAndSignCore( + plainText: message, + keys: publicKeys, + senderPgpPrivateKey: senderPgpPrivateKey, + secretKey: secretKey + ) + + return IEncryptedRequest( + message: core["cipherText"]!, + encryptionType: "pgp", + aesEncryptedSecret: core["encryptedSecret"]!, + signature: core["signature"]! + ) + } + } + } else { + fatalError("Unable to find Group Data") + } + } + + public static func signMessageWithPGPCore(message: String, privateKeyArmored: String) async throws -> String { + return try Pgp.sign(message: message, privateKey: privateKeyArmored) + } + + public static func encryptAndSignCore( + plainText: String, + keys: [String], + senderPgpPrivateKey: String, + secretKey: String + ) async throws -> [String: String] { + let cipherText = try AESCBCHelper.encrypt(messageText: plainText, secretKey: secretKey) + + let encryptedSecret = try Pgp.pgpEncryptV2(message: secretKey, pgpPublicKeys: keys) + + let signature = try Pgp.sign(message: cipherText, privateKey: senderPgpPrivateKey) + + let result = [ + "cipherText": cipherText, + "encryptedSecret": encryptedSecret, + "signature": signature, + "sigType": "pgp", + "encType": "pgp", + ] + return result + } +} diff --git a/Sources/Chat/helpers/aes.swift b/Sources/Chat/helpers/aes.swift new file mode 100644 index 0000000..38f2ed0 --- /dev/null +++ b/Sources/Chat/helpers/aes.swift @@ -0,0 +1,69 @@ +import CryptoSwift +import Foundation + + +import CommonCrypto + +enum CryptoError: Error { + case encryptionFailed + case decryptionFailed +} + +public func encryptionAESModeECB( data: Data, key: String) -> String? { + guard let keyData = key.data(using: String.Encoding.utf8) else { return nil } + guard let cryptData = NSMutableData(length: Int((data.count)) + kCCBlockSizeAES128) else { return nil } + + let keyLength = size_t(kCCKeySizeAES128) + let operation: CCOperation = UInt32(kCCEncrypt) + let algoritm: CCAlgorithm = UInt32(kCCAlgorithmAES) + let options: CCOptions = UInt32(kCCOptionECBMode + kCCOptionPKCS7Padding) + let iv: String = "" + + var numBytesEncrypted: size_t = 0 + + let cryptStatus = CCCrypt(operation, + algoritm, + options, + (keyData as NSData).bytes, keyLength, + iv, + (data as NSData).bytes, data.count, + cryptData.mutableBytes, cryptData.length, + &numBytesEncrypted) + + if UInt32(cryptStatus) == UInt32(kCCSuccess) { + cryptData.length = Int(numBytesEncrypted) + let encryptedString = cryptData.base64EncodedString(options: .lineLength64Characters) + return encryptedString +// return encryptedString.data(using: .utf8) + } else { + return nil + } +} + +public func aesEncrypt(plainText: String, secretKey: String) -> String? { + guard let keyData = secretKey.data(using: .utf8), + let encrypted = try? AES(key: keyData.bytes, blockMode: ECB()).encrypt(plainText.bytes) else { + return nil + } + return encrypted.toBase64() +} + + +func aesDecrypt(cipherText: String, secretKey: String) throws -> String { + guard let data = Data(base64Encoded: cipherText) else { + fatalError("decryption failed") + } + guard let aes = try? AES(key: secretKey.bytes, blockMode: ECB(), padding: .pkcs7) else { + fatalError("decryption failed") + } + let decryptedData = try aes.decrypt(data.bytes) + guard let decryptedString = String(bytes: decryptedData, encoding: .utf8) else { + fatalError("decryption failed") + } + return decryptedString +} + +func generateRandomSecret(length: Int) -> String { + let characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789" + return String((0.. Self { - return PushEndpoint( - env: env, - path: "chat/users/\(did)/chats", - queryItems: [ - URLQueryItem( - name: "page", - value: String(page) - ), - URLQueryItem( - name: "limit", - value: String(limit) - ), - ] - ) - } - - static func getConversationHash( - converationId: String, - account: String, - env: ENV - ) throws -> Self { - return PushEndpoint( - env: env, - path: "chat/users/\(account)/conversations/\(converationId)/hash" - ) - } - - static func getConversationHashReslove( - threadHash: String, - fetchLimit: Int, - env: ENV - ) throws -> Self { - return PushEndpoint( - env: env, - path: "chat/conversationhash/\(threadHash)", - queryItems: [ - URLQueryItem( - name: "fetchLimit", - value: "\(fetchLimit)" - ) - ] - ) - } - - static func sendChatIntent( - env: ENV - ) throws -> Self { - return PushEndpoint( - env: env, - path: "chat/request" - ) - } - - static func sendChatMessage( - env: ENV - ) throws -> Self { - return PushEndpoint( - env: env, - path: "chat/message" - ) - } - - static func acceptChatRequest( - env: ENV - ) throws -> Self { - return PushEndpoint( - env: env, - path: "chat/request/accept" - ) - } - - static func createChatGroup( - env: ENV - ) throws -> Self { - return PushEndpoint( - env: env, - path: "chat/groups", - apiVersion: "v2" - ) - } - - static func updatedChatGroup( - chatId: String, - env: ENV - ) throws -> Self { - return PushEndpoint( - env: env, - path: "chat/groups/\(chatId)" - ) - } - - static func getGroup( - chatId: String, - apiVersion: String = "v1", - env: ENV - ) throws -> Self { - return PushEndpoint( - env: env, - path: "chat/groups/\(chatId)", - apiVersion: apiVersion - ) - } - - static func getGroupSession( - chatId: String, - apiVersion: String = "v1", - env: ENV - ) throws -> Self { - return PushEndpoint( - env: env, - path: "chat/encryptedsecret/sessionKey/\(chatId)", - apiVersion: apiVersion - ) - } + static func getChats( + did: String, + page: Int, + limit: Int, + env: ENV + ) throws -> Self { + return PushEndpoint( + env: env, + path: "chat/users/\(did)/chats", + queryItems: [ + URLQueryItem( + name: "page", + value: String(page) + ), + URLQueryItem( + name: "limit", + value: String(limit) + ), + ] + ) + } + + static func getConversationHash( + converationId: String, + account: String, + env: ENV + ) throws -> Self { + return PushEndpoint( + env: env, + path: "chat/users/\(account)/conversations/\(converationId)/hash" + ) + } + + static func getConversationHashReslove( + threadHash: String, + fetchLimit: Int, + env: ENV + ) throws -> Self { + return PushEndpoint( + env: env, + path: "chat/conversationhash/\(threadHash)", + queryItems: [ + URLQueryItem( + name: "fetchLimit", + value: "\(fetchLimit)" + ), + ] + ) + } + + static func sendChatIntent( + env: ENV + ) throws -> Self { + return PushEndpoint( + env: env, + path: "chat/request" + ) + } + + static func sendChatMessage( + env: ENV + ) throws -> Self { + return PushEndpoint( + env: env, + path: "chat/message" + ) + } + + static func acceptChatRequest( + env: ENV + ) throws -> Self { + return PushEndpoint( + env: env, + path: "chat/request/accept" + ) + } + + static func createChatGroup( + env: ENV + ) throws -> Self { + return PushEndpoint( + env: env, + path: "chat/groups", + apiVersion: "v2" + ) + } + + static func updatedChatGroup( + chatId: String, + env: ENV + ) throws -> Self { + return PushEndpoint( + env: env, + path: "chat/groups/\(chatId)" + ) + } + + static func getGroup( + chatId: String, + apiVersion: String = "v1", + env: ENV + ) throws -> Self { + return PushEndpoint( + env: env, + path: "chat/groups/\(chatId)", + apiVersion: apiVersion + ) + } + + static func getGroupMemberCount( + chatId: String, + apiVersion: String = "v1", + env: ENV + ) throws -> Self { + return PushEndpoint( + env: env, + path: "chat/groups/\(chatId)/members/count", + apiVersion: apiVersion + ) + } + + static func getGroupMemberStatus( + chatId: String, + did: String, + apiVersion: String = "v1", + env: ENV + ) throws -> Self { + return PushEndpoint( + env: env, + path: "chat/groups/\(chatId)/members/\(did)/status", + apiVersion: apiVersion + ) + } + + static func getGroupAccess( + chatId: String, + did: String, + apiVersion: String = "v1", + env: ENV + ) throws -> Self { + return PushEndpoint( + env: env, + path: "chat/groups/\(chatId)/access/\(did)", + apiVersion: apiVersion + ) + } + + static func getGroupSession( + chatId: String, + apiVersion: String = "v1", + env: ENV + ) throws -> Self { + return PushEndpoint( + env: env, + path: "chat/encryptedsecret/sessionKey/\(chatId)", + apiVersion: apiVersion + ) + } + + static func updateGroupMembers( + chatId: String, + apiVersion: String = "v1", + env: ENV + ) throws -> Self { + return PushEndpoint( + env: env, + path: "chat/groups/\(chatId)/members", + apiVersion: apiVersion + ) + } + + static func getGroupMembers( + options: PushChat.FetchChatGroupInfo, + apiVersion: String = "v1", + env: ENV + ) throws -> Self { + let path = "chat/groups/\(options.chatId)/members" + var query = [URLQueryItem]() + query.append(URLQueryItem(name: "pageNumber", value: "\(options.page)")) + query.append(URLQueryItem(name: "pageSize", value: "\(options.limit)")) + + if options.pending != nil { + query.append(URLQueryItem(name: "pending", value: "\(options.pending ?? false)")) + } + + if options.role != nil { + query.append(URLQueryItem(name: "role", value: "\(options.role ?? "")")) + } + return PushEndpoint( + env: env, + path: path, + queryItems: query, + apiVersion: apiVersion + ) + } + + static func getGroupMembersPublicKeys( + chatId: String, + page: Int, + limit: Int, + apiVersion: String = "v1", + env: ENV + ) throws -> Self { + return PushEndpoint( + env: env, + path: "chat/groups/\(chatId)/members/publicKeys?pageNumber=\(page)&pageSize=\(limit)", + apiVersion: apiVersion + ) + } } diff --git a/Sources/Endpoints/PushEndpoint.swift b/Sources/Endpoints/PushEndpoint.swift index ca43da5..c04e94b 100644 --- a/Sources/Endpoints/PushEndpoint.swift +++ b/Sources/Endpoints/PushEndpoint.swift @@ -13,7 +13,10 @@ extension PushEndpoint { components.scheme = "https" components.host = ENV.getHost(withEnv: env) components.path = "/apis/\(apiVersion)/" + path - components.queryItems = queryItems + if !queryItems.isEmpty { + components.queryItems = queryItems + } + guard let url = components.url else { preconditionFailure( diff --git a/Sources/Helpers/Crypto/AESGCM.swift b/Sources/Helpers/Crypto/AESGCM.swift index e41e685..613e692 100644 --- a/Sources/Helpers/Crypto/AESGCM.swift +++ b/Sources/Helpers/Crypto/AESGCM.swift @@ -2,126 +2,135 @@ import CryptoKit import Foundation public struct AESGCMHelper { + static func generateRandomSecret(length: Int) -> String { + let characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789" + let charactersLength = characters.count + var randomString = "" + for _ in 0 ..< length { + let randomIndex = Int.random(in: 0 ..< charactersLength) + let character = characters[characters.index(characters.startIndex, offsetBy: randomIndex)] + randomString.append(character) + } + return randomString + } + + static func hexToData(characters: String) -> Data { + var data = Data(capacity: characters.count / 2) + let regex = try! NSRegularExpression(pattern: "[0-9a-f]{1,2}", options: .caseInsensitive) + regex.enumerateMatches(in: characters, options: [], range: NSMakeRange(0, characters.count)) { + match, _, _ in + let byteString = (characters as NSString).substring(with: match!.range) + var num = UInt8(byteString, radix: 16)! + data.append(&num, count: 1) + } + + return data + } + + static func hexToBytes(hex: String) -> [UInt8] { + var bytes = [UInt8]() + + // TODO: this is a temp fix for this issue: https://github.com/ethereum-push-notification-service/push-sdk/blob/e912f94fc72847[…]d303fd4e50e6bf001041/packages/restapi/src/lib/helpers/crypto.ts + bytes.append(contentsOf: [14, 0, 145, 0, 0]) + + for i in stride(from: 0, to: hex.count - 1, by: 2) { + let start: String.Index = hex.index(hex.startIndex, offsetBy: i) + let end = hex.index(hex.startIndex, offsetBy: i + 1) + let sub = hex[start ... end] + + let pp = Int(sub, radix: 16) + let num = (pp != nil ? pp : 0)! - static func hexToData(characters: String) -> Data { - var data = Data(capacity: characters.count / 2) - let regex = try! NSRegularExpression(pattern: "[0-9a-f]{1,2}", options: .caseInsensitive) - regex.enumerateMatches(in: characters, options: [], range: NSMakeRange(0, characters.count)) { - match, flags, stop in - let byteString = (characters as NSString).substring(with: match!.range) - var num = UInt8(byteString, radix: 16)! - data.append(&num, count: 1) + bytes.append(UInt8(num)) + } + + return bytes } - return data - } + static func getSigToBytes(sig: String) -> [UInt8] { + let com = sig.components(separatedBy: ":")[1] + let remaning = String(com.dropFirst(3)) + let res = hexToBytes(hex: remaning) + return res + } - static func hexToBytes(hex: String) -> [UInt8] { - var bytes = [UInt8]() + static func dataToHex(_ data: Data) -> String { + return data.map { String(format: "%02x", $0) }.joined() + } - // TODO this is a temp fix for this issue: https://github.com/ethereum-push-notification-service/push-sdk/blob/e912f94fc72847[…]d303fd4e50e6bf001041/packages/restapi/src/lib/helpers/crypto.ts - bytes.append(contentsOf: [14, 0, 145, 0, 0]) + public static func decrypt(chiperHex: String, secret: String, nonceHex: String, saltHex: String) + throws -> String { + // Chat AES Info + let chiper = hexToData(characters: chiperHex) + let nonce = hexToData(characters: nonceHex) + let salt = hexToData(characters: saltHex) + + let ciphertextBytes = chiper[0 ..< chiper.count - 16] + let tag = chiper[chiper.count - 16 ..< chiper.count] + + let box = try AES.GCM.SealedBox( + nonce: AES.GCM.Nonce(data: nonce), ciphertext: ciphertextBytes, tag: tag) + + let sk = Data( + getSigToBytes(sig: secret) + ) + let derivedKey = HKDF.deriveKey( + inputKeyMaterial: SymmetricKey(data: sk), + salt: salt, + outputByteCount: 32 + ) + + let res = try AES.GCM.open(box, using: derivedKey) + return try res.toString() + } - for i in stride(from: 0, to: hex.count - 1, by: 2) { - let start: String.Index = hex.index(hex.startIndex, offsetBy: i) - let end = hex.index(hex.startIndex, offsetBy: i + 1) - let sub = hex[start...end] + public static func encrypt(message: String, secret: String, nonceHex: String, saltHex: String) + throws -> String { + // Chat AES Info + let messageData = try message.toData() + let nonce = hexToData(characters: nonceHex) + let salt = hexToData(characters: saltHex) + + let sk = Data( + getSigToBytes(sig: secret) + ) + + let derivedKey = HKDF.deriveKey( + inputKeyMaterial: SymmetricKey(data: sk), + salt: salt, + outputByteCount: 32 + ) + + let sealedBox = try AES.GCM.seal( + messageData, using: derivedKey, nonce: AES.GCM.Nonce(data: nonce)) + let combinedEnc = sealedBox.ciphertext + sealedBox.tag + let hexStr = dataToHex(combinedEnc) + + return hexStr + } - let pp = Int(sub, radix: 16) - let num = (pp != nil ? pp : 0)! + public static func decrypt(cipherText: String, secretKey: String) throws -> String { + let cipherData = hexToData(characters: cipherText) + let sk = Data( + getSigToBytes(sig: secretKey) + ) + let derivedKey = HKDF.deriveKey( + inputKeyMaterial: SymmetricKey(data: sk), + salt: Data(), + outputByteCount: 32 + ) + let sealedBox = try AES.GCM.SealedBox(combined: cipherData) + let decryptedData = try AES.GCM.open(sealedBox, using: derivedKey) + return try decryptedData.toString() + } - bytes.append(UInt8(num)) + func encryptAES(message: Data, key: SymmetricKey) throws -> Data { + let sealedBox = try AES.GCM.seal(message, using: key) + return sealedBox.combined! } - return bytes - } - - static func getSigToBytes(sig: String) -> [UInt8] { - let com = sig.components(separatedBy: ":")[1] - let remaning = String(com.dropFirst(3)) - let res = hexToBytes(hex: remaning) - return res - } - - static func dataToHex(_ data: Data) -> String { - return data.map { String(format: "%02x", $0) }.joined() - } - - public static func decrypt(chiperHex: String, secret: String, nonceHex: String, saltHex: String) - throws -> String - { - // Chat AES Info - let chiper = hexToData(characters: chiperHex) - let nonce = hexToData(characters: nonceHex) - let salt = hexToData(characters: saltHex) - - let ciphertextBytes = chiper[0...deriveKey( - inputKeyMaterial: SymmetricKey(data: sk), - salt: salt, - outputByteCount: 32 - ) - - let res = try AES.GCM.open(box, using: derivedKey) - return try res.toString() - } - - public static func encrypt(message: String, secret: String, nonceHex: String, saltHex: String) - throws -> String - { - // Chat AES Info - let messageData = try message.toData() - let nonce = hexToData(characters: nonceHex) - let salt = hexToData(characters: saltHex) - - let sk = Data( - getSigToBytes(sig: secret) - ) - - let derivedKey = HKDF.deriveKey( - inputKeyMaterial: SymmetricKey(data: sk), - salt: salt, - outputByteCount: 32 - ) - - let sealedBox = try AES.GCM.seal( - messageData, using: derivedKey, nonce: AES.GCM.Nonce(data: nonce)) - let combinedEnc = sealedBox.ciphertext + sealedBox.tag - let hexStr = dataToHex(combinedEnc) - - return hexStr - } - - public static func decrypt(cipherText: String, secretKey: String) throws -> String { - let cipherData = hexToData(characters: cipherText) - let sk = Data( - getSigToBytes(sig: secretKey) - ) - let derivedKey = HKDF.deriveKey( - inputKeyMaterial: SymmetricKey(data: sk), - salt: Data(), - outputByteCount: 32 - ) - let sealedBox = try AES.GCM.SealedBox(combined: cipherData) - let decryptedData = try AES.GCM.open(sealedBox, using: derivedKey) - return try decryptedData.toString() - } - - func encryptAES(message: Data, key: SymmetricKey) throws -> Data { - let sealedBox = try AES.GCM.seal(message, using: key) - return sealedBox.combined! - } - - func decryptAES(ciphertext: Data, key: SymmetricKey) throws -> Data { - let sealedBox = try AES.GCM.SealedBox(combined: ciphertext) - return try AES.GCM.open(sealedBox, using: key) - } + func decryptAES(ciphertext: Data, key: SymmetricKey) throws -> Data { + let sealedBox = try AES.GCM.SealedBox(combined: ciphertext) + return try AES.GCM.open(sealedBox, using: key) + } } diff --git a/Sources/Helpers/Ipfs/Cid.swift b/Sources/Helpers/Ipfs/Cid.swift index 94f0f61..fc79a1f 100644 --- a/Sources/Helpers/Ipfs/Cid.swift +++ b/Sources/Helpers/Ipfs/Cid.swift @@ -18,6 +18,26 @@ public struct Message: Codable { public var sessionKey: String? } +extension Message { + enum CodingKeys: String, CodingKey { + case fromCAIP10 + case toCAIP10 + case fromDID + case toDID + case messageType + case messageContent + case messageObj + case signature + case sigType + case timestamp + case encType + case encryptedSecret + case link + case cid + case sessionKey + } +} + public func getCID(env: ENV, cid: String) async throws -> Message { let url: URL = PushEndpoint.getCID(env: env, cid: cid).url diff --git a/Sources/PushAPI/Chat.swift b/Sources/PushAPI/Chat.swift index 182bcae..24a8f4e 100644 --- a/Sources/PushAPI/Chat.swift +++ b/Sources/PushAPI/Chat.swift @@ -1,8 +1,12 @@ +import Foundation + public struct Chat { private var account: String private var decryptedPgpPvtKey: String private var env: ENV + public var group: Group + init( account: String, decryptedPgpPvtKey: String, @@ -11,6 +15,13 @@ public struct Chat { self.account = account self.decryptedPgpPvtKey = decryptedPgpPvtKey self.env = env + + group = Group(account: account, decryptedPgpPvtKey: decryptedPgpPvtKey, env: env) + } + + public enum ChatListType { + case CHAT + case REQUESTS } public func list(type: ChatListType, page: Int = 1, limit: Int = 10, overrideAccount: String? = nil) async throws -> [PushChat.Feeds] { @@ -32,9 +43,349 @@ public struct Chat { return try await PushChat.requests(options: options) } } + + public func latest(target: String) async throws -> Message? { + let threadHash = try await PushChat.ConversationHash(conversationId: target, account: account, env: env) + + if threadHash == nil { + return nil + } + + let latestMessage = try await PushChat.History( + threadHash: threadHash!, limit: 1, pgpPrivateKey: decryptedPgpPvtKey, toDecrypt: true, env: env + ).first + + return latestMessage + } + + public func history( + target: String, reference: String? = nil, limit: Int = 10 + ) async throws -> [Message] { + var ref = reference + if ref == nil { + let threadHash = try await PushChat.ConversationHash(conversationId: target, account: account, env: env)! + ref = threadHash + } + + if ref == nil { + return [] + } + + return try await PushChat.History( + threadHash: ref!, limit: limit, + pgpPrivateKey: decryptedPgpPvtKey, + toDecrypt: true, env: env) + } + + public func accept(target: String) async throws -> String? { + let options = PushChat.ApproveOptions(requesterAddress: target, approverAddress: account, privateKey: decryptedPgpPvtKey, env: env) + + return try await PushChat.approve(options) + } + + public func block(users: [String]) async throws { + let user = try await PushUser.get(account: account, env: env) + var profile = user?.profile + + for address in users { + if !isValidETHAddress(address: address) { + fatalError("Invalid member address!") + } + } + + var updatedBlockedUsersList = profile?.blockedUsersList ?? [] + updatedBlockedUsersList.append(contentsOf: users) + profile!.blockedUsersList = Array(Set(updatedBlockedUsersList)) + + try await PushUser.updateUserProfile(account: account, pgpPrivateKey: decryptedPgpPvtKey, newProfile: profile!, env: env) + } + + public func unblock(users: [String]) async throws { + let user = try await PushUser.get(account: account, env: env) + var profile = user?.profile + + for address in users { + if !isValidETHAddress(address: address) { + fatalError("Invalid member address!") + } + } + + var updatedBlockedUsersList = profile?.blockedUsersList ?? [] + updatedBlockedUsersList.append(contentsOf: users) + profile!.blockedUsersList = profile?.blockedUsersList?.filter { blockedUser in + !users.contains(blockedUser.lowercased()) + } + + try await PushUser.updateUserProfile(account: account, pgpPrivateKey: decryptedPgpPvtKey, newProfile: profile!, env: env) + } + +// public func send(target: String, messageContent: String, messageType: String = "Text") async throws -> Message { +// return try await Push.PushChat.sendV2( +// chatOptions: PushChat.SendOptions( +// +// messageContent: messageContent, +// messageType: messageType, +// receiverAddress: target, +// account: account, +// pgpPrivateKey: decryptedPgpPvtKey +// )) +// } + + public func send(target: String, message: PushChat.SendMessage) async throws -> MessageV2 { + + let sendOption = PushChat.SendOptions( + message: message, + messageContent: target, + messageType: message.type.rawValue, + receiverAddress: target, + account: account, + pgpPrivateKey: decryptedPgpPvtKey, + env: env, + to: target + ) + + return try await Push.PushChat.sendV2( + chatOptions: sendOption) + } + + public func info(chatId: String) async throws -> PushChat + .PushGroupInfoDTO { + return try await PushChat.getGroupInfoDTO(chatId: chatId, env: env) + } } -public enum ChatListType { - case CHAT - case REQUESTS +public struct Group { + private var account: String + private var decryptedPgpPvtKey: String + private var env: ENV + + public var participants: GroupParticipants + + public init( + account: String, + decryptedPgpPvtKey: String, + env: ENV + ) { + self.account = account + self.decryptedPgpPvtKey = decryptedPgpPvtKey + self.env = env + + participants = GroupParticipants(env: env) + } + + public func leave(target: String) async throws -> PushChat.PushGroupInfoDTO? { + let options = PushChat.UpdateGroupMemberOptions(account: account, chatId: target, remove: [account], pgpPrivateKey: decryptedPgpPvtKey) + + return try await PushChat.updateGroupMember(options: options, env: env) + } + + public func join(target: String) async throws -> PushChat.PushGroupInfoDTO? { + let status = try? await PushChat.getGroupMemberStatus(chatId: target, did: account, env: env) + print("Status: is null \(status == nil)") + + if status != nil && status?.isPending == true { + let _ = try await PushChat.approve(PushChat.ApproveOptions(requesterAddress: target, approverAddress: account, privateKey: decryptedPgpPvtKey, env: env)) + + return try await info(chatId: target) + + } else { + let options = PushChat.UpdateGroupMemberOptions( + account: account, + chatId: target, + upsert: PushChat.UpsertData(members: [account]), + pgpPrivateKey: decryptedPgpPvtKey) + return try await PushChat.updateGroupMember(options: options, env: env) + } + } + + public enum GroupRoles { + case MEMBER + case ADMIN + } + + public func add(chatId: String, role: GroupRoles, accounts: [String]) async throws -> PushChat.PushGroupInfoDTO? { + if accounts.isEmpty { + fatalError("accounts array cannot be empty!") + } + + for acc in accounts { + if !isValidETHAddress(address: acc) { + fatalError("Invalid account address: \(acc)") + } + } + + if role == .ADMIN { + let options = PushChat.UpdateGroupMemberOptions( + account: account, + chatId: chatId, + upsert: PushChat.UpsertData(admins: accounts), + pgpPrivateKey: decryptedPgpPvtKey) + return try await PushChat.updateGroupMember(options: options, env: env) + } else { + let options = PushChat.UpdateGroupMemberOptions( + account: account, + chatId: chatId, + upsert: PushChat.UpsertData(members: accounts), + pgpPrivateKey: decryptedPgpPvtKey) + return try await PushChat.updateGroupMember(options: options, env: env) + } + } + + public func remove(chatId: String, role: GroupRoles, accounts: [String]) async throws -> PushChat.PushGroupInfoDTO? { + if accounts.isEmpty { + fatalError("accounts array cannot be empty!") + } + + for acc in accounts { + if !isValidETHAddress(address: acc) { + fatalError("Invalid account address: \(acc)") + } + } + let options = PushChat.UpdateGroupMemberOptions( + account: account, + chatId: chatId, + upsert: PushChat.UpsertData(members: accounts), + pgpPrivateKey: decryptedPgpPvtKey) + return try await PushChat.updateGroupMember(options: options, env: env) + } + + public func modify(chatId: String, role: GroupRoles, accounts: [String]) async throws -> PushChat.PushGroupInfoDTO? { + if accounts.isEmpty { + fatalError("accounts array cannot be empty!") + } + + for acc in accounts { + if !isValidETHAddress(address: acc) { + fatalError("Invalid account address: \(acc)") + } + } + + let options = PushChat.UpdateGroupMemberOptions( + account: account, + chatId: chatId, + upsert: role == .MEMBER ? PushChat.UpsertData(members: accounts) : PushChat.UpsertData(admins: accounts), + pgpPrivateKey: decryptedPgpPvtKey) + return try await PushChat.updateGroupMember(options: options, env: env) + } + + public func info(chatId: String) async throws -> PushChat + .PushGroupInfoDTO { + return try await PushChat.getGroupInfoDTO(chatId: chatId, env: env) + } + + public struct GroupCreationOptions { + let description: String + let image: String + let members: [String] + let admins: [String] + let isPrivate: Bool + let rules: Data? + + public init(description: String, + image: String, + members: [String] = [], + admins: [String] = [], + isPrivate: Bool = false, + rules: Data? = nil) { + self.description = description + self.image = image + self.members = members + self.admins = admins + self.isPrivate = isPrivate + self.rules = rules + } + } + + public func create(name: String, options: GroupCreationOptions) async throws -> PushChat.PushGroupInfoDTO? { + let createGroupOptions = try PushChat.CreateGroupOptions( + name: name, + description: options.description, + image: options.image, + members: options.members, + + isPublic: !options.isPrivate, + creatorAddress: account, + creatorPgpPrivateKey: decryptedPgpPvtKey, + env: env + ) + + return try await PushChat.createGroup(options: createGroupOptions) + } +} + +public struct GroupParticipants { + private var env: ENV + + public init(env: ENV) { + self.env = env + } + + public struct FilterOptions { + var pending: Bool? + var role: String? + + /// role: 'admin' | 'member'; + public init(pending: Bool? = nil, role: String? = nil) { + self.pending = pending + self.role = role + } + } + + public struct GetGroupParticipantsOptions { + var page: Int + var limit: Int + var filter: FilterOptions? + + public init(page: Int = 1, limit: Int = 20, filter: FilterOptions? = nil) { + self.page = page + self.limit = limit + self.filter = filter + } + } + + public struct GroupCountInfo { + public var participants: Int + public var pending: Int + + public init(participants: Int, pending: Int) { + self.participants = participants + self.pending = pending + } + } + + public struct ParticipantStatus { + public var pending: Bool + public var role: String + public var participant: Bool + + public init(pending: Bool, role: String, participant: Bool) { + self.pending = pending + self.role = role + self.participant = participant + } + } + + public func list(chatId: String, options: GetGroupParticipantsOptions = GetGroupParticipantsOptions()) async throws -> [PushChat.ChatMemberProfile]? { + let fetchOption = PushChat.FetchChatGroupInfo( + chatId: chatId, + limit: options.limit, + pending: options.filter?.pending, + role: options.filter?.role) + return try await PushChat.getGroupMembers(options: fetchOption, env: env) + } + + public func count(chatId: String) async throws -> GroupCountInfo { + let count = try await PushChat.getGroupMemberCount(chatId: chatId, env: env) + + return GroupCountInfo(participants: count!.overallCount - count!.pendingCount, pending: count!.pendingCount) + } + + public func status(chatId: String, accountId: String) async throws -> ParticipantStatus { + let status = try await PushChat.getGroupMemberStatus(chatId: chatId, did: accountId, env: env) + + return ParticipantStatus( + pending: status?.isPending == true, + role: status?.isAdmin == true ? "ADMIN" : "MEMBER", + participant: (status?.isMember ?? false) || (status?.isAdmin ?? false)) + } } diff --git a/Sources/User/User.swift b/Sources/User/User.swift index 7d8436f..9064e4c 100644 --- a/Sources/User/User.swift +++ b/Sources/User/User.swift @@ -1,7 +1,6 @@ import Foundation public struct PushUser: Decodable { - public enum UserError: Error { case ONE_OF_ACCOUNT_OR_SIGNER_REQUIRED case INVALID_ETH_ADDRESS @@ -33,7 +32,7 @@ public struct PushUser: Decodable { public let profile: UserProfile public func getPGPPublickey() -> String { - return PushUser.getPGPPublickey(publicKey: self.publicKey) + return PushUser.getPGPPublickey(publicKey: publicKey) } public static func getPGPPublickey(publicKey: String) -> String { @@ -47,8 +46,8 @@ public struct PushUser: Decodable { } } -extension PushUser { - public static func get( +public extension PushUser { + static func get( account userAddress: String, env: ENV ) async throws -> PushUser? { @@ -64,21 +63,20 @@ extension PushUser { throw URLError(.badServerResponse) } - guard (200...299).contains(httpResponse.statusCode) else { + guard (200 ... 299).contains(httpResponse.statusCode) else { throw URLError(.badServerResponse) } - //check if user is null + // check if user is null if data.count == 4 { return nil } let userProfile = try JSONDecoder().decode(PushUser.self, from: data) return userProfile - } - public static func userProfileCreated(account: String, env: ENV) async throws -> Bool { + static func userProfileCreated(account: String, env: ENV) async throws -> Bool { let userInfo = try await PushUser.get(account: account, env: env) return userInfo != nil } diff --git a/Tests/Chat/P2P/GetChatsTests.swift b/Tests/Chat/P2P/GetChatsTests.swift index 7ea1f39..c6d9027 100644 --- a/Tests/Chat/P2P/GetChatsTests.swift +++ b/Tests/Chat/P2P/GetChatsTests.swift @@ -64,9 +64,9 @@ class GetChatsTests: XCTestCase { pgpPrivateKey: UserPrivateKey )) - let userReqs = try await PushChat.requests( - options: PushChat.RequestOptionsType(account: reqAddress, pgpPrivateKey: "", toDecrypt: false) - ) +// let userReqs = try await PushChat.requests( +// options: PushChat.RequestOptionsType(account: reqAddress, pgpPrivateKey: "", toDecrypt: false) +// ) // XCTAssertEqual(userReqs[0].msg!.messageContent, messageToSen1) } diff --git a/Tests/PushAPI/ChatTests.swift b/Tests/PushAPI/ChatTests.swift index e12a876..82f32d8 100644 --- a/Tests/PushAPI/ChatTests.swift +++ b/Tests/PushAPI/ChatTests.swift @@ -20,5 +20,133 @@ class ChatTests: XCTestCase { let chats = try await pushAPI.chat.list(type: .CHAT) XCTAssertEqual(chats.count, 0) } + + func testLatestChat() async throws { + let userPk = getRandomAccount() + let signer = try SignerPrivateKey(privateKey: userPk) + + let pushAPI = try await PushAPI + .initializePush( + signer: signer, + options: PushAPI.PushAPIInitializeOptions() + ) + + let latest = try await pushAPI.chat.latest(target: "064ae7a086bc1d25cf45231a9725fec6789e1013b99bb482f41136268ffa73c6") + + print(latest?.messageContent ?? "No message") + } + + func testSendP2P() async throws{ + let alicePk = "a59c37c9b61b73f824972b901e0b4ae914750fd8de94c5dfebc4934ff1d12d3c" ///getRandomAccount() + let bobPk = "0ab2b8f38a851c8e8782119fd1e202290b5c86736506525acde7b404260beba7"//getRandomAccount() + + print("alicePk: \(alicePk)") + print("bobPk: \(bobPk)") + + + // Initialize signers + let aliceSigner = try SignerPrivateKey(privateKey: alicePk) + let bobSigner = try SignerPrivateKey(privateKey: bobPk) + + // Store Address + let aliceAddress = try await aliceSigner.getAddress() + let bobAddress = try await bobSigner.getAddress() + print("aliceAddress: \(aliceAddress)") + print("bobAddress: \(bobAddress)") + + // Initialize PushAPI + let userAlice = try await PushAPI + .initializePush( + signer: aliceSigner, + options: PushAPI.PushAPIInitializeOptions( + env: .STAGING) + ) + + let userBob = try await PushAPI + .initializePush( + signer: bobSigner, + options: PushAPI.PushAPIInitializeOptions(env: .STAGING) + ) + + + + + let messageToUnregisteredWallet = try await userAlice.chat.send( + target: "0xd53f7207d84b188d1C70ac7d0BBd52C42CD5EcCc", + message: PushChat.SendMessage(content: "Food", type: .Text)) + print("messageToUnregisteredWallet: \(messageToUnregisteredWallet)") + + + let message = try await userAlice.chat.send( + target: bobAddress, + message: PushChat.SendMessage(content: "Food", type: .Text)) + + print("Message: \(message)") + } + + func testSendToGroup() async throws{ + let alicePk = "a59c37c9b61b73f824972b901e0b4ae914750fd8de94c5dfebc4934ff1d12d3c" ///getRandomAccount() + let bobPk = "0ab2b8f38a851c8e8782119fd1e202290b5c86736506525acde7b404260beba7"//getRandomAccount() + let johnPk = getRandomAccount() + print("alicePk: \(alicePk)") + print("bobPk: \(bobPk)") + + + // Initialize signers + let aliceSigner = try SignerPrivateKey(privateKey: alicePk) + let bobSigner = try SignerPrivateKey(privateKey: bobPk) + let johnSigner = try SignerPrivateKey(privateKey: johnPk) + + // Store Address + let aliceAddress = try await aliceSigner.getAddress() + let bobAddress = try await bobSigner.getAddress() + let johnAddress = try await johnSigner.getAddress() + print("aliceAddress: \(aliceAddress)") + print("bobAddress: \(bobAddress)") + print("johnAddress: \(johnAddress)") + + + let userAlice = try await PushAPI + .initializePush( + signer: aliceSigner, + options: PushAPI.PushAPIInitializeOptions( + env: .STAGING) + ) + + let userBob = try await PushAPI + .initializePush( + signer: bobSigner, + options: PushAPI.PushAPIInitializeOptions(env: .STAGING) + ) + + let userJohn = try await PushAPI + .initializePush( + signer: johnSigner, + options: PushAPI.PushAPIInitializeOptions(env: .STAGING) + ) + + + let newName = "Push Swift" + let groupOptions = Group.GroupCreationOptions( + description: "Push Swift Test", + image: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAQAAAAEACAIAAADTED8xAAADMElEQVR4nOzVwQnAIBQFQYXff81RUkQCOyDj1YOPnbXWPmeTRef+/3O/OyBjzh3CD95BfqICMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMO0TAAD//2Anhf4QtqobAAAAAElFTkSuQmCC", + members: [aliceAddress], + admins: [johnAddress], + isPrivate: true + ) + + // Create Oublic group + let group = try await userBob.chat.group.create(name: newName, options: groupOptions) + + XCTAssertEqual(group?.groupName, newName) + print("group: \(group?.chatId ?? "null")") + + let message = try await userBob.chat.send( + target: group!.chatId, + message: PushChat.SendMessage(content: "Food", type: .Text)) + + print("Message: \(message)") + } + } diff --git a/Tests/PushAPI/PushAPITests.swift b/Tests/PushAPI/PushAPITests.swift index aa5b456..2d121d6 100644 --- a/Tests/PushAPI/PushAPITests.swift +++ b/Tests/PushAPI/PushAPITests.swift @@ -17,26 +17,126 @@ class PushAPITests: XCTestCase { XCTAssertEqual(user?.did, "eip155:\(address)") } - + func testProfileUpdate() async throws { let userPk = getRandomAccount() let signer = try SignerPrivateKey(privateKey: userPk) - let address = try await signer.getAddress() let pushAPI = try await PushAPI .initializePush( signer: signer, options: PushAPI.PushAPIInitializeOptions() ) - + let newName = "Push Swift" - + try await pushAPI.profile.update(name: newName, desc: "Push Swift Tester") - + let user = try await pushAPI.profile.info() XCTAssertEqual(user?.profile.name, newName) } + + func testPublicGroup() async throws { + // Generate Private Keys + let alicePk = getRandomAccount() + let bobPk = getRandomAccount() + let johnPk = getRandomAccount() + let markPk = getRandomAccount() + + // Initialize signers + let aliceSigner = try SignerPrivateKey(privateKey: alicePk) + let bobSigner = try SignerPrivateKey(privateKey: bobPk) + let johnSigner = try SignerPrivateKey(privateKey: johnPk) + let markSigner = try SignerPrivateKey(privateKey: markPk) + + // Store Address + let aliceAddress = try await aliceSigner.getAddress() + let bobAddress = try await bobSigner.getAddress() + let johnAddress = try await johnSigner.getAddress() + let markAddress = try await johnSigner.getAddress() + + // Initialize PushAPI + let userAlice = try await PushAPI + .initializePush( + signer: aliceSigner, + options: PushAPI.PushAPIInitializeOptions( + env: .STAGING) + ) + + let userBob = try await PushAPI + .initializePush( + signer: bobSigner, + options: PushAPI.PushAPIInitializeOptions(env: .STAGING) + ) + + let userJohn = try await PushAPI + .initializePush( + signer: johnSigner, + options: PushAPI.PushAPIInitializeOptions(env: .STAGING) + ) + + let userMark = try await PushAPI + .initializePush( + signer: markSigner, + options: PushAPI.PushAPIInitializeOptions(env: .STAGING) + ) + + let newName = "Push Swift" + let groupOptions = Group.GroupCreationOptions( + description: "Push Swift Test", + image: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAQAAAAEACAIAAADTED8xAAADMElEQVR4nOzVwQnAIBQFQYXff81RUkQCOyDj1YOPnbXWPmeTRef+/3O/OyBjzh3CD95BfqICMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMO0TAAD//2Anhf4QtqobAAAAAElFTkSuQmCC", + members: [aliceAddress], + admins: [johnAddress] + ) + + // Create Oublic group + let group = try await userBob.chat.group.create(name: newName, options: groupOptions) + XCTAssertEqual(group?.groupName, newName) + + // Member accepted + let aliceRequests = try await userAlice.chat.list(type: .REQUESTS) + XCTAssertEqual(aliceRequests.count, 1) + let aliceAccepted = try await userAlice.chat.accept(target: group!.chatId) + XCTAssertNotNil(aliceAccepted) + let newAliceRequests = try await userAlice.chat.list(type: .REQUESTS) + XCTAssertEqual(newAliceRequests.count, 0) + let aliceChats = try await userAlice.chat.list(type: .CHAT) + XCTAssertEqual(aliceChats.count, 1) + + // Member Status + let aliceStatus = try await userAlice.chat.group.participants.status(chatId: group!.chatId, accountId: aliceAddress) + XCTAssertEqual(aliceStatus.role, "MEMBER") + XCTAssertEqual(aliceStatus.pending, false) + XCTAssertEqual(aliceStatus.participant, true) + + let johnStatus = try await userJohn.chat.group.participants.status(chatId: group!.chatId, accountId: johnAddress) + XCTAssertEqual(johnStatus.pending, false) + XCTAssertEqual(johnStatus.participant, false) + + // Pending Members List (with pending included) + let pendingMembers = try await userBob.chat.group.participants.list(chatId: group!.chatId, options: GroupParticipants.GetGroupParticipantsOptions(filter: GroupParticipants.FilterOptions(pending: true) + )) + + XCTAssertEqual(pendingMembers?.count, 0) + + + // Members List + let groupMembers = try await userBob.chat.group.participants.list(chatId: group!.chatId) + + XCTAssertEqual(groupMembers?.count, 2) + } + func testAesEncryptDecrypt() async throws{ + let plaintext = "Hello, AES!" + let secretKey = "0123456789abcdef" + +// let cipher = try aesEncrypt(plainText: plaintext, secretKey: secretKey) + let cipher = try AESCBCHelper.encrypt(messageText: plaintext, secretKey: secretKey) + + print("plaintext: \(plaintext), secretKey: \(secretKey), cipher: \(cipher)") + } + + }