mirror of https://github.com/oxen-io/session-ios
You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
249 lines
15 KiB
Swift
249 lines
15 KiB
Swift
import PromiseKit
|
|
|
|
@objc(LKPublicChatPoller)
|
|
public final class PublicChatPoller : NSObject {
|
|
private let publicChat: OpenGroup
|
|
private var pollForNewMessagesTimer: Timer? = nil
|
|
private var pollForDeletedMessagesTimer: Timer? = nil
|
|
private var pollForModeratorsTimer: Timer? = nil
|
|
private var pollForDisplayNamesTimer: Timer? = nil
|
|
private var hasStarted = false
|
|
private var isPolling = false
|
|
|
|
// MARK: Settings
|
|
private let pollForNewMessagesInterval: TimeInterval = 4
|
|
private let pollForDeletedMessagesInterval: TimeInterval = 60
|
|
private let pollForModeratorsInterval: TimeInterval = 10 * 60
|
|
private let pollForDisplayNamesInterval: TimeInterval = 60
|
|
|
|
// MARK: Lifecycle
|
|
@objc(initForPublicChat:)
|
|
public init(for publicChat: OpenGroup) {
|
|
self.publicChat = publicChat
|
|
super.init()
|
|
}
|
|
|
|
@objc public func startIfNeeded() {
|
|
if hasStarted { return }
|
|
DispatchQueue.main.async { [weak self] in // Timers don't do well on background queues
|
|
guard let strongSelf = self else { return }
|
|
strongSelf.pollForNewMessagesTimer = Timer.scheduledTimer(withTimeInterval: strongSelf.pollForNewMessagesInterval, repeats: true) { _ in self?.pollForNewMessages() }
|
|
strongSelf.pollForDeletedMessagesTimer = Timer.scheduledTimer(withTimeInterval: strongSelf.pollForDeletedMessagesInterval, repeats: true) { _ in self?.pollForDeletedMessages() }
|
|
strongSelf.pollForModeratorsTimer = Timer.scheduledTimer(withTimeInterval: strongSelf.pollForModeratorsInterval, repeats: true) { _ in self?.pollForModerators() }
|
|
strongSelf.pollForDisplayNamesTimer = Timer.scheduledTimer(withTimeInterval: strongSelf.pollForDisplayNamesInterval, repeats: true) { _ in self?.pollForDisplayNames() }
|
|
// Perform initial updates
|
|
strongSelf.pollForNewMessages()
|
|
strongSelf.pollForDeletedMessages()
|
|
strongSelf.pollForModerators()
|
|
strongSelf.pollForDisplayNames()
|
|
strongSelf.hasStarted = true
|
|
}
|
|
}
|
|
|
|
@objc public func stop() {
|
|
pollForNewMessagesTimer?.invalidate()
|
|
pollForDeletedMessagesTimer?.invalidate()
|
|
pollForModeratorsTimer?.invalidate()
|
|
pollForDisplayNamesTimer?.invalidate()
|
|
hasStarted = false
|
|
}
|
|
|
|
// MARK: Polling
|
|
@objc(pollForNewMessages)
|
|
public func objc_pollForNewMessages() -> AnyPromise {
|
|
AnyPromise.from(pollForNewMessages())
|
|
}
|
|
|
|
public func pollForNewMessages() -> Promise<Void> {
|
|
guard !self.isPolling else { return Promise.value(()) }
|
|
self.isPolling = true
|
|
let publicChat = self.publicChat
|
|
let userPublicKey = getUserHexEncodedPublicKey()
|
|
return OpenGroupAPI.getMessages(for: publicChat.channel, on: publicChat.server).done(on: DispatchQueue.global(qos: .default)) { messages in
|
|
self.isPolling = false
|
|
let uniquePublicKeys = Set(messages.map { $0.senderPublicKey })
|
|
func proceed() {
|
|
let storage = OWSPrimaryStorage.shared()
|
|
/*
|
|
var newDisplayNameUpdatees: Set<String> = []
|
|
storage.dbReadConnection.read { transaction in
|
|
newDisplayNameUpdatees = Set(uniquePublicKeys.filter { storage.getMasterHexEncodedPublicKey(for: $0, in: transaction) != $0 }.compactMap { storage.getMasterHexEncodedPublicKey(for: $0, in: transaction) })
|
|
}
|
|
if !newDisplayNameUpdatees.isEmpty {
|
|
let displayNameUpdatees = OpenGroupAPI.displayNameUpdatees[publicChat.id] ?? []
|
|
OpenGroupAPI.displayNameUpdatees[publicChat.id] = displayNameUpdatees.union(newDisplayNameUpdatees)
|
|
}
|
|
*/
|
|
// Sorting the messages by timestamp before importing them fixes an issue where messages that quote older messages can't find those older messages
|
|
messages.sorted { $0.serverTimestamp < $1.serverTimestamp }.forEach { message in
|
|
var wasSentByCurrentUser = false
|
|
OWSPrimaryStorage.shared().dbReadConnection.read { transaction in
|
|
wasSentByCurrentUser = LokiDatabaseUtilities.isUserLinkedDevice(message.senderPublicKey, transaction: transaction)
|
|
}
|
|
var masterPublicKey: String? = nil
|
|
storage.dbReadConnection.read { transaction in
|
|
masterPublicKey = storage.getMasterHexEncodedPublicKey(for: message.senderPublicKey, in: transaction)
|
|
}
|
|
let senderPublicKey = masterPublicKey ?? message.senderPublicKey
|
|
func generateDisplayName(from rawDisplayName: String) -> String {
|
|
let endIndex = senderPublicKey.endIndex
|
|
let cutoffIndex = senderPublicKey.index(endIndex, offsetBy: -8)
|
|
return "\(rawDisplayName) (...\(senderPublicKey[cutoffIndex..<endIndex]))"
|
|
}
|
|
var senderDisplayName = ""
|
|
if let masterHexEncodedPublicKey = masterPublicKey {
|
|
senderDisplayName = UserDisplayNameUtilities.getPublicChatDisplayName(for: senderPublicKey, in: publicChat.channel, on: publicChat.server) ?? generateDisplayName(from: NSLocalizedString("Anonymous", comment: ""))
|
|
} else {
|
|
senderDisplayName = generateDisplayName(from: message.displayName)
|
|
}
|
|
let id = LKGroupUtilities.getEncodedOpenGroupIDAsData(publicChat.id)
|
|
let groupContext = SSKProtoGroupContext.builder(id: id, type: .deliver)
|
|
groupContext.setName(publicChat.displayName)
|
|
let dataMessage = SSKProtoDataMessage.builder()
|
|
let attachments: [SSKProtoAttachmentPointer] = message.attachments.compactMap { attachment in
|
|
guard attachment.kind == .attachment else { return nil }
|
|
let result = SSKProtoAttachmentPointer.builder(id: attachment.serverID)
|
|
result.setContentType(attachment.contentType)
|
|
result.setSize(UInt32(attachment.size))
|
|
result.setFileName(attachment.fileName)
|
|
result.setFlags(UInt32(attachment.flags))
|
|
result.setWidth(UInt32(attachment.width))
|
|
result.setHeight(UInt32(attachment.height))
|
|
if let caption = attachment.caption {
|
|
result.setCaption(caption)
|
|
}
|
|
result.setUrl(attachment.url)
|
|
return try! result.build()
|
|
}
|
|
dataMessage.setAttachments(attachments)
|
|
if let linkPreview = message.attachments.first(where: { $0.kind == .linkPreview }) {
|
|
let signalLinkPreview = SSKProtoDataMessagePreview.builder(url: linkPreview.linkPreviewURL!)
|
|
signalLinkPreview.setTitle(linkPreview.linkPreviewTitle!)
|
|
let attachment = SSKProtoAttachmentPointer.builder(id: linkPreview.serverID)
|
|
attachment.setContentType(linkPreview.contentType)
|
|
attachment.setSize(UInt32(linkPreview.size))
|
|
attachment.setFileName(linkPreview.fileName)
|
|
attachment.setFlags(UInt32(linkPreview.flags))
|
|
attachment.setWidth(UInt32(linkPreview.width))
|
|
attachment.setHeight(UInt32(linkPreview.height))
|
|
if let caption = linkPreview.caption {
|
|
attachment.setCaption(caption)
|
|
}
|
|
attachment.setUrl(linkPreview.url)
|
|
signalLinkPreview.setImage(try! attachment.build())
|
|
dataMessage.setPreview([ try! signalLinkPreview.build() ])
|
|
}
|
|
let profile = SSKProtoDataMessageLokiProfile.builder()
|
|
profile.setDisplayName(message.displayName)
|
|
if let profilePicture = message.profilePicture {
|
|
profile.setProfilePicture(profilePicture.url)
|
|
dataMessage.setProfileKey(profilePicture.profileKey)
|
|
}
|
|
dataMessage.setProfile(try! profile.build())
|
|
dataMessage.setTimestamp(message.timestamp)
|
|
dataMessage.setGroup(try! groupContext.build())
|
|
if let quote = message.quote {
|
|
let signalQuote = SSKProtoDataMessageQuote.builder(id: quote.quotedMessageTimestamp, author: quote.quoteePublicKey)
|
|
signalQuote.setText(quote.quotedMessageBody)
|
|
dataMessage.setQuote(try! signalQuote.build())
|
|
}
|
|
let body = (message.body == message.timestamp.description) ? "" : message.body // Workaround for the fact that the back-end doesn't accept messages without a body
|
|
dataMessage.setBody(body)
|
|
if let messageServerID = message.serverID {
|
|
let publicChatInfo = SSKProtoPublicChatInfo.builder()
|
|
publicChatInfo.setServerID(messageServerID)
|
|
dataMessage.setPublicChatInfo(try! publicChatInfo.build())
|
|
}
|
|
let content = SSKProtoContent.builder()
|
|
if !wasSentByCurrentUser {
|
|
content.setDataMessage(try! dataMessage.build())
|
|
} else {
|
|
// The line below is necessary to make it so that when a user sends a message in an open group and then
|
|
// deletes and re-joins the open group without closing the app in between, the message isn't ignored.
|
|
SyncMessagesProtocol.dropFromSyncMessageTimestampCache(message.timestamp, for: senderPublicKey)
|
|
let syncMessageSentBuilder = SSKProtoSyncMessageSent.builder()
|
|
syncMessageSentBuilder.setMessage(try! dataMessage.build())
|
|
syncMessageSentBuilder.setDestination(userPublicKey)
|
|
syncMessageSentBuilder.setTimestamp(message.timestamp)
|
|
let syncMessageSent = try! syncMessageSentBuilder.build()
|
|
let syncMessageBuilder = SSKProtoSyncMessage.builder()
|
|
syncMessageBuilder.setSent(syncMessageSent)
|
|
content.setSyncMessage(try! syncMessageBuilder.build())
|
|
}
|
|
let envelope = SSKProtoEnvelope.builder(type: .ciphertext, timestamp: message.timestamp)
|
|
envelope.setSource(senderPublicKey)
|
|
envelope.setSourceDevice(OWSDevicePrimaryDeviceId)
|
|
envelope.setContent(try! content.build().serializedData())
|
|
envelope.setServerTimestamp(message.serverTimestamp)
|
|
Storage.writeSync { transaction in
|
|
transaction.setObject(senderDisplayName, forKey: senderPublicKey, inCollection: publicChat.id)
|
|
let messageServerID = message.serverID
|
|
SSKEnvironment.shared.messageManager.throws_processEnvelope(try! envelope.build(), plaintextData: try! content.build().serializedData(), wasReceivedByUD: false, transaction: transaction, serverID: messageServerID ?? 0)
|
|
// If we got a message from our master device then we should use its profile picture
|
|
if let profilePicture = message.profilePicture, masterPublicKey == message.senderPublicKey {
|
|
if (message.displayName.count > 0) {
|
|
SSKEnvironment.shared.profileManager.updateProfileForContact(withID: masterPublicKey!, displayName: message.displayName, with: transaction)
|
|
}
|
|
SSKEnvironment.shared.profileManager.updateService(withProfileName: message.displayName, avatarURL: profilePicture.url)
|
|
SSKEnvironment.shared.profileManager.setProfileKeyData(profilePicture.profileKey, forRecipientId: masterPublicKey!, avatarURL: profilePicture.url)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
/*
|
|
let hexEncodedPublicKeysToUpdate = uniquePublicKeys.filter { hexEncodedPublicKey in
|
|
let timeSinceLastUpdate: TimeInterval
|
|
if let lastDeviceLinkUpdate = MultiDeviceProtocol.lastDeviceLinkUpdate[hexEncodedPublicKey] {
|
|
timeSinceLastUpdate = Date().timeIntervalSince(lastDeviceLinkUpdate)
|
|
} else {
|
|
timeSinceLastUpdate = .infinity
|
|
}
|
|
return timeSinceLastUpdate > MultiDeviceProtocol.deviceLinkUpdateInterval
|
|
}
|
|
if !hexEncodedPublicKeysToUpdate.isEmpty {
|
|
FileServerAPI.getDeviceLinks(associatedWith: hexEncodedPublicKeysToUpdate).done(on: DispatchQueue.global(qos: .default)) { _ in
|
|
proceed()
|
|
hexEncodedPublicKeysToUpdate.forEach {
|
|
MultiDeviceProtocol.lastDeviceLinkUpdate[$0] = Date() // TODO: Doing this from a global queue seems a bit iffy
|
|
}
|
|
}.catch(on: DispatchQueue.global(qos: .default)) { error in
|
|
if (error as? DotNetAPI.DotNetAPIError) == DotNetAPI.DotNetAPIError.parsingFailed {
|
|
// Don't immediately re-fetch in case of failure due to a parsing error
|
|
hexEncodedPublicKeysToUpdate.forEach {
|
|
MultiDeviceProtocol.lastDeviceLinkUpdate[$0] = Date() // TODO: Doing this from a global queue seems a bit iffy
|
|
}
|
|
}
|
|
proceed()
|
|
}
|
|
} else {
|
|
*/
|
|
DispatchQueue.global(qos: .default).async {
|
|
proceed()
|
|
}
|
|
/*
|
|
}
|
|
*/
|
|
}
|
|
}
|
|
|
|
private func pollForDeletedMessages() {
|
|
let publicChat = self.publicChat
|
|
let _ = OpenGroupAPI.getDeletedMessageServerIDs(for: publicChat.channel, on: publicChat.server).done(on: DispatchQueue.global(qos: .default)) { deletedMessageServerIDs in
|
|
Storage.writeSync { transaction in
|
|
let deletedMessageIDs = deletedMessageServerIDs.compactMap { OWSPrimaryStorage.shared().getIDForMessage(withServerID: UInt($0), in: transaction) }
|
|
deletedMessageIDs.forEach { messageID in
|
|
TSMessage.fetch(uniqueId: messageID)?.remove(with: transaction)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private func pollForModerators() {
|
|
let _ = OpenGroupAPI.getModerators(for: publicChat.channel, on: publicChat.server)
|
|
}
|
|
|
|
private func pollForDisplayNames() {
|
|
let _ = OpenGroupAPI.getDisplayNames(for: publicChat.channel, on: publicChat.server)
|
|
}
|
|
}
|