blob: c6fabb578a00ea206a98742fe646eb5646a5eb00 [file] [log] [blame]
/*
* Copyright (C) 2017-2019 Savoir-faire Linux Inc.
*
* Author: Kateryna Kostiuk <kateryna.kostiuk@savoirfairelinux.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/
// swiftlint:disable identifier_name
import RxSwift
import SwiftyBeaver
@objc protocol ProfilesAdapterDelegate {
func profileReceived(contact uri: String, withAccountId accountId: String, path: String)
}
enum ProfileNotifications: String {
case messageReceived
case contactAdded
}
enum ProfileNotificationsKeys: String {
case ringID
case accountId
case message
}
struct Base64VCard {
var data: [Int: String] // The key is the number of vCard part
var partsReceived: Int
}
class ProfilesService: ProfilesAdapterDelegate {
private let ringVCardMIMEType = "x-ring/ring.profile.vcard;"
private var base64VCards = [Int: Base64VCard]()
private let log = SwiftyBeaver.self
private let profilesAdapter: ProfilesAdapter
var profiles = [String: ReplaySubject<Profile>]()
var accountProfiles = [String: ReplaySubject<Profile>]()
let dbManager: DBManager
let disposeBag = DisposeBag()
init(withProfilesAdapter adapter: ProfilesAdapter, dbManager: DBManager) {
profilesAdapter = adapter
self.dbManager = dbManager
NotificationCenter.default.addObserver(self, selector: #selector(self.messageReceived(_:)),
name: NSNotification.Name(rawValue: ProfileNotifications.messageReceived.rawValue),
object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(self.contactAdded(_:)),
name: NSNotification.Name(rawValue: ProfileNotifications.contactAdded.rawValue),
object: nil)
ProfilesAdapter.delegate = self
}
func profileReceived(contact uri: String, withAccountId accountId: String, path: String) {
let uri = JamiURI(schema: URIType.ring, infoHash: uri)
guard let uriString = uri.uriString,
let data = FileManager.default.contents(atPath: path),
var profile = VCardUtils.parseToProfile(data: data) else { return }
if let imageString = profile.photo, let image = imageString.createImage(),
let resizedImage = image.resizeProfileImage() {
let imageData = resizedImage.jpegData(compressionQuality: 1)
if let base64String = imageData?.base64EncodedString() {
profile.photo = base64String
}
}
_ = self.dbManager
.createOrUpdateRingProfile(profileUri: uriString,
alias: profile.alias,
image: profile.photo,
accountId: accountId)
self.triggerProfileSignal(uri: uriString, createIfNotexists: false, accountId: accountId)
}
@objc
private func contactAdded(_ notification: NSNotification) {
guard let ringId = notification.userInfo?[ProfileNotificationsKeys.ringID.rawValue] as? String else {
return
}
guard let accountId = notification.userInfo?[ProfileNotificationsKeys.accountId.rawValue] as? String else {
return
}
let uri = JamiURI(schema: URIType.ring, infoHash: ringId)
let uriString = uri.uriString ?? ringId
self.triggerProfileSignal(uri: uriString, createIfNotexists: false, accountId: accountId)
}
@objc
private func messageReceived(_ notification: NSNotification) {
guard let ringId = notification.userInfo?[ProfileNotificationsKeys.ringID.rawValue] as? String else {
return
}
guard let message = notification.userInfo?[ProfileNotificationsKeys.message.rawValue] as? [String: String] else {
return
}
guard let accountId = notification.userInfo?[ProfileNotificationsKeys.accountId.rawValue] as? String else {
return
}
if let vCardKey = message.keys.filter({ $0.hasPrefix(self.ringVCardMIMEType) }).first,
let decoded = vCardKey.removingPercentEncoding {
guard let regex = try? NSRegularExpression(pattern: "x-ring/ring.profile.vcard;id=([A-z0-9]+),part=([0-9]+),of=([0-9]+)") else {
return
}
let matches = regex.matches(in: decoded, range: NSRange(decoded.startIndex..., in: decoded))
guard let match = matches.first,
let idRange = Range(match.range(at: 1), in: decoded),
let partRange = Range(match.range(at: 2), in: decoded),
let ofRange = Range(match.range(at: 3), in: decoded) else { return }
let idString = String(decoded[idRange])
let partString = String(decoded[partRange])
let ofString = String(decoded[ofRange])
guard let part = Int(partString),
let of = Int(ofString),
let id = Int(idString) else { return }
var numberOfReceivedChunk = 1
if var chunk = self.base64VCards[id] {
chunk.data[part] = message[vCardKey]
chunk.partsReceived += 1
numberOfReceivedChunk = chunk.partsReceived
self.base64VCards[id] = chunk
} else {
if let partMessage = message[vCardKey] {
let data: [Int: String] = [part: partMessage]
let chunk = Base64VCard(data: data, partsReceived: numberOfReceivedChunk)
self.base64VCards[id] = chunk
}
}
// Build the vCard when all data are appended
if of == numberOfReceivedChunk {
self.buildVCardFromChunks(cardID: id, ringID: ringId, accountId: accountId)
}
}
}
private func buildVCardFromChunks(cardID: Int, ringID: String, accountId: String) {
guard let vcard = self.base64VCards[cardID] else {
return
}
let vCardChunks = vcard.data
// Append data from sorted part numbers
var vCardData = Data()
for currentPartNumber in vCardChunks.keys.sorted() {
if let currentData = vCardChunks[currentPartNumber]?.data(using: String.Encoding.utf8) {
vCardData.append(currentData)
}
}
// Create the vCard, save and db and emit a new event
if let profile = VCardUtils.parseToProfile(data: vCardData) {
guard let uri = JamiURI.init(schema: URIType.ring,
infoHash: ringID).uriString else {
return
}
_ = self.dbManager
.createOrUpdateRingProfile(profileUri: uri,
alias: profile.alias,
image: profile.photo,
accountId: accountId)
self.triggerProfileSignal(uri: uri, createIfNotexists: false, accountId: accountId)
}
}
private func triggerProfileSignal(uri: String, createIfNotexists: Bool, accountId: String) {
guard let profileObservable = self.profiles[uri] else {
return
}
self.dbManager
.profileObservable(for: uri, createIfNotExists: createIfNotexists, accountId: accountId)
.subscribe(on: ConcurrentDispatchQueueScheduler(qos: .background))
.subscribe { profile in
profileObservable.onNext(profile)
} onError: { error in
profileObservable.onError(error)
}
.disposed(by: self.disposeBag)
}
func getProfile(uri: String, createIfNotexists: Bool, accountId: String) -> Observable<Profile> {
if let profile = self.profiles[uri] {
return profile.asObservable().share()
}
let profileObservable = ReplaySubject<Profile>.create(bufferSize: 1)
self.profiles[uri] = profileObservable
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { [weak self] in
self?.triggerProfileSignal(uri: uri,
createIfNotexists: createIfNotexists,
accountId: accountId)
}
return profileObservable.share()
}
}
// MARK: account profile
extension ProfilesService {
func getAccountProfile(accountId: String) -> Observable<Profile> {
if let profile = self.accountProfiles[accountId] {
return profile.asObservable().share()
}
let profileObservable = ReplaySubject<Profile>.create(bufferSize: 1)
self.accountProfiles[accountId] = profileObservable
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { [weak self] in
self?.triggerAccountProfileSignal(accountId: accountId)
}
return profileObservable.share()
}
private func triggerAccountProfileSignal(accountId: String) {
guard let profileObservable = self.accountProfiles[accountId] else {
return
}
self.dbManager
.accountProfileObservable(for: accountId)
.subscribe(on: ConcurrentDispatchQueueScheduler(qos: .background))
.subscribe(onNext: { profile in
profileObservable.onNext(profile)
}, onError: { (_) in
profileObservable.onNext(Profile(uri: "", alias: nil, photo: nil, type: ""))
})
.disposed(by: self.disposeBag)
}
func updateAccountProfile(accountId: String, alias: String?, photo: String?, accountURI: String) {
if self.dbManager
.saveAccountProfile(alias: alias, photo: photo,
accountId: accountId, accountURI: accountURI) {
self.triggerAccountProfileSignal(accountId: accountId)
}
}
}