blob: be018e6403b72b66b0eeb92b5532241f67c3476c [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) {
190 guard let handler = self.eventHandler else {
191 return
192 }
193 let contentMessage = "an invitation received"
194 handler(.invitation, EventData(accountId, peerId, "", contentMessage, ""))
195 }
196
197 func receivedConversationRequest(accountId: String, conversationId: String, metadata: [String: String]) {
198 guard let handler = self.eventHandler else {
199 return
200 }
201 let contentMessage = L10n.GeneratedMessage.invitationReceived
202 var groupTitle = ""
203 var peerId = ""
204 if let title = metadata["title"], !title.isEmpty {
205 groupTitle = title
206 }
207 if let from = metadata["from"] {
208 peerId = from
209 }
210 if !peerId.isEmpty || !groupTitle.isEmpty {
211 handler(.invitation, EventData(accountId, peerId, conversationId, contentMessage, groupTitle))
212 }
kkostiuk74d1ae42021-06-17 11:10:15 -0400213 }
214
215 func newInteraction(conversationId: String, accountId: String, message: [String: String]) {
216 guard let handler = self.eventHandler else {
217 return
218 }
219 guard let type = message[InteractionAttributes.type.rawValue],
220 let interactionType = InteractionType(rawValue: type) else {
221 return
222 }
223 let from = message[InteractionAttributes.author.rawValue] ?? ""
224 let content = message[InteractionAttributes.body.rawValue] ?? ""
225 switch interactionType {
226 case .message:
Kateryna Kostiuk2648aab2022-08-30 14:24:03 -0400227 handler(.message, EventData(accountId, from, conversationId, content, ""))
kkostiuk74d1ae42021-06-17 11:10:15 -0400228 case.fileTransfer:
229 guard let fileId = message[InteractionAttributes.fileId.rawValue],
230 let url = self.getFileUrlFor(fileName: fileId, accountId: accountId, conversationId: conversationId) else {
231 return
232 }
Kateryna Kostiuk2648aab2022-08-30 14:24:03 -0400233 let data = EventData(accountId, from, conversationId, url.path, "")
234 // 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 -0400235 if fileAlreadyDownloaded(fileName: fileId, accountId: accountId, conversationId: conversationId) {
236 handler(.fileTransferDone, data)
237 } else {
238 guard let interactionId = message[InteractionAttributes.interactionId.rawValue],
239 let size = message["totalSize"],
240 (Int(size) ?? (maxSizeForAutoaccept + 1)) <= maxSizeForAutoaccept else { return }
241 let path = ""
242 self.adapter.downloadFile(withFileId: fileId, accountId: accountId, conversationId: conversationId, interactionId: interactionId, withFilePath: path)
243 self.loadingFiles[fileId] = data
244 handler(.fileTransferInProgress, data)
245 }
246 }
247 }
248}