Merge pull request #41 from loki-project/rss

RSS Feed Support
This commit is contained in:
gmbnt 2019-08-27 16:04:27 +10:00 committed by GitHub
commit b61b440063
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 197 additions and 65 deletions

View File

@ -73,6 +73,7 @@ target 'Signal' do
# Loki
pod 'GCDWebServer', '~> 3.0'
pod 'FeedKit', '~> 8.1'
target 'SignalTests' do
inherit! :search_paths

View File

@ -36,6 +36,7 @@ PODS:
- Curve25519Kit/Tests (2.1.0):
- CocoaLumberjack
- SignalCoreKit
- FeedKit (8.1.1)
- GCDWebServer (3.5.2):
- GCDWebServer/Core (= 3.5.2)
- GCDWebServer/Core (3.5.2)
@ -198,6 +199,7 @@ DEPENDENCIES:
- CryptoSwift
- Curve25519Kit (from `https://github.com/signalapp/Curve25519Kit`)
- Curve25519Kit/Tests (from `https://github.com/signalapp/Curve25519Kit`)
- FeedKit (~> 8.1)
- GCDWebServer (~> 3.0)
- GRKOpenSSLFramework (from `https://github.com/signalapp/GRKOpenSSLFramework`)
- HKDFKit (from `https://github.com/signalapp/HKDFKit.git`)
@ -224,6 +226,7 @@ SPEC REPOS:
- AFNetworking
- CocoaLumberjack
- CryptoSwift
- FeedKit
- GCDWebServer
- IGIdenticon
- libPhoneNumber-iOS
@ -297,6 +300,7 @@ SPEC CHECKSUMS:
CocoaLumberjack: 2f44e60eb91c176d471fdba43b9e3eae6a721947
CryptoSwift: d81eeaa59dc5a8d03720fe919a6fd07b51f7439f
Curve25519Kit: b3e77b7152ebe95fee2b3fb6c955449492bc14f7
FeedKit: 3418eed25f0b493b205b4de1b8511ac21d413fa9
GCDWebServer: ead88cd14596dd4eae4f5830b8877c87c8728990
GRKOpenSSLFramework: 8a3735ad41e7dc1daff460467bccd32ca5d6ae3e
HKDFKit: 3b6dbbb9d59c221cc6c52c3aa915700cbf24e376
@ -309,7 +313,7 @@ SPEC CHECKSUMS:
SAMKeychain: 483e1c9f32984d50ca961e26818a534283b4cd5c
SignalCoreKit: c2d8132cdedb95d35eb2f8ae7eac0957695d0a8b
SignalMetadataKit: 6fa5e9a53c7f104568662521a2f3874672ff7a02
SignalServiceKit: 5c5b63a39d5054201ab59ef6daf0fa0a1a0c7887
SignalServiceKit: 102576f58e17a5fe3093899adce7e7c192a7bee0
SQLCipher: efbdb52cdbe340bcd892b1b14297df4e07241b7f
SSZipArchive: 8e859da2520142e09166bc9161967db296e9d02f
Starscream: ef3ece99d765eeccb67de105bfa143f929026cf5
@ -317,6 +321,6 @@ SPEC CHECKSUMS:
YapDatabase: b418a4baa6906e8028748938f9159807fd039af4
YYImage: 1e1b62a9997399593e4b9c4ecfbbabbf1d3f3b54
PODFILE CHECKSUM: 10152a1fffafd51206b62fdd8cac86a5de8cf083
PODFILE CHECKSUM: 95f41137d4fe8c5b8a27de951b328f8c9531d166
COCOAPODS: 1.7.2
COCOAPODS: 1.5.3

2
Pods

@ -1 +1 @@
Subproject commit d9ab8b13002bf6ebc932ca4f45df56b577b6a188
Subproject commit 20b736ae28ecd42b5fc13f583a010ac9354be507

View File

@ -3276,12 +3276,13 @@
files = (
);
inputPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Signal/Pods-Signal-frameworks.sh",
"${SRCROOT}/Pods/Target Support Files/Pods-Signal/Pods-Signal-frameworks.sh",
"${BUILT_PRODUCTS_DIR}/AFNetworking/AFNetworking.framework",
"${BUILT_PRODUCTS_DIR}/AxolotlKit/AxolotlKit.framework",
"${BUILT_PRODUCTS_DIR}/CocoaLumberjack/CocoaLumberjack.framework",
"${BUILT_PRODUCTS_DIR}/CryptoSwift/CryptoSwift.framework",
"${BUILT_PRODUCTS_DIR}/Curve25519Kit/Curve25519Kit.framework",
"${BUILT_PRODUCTS_DIR}/FeedKit/FeedKit.framework",
"${BUILT_PRODUCTS_DIR}/GCDWebServer/GCDWebServer.framework",
"${PODS_ROOT}/GRKOpenSSLFramework/OpenSSL-iOS/bin/openssl.framework",
"${BUILT_PRODUCTS_DIR}/HKDFKit/HKDFKit.framework",
@ -3309,6 +3310,7 @@
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/CocoaLumberjack.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/CryptoSwift.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Curve25519Kit.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/FeedKit.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/GCDWebServer.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/openssl.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/HKDFKit.framework",
@ -3331,7 +3333,7 @@
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Signal/Pods-Signal-frameworks.sh\"\n";
shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-Signal/Pods-Signal-frameworks.sh\"\n";
showEnvVarsInLog = 0;
};
6565655F4068F9E5CDC5687F /* [CP] Check Pods Manifest.lock */ = {
@ -3358,7 +3360,7 @@
files = (
);
inputPaths = (
"${PODS_ROOT}/Target Support Files/Pods-SignalTests/Pods-SignalTests-frameworks.sh",
"${SRCROOT}/Pods/Target Support Files/Pods-SignalTests/Pods-SignalTests-frameworks.sh",
"${BUILT_PRODUCTS_DIR}/AFNetworking/AFNetworking.framework",
"${BUILT_PRODUCTS_DIR}/AxolotlKit/AxolotlKit.framework",
"${BUILT_PRODUCTS_DIR}/CocoaLumberjack/CocoaLumberjack.framework",
@ -3409,7 +3411,7 @@
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-SignalTests/Pods-SignalTests-frameworks.sh\"\n";
shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-SignalTests/Pods-SignalTests-frameworks.sh\"\n";
showEnvVarsInLog = 0;
};
F4C416F20E3CB0B25DC10C56 /* [CP] Check Pods Manifest.lock */ = {

View File

@ -8,6 +8,7 @@ extern NSString *const AppDelegateStoryboardMain;
@interface AppDelegate : UIResponder <UIApplicationDelegate>
- (void)startPublicChatPollingIfNeeded;
- (void)createGroupChatsIfNeeded;
- (void)startGroupChatPollersIfNeeded;
@end

View File

@ -65,6 +65,8 @@ static NSTimeInterval launchStartedAt;
@property (nonatomic) BOOL didAppLaunchFail;
@property (nonatomic) LKP2PServer *lokiP2PServer;
@property (nonatomic) LKGroupChatPoller *lokiPublicChatPoller;
@property (nonatomic) LKGroupChatPoller *lokiNewsPoller;
@property (nonatomic) LKGroupChatPoller *lokiMessengerUpdatesPoller;
@end
@ -1485,34 +1487,60 @@ static NSTimeInterval launchStartedAt;
#pragma mark - Loki
- (void)setUpPublicChatIfNeeded
- (LKGroupChat *)lokiPublicChat
{
if (self.lokiPublicChatPoller != nil) { return; }
self.lokiPublicChatPoller = [[LKGroupChatPoller alloc] initForGroup:(NSUInteger)LKGroupChatAPI.publicChatID onServer:LKGroupChatAPI.publicChatServer];
BOOL isPublicChatSetUp = [NSUserDefaults.standardUserDefaults boolForKey:@"isPublicChatSetUp"];
if (isPublicChatSetUp) { return; }
NSString *title = NSLocalizedString(@"Loki Public Chat", @"");
NSData *groupID = [[[LKGroupChatAPI.publicChatServer stringByAppendingString:@"."] stringByAppendingString:@(LKGroupChatAPI.publicChatID).stringValue] dataUsingEncoding:NSUTF8StringEncoding];
TSGroupModel *group = [[TSGroupModel alloc] initWithTitle:title memberIds:@[ OWSIdentityManager.sharedManager.identityKeyPair.hexEncodedPublicKey, LKGroupChatAPI.publicChatServer ] image:nil groupId:groupID];
__block TSGroupThread *thread;
[OWSPrimaryStorage.dbReadWriteConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
thread = [TSGroupThread getOrCreateThreadWithGroupModel:group transaction:transaction];
NSTimeZone *timeZone = [NSTimeZone timeZoneWithName:@"UTC"];
NSCalendar *calendar = NSCalendar.currentCalendar;
[calendar setTimeZone:timeZone];
NSDateComponents *dateComponents = [NSDateComponents new];
[dateComponents setYear:999];
NSDate *date = [calendar dateByAddingComponents:dateComponents toDate:[NSDate new] options:0];
[thread updateWithMutedUntilDate:date transaction:transaction];
}];
[OWSProfileManager.sharedManager addThreadToProfileWhitelist:thread];
[NSUserDefaults.standardUserDefaults setBool:YES forKey:@"isPublicChatSetUp"];
return [[LKGroupChat alloc] initWithKindAsString:@"publicChat" id:@(LKGroupChatAPI.publicChatID).stringValue server:LKGroupChatAPI.publicChatServer displayName:NSLocalizedString(@"Loki Public Chat", @"") isDeletable:true];
}
- (void)startPublicChatPollingIfNeeded
- (LKGroupChat *)lokiNews
{
[self setUpPublicChatIfNeeded];
return [[LKGroupChat alloc] initWithKindAsString:@"rss" id:@"loki.network.feed" server:@"https://loki.network/feed/" displayName:NSLocalizedString(@"Loki News", @"") isDeletable:true];
}
- (LKGroupChat *)lokiMessengerUpdates
{
return [[LKGroupChat alloc] initWithKindAsString:@"rss" id:@"loki.network.messenger-update" server:@"https://loki.network/category/messenger-updates/feed/" displayName:NSLocalizedString(@"Loki Messenger Updates", @"") isDeletable:false];
}
- (void)createGroupChatsIfNeeded
{
NSArray *allGroupChats = @[ self.lokiPublicChat, self.lokiNews, self.lokiMessengerUpdates ];
NSString *userHexEncodedPublicKey = OWSIdentityManager.sharedManager.identityKeyPair.hexEncodedPublicKey;
for (LKGroupChat *chat in allGroupChats) {
NSString *userDefaultsKey = [@"isSetUp." stringByAppendingString:chat.id];
BOOL isChatSetUp = [NSUserDefaults.standardUserDefaults boolForKey:userDefaultsKey];
if (!isChatSetUp || !chat.isDeletable) {
TSGroupModel *group = [[TSGroupModel alloc] initWithTitle:chat.displayName memberIds:@[ userHexEncodedPublicKey, chat.server ] image:nil groupId:[chat.id dataUsingEncoding:NSUTF8StringEncoding]];
__block TSGroupThread *thread;
[OWSPrimaryStorage.dbReadWriteConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
thread = [TSGroupThread getOrCreateThreadWithGroupModel:group transaction:transaction];
NSTimeZone *timeZone = [NSTimeZone timeZoneWithName:@"UTC"];
NSCalendar *calendar = NSCalendar.currentCalendar;
[calendar setTimeZone:timeZone];
NSDateComponents *dateComponents = [NSDateComponents new];
[dateComponents setYear:999];
NSDate *date = [calendar dateByAddingComponents:dateComponents toDate:[NSDate new] options:0];
[thread updateWithMutedUntilDate:date transaction:transaction];
}];
[OWSProfileManager.sharedManager addThreadToProfileWhitelist:thread];
[NSUserDefaults.standardUserDefaults setBool:YES forKey:userDefaultsKey];
}
}
}
- (void)createGroupChatPollersIfNeeded
{
if (self.lokiPublicChatPoller == nil) { self.lokiPublicChatPoller = [[LKGroupChatPoller alloc] initForGroup:self.lokiPublicChat]; }
if (self.lokiNewsPoller == nil) { self.lokiNewsPoller = [[LKGroupChatPoller alloc] initForGroup:self.lokiNews]; }
if (self.lokiMessengerUpdatesPoller == nil) { self.lokiMessengerUpdatesPoller = [[LKGroupChatPoller alloc] initForGroup:self.lokiMessengerUpdates]; }
}
- (void)startGroupChatPollersIfNeeded
{
[self createGroupChatPollersIfNeeded];
[self.lokiPublicChatPoller startIfNeeded];
[self.lokiNewsPoller startIfNeeded];
[self.lokiMessengerUpdatesPoller startIfNeeded];
}
@end

View File

@ -1,26 +1,41 @@
import FeedKit
// TODO: Move the RSS feed logic into its own file
@objc(LKGroupChatPoller)
public final class LokiGroupChatPoller : NSObject {
private let group: UInt
private let server: String
private let group: LokiGroupChat
private var pollForNewMessagesTimer: Timer? = nil
private var pollForDeletedMessagesTimer: Timer? = nil
private var hasStarted = false
private let pollForNewMessagesInterval: TimeInterval = 4
private let pollForDeletedMessagesInterval: TimeInterval = 120
private lazy var pollForNewMessagesInterval: TimeInterval = {
switch group.kind {
case .publicChat(_): return 4
case .rss(_): return 8 * 60
}
}()
@objc(initForGroup:onServer:)
public init(for group: UInt, on server: String) {
private lazy var pollForDeletedMessagesInterval: TimeInterval = {
switch group.kind {
case .publicChat(_): return 32 * 60
case .rss(_): preconditionFailure()
}
}()
@objc(initForGroup:)
public init(for group: LokiGroupChat) {
self.group = group
self.server = server
super.init()
}
@objc public func startIfNeeded() {
if hasStarted { return }
pollForNewMessagesTimer = Timer.scheduledTimer(withTimeInterval: pollForNewMessagesInterval, repeats: true) { [weak self] _ in self?.pollForNewMessages() }
pollForDeletedMessagesTimer = Timer.scheduledTimer(withTimeInterval: pollForDeletedMessagesInterval, repeats: true) { [weak self] _ in self?.pollForDeletedMessages() }
pollForNewMessages() // Perform initial update
if group.isPublicChat {
pollForDeletedMessagesTimer = Timer.scheduledTimer(withTimeInterval: pollForDeletedMessagesInterval, repeats: true) { [weak self] _ in self?.pollForDeletedMessages() }
}
hasStarted = true
}
@ -32,28 +47,57 @@ public final class LokiGroupChatPoller : NSObject {
private func pollForNewMessages() {
let group = self.group
let server = self.server
let _ = LokiGroupChatAPI.getMessages(for: group, on: server).map { messages in
messages.reversed().map { message in
let id = "\(server).\(group)".data(using: String.Encoding.utf8)!
let x1 = SSKProtoGroupContext.builder(id: id, type: .deliver)
x1.setName(NSLocalizedString("Loki Public Chat", comment: ""))
let x2 = SSKProtoDataMessage.builder()
x2.setTimestamp(message.timestamp)
x2.setGroup(try! x1.build())
x2.setBody(message.body)
let x3 = SSKProtoContent.builder()
x3.setDataMessage(try! x2.build())
let x4 = SSKProtoEnvelope.builder(type: .ciphertext, timestamp: message.timestamp)
let senderHexEncodedPublicKey = message.hexEncodedPublicKey
let endIndex = senderHexEncodedPublicKey.endIndex
let cutoffIndex = senderHexEncodedPublicKey.index(endIndex, offsetBy: -8)
let senderDisplayName = "\(message.displayName) (...\(senderHexEncodedPublicKey[cutoffIndex..<endIndex]))"
x4.setSource(senderDisplayName)
x4.setSourceDevice(OWSDevicePrimaryDeviceId)
x4.setContent(try! x3.build().serializedData())
OWSPrimaryStorage.shared().dbReadWriteConnection.readWrite { transaction in
SSKEnvironment.shared.messageManager.throws_processEnvelope(try! x4.build(), plaintextData: try! x3.build().serializedData(), wasReceivedByUD: false, transaction: transaction)
func parseGroupMessage(body: String, timestamp: UInt64, senderDisplayName: String) {
let id = group.id.data(using: String.Encoding.utf8)!
let x1 = SSKProtoGroupContext.builder(id: id, type: .deliver)
x1.setName(group.displayName)
let x2 = SSKProtoDataMessage.builder()
x2.setTimestamp(timestamp)
x2.setGroup(try! x1.build())
x2.setBody(body)
let x3 = SSKProtoContent.builder()
x3.setDataMessage(try! x2.build())
let x4 = SSKProtoEnvelope.builder(type: .ciphertext, timestamp: timestamp)
x4.setSource(senderDisplayName)
x4.setSourceDevice(OWSDevicePrimaryDeviceId)
x4.setContent(try! x3.build().serializedData())
OWSPrimaryStorage.shared().dbReadWriteConnection.readWrite { transaction in
SSKEnvironment.shared.messageManager.throws_processEnvelope(try! x4.build(), plaintextData: try! x3.build().serializedData(), wasReceivedByUD: false, transaction: transaction)
}
}
switch group.kind {
case .publicChat(let id):
let _ = LokiGroupChatAPI.getMessages(for: id, on: group.server).done { messages in
messages.reversed().forEach { message in
let senderHexEncodedPublicKey = message.hexEncodedPublicKey
let endIndex = senderHexEncodedPublicKey.endIndex
let cutoffIndex = senderHexEncodedPublicKey.index(endIndex, offsetBy: -8)
let senderDisplayName = "\(message.displayName) (...\(senderHexEncodedPublicKey[cutoffIndex..<endIndex]))"
parseGroupMessage(body: message.body, timestamp: message.timestamp, senderDisplayName: senderDisplayName)
}
}
case .rss(_):
let url = URL(string: group.server)!
FeedParser(URL: url).parseAsync { wrapper in
guard case .rss(let feed) = wrapper, let items = feed.items else { return print("[Loki] Failed to parse RSS feed for: \(group.server)") }
items.reversed().forEach { item in
guard let title = item.title, let description = item.description, let date = item.pubDate else { return }
let timestamp = UInt64(date.timeIntervalSince1970 * 1000)
let regex = try! NSRegularExpression(pattern: "<a\\s+(?:[^>]*?\\s+)?href=\"([^\"]*)\".*?>(.*?)<.*?\\/a>")
var bodyAsHTML = "<b>\(title)</b>\(description)"
while true {
guard let match = regex.firstMatch(in: bodyAsHTML, options: [], range: NSRange(location: 0, length: bodyAsHTML.utf16.count)) else { break }
let matchRange = match.range(at: 0)
let urlRange = match.range(at: 1)
let descriptionRange = match.range(at: 2)
let url = (bodyAsHTML as NSString).substring(with: urlRange)
let description = (bodyAsHTML as NSString).substring(with: descriptionRange)
bodyAsHTML = (bodyAsHTML as NSString).replacingCharacters(in: matchRange, with: "\(description) (\(url))") as String
}
guard let bodyAsData = bodyAsHTML.data(using: String.Encoding.unicode) else { return }
let options = [ NSAttributedString.DocumentReadingOptionKey.documentType : NSAttributedString.DocumentType.html ]
guard let body = try? NSAttributedString(data: bodyAsData, options: options, documentAttributes: nil) else { return }
parseGroupMessage(body: body.string, timestamp: timestamp, senderDisplayName: NSLocalizedString("Loki", comment: ""))
}
}
}

View File

@ -692,7 +692,8 @@ typedef NS_ENUM(NSInteger, HomeViewControllerSection) {
}
if (OWSIdentityManager.sharedManager.identityKeyPair != nil) {
AppDelegate *appDelegate = (AppDelegate *)UIApplication.sharedApplication.delegate;
[appDelegate startPublicChatPollingIfNeeded];
[appDelegate createGroupChatsIfNeeded];
[appDelegate startGroupChatPollersIfNeeded];
}
}

View File

@ -2607,6 +2607,8 @@
"Update Required" = "Update Required";
"This version of Loki Messenger is no longer supported. Please press OK to reset your account and migrate to the latest version." = "This version of Loki Messenger is no longer supported. Please press OK to reset your account and migrate to the latest version.";
"Loki Public Chat" = "Loki Public Chat";
"Loki News" = "Loki News";
"Loki Messenger Updates" = "Loki Messenger Updates";
"Show QR Code" = "Show QR Code";
"This is your personal QR code. Other people can scan it to start a secure conversation with you." = "This is your personal QR code. Other people can scan it to start a secure conversation with you.";
"Scan a QR Code Instead" = "Scan a QR Code Instead";
@ -2614,3 +2616,4 @@
"You can enable camera access in your device settings." = "You can enable camera access in your device settings.";
"Scan the QR code of the person you'd like to securely message. They can find their QR code by going into Loki Messenger's in-app settings and clicking \"Show QR Code\"." = "Scan the QR code of the person you'd like to securely message. They can find their QR code by going into Loki Messenger's in-app settings and clicking \"Show QR Code\".";
"Scan QR Code" = "Scan QR Code";
"Loki" = "Loki";

View File

@ -0,0 +1,48 @@
@objc(LKGroupChat)
public final class LokiGroupChat : NSObject {
public let kind: Kind
@objc public let server: String
@objc public let displayName: String
@objc public let isDeletable: Bool
@objc public var id: String {
switch kind {
case .publicChat(let id): return "\(server).\(id)"
case .rss(let customID): return "rss://\(customID)"
}
}
// MARK: Convenience
@objc public var isPublicChat: Bool {
if case .publicChat(_) = kind { return true } else { return false }
}
@objc public var isRSS: Bool {
if case .rss(_) = kind { return true } else { return false }
}
// MARK: Kind
public enum Kind { case publicChat(id: UInt), rss(customID: String) }
// MARK: Initialization
public init(kind: Kind, server: String, displayName: String, isDeletable: Bool) {
self.kind = kind
self.server = server
self.displayName = displayName
self.isDeletable = isDeletable
}
@objc public convenience init(kindAsString: String, id: String, server: String, displayName: String, isDeletable: Bool) {
let kind: Kind
switch kindAsString {
case "publicChat": kind = .publicChat(id: UInt(id)!)
case "rss": kind = .rss(customID: id)
default: preconditionFailure()
}
self.init(kind: kind, server: server, displayName: displayName, isDeletable: isDeletable)
}
// MARK: Description
override public var description: String { return "\(id) (\(displayName))" }
}

View File

@ -169,7 +169,7 @@ public final class LokiGroupChatAPI : NSObject {
dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSZ"
guard let json = rawResponse as? JSON, let messageAsJSON = json["data"] as? JSON, let serverID = messageAsJSON["id"] as? UInt, let body = messageAsJSON["text"] as? String,
let dateAsString = messageAsJSON["created_at"] as? String, let date = dateFormatter.date(from: dateAsString) else {
print("[Loki] Couldn't parse messages for group chat with ID: \(group) on server: \(server) from: \(rawResponse).")
print("[Loki] Couldn't parse message for group chat with ID: \(group) on server: \(server) from: \(rawResponse).")
throw Error.messageParsingFailed
}
let timestamp = UInt64(date.timeIntervalSince1970) * 1000
@ -197,7 +197,7 @@ public final class LokiGroupChatAPI : NSObject {
}
return rawMessages.flatMap { message in
guard let serverID = message["id"] as? UInt else {
print("[Loki] Couldn't parse message for group chat with ID: \(group) on server: \(server) from: \(message).")
print("[Loki] Couldn't parse deleted message for group chat with ID: \(group) on server: \(server) from: \(message).")
return nil
}
let isDeleted = (message["is_deleted"] as? Bool ?? false)