// C o p y r i g h t © 2 0 2 2 R a n g e p r o o f P t y L t d . A l l r i g h t s r e s e r v e d .
import Foundation
import GRDB
import PromiseKit
import SessionSnodeKit
import SessionUtilitiesKit
@objc ( LKClosedGroupPoller )
public final class ClosedGroupPoller : NSObject {
private var isPolling : Atomic < [ String : Bool ] > = Atomic ( [ : ] )
private var timers : [ String : Timer ] = [ : ]
private let internalQueue : DispatchQueue = DispatchQueue ( label : " isPollingQueue " )
// MARK: - S e t t i n g s
private static let minPollInterval : Double = 2
private static let maxPollInterval : Double = 30
// MARK: - E r r o r
private enum Error : LocalizedError {
case insufficientSnodes
case pollingCanceled
internal var errorDescription : String ? {
switch self {
case . insufficientSnodes : return " No snodes left to poll. "
case . pollingCanceled : return " Polling canceled. "
}
}
}
// MARK: - I n i t i a l i z a t i o n
public static let shared = ClosedGroupPoller ( )
private override init ( ) { }
// MARK: - P u b l i c A P I
@objc public func start ( ) {
#if DEBUG
assert ( Thread . current . isMainThread ) // T i m e r s d o n ' t d o w e l l o n b a c k g r o u n d q u e u e s
#endif
// F e t c h a l l c l o s e d g r o u p s ( e x c l u d i n g a n y d o n ' t c o n t a i n t h e c u r r e n t u s e r a s a
// G r o u p M e m e b e r a s t h e u s e r i s n o l o n g e r a m e m b e r o f t h o s e )
GRDBStorage . shared
. read { db in
try ClosedGroup
. select ( . threadId )
. joining (
required : ClosedGroup . members
. filter ( GroupMember . Columns . profileId = = getUserHexEncodedPublicKey ( db ) )
)
. asRequest ( of : String . self )
. fetchAll ( db )
}
. defaulting ( to : [ ] )
. forEach { [ weak self ] groupPublicKey in
self ? . startPolling ( for : groupPublicKey )
}
}
public func startPolling ( for groupPublicKey : String ) {
guard isPolling . wrappedValue [ groupPublicKey ] != true else { return }
// M i g h t b e a r a c e c o n d i t i o n t h a t t h e s e t U p P o l l i n g f i n i s h e s t o o s o o n ,
// a n d t h e t i m e r i s n o t c r e a t e d , i f w e m a r k t h e g r o u p a s i s p o l l i n g
// a f t e r s e t U p P o l l i n g . S o t h e p o l l e r m a y n o t w o r k , t h u s m i s s e s m e s s a g e s .
isPolling . mutate { $0 [ groupPublicKey ] = true }
setUpPolling ( for : groupPublicKey )
}
@objc public func stop ( ) {
GRDBStorage . shared
. read { db in
try ClosedGroup
. select ( . threadId )
. asRequest ( of : String . self )
. fetchAll ( db )
}
. defaulting ( to : [ ] )
. forEach { [ weak self ] groupPublicKey in
self ? . stopPolling ( for : groupPublicKey )
}
}
public func stopPolling ( for groupPublicKey : String ) {
isPolling . mutate { $0 [ groupPublicKey ] = false }
timers [ groupPublicKey ] ? . invalidate ( )
}
// MARK: - P r i v a t e A P I
private func setUpPolling ( for groupPublicKey : String ) {
Threading . pollerQueue . async {
self . poll ( groupPublicKey )
. done ( on : Threading . pollerQueue ) { [ weak self ] _ in
self ? . pollRecursively ( groupPublicKey )
}
. catch ( on : Threading . pollerQueue ) { [ weak self ] error in
// T h e e r r o r i s l o g g e d i n p o l l ( _ : )
self ? . pollRecursively ( groupPublicKey )
}
}
}
private func pollRecursively ( _ groupPublicKey : String ) {
guard
isPolling . wrappedValue [ groupPublicKey ] = = true ,
let thread : SessionThread = GRDBStorage . shared . read ( { db in try SessionThread . fetchOne ( db , id : groupPublicKey ) } )
else { return }
// G e t t h e r e c e i v e d d a t e o f t h e l a s t m e s s a g e i n t h e t h r e a d . I f w e d o n ' t h a v e a n y m e s s a g e s y e t , p i c k s o m e
// r e a s o n a b l e f a k e t i m e i n t e r v a l t o u s e i n s t e a d
let lastMessageDate : Date = GRDBStorage . shared
. read { db in
try thread
. interactions
. select ( . receivedAtTimestampMs )
. order ( Interaction . Columns . timestampMs . desc )
. asRequest ( of : Int64 . self )
. fetchOne ( db )
}
. map { receivedAtTimestampMs -> Date ? in
guard receivedAtTimestampMs > 0 else { return nil }
return Date ( timeIntervalSince1970 : ( TimeInterval ( receivedAtTimestampMs ) / 1000 ) )
}
. defaulting ( to : Date ( ) . addingTimeInterval ( - 5 * 60 ) )
let timeSinceLastMessage : TimeInterval = Date ( ) . timeIntervalSince ( lastMessageDate )
let minPollInterval : Double = ClosedGroupPoller . minPollInterval
let limit : Double = ( 12 * 60 * 60 )
let a = ( ClosedGroupPoller . maxPollInterval - minPollInterval ) / limit
let nextPollInterval = a * min ( timeSinceLastMessage , limit ) + minPollInterval
SNLog ( " Next poll interval for closed group with public key: \( groupPublicKey ) is \( nextPollInterval ) s. " )
timers [ groupPublicKey ] = Timer . scheduledTimerOnMainThread ( withTimeInterval : nextPollInterval , repeats : false ) { [ weak self ] timer in
timer . invalidate ( )
Threading . pollerQueue . async {
self ? . poll ( groupPublicKey ) . done ( on : Threading . pollerQueue ) { _ in
self ? . pollRecursively ( groupPublicKey )
} . catch ( on : Threading . pollerQueue ) { error in
// T h e e r r o r i s l o g g e d i n p o l l ( _ : )
self ? . pollRecursively ( groupPublicKey )
}
}
}
}
private func poll ( _ groupPublicKey : String ) -> Promise < Void > {
guard isPolling . wrappedValue [ groupPublicKey ] = = true else { return Promise . value ( ( ) ) }
let promise : Promise < Void > = SnodeAPI . getSwarm ( for : groupPublicKey )
. then2 { [ weak self ] swarm -> Promise < ( Snode , [ SnodeReceivedMessage ] ) > in
// r a n d o m E l e m e n t ( ) u s e s t h e s y s t e m ' s d e f a u l t r a n d o m g e n e r a t o r , w h i c h i s c r y p t o g r a p h i c a l l y s e c u r e
guard let snode = swarm . randomElement ( ) else { return Promise ( error : Error . insufficientSnodes ) }
guard self ? . isPolling . wrappedValue [ groupPublicKey ] = = true else {
return Promise ( error : Error . pollingCanceled )
}
return SnodeAPI . getMessages ( from : snode , associatedWith : groupPublicKey )
. map2 { messages in ( snode , messages ) }
}
. done2 { [ weak self ] snode , messages in
guard self ? . isPolling . wrappedValue [ groupPublicKey ] = = true else { return }
if ! messages . isEmpty {
var messageCount : Int = 0
GRDBStorage . shared . write { db in
var jobDetailMessages : [ MessageReceiveJob . Details . MessageInfo ] = [ ]
messages . forEach { message in
do {
let processedMessage : ProcessedMessage ? = try Message . processRawReceivedMessage ( db , rawMessage : message )
jobDetailMessages = jobDetailMessages
. appending ( processedMessage ? . messageInfo )
}
catch {
switch error {
// I g n o r e d u p l i c a t e & s e l f S e n d m e s s a g e e r r o r s ( a n d d o n ' t b o t h e r l o g g i n g
// t h e m a s t h e r e w i l l b e a l o t s i n c e w e e a c h s e r v i c e n o d e d u p l i c a t e s m e s s a g e s )
case DatabaseError . SQLITE_CONSTRAINT_UNIQUE ,
MessageReceiverError . duplicateMessage ,
MessageReceiverError . duplicateControlMessage ,
MessageReceiverError . selfSend :
break
default : SNLog ( " Failed to deserialize envelope due to error: \( error ) . " )
}
}
}
messageCount = jobDetailMessages . count
JobRunner . add (
db ,
job : Job (
variant : . messageReceive ,
behaviour : . runOnce ,
threadId : groupPublicKey ,
details : MessageReceiveJob . Details (
messages : jobDetailMessages ,
isBackgroundPoll : false
)
)
)
}
SNLog ( " Received \( messageCount ) new message \( messageCount = = 1 ? " " : " s " ) in closed group with public key: \( groupPublicKey ) (duplicates: \( messages . count - messageCount ) ) " )
}
}
. map { _ in }
promise . catch2 { error in
SNLog ( " Polling failed for closed group with public key: \( groupPublicKey ) due to error: \( error ) . " )
}
return promise
}
}