blob: 71b41802a5ef4ab9347a8018ff8a81aa407932f1 [file] [log] [blame]
kkostiuk74d1ae42021-06-17 11:10:15 -04001/*
2 * Copyright (C) 2021-2022 Savoir-faire Linux Inc.
3 *
4 * Author: Kateryna Kostiuk <kateryna.kostiuk@savoirfairelinux.com>
5 *
6 * This program is free software; you can redistribute it and/or modify
7 * it under the terms of the GNU General Public License as published by
8 * the Free Software Foundation; either version 3 of the License, or
9 * (at your option) any later version.
10 *
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU General Public License for more details.
15 *
16 * You should have received a copy of the GNU General Public License
17 * along with this program; if not, write to the Free Software
18 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
19 */
20
21import Foundation
22import UIKit
23import MobileCoreServices
24import Photos
25import os
26
27class AdapterService {
28 enum InteractionAttributes: String {
29 case interactionId = "id"
30 case type = "type"
31 case invited = "invited"
32 case fileId = "fileId"
33 case displayName = "displayName"
34 case body = "body"
35 case author = "author"
36 case timestamp = "timestamp"
37 case parent = "linearizedParent"
38 case action = "action"
39 case duration = "duration"
40 }
41
42 enum InteractionType: String {
43 case message = "text/plain"
44 case fileTransfer = "application/data-transfer+json"
45 }
46
47 enum EventType: Int {
48 case message
49 case fileTransferDone
50 case fileTransferInProgress
51 case syncCompleted
52 case call
Kateryna Kostiuk2648aab2022-08-30 14:24:03 -040053 case invitation
kkostiuk74d1ae42021-06-17 11:10:15 -040054 }
55
56 enum PeerConnectionRequestType {
57 case call(peerId: String, isVideo: Bool)
58 case gitMessage
59 case unknown
60 }
61
62 enum DataTransferEventCode: Int {
63 case invalid
64 case created
65 case unsupported
66 case waitPeeracceptance
67 case waitHostAcceptance
68 case ongoing
69 case finished
70 case closedByHost
71 case closedByPeer
72 case invalidPathname
73 case unjoinablePeer
74
75 func isCompleted() -> Bool {
76 switch self {
77 case .finished, .closedByHost, .closedByPeer, .unjoinablePeer, .invalidPathname:
78 return true
79 default:
80 return false
81 }
82 }
83 }
Kateryna Kostiuk2648aab2022-08-30 14:24:03 -040084
kkostiuk74d1ae42021-06-17 11:10:15 -040085 private let maxSizeForAutoaccept = 20 * 1024 * 1024
86
Kateryna Kostiuk2648aab2022-08-30 14:24:03 -040087 typealias EventData = (accountId: String, jamiId: String, conversationId: String, content: String, groupTitle: String)
kkostiuk74d1ae42021-06-17 11:10:15 -040088
89 private let adapter: Adapter
90 var eventHandler: ((EventType, EventData) -> Void)?
91 var loadingFiles = [String: EventData]()
92
93 init(withAdapter adapter: Adapter) {
94 self.adapter = adapter
95 Adapter.delegate = self
96 }
97
Kateryna Kostiuk19437652022-08-02 13:02:21 -040098 func startAccountsWithListener(accountId: String, listener: @escaping (EventType, EventData) -> Void) {
kkostiuk74d1ae42021-06-17 11:10:15 -040099 self.eventHandler = listener
Kateryna Kostiuk19437652022-08-02 13:02:21 -0400100 start(accountId: accountId)
kkostiuk74d1ae42021-06-17 11:10:15 -0400101 }
102
103 func decrypt(keyPath: String, messagesPath: String, value: [String: Any]) -> PeerConnectionRequestType {
104 let result = adapter.decrypt(keyPath, treated: messagesPath, value: value)
105 guard let peerId = result?.keys.first,
106 let type = result?.values.first else {
107 return .unknown}
108 switch type {
109 case "videoCall":
110 return PeerConnectionRequestType.call(peerId: peerId, isVideo: true)
111 case "audioCall":
112 return PeerConnectionRequestType.call(peerId: peerId, isVideo: false)
113 case "text/plain", "application/im-gitmessage-id":
114 return PeerConnectionRequestType.gitMessage
115 default:
116 return .unknown
117 }
118 }
119
Kateryna Kostiuk19437652022-08-02 13:02:21 -0400120 func start(accountId: String) {
121 self.adapter.start(accountId)
kkostiuk74d1ae42021-06-17 11:10:15 -0400122 }
123
124 func stop() {
125 self.adapter.stop()
126 }
127
kkostiuke10e6572022-07-26 15:41:12 -0400128 func getNameFor(address: String, accountId: String) -> String {
129 return adapter.getNameFor(address, accountId: accountId)
130 }
131
132 func getNameServerFor(accountId: String) -> String {
133 return adapter.nameServer(forAccountId: accountId)
134 }
135
kkostiuk74d1ae42021-06-17 11:10:15 -0400136 private func fileAlreadyDownloaded(fileName: String, accountId: String, conversationId: String) -> Bool {
137 guard let url = getFileUrlFor(fileName: fileName, accountId: accountId, conversationId: conversationId) else {
138 return false
139 }
140 return FileManager.default.fileExists(atPath: url.path)
141 }
142
143 private func getFileUrlFor(fileName: String, accountId: String, conversationId: String) -> URL? {
144 guard let documentsURL = Constants.documentsPath else {
145 return nil
146 }
147 let pathUrl = documentsURL.appendingPathComponent(accountId)
148 .appendingPathComponent("conversation_data")
149 .appendingPathComponent(conversationId)
150 .appendingPathComponent(fileName)
151 return pathUrl
152 }
153}
154
155extension AdapterService: AdapterDelegate {
156
157 func didReceiveMessage(_ message: [String: String],
158 from senderAccount: String,
159 messageId: String,
160 to receiverAccountId: String) {
161 guard let content = message["text/plain"],
162 let handler = self.eventHandler else { return }
Kateryna Kostiuk2648aab2022-08-30 14:24:03 -0400163 handler(.message, EventData(receiverAccountId, senderAccount, "", content, ""))
kkostiuk74d1ae42021-06-17 11:10:15 -0400164 }
165
166 func dataTransferEvent(withFileId transferId: String, withEventCode eventCode: Int, accountId: String, conversationId: String, interactionId: String) {
167 guard let handler = self.eventHandler,
168 let data = loadingFiles[transferId],
169 let code = DataTransferEventCode(rawValue: eventCode),
170 code.isCompleted() else { return }
171 handler(.fileTransferDone, data)
172 loadingFiles.removeValue(forKey: transferId)
173 }
174
175 func conversationSyncCompleted(accountId: String) {
176 guard let handler = self.eventHandler else {
177 return
178 }
Kateryna Kostiuk2648aab2022-08-30 14:24:03 -0400179 handler(.syncCompleted, EventData(accountId, "", "", "", ""))
kkostiuk74d1ae42021-06-17 11:10:15 -0400180 }
181
182 func receivedCallConnectionRequest(accountId: String, peerId: String, hasVideo: Bool) {
183 guard let handler = self.eventHandler else {
184 return
185 }
Kateryna Kostiuk2648aab2022-08-30 14:24:03 -0400186 handler(.call, EventData(accountId, peerId, "", "\(hasVideo)", ""))
187 }
188
189 func receivedContactRequest(accountId: String, peerId: String) {
Kateryna Kostiuk52f7c652022-11-09 13:54:33 -0500190 /// TODO add invitation notification
Kateryna Kostiuk2648aab2022-08-30 14:24:03 -0400191 }
192
193 func receivedConversationRequest(accountId: String, conversationId: String, metadata: [String: String]) {
194 guard let handler = self.eventHandler else {
195 return
196 }
197 let contentMessage = L10n.GeneratedMessage.invitationReceived
198 var groupTitle = ""
199 var peerId = ""
200 if let title = metadata["title"], !title.isEmpty {
201 groupTitle = title
202 }
203 if let from = metadata["from"] {
204 peerId = from
205 }
206 if !peerId.isEmpty || !groupTitle.isEmpty {
207 handler(.invitation, EventData(accountId, peerId, conversationId, contentMessage, groupTitle))
208 }
kkostiuk74d1ae42021-06-17 11:10:15 -0400209 }
210
211 func newInteraction(conversationId: String, accountId: String, message: [String: String]) {
212 guard let handler = self.eventHandler else {
213 return
214 }
215 guard let type = message[InteractionAttributes.type.rawValue],
216 let interactionType = InteractionType(rawValue: type) else {
217 return
218 }
219 let from = message[InteractionAttributes.author.rawValue] ?? ""
220 let content = message[InteractionAttributes.body.rawValue] ?? ""
221 switch interactionType {
222 case .message:
Kateryna Kostiuk2648aab2022-08-30 14:24:03 -0400223 handler(.message, EventData(accountId, from, conversationId, content, ""))
kkostiuk74d1ae42021-06-17 11:10:15 -0400224 case.fileTransfer:
225 guard let fileId = message[InteractionAttributes.fileId.rawValue],
226 let url = self.getFileUrlFor(fileName: fileId, accountId: accountId, conversationId: conversationId) else {
227 return
228 }
Kateryna Kostiuk2648aab2022-08-30 14:24:03 -0400229 let data = EventData(accountId, from, conversationId, url.path, "")
230 // check if the file has already been downloaded. If no, download the file if filesize is less than a downloading limit
kkostiuk74d1ae42021-06-17 11:10:15 -0400231 if fileAlreadyDownloaded(fileName: fileId, accountId: accountId, conversationId: conversationId) {
232 handler(.fileTransferDone, data)
233 } else {
234 guard let interactionId = message[InteractionAttributes.interactionId.rawValue],
235 let size = message["totalSize"],
236 (Int(size) ?? (maxSizeForAutoaccept + 1)) <= maxSizeForAutoaccept else { return }
237 let path = ""
238 self.adapter.downloadFile(withFileId: fileId, accountId: accountId, conversationId: conversationId, interactionId: interactionId, withFilePath: path)
239 self.loadingFiles[fileId] = data
240 handler(.fileTransferInProgress, data)
241 }
242 }
243 }
244}