diff --git a/Podfile b/Podfile index 7b1bd834f..bf1a97832 100644 --- a/Podfile +++ b/Podfile @@ -73,6 +73,7 @@ target 'Signal' do # Loki pod 'GCDWebServer', '~> 3.0' + pod 'FeedKit', '~> 8.1' target 'SignalTests' do inherit! :search_paths diff --git a/Podfile.lock b/Podfile.lock index 79a4f123f..5b624389a 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -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 diff --git a/Pods b/Pods index d9ab8b130..20b736ae2 160000 --- a/Pods +++ b/Pods @@ -1 +1 @@ -Subproject commit d9ab8b13002bf6ebc932ca4f45df56b577b6a188 +Subproject commit 20b736ae28ecd42b5fc13f583a010ac9354be507 diff --git a/Signal.xcodeproj/project.pbxproj b/Signal.xcodeproj/project.pbxproj index 29912f5ee..50cca813b 100644 --- a/Signal.xcodeproj/project.pbxproj +++ b/Signal.xcodeproj/project.pbxproj @@ -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 */ = { diff --git a/Signal/src/AppDelegate.h b/Signal/src/AppDelegate.h index c14a3e70b..a0c86290a 100644 --- a/Signal/src/AppDelegate.h +++ b/Signal/src/AppDelegate.h @@ -8,6 +8,7 @@ extern NSString *const AppDelegateStoryboardMain; @interface AppDelegate : UIResponder -- (void)startPublicChatPollingIfNeeded; +- (void)createGroupChatsIfNeeded; +- (void)startGroupChatPollersIfNeeded; @end diff --git a/Signal/src/AppDelegate.m b/Signal/src/AppDelegate.m index fe9d21603..b1ca8310c 100644 --- a/Signal/src/AppDelegate.m +++ b/Signal/src/AppDelegate.m @@ -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]; +} + +- (LKGroupChat *)lokiNews +{ + 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)startPublicChatPollingIfNeeded +- (void)startGroupChatPollersIfNeeded { - [self setUpPublicChatIfNeeded]; + [self createGroupChatPollersIfNeeded]; [self.lokiPublicChatPoller startIfNeeded]; + [self.lokiNewsPoller startIfNeeded]; + [self.lokiMessengerUpdatesPoller startIfNeeded]; } @end diff --git a/Signal/src/Loki/LokiGroupChatPoller.swift b/Signal/src/Loki/LokiGroupChatPoller.swift index 4acaf58e8..fe1012f70 100644 --- a/Signal/src/Loki/LokiGroupChatPoller.swift +++ b/Signal/src/Loki/LokiGroupChatPoller.swift @@ -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..]*?\\s+)?href=\"([^\"]*)\".*?>(.*?)<.*?\\/a>") + var bodyAsHTML = "\(title)\(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: "")) } } } diff --git a/Signal/src/ViewControllers/HomeView/HomeViewController.m b/Signal/src/ViewControllers/HomeView/HomeViewController.m index 17e0202e3..732142441 100644 --- a/Signal/src/ViewControllers/HomeView/HomeViewController.m +++ b/Signal/src/ViewControllers/HomeView/HomeViewController.m @@ -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]; } } diff --git a/Signal/translations/en.lproj/Localizable.strings b/Signal/translations/en.lproj/Localizable.strings index 0e17a7b9e..94e785776 100644 --- a/Signal/translations/en.lproj/Localizable.strings +++ b/Signal/translations/en.lproj/Localizable.strings @@ -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"; diff --git a/SignalServiceKit/src/Loki/API/LokiGroupChat.swift b/SignalServiceKit/src/Loki/API/LokiGroupChat.swift new file mode 100644 index 000000000..07bb21d27 --- /dev/null +++ b/SignalServiceKit/src/Loki/API/LokiGroupChat.swift @@ -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))" } +} diff --git a/SignalServiceKit/src/Loki/API/LokiGroupChatAPI.swift b/SignalServiceKit/src/Loki/API/LokiGroupChatAPI.swift index 9e430926d..205d58c5a 100644 --- a/SignalServiceKit/src/Loki/API/LokiGroupChatAPI.swift +++ b/SignalServiceKit/src/Loki/API/LokiGroupChatAPI.swift @@ -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)