blob: 4319b1dae4d24b7b33a77014e1c667af51447c39 [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
53 }
54
55 enum PeerConnectionRequestType {
56 case call(peerId: String, isVideo: Bool)
57 case gitMessage
58 case unknown
59 }
60
61 enum DataTransferEventCode: Int {
62 case invalid
63 case created
64 case unsupported
65 case waitPeeracceptance
66 case waitHostAcceptance
67 case ongoing
68 case finished
69 case closedByHost
70 case closedByPeer
71 case invalidPathname
72 case unjoinablePeer
73
74 func isCompleted() -> Bool {
75 switch self {
76 case .finished, .closedByHost, .closedByPeer, .unjoinablePeer, .invalidPathname:
77 return true
78 default:
79 return false
80 }
81 }
82 }
83 private let maxSizeForAutoaccept = 20 * 1024 * 1024
84
85 typealias EventData = (accountId: String, jamiId: String, content: String)
86
87 private let adapter: Adapter
88 var eventHandler: ((EventType, EventData) -> Void)?
89 var loadingFiles = [String: EventData]()
90
91 init(withAdapter adapter: Adapter) {
92 self.adapter = adapter
93 Adapter.delegate = self
94 }
95
96 func startAccountsWithListener(listener: @escaping (EventType, EventData) -> Void) {
97 self.eventHandler = listener
98 start()
99 }
100
101 func decrypt(keyPath: String, messagesPath: String, value: [String: Any]) -> PeerConnectionRequestType {
102 let result = adapter.decrypt(keyPath, treated: messagesPath, value: value)
103 guard let peerId = result?.keys.first,
104 let type = result?.values.first else {
105 return .unknown}
106 switch type {
107 case "videoCall":
108 return PeerConnectionRequestType.call(peerId: peerId, isVideo: true)
109 case "audioCall":
110 return PeerConnectionRequestType.call(peerId: peerId, isVideo: false)
111 case "text/plain", "application/im-gitmessage-id":
112 return PeerConnectionRequestType.gitMessage
113 default:
114 return .unknown
115 }
116 }
117
118 func start() {
119 self.adapter.start()
120 }
121
122 func stop() {
123 self.adapter.stop()
124 }
125
126 private func fileAlreadyDownloaded(fileName: String, accountId: String, conversationId: String) -> Bool {
127 guard let url = getFileUrlFor(fileName: fileName, accountId: accountId, conversationId: conversationId) else {
128 return false
129 }
130 return FileManager.default.fileExists(atPath: url.path)
131 }
132
133 private func getFileUrlFor(fileName: String, accountId: String, conversationId: String) -> URL? {
134 guard let documentsURL = Constants.documentsPath else {
135 return nil
136 }
137 let pathUrl = documentsURL.appendingPathComponent(accountId)
138 .appendingPathComponent("conversation_data")
139 .appendingPathComponent(conversationId)
140 .appendingPathComponent(fileName)
141 return pathUrl
142 }
143}
144
145extension AdapterService: AdapterDelegate {
146
147 func didReceiveMessage(_ message: [String: String],
148 from senderAccount: String,
149 messageId: String,
150 to receiverAccountId: String) {
151 guard let content = message["text/plain"],
152 let handler = self.eventHandler else { return }
153 handler(.message, EventData(receiverAccountId, senderAccount, content))
154 }
155
156 func dataTransferEvent(withFileId transferId: String, withEventCode eventCode: Int, accountId: String, conversationId: String, interactionId: String) {
157 guard let handler = self.eventHandler,
158 let data = loadingFiles[transferId],
159 let code = DataTransferEventCode(rawValue: eventCode),
160 code.isCompleted() else { return }
161 handler(.fileTransferDone, data)
162 loadingFiles.removeValue(forKey: transferId)
163 }
164
165 func conversationSyncCompleted(accountId: String) {
166 guard let handler = self.eventHandler else {
167 return
168 }
169 handler(.syncCompleted, EventData(accountId, "", ""))
170 }
171
172 func receivedCallConnectionRequest(accountId: String, peerId: String, hasVideo: Bool) {
173 guard let handler = self.eventHandler else {
174 return
175 }
176 handler(.call, EventData(accountId, peerId, "\(hasVideo)"))
177 }
178
179 func newInteraction(conversationId: String, accountId: String, message: [String: String]) {
180 guard let handler = self.eventHandler else {
181 return
182 }
183 guard let type = message[InteractionAttributes.type.rawValue],
184 let interactionType = InteractionType(rawValue: type) else {
185 return
186 }
187 let from = message[InteractionAttributes.author.rawValue] ?? ""
188 let content = message[InteractionAttributes.body.rawValue] ?? ""
189 switch interactionType {
190 case .message:
191 handler(.message, EventData(accountId, from, content))
192 case.fileTransfer:
193 guard let fileId = message[InteractionAttributes.fileId.rawValue],
194 let url = self.getFileUrlFor(fileName: fileId, accountId: accountId, conversationId: conversationId) else {
195 return
196 }
197 let data = EventData(accountId, from, url.path)
198 /// check if the file has already been downloaded. If no, download the file if filesize is less than a downloading limit
199 if fileAlreadyDownloaded(fileName: fileId, accountId: accountId, conversationId: conversationId) {
200 handler(.fileTransferDone, data)
201 } else {
202 guard let interactionId = message[InteractionAttributes.interactionId.rawValue],
203 let size = message["totalSize"],
204 (Int(size) ?? (maxSizeForAutoaccept + 1)) <= maxSizeForAutoaccept else { return }
205 let path = ""
206 self.adapter.downloadFile(withFileId: fileId, accountId: accountId, conversationId: conversationId, interactionId: interactionId, withFilePath: path)
207 self.loadingFiles[fileId] = data
208 handler(.fileTransferInProgress, data)
209 }
210 }
211 }
212}