Added friend request message expiration.

pull/16/head
Mikunj 6 years ago
parent dd46b5eb38
commit f1cbc2e0d1

@ -1 +1 @@
Subproject commit 76e8209c9ef47b3d1e4e9a5d2039b883e4cd4640
Subproject commit 7fc33eedb29c3e93aacbc5c04b2a26571771c39b

@ -158,6 +158,11 @@ static NSTimeInterval launchStartedAt;
return AppEnvironment.shared.legacyNotificationActionHandler;
}
- (OWSLokiFriendRequestExpireJob *)lokiFriendRequestExpireJob
{
return SSKEnvironment.shared.lokiFriendRequestExpireJob;
}
#pragma mark -
- (void)applicationDidEnterBackground:(UIApplication *)application
@ -682,6 +687,9 @@ static NSTimeInterval launchStartedAt;
// Clean up any messages that expired since last launch immediately
// and continue cleaning in the background.
[self.disappearingMessagesJob startIfNecessary];
// Start loki friend request expire job
[self.lokiFriendRequestExpireJob startIfNecessary];
[self enableBackgroundRefreshIfNecessary];
@ -1317,6 +1325,9 @@ static NSTimeInterval launchStartedAt;
// enables this feature
[self.disappearingMessagesJob startIfNecessary];
[self.profileManager ensureLocalProfileCached];
// Start loki friend request expire job
[self.lokiFriendRequestExpireJob startIfNecessary];
// For non-legacy users, read receipts are on by default.
[self.readReceiptManager setAreReadReceiptsEnabled:YES];

@ -125,6 +125,10 @@ NS_ASSUME_NONNULL_BEGIN
syncManager:syncManager
typingIndicators:typingIndicators
attachmentDownloads:attachmentDownloads]];
// Loki
OWSLokiFriendRequestExpireJob *lokiFriendRequestExpireJob = [[OWSLokiFriendRequestExpireJob alloc] initWithPrimaryStorage:primaryStorage];
[SSKEnvironment.shared setLokiFriendRequestExpireJob:lokiFriendRequestExpireJob];
appSpecificSingletonBlock();

@ -0,0 +1,213 @@
/*
This class is used for settings friend requests to expired
This is modelled after `OWSDisappearingMessagesJob`.
*/
@objc(OWSLokiFriendRequestExpireJob)
public class FriendRequestExpireJob: NSObject {
private let databaseConnection: YapDatabaseConnection
private let messageFinder: FriendRequestExpireMessageFinder
// These three properties should only be accessed on the main thread.
private var hasStarted = false
private var fallbackTimer: Timer?
private var nextExpireTimer: Timer?
private var nextExpireDate: Date?
// Our queue
public static var serialQueue: DispatchQueue = {
return DispatchQueue(label: "network.loki.friendrequest.expire")
}()
/// Create a `FriendRequestExpireJob`.
/// This will create a auto-running job which will set friend requests to expired.
///
/// - Parameter primaryStorage: The primary storage.
@objc public init(withPrimaryStorage primaryStorage: OWSPrimaryStorage) {
databaseConnection = primaryStorage.newDatabaseConnection()
messageFinder = FriendRequestExpireMessageFinder()
super.init()
// This makes sure we only ever have one instance of this class
SwiftSingletons.register(self)
// Setup a timer that runs periodically to check for new friend request messages that will soon expire
AppReadiness.runNowOrWhenAppDidBecomeReady {
if (CurrentAppContext().isMainApp) {
let fallbackInterval = 5 * kMinuteInterval
self.fallbackTimer = WeakTimer.scheduledTimer(timeInterval: fallbackInterval, target: self, userInfo: nil, repeats: true) { [weak self] _ in
AssertIsOnMainThread()
guard let strongSelf = self else {
return
}
strongSelf.timerDidFire(mainTimer: false)
}
}
}
NotificationCenter.default.addObserver(self, selector: #selector(didBecomeActive), name: .OWSApplicationDidBecomeActive, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(willResignActive), name: .OWSApplicationWillResignActive, object: nil)
}
deinit {
NotificationCenter.default.removeObserver(self)
}
/// Start the job if we haven't done it yet
@objc public func startIfNecessary() {
DispatchQueue.main.async {
guard !self.hasStarted else {
return
}
self.hasStarted = true;
FriendRequestExpireJob.serialQueue.async {
self.runLoop()
}
}
}
/// The main loop
private func runLoop() {
AssertIsOnFriendRequestExpireQueue();
// Expire any messages
self.expireMessages()
var nextExpirationTimestamp: UInt64? = nil
self.databaseConnection.readWrite { transaction in
nextExpirationTimestamp = self.messageFinder.nextExpirationTimestamp(with: transaction)
}
guard let timestamp = nextExpirationTimestamp,
let nextExpireDate = NSDate.ows_date(withMillisecondsSince1970: timestamp) as? Date else {
return
}
// Schedule the next timer
self.scheduleRun(by: nextExpireDate)
}
// Schedule the next timer to run
private func scheduleRun(by date: Date) {
DispatchQueue.main.async {
guard CurrentAppContext().isMainAppAndActive else {
// Don't schedule run when inactive or not in main app.
return
}
let minDelaySeconds: TimeInterval = 1.0
let delaySeconds = max(minDelaySeconds, date.timeIntervalSinceNow)
let newTimerScheduleDate = Date(timeIntervalSinceNow: delaySeconds)
// check that we only set the date if needed
if let previousDate = self.nextExpireDate, previousDate < date {
// If the date is later than the one we have stored then just ignore
return
}
self.resetNextExpireTimer()
self.nextExpireDate = newTimerScheduleDate
self.nextExpireTimer = WeakTimer.scheduledTimer(timeInterval: delaySeconds, target: self, userInfo: nil, repeats: false) { [weak self] _ in
guard let strongSelf = self else {
return
}
strongSelf.timerDidFire(mainTimer: true)
}
}
}
// Expire any friend request messages
private func expireMessages() {
AssertIsOnFriendRequestExpireQueue()
let now = NSDate.ows_millisecondTimeStamp()
var backgroundTask: OWSBackgroundTask? = OWSBackgroundTask(label: "\(#function)", completionBlock: { [weak self] status in
AssertIsOnMainThread()
guard status == .expired else {
return
}
guard let strongSelf = self else {
return
}
strongSelf.databaseConnection.readWrite { transaction in
strongSelf.messageFinder.enumurateExpiredMessages(with: { message in
// Sanity check
guard message.friendRequestExpiresAt <= now else {
owsFailDebug("Refusing to expire friend request which doesn't expire until: \(message.friendRequestExpiresAt)")
return;
}
// Check that we only expire sent messages
guard message.thread.friendRequestStatus == .requestSent else {
// Set message to not expire, so our other logic works correctly
message.saveFriendRequestExpires(at: 0, with: transaction)
return;
}
// Loki: Expire the friend request message
message.thread.setFriendRequestStatus(.requestExpired, with: transaction)
message.saveFriendRequestExpires(at: 0, with: transaction)
}, transaction: transaction)
}
})
}
private func resetNextExpireTimer() {
nextExpireTimer?.invalidate()
nextExpireTimer = nil
nextExpireDate = nil
}
private func timerDidFire(mainTimer: Bool) {
guard CurrentAppContext().isMainAppAndActive else {
let infoString = mainTimer ? "Main timer fired while main app is inactive" : "Ignoring fallbacktimer for app which is not main and active."
Logger.info("[Loki Friend Request Expire Job] \(infoString)")
return
}
if (mainTimer) { self.resetNextExpireTimer() }
FriendRequestExpireJob.serialQueue.async {
self.runLoop()
}
}
}
// MARK: Events
private extension FriendRequestExpireJob {
@objc func didBecomeActive() {
AssertIsOnMainThread()
AppReadiness.runNowOrWhenAppDidBecomeReady {
FriendRequestExpireJob.serialQueue.async {
self.runLoop()
}
}
}
@objc func willResignActive() {
AssertIsOnMainThread()
resetNextExpireTimer()
}
}
// MARK: Asserts
private extension FriendRequestExpireJob {
func AssertIsOnFriendRequestExpireQueue() {
#if DEBUG
guard #available(iOS 10.0, *) else { return }
dispatchPrecondition(condition: .onQueue(FriendRequestExpireJob.serialQueue))
#endif
}
}

@ -0,0 +1,88 @@
/*
This class is used for finding friend request messages which are expired.
This is modelled after `OWSDisappearingMessagesFinder`.
*/
@objc(OWSLokiFriendRequestExpireMessageFinder)
public class FriendRequestExpireMessageFinder: NSObject {
public static let friendRequestExpireColumn = "friend_request_expires_at"
public static let friendRequestExpireIndex = "loki_index_friend_request_expires_at"
public func nextExpirationTimestamp(with transaction: YapDatabaseReadTransaction) -> UInt64? {
let query = "WHERE \(FriendRequestExpireMessageFinder.friendRequestExpireColumn) > 0 ORDER BY \(FriendRequestExpireMessageFinder.friendRequestExpireColumn) ASC"
let dbQuery = YapDatabaseQuery(string: query, parameters: [])
let ext = transaction.ext(FriendRequestExpireMessageFinder.friendRequestExpireIndex) as? YapDatabaseSecondaryIndexTransaction
var firstMessage: TSMessage? = nil
ext?.enumerateKeysAndObjects(matching: dbQuery) { (collection, key, object, stop) in
firstMessage = object as? TSMessage
stop.pointee = true
}
guard let expireTime = firstMessage?.friendRequestExpiresAt, expireTime > 0 else {
return nil
}
return expireTime
}
public func enumurateExpiredMessages(with block: (TSMessage) -> Void, transaction: YapDatabaseReadTransaction) {
for messageId in self.fetchExpiredMessageIds(with: transaction) {
guard let message = TSMessage.fetch(uniqueId: messageId, transaction: transaction) else {
continue
}
block(message)
}
}
private func fetchExpiredMessageIds(with transaction: YapDatabaseReadTransaction) -> [String] {
var messageIds = [String]()
let now = NSDate.ows_millisecondTimeStamp()
let query = "WHERE \(FriendRequestExpireMessageFinder.friendRequestExpireColumn) > 0 AND \(FriendRequestExpireMessageFinder.friendRequestExpireColumn) <= \(now)"
// When (expireAt == 0) then the friend request SHOULD NOT expire
let dbQuery = YapDatabaseQuery(string: query, parameters: [])
let ext = transaction.ext(FriendRequestExpireMessageFinder.friendRequestExpireIndex) as? YapDatabaseSecondaryIndexTransaction
ext?.enumerateKeys(matching: dbQuery) { (collection, key, stop) in
messageIds.append(key)
}
return Array(messageIds)
}
}
// MARK: YapDatabaseExtension
public extension FriendRequestExpireMessageFinder {
@objc public static var indexDatabaseExtension: YapDatabaseSecondaryIndex {
let setup = YapDatabaseSecondaryIndexSetup()
setup.addColumn(friendRequestExpireColumn, with: .integer)
let handler = YapDatabaseSecondaryIndexHandler.withObjectBlock { (transaction, dict, collection, key, object) in
guard let message = object as? TSMessage else {
return
}
// Only select messages whose status is sent
guard message.thread.friendRequestStatus == .requestSent else {
return
}
// TODO: Replace this with unlock timer
dict[friendRequestExpireColumn] = message.expiresAt
}
return YapDatabaseSecondaryIndex(setup: setup, handler: handler)
}
@objc public static var databaseExtensionName: String {
return friendRequestExpireIndex
}
@objc public static func asyncRegisterDatabaseExtensions(_ storage: OWSStorage) {
storage.register(self.indexDatabaseExtension, withName: friendRequestExpireIndex)
}
}

@ -29,6 +29,7 @@ NS_ASSUME_NONNULL_BEGIN
@property (nonatomic, readonly, nullable) OWSContact *contactShare;
@property (nonatomic, readonly, nullable) OWSLinkPreview *linkPreview;
@property (nonatomic) BOOL isFriendRequest; // Loki
@property (nonatomic) uint64_t friendRequestExpiresAt;
- (instancetype)initInteractionWithTimestamp:(uint64_t)timestamp inThread:(TSThread *)thread NS_UNAVAILABLE;
@ -72,6 +73,7 @@ NS_ASSUME_NONNULL_BEGIN
#pragma mark - Loki Friend Request Handling
- (void)setIsFriendRequest:(BOOL)isFriendRequest withTransaction:(YapDatabaseReadWriteTransaction *)transaction;
- (void)saveFriendRequestExpiresAt:(u_int64_t)expiresAt withTransaction:(YapDatabaseReadWriteTransaction *_Nullable)transaction;
@end

@ -82,6 +82,7 @@ static const NSUInteger OWSMessageSchemaVersion = 4;
_quotedMessage = quotedMessage;
_contactShare = contactShare;
_linkPreview = linkPreview;
_friendRequestExpiresAt = 0;
return self;
}
@ -450,6 +451,16 @@ static const NSUInteger OWSMessageSchemaVersion = 4;
}
}
- (void)saveFriendRequestExpiresAt:(u_int64_t)expiresAt withTransaction:(YapDatabaseReadWriteTransaction *_Nullable)transaction
{
self.friendRequestExpiresAt = expiresAt;
if (transaction == nil) {
[self save];
} else {
[self saveWithTransaction:transaction];
}
}
@end
NS_ASSUME_NONNULL_END

@ -1476,6 +1476,7 @@ NS_ASSUME_NONNULL_BEGIN
// we can end up in a deadlock where both users' threads' friend request statuses are
// TSThreadFriendRequestStatusRequestSent.
[thread setFriendRequestStatus:TSThreadFriendRequestStatusFriends withTransaction:transaction];
// The two lines below are equivalent to calling [ThreadUtil enqueueAcceptFriendRequestMessageInThread:thread]
OWSEphemeralMessage *emptyMessage = [OWSEphemeralMessage createEmptyOutgoingMessageInThread:thread];
[self.messageSenderJobQueue addMessage:emptyMessage transaction:transaction];

@ -1119,6 +1119,11 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException";
// ========
if (messageType == TSFriendRequestMessageType) {
[message.thread setFriendRequestStatus:TSThreadFriendRequestStatusRequestSent withTransaction:nil];
// We also want to expire the message after 72 hours
NSTimeInterval expireTimeInterval = 72 * kHourInterval;
NSDate *expireDate = [[NSDate new] dateByAddingTimeInterval:expireTimeInterval];
[message saveFriendRequestExpiresAt:[NSDate ows_millisecondsSince1970ForDate:expireDate] withTransaction:nil];
}
// ========
// Invoke the completion handler

@ -27,6 +27,9 @@ NS_ASSUME_NONNULL_BEGIN
@class TSSocketManager;
@class YapDatabaseConnection;
// Loki
@class OWSLokiFriendRequestExpireJob;
@protocol ContactsManagerProtocol;
@protocol NotificationsProtocol;
@protocol OWSCallMessageHandler;
@ -111,6 +114,10 @@ NS_ASSUME_NONNULL_BEGIN
@property (atomic, readonly) YapDatabaseConnection *migrationDBConnection;
@property (atomic, readonly) YapDatabaseConnection *analyticsDBConnection;
// Loki
@property (nonatomic, readonly) OWSLokiFriendRequestExpireJob *lokiFriendRequestExpireJob;
- (void)setLokiFriendRequestExpireJob:(OWSLokiFriendRequestExpireJob *)lokiFriendRequestExpireJob;
- (BOOL)isComplete;
@end

@ -37,6 +37,9 @@ static SSKEnvironment *sharedSSKEnvironment;
@property (nonatomic) id<OWSTypingIndicators> typingIndicators;
@property (nonatomic) OWSAttachmentDownloads *attachmentDownloads;
// Loki
@property (nonatomic) OWSLokiFriendRequestExpireJob *lokiFriendRequestExpireJob;
@end
#pragma mark -
@ -196,6 +199,15 @@ static SSKEnvironment *sharedSSKEnvironment;
}
}
- (void)setLokiFriendRequestExpireJob:(OWSLokiFriendRequestExpireJob *)lokiFriendRequestExpireJob {
@synchronized(self) {
OWSAssertDebug(lokiFriendRequestExpireJob);
OWSAssertDebug(!_lokiFriendRequestExpireJob);
_lokiFriendRequestExpireJob = lokiFriendRequestExpireJob;
}
}
- (BOOL)isComplete
{
return (self.callMessageHandler != nil && self.notificationsManager != nil);

@ -214,6 +214,8 @@ void VerifyRegistrationsForPrimaryStorage(OWSStorage *storage)
[OWSMediaGalleryFinder asyncRegisterDatabaseExtensionsWithPrimaryStorage:self];
[TSDatabaseView asyncRegisterLazyRestoreAttachmentsDatabaseView:self];
[SSKJobRecordFinder asyncRegisterDatabaseExtensionObjCWithStorage:self];
[OWSLokiFriendRequestExpireMessageFinder asyncRegisterDatabaseExtensions:self];
[self.database
flushExtensionRequestsWithCompletionQueue:dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)

Loading…
Cancel
Save