@ -8,6 +8,8 @@ import SessionUtilitiesKit
extension OpenGroupAPI {
public final class Poller {
typealias PollResponse = [ OpenGroupAPI . Endpoint : ( info : OnionRequestResponseInfoType , data : Codable ? ) ]
private let server : String
private var timer : Timer ? = nil
private var hasStarted = false
@ -71,6 +73,7 @@ extension OpenGroupAPI {
@ discardableResult
public func poll (
isBackgroundPoll : Bool ,
isBackgroundPollerValid : @ escaping ( ( ) -> Bool ) = { true } ,
isPostCapabilitiesRetry : Bool ,
using dependencies : OpenGroupManager . OGMDependencies = OpenGroupManager . OGMDependencies ( )
) -> Promise < Void > {
@ -83,8 +86,14 @@ extension OpenGroupAPI {
Threading . pollerQueue . async {
dependencies . storage
. read { db in
OpenGroupAPI
. read { db -> Promise < ( Int64 , PollResponse ) > in
let failureCount : Int64 = ( try ? OpenGroup
. select ( max ( OpenGroup . Columns . pollFailureCount ) )
. asRequest ( of : Int64 . self )
. fetchOne ( db ) )
. defaulting ( to : 0 )
return OpenGroupAPI
. poll (
db ,
server : server ,
@ -95,10 +104,24 @@ extension OpenGroupAPI {
) ,
using : dependencies
)
. map ( on : OpenGroupAPI . workQueue ) { ( failureCount , $0 ) }
}
. done ( on : OpenGroupAPI . workQueue ) { [ weak self ] response in
. done ( on : OpenGroupAPI . workQueue ) { [ weak self ] failureCount , response in
guard ! isBackgroundPoll || isBackgroundPollerValid ( ) else {
// I f t h i s w a s a b a c k g r o u n d p o l l a n d t h e b a c k g r o u n d p o l l i s n o l o n g e r v a l i d
// t h e n j u s t s t o p
self ? . isPolling = false
seal . fulfill ( ( ) )
return
}
self ? . isPolling = false
self ? . handlePollResponse ( response , isBackgroundPoll : isBackgroundPoll , using : dependencies )
self ? . handlePollResponse (
response ,
failureCount : failureCount ,
isBackgroundPoll : isBackgroundPoll ,
using : dependencies
)
dependencies . mutableCache . mutate { cache in
cache . hasPerformedInitialPoll [ server ] = true
@ -106,17 +129,18 @@ extension OpenGroupAPI {
UserDefaults . standard [ . lastOpen ] = Date ( )
}
// R e s e t t h e f a i l u r e c o u n t
Storage . shared . writeAsync { db in
try OpenGroup
. filter ( OpenGroup . Columns . server = = server )
. updateAll ( db , OpenGroup . Columns . pollFailureCount . set ( to : 0 ) )
}
SNLog ( " Open group polling finished for \( server ) . " )
seal . fulfill ( ( ) )
}
. catch ( on : OpenGroupAPI . workQueue ) { [ weak self ] error in
guard ! isBackgroundPoll || isBackgroundPollerValid ( ) else {
// I f t h i s w a s a b a c k g r o u n d p o l l a n d t h e b a c k g r o u n d p o l l i s n o l o n g e r v a l i d
// t h e n j u s t s t o p
self ? . isPolling = false
seal . fulfill ( ( ) )
return
}
// I f w e a r e r e t r y i n g t h e n t h e e r r o r i s b e i n g h a n d l e d s o n o n e e d t o c o n t i n u e ( t h i s
// m e t h o d w i l l a l w a y s r e s o l v e )
self ? . updateCapabilitiesAndRetryIfNeeded (
@ -141,7 +165,10 @@ extension OpenGroupAPI {
Storage . shared . writeAsync { db in
try OpenGroup
. filter ( OpenGroup . Columns . server = = server )
. updateAll ( db , OpenGroup . Columns . pollFailureCount . set ( to : ( pollFailureCount + 1 ) ) )
. updateAll (
db ,
OpenGroup . Columns . pollFailureCount . set ( to : ( pollFailureCount + 1 ) )
)
}
SNLog ( " Open group polling failed due to error: \( error ) . Setting failure count to \( pollFailureCount ) . " )
@ -221,50 +248,47 @@ extension OpenGroupAPI {
return promise
}
private func handlePollResponse ( _ response : [ OpenGroupAPI . Endpoint : ( info : OnionRequestResponseInfoType , data : Codable ? ) ] , isBackgroundPoll : Bool , using dependencies : OpenGroupManager . OGMDependencies = OpenGroupManager . OGMDependencies ( ) ) {
private func handlePollResponse (
_ response : PollResponse ,
failureCount : Int64 ,
isBackgroundPoll : Bool ,
using dependencies : OpenGroupManager . OGMDependencies = OpenGroupManager . OGMDependencies ( )
) {
let server : String = self . server
dependencies . storage . write { db in
try response . forEach { endpoint , endpointResponse in
let validResponses : PollResponse = response
. filter { endpoint , endpointResponse in
switch endpoint {
case . capabilities :
guard let responseData : BatchSubResponse < Capabilities > = endpointResponse . data as ? BatchSubResponse < Capabilities > , let responseBody : Capabilities = responseData . body else {
guard ( endpointResponse . data as ? BatchSubResponse < Capabilities > )? . body != nil else {
SNLog ( " Open group polling failed due to invalid capability data. " )
return
return false
}
OpenGroupManager . handleCapabilities (
db ,
capabilities : responseBody ,
on : server
)
return true
case . roomPollInfo ( let roomToken , _ ) :
guard let responseData : BatchSubResponse < RoomPollInfo > = endpointResponse . data as ? BatchSubResponse < RoomPollInfo > , let responseBody : RoomPollInfo = responseData . body else {
guard ( endpointResponse . data as ? BatchSubResponse < RoomPollInfo > ) ? . body != nil else {
switch ( endpointResponse . data as ? BatchSubResponse < RoomPollInfo > ) ? . code {
case 404 : SNLog ( " Open group polling failed to retrieve info for unknown room ' \( roomToken ) '. " )
default : SNLog ( " Open group polling failed due to invalid room info data. " )
}
return
return false
}
try OpenGroupManager . handlePollInfo (
db ,
pollInfo : responseBody ,
publicKey : nil ,
for : roomToken ,
on : server ,
dependencies : dependencies
)
return true
case . roomMessagesRecent ( let roomToken ) , . roomMessagesBefore ( let roomToken , _ ) , . roomMessagesSince ( let roomToken , _ ) :
guard let responseData : BatchSubResponse < [ Failable < Message > ] > = endpointResponse . data as ? BatchSubResponse < [ Failable < Message > ] > , let responseBody : [ Failable < Message > ] = responseData . body else {
guard
let responseData : BatchSubResponse < [ Failable < Message > ] > = endpointResponse . data as ? BatchSubResponse < [ Failable < Message > ] > ,
let responseBody : [ Failable < Message > ] = responseData . body
else {
switch ( endpointResponse . data as ? BatchSubResponse < [ Failable < Message > ] > ) ? . code {
case 404 : SNLog ( " Open group polling failed to retrieve messages for unknown room ' \( roomToken ) '. " )
default : SNLog ( " Open group polling failed due to invalid messages data. " )
}
return
return false
}
let successfulMessages : [ Message ] = responseBody . compactMap { $0 . value }
if successfulMessages . count != responseBody . count {
@ -273,9 +297,147 @@ extension OpenGroupAPI {
SNLog ( " Dropped \( droppedCount ) invalid open group message \( droppedCount = = 1 ? " " : " s " ) . " )
}
return ! successfulMessages . isEmpty
case . inbox , . inboxSince , . outbox , . outboxSince :
guard
let responseData : BatchSubResponse < [ DirectMessage ] ? > = endpointResponse . data as ? BatchSubResponse < [ DirectMessage ] ? > ,
! responseData . failedToParseBody
else {
SNLog ( " Open group polling failed due to invalid inbox/outbox data. " )
return false
}
// D o u b l e o p t i o n a l b e c a u s e t h e s e r v e r c a n r e t u r n a ` 3 0 4 ` w i t h a n e m p t y b o d y
let messages : [ OpenGroupAPI . DirectMessage ] = ( ( responseData . body ? ? [ ] ) ? ? [ ] )
return ! messages . isEmpty
default : return false // N o c u s t o m h a n d l i n g n e e d e d
}
}
// I f t h e r e a r e n o r e m a i n i n g ' v a l i d R e s p o n s e s ' a n d t h e r e h a s n ' t b e e n a f a i l u r e t h e n t h e r e i s
// n o n e e d t o d o a n y t h i n g e l s e
guard ! validResponses . isEmpty || failureCount != 0 else { return }
// R e t r i e v e t h e c u r r e n t c a p a b i l i t y & g r o u p i n f o t o c h e c k i f a n y t h i n g c h a n g e d
let rooms : [ String ] = validResponses
. keys
. compactMap { endpoint -> String ? in
switch endpoint {
case . roomPollInfo ( let roomToken , _ ) : return roomToken
default : return nil
}
}
let currentInfo : ( capabilities : Capabilities , groups : [ OpenGroup ] ) ? = dependencies . storage . read { db in
let allCapabilities : [ Capability ] = try Capability
. filter ( Capability . Columns . openGroupServer = = server )
. fetchAll ( db )
let capabilities : Capabilities = Capabilities (
capabilities : allCapabilities
. filter { ! $0 . isMissing }
. map { $0 . variant } ,
missing : {
let missingCapabilities : [ Capability . Variant ] = allCapabilities
. filter { $0 . isMissing }
. map { $0 . variant }
return ( missingCapabilities . isEmpty ? nil : missingCapabilities )
} ( )
)
let openGroupIds : [ String ] = rooms
. map { OpenGroup . idFor ( roomToken : $0 , server : server ) }
let groups : [ OpenGroup ] = try OpenGroup
. filter ( ids : openGroupIds )
. fetchAll ( db )
return ( capabilities , groups )
}
let changedResponses : PollResponse = validResponses
. filter { endpoint , endpointResponse in
switch endpoint {
case . capabilities :
guard
let responseData : BatchSubResponse < Capabilities > = endpointResponse . data as ? BatchSubResponse < Capabilities > ,
let responseBody : Capabilities = responseData . body
else { return false }
return ( responseBody != currentInfo ? . capabilities )
case . roomPollInfo ( let roomToken , _ ) :
guard
let responseData : BatchSubResponse < RoomPollInfo > = endpointResponse . data as ? BatchSubResponse < RoomPollInfo > ,
let responseBody : RoomPollInfo = responseData . body
else { return false }
guard let existingOpenGroup : OpenGroup = currentInfo ? . groups . first ( where : { $0 . roomToken = = roomToken } ) else {
return true
}
// N o t e : T h i s m i g h t n e e d t o b e u p d a t e d i n t h e f u t u r e w h e n w e s t a r t t r a c k i n g
// u s e r p e r m i s s i o n s i f c h a n g e s t o p e r m i s s i o n s d o n ' t t r i g g e r a c h a n g e t o
// t h e ' i n f o U p d a t e s '
return (
responseBody . activeUsers != existingOpenGroup . userCount || (
responseBody . details != nil &&
responseBody . details ? . infoUpdates != existingOpenGroup . infoUpdates
)
)
default : return true
}
}
// I f t h e r e a r e n o ' c h a n g e d R e s p o n s e s ' a n d t h e r e h a s n ' t b e e n a f a i l u r e t h e n t h e r e i s
// n o n e e d t o d o a n y t h i n g e l s e
guard ! changedResponses . isEmpty || failureCount != 0 else { return }
dependencies . storage . write { db in
// R e s e t t h e f a i l u r e c o u n t
if failureCount > 0 {
try OpenGroup
. filter ( OpenGroup . Columns . server = = server )
. updateAll ( db , OpenGroup . Columns . pollFailureCount . set ( to : 0 ) )
}
try changedResponses . forEach { endpoint , endpointResponse in
switch endpoint {
case . capabilities :
guard
let responseData : BatchSubResponse < Capabilities > = endpointResponse . data as ? BatchSubResponse < Capabilities > ,
let responseBody : Capabilities = responseData . body
else { return }
OpenGroupManager . handleCapabilities (
db ,
capabilities : responseBody ,
on : server
)
case . roomPollInfo ( let roomToken , _ ) :
guard
let responseData : BatchSubResponse < RoomPollInfo > = endpointResponse . data as ? BatchSubResponse < RoomPollInfo > ,
let responseBody : RoomPollInfo = responseData . body
else { return }
try OpenGroupManager . handlePollInfo (
db ,
pollInfo : responseBody ,
publicKey : nil ,
for : roomToken ,
on : server ,
dependencies : dependencies
)
case . roomMessagesRecent ( let roomToken ) , . roomMessagesBefore ( let roomToken , _ ) , . roomMessagesSince ( let roomToken , _ ) :
guard
let responseData : BatchSubResponse < [ Failable < Message > ] > = endpointResponse . data as ? BatchSubResponse < [ Failable < Message > ] > ,
let responseBody : [ Failable < Message > ] = responseData . body
else { return }
OpenGroupManager . handleMessages (
db ,
messages : successfulMessages ,
messages : responseBody. compactMap { $0 . value } ,
for : roomToken ,
on : server ,
isBackgroundPoll : isBackgroundPoll ,
@ -283,10 +445,10 @@ extension OpenGroupAPI {
)
case . inbox , . inboxSince , . outbox , . outboxSince :
guard let responseData : BatchSubResponse < [ DirectMessage ] ? > = endpointResponse . data as ? BatchSubResponse < [ DirectMessage ] ? > , ! responseData . failedToParseBody else {
SNLog ( " Open group polling failed due to invalid inbox/outbox data. " )
return
}
guard
let responseData : BatchSubResponse < [ DirectMessage ] ? > = endpointResponse . data as ? BatchSubResponse < [ DirectMessage ] ? > ,
! responseData . failedToParseBody
else { return }
// D o u b l e o p t i o n a l b e c a u s e t h e s e r v e r c a n r e t u r n a ` 3 0 4 ` w i t h a n e m p t y b o d y
let messages : [ OpenGroupAPI . DirectMessage ] = ( ( responseData . body ? ? [ ] ) ? ? [ ] )