@ -8,16 +8,56 @@ import SessionUtilitiesKit
import SessionSnodeKit
extension MessageReceiver {
public static func handleClosedGroupControlMessage ( _ db : Database , _ message : ClosedGroupControlMessage ) throws {
public static func handleClosedGroupControlMessage (
_ db : Database ,
threadId : String ,
threadVariant : SessionThread . Variant ,
message : ClosedGroupControlMessage
) throws {
switch message . kind {
case . new : try handleNewClosedGroup ( db , message : message )
case . encryptionKeyPair : try handleClosedGroupEncryptionKeyPair ( db , message : message )
case . nameChange : try handleClosedGroupNameChanged ( db , message : message )
case . membersAdded : try handleClosedGroupMembersAdded ( db , message : message )
case . membersRemoved : try handleClosedGroupMembersRemoved ( db , message : message )
case . memberLeft : try handleClosedGroupMemberLeft ( db , message : message )
case . encryptionKeyPairRequest :
handleClosedGroupEncryptionKeyPairRequest ( db , message : message ) // C u r r e n t l y n o t u s e d
case . encryptionKeyPair :
try handleClosedGroupEncryptionKeyPair (
db ,
threadId : threadId ,
threadVariant : threadVariant ,
message : message
)
case . nameChange :
try handleClosedGroupNameChanged (
db ,
threadId : threadId ,
threadVariant : threadVariant ,
message : message
)
case . membersAdded :
try handleClosedGroupMembersAdded (
db ,
threadId : threadId ,
threadVariant : threadVariant ,
message : message
)
case . membersRemoved :
try handleClosedGroupMembersRemoved (
db ,
threadId : threadId ,
threadVariant : threadVariant ,
message : message
)
case . memberLeft :
try handleClosedGroupMemberLeft (
db ,
threadId : threadId ,
threadVariant : threadVariant ,
message : message
)
case . encryptionKeyPairRequest : break // C u r r e n t l y n o t u s e d
default : throw MessageReceiverError . invalidMessage
}
@ -39,7 +79,8 @@ extension MessageReceiver {
members : membersAsData . map { $0 . toHexString ( ) } ,
admins : adminsAsData . map { $0 . toHexString ( ) } ,
expirationTimer : expirationTimer ,
messageSentTimestamp : sentTimestamp
messageSentTimestamp : sentTimestamp ,
calledFromConfigHandling : false
)
}
@ -51,7 +92,8 @@ extension MessageReceiver {
members : [ String ] ,
admins : [ String ] ,
expirationTimer : UInt32 ,
messageSentTimestamp : UInt64
messageSentTimestamp : UInt64 ,
calledFromConfigHandling : Bool
) throws {
// W i t h n e w c l o s e d g r o u p s w e o n l y w a n t t o c r e a t e t h e m i f t h e a d m i n c r e a t i n g t h e c l o s e d g r o u p i s a n
// a p p r o v e d c o n t a c t ( t o p r e v e n t s p a m v i a c l o s e d g r o u p s g e t t i n g a r o u n d m e s s a g e r e q u e s t s i f u s e r s a r e
@ -65,13 +107,14 @@ extension MessageReceiver {
}
}
guard hasApprovedAdmin else { return }
// I f t h e g r o u p c a m e f r o m t h e u p d a t e d c o n f i g h a n d l i n g t h e n i t d o e s n ' t m a t t e r i f w e
// h a v e a n a p p r o v e d a d m i n - w e s h o u l d a d d i t r e g a r d l e s s ( a s i t ' s b e e n s y n c e d f r o m
// a n t o h e r d e v i c e )
guard hasApprovedAdmin || calledFromConfigHandling else { return }
// C r e a t e t h e g r o u p
let thread : SessionThread = try SessionThread
. fetchOrCreate ( db , id : groupPublicKey , variant : . legacyGroup )
. with ( shouldBeVisible : true )
. saved ( db )
. fetchOrCreate ( db , id : groupPublicKey , variant : . legacyGroup , shouldBeVisible : true )
let closedGroup : ClosedGroup = try ClosedGroup (
threadId : groupPublicKey ,
name : name ,
@ -103,7 +146,7 @@ extension MessageReceiver {
}
// U p d a t e t h e D i s a p p e a r i n g M e s s a g e s c o n f i g
try thread . disappearingMessagesConfiguration
let disappearingConfig : DisappearingMessagesConfiguration = try thread . disappearingMessagesConfiguration
. fetchOne ( db )
. defaulting ( to : DisappearingMessagesConfiguration . defaultWith ( thread . id ) )
. with (
@ -113,15 +156,38 @@ extension MessageReceiver {
( 24 * 60 * 60 )
)
)
. save ( db )
. save d ( db )
// S t o r e t h e k e y p a i r
try ClosedGroupKeyPair (
// S t o r e t h e k e y p a i r i f i t d o e s n ' t a l r e a d y e x i s t
let receivedTimestamp : TimeInterval = ( TimeInterval ( SnodeAPI . currentOffsetTimestampMs ( ) ) / 1000 )
let newKeyPair : ClosedGroupKeyPair = ClosedGroupKeyPair (
threadId : groupPublicKey ,
publicKey : Data ( encryptionKeyPair . publicKey ) ,
secretKey : Data ( encryptionKeyPair . secretKey ) ,
receivedTimestamp : ( TimeInterval ( SnodeAPI . currentOffsetTimestampMs ( ) ) / 1000 )
) . insert ( db )
receivedTimestamp : receivedTimestamp
)
let keyPairExists : Bool = ClosedGroupKeyPair
. filter ( ClosedGroupKeyPair . Columns . threadKeyPairHash = = newKeyPair . threadKeyPairHash )
. isNotEmpty ( db )
if ! keyPairExists {
try newKeyPair . insert ( db )
}
if ! calledFromConfigHandling {
// U p d a t e l i b S e s s i o n
try ? SessionUtil . add (
db ,
groupPublicKey : groupPublicKey ,
name : name ,
latestKeyPairPublicKey : Data ( encryptionKeyPair . publicKey ) ,
latestKeyPairSecretKey : Data ( encryptionKeyPair . secretKey ) ,
latestKeyPairReceivedTimestamp : receivedTimestamp ,
disappearingConfig : disappearingConfig ,
members : members . asSet ( ) ,
admins : admins . asSet ( )
)
}
// S t a r t p o l l i n g
ClosedGroupPoller . shared . startIfNeeded ( for : groupPublicKey )
@ -132,18 +198,24 @@ extension MessageReceiver {
// / E x t r a c t s a n d a d d s t h e n e w e n c r y p t i o n k e y p a i r t o o u r l i s t o f k e y p a i r s i f t h e r e i s o n e f o r o u r p u b l i c k e y , A N D t h e m e s s a g e w a s
// / s e n t b y t h e g r o u p a d m i n .
private static func handleClosedGroupEncryptionKeyPair ( _ db : Database , message : ClosedGroupControlMessage ) throws {
guard
case let . encryptionKeyPair ( explicitGroupPublicKey , wrappers ) = message . kind ,
let groupPublicKey : String = ( explicitGroupPublicKey ? . toHexString ( ) ? ? message . groupPublicKey )
else { return }
private static func handleClosedGroupEncryptionKeyPair (
_ db : Database ,
threadId : String ,
threadVariant : SessionThread . Variant ,
message : ClosedGroupControlMessage
) throws {
guard case let . encryptionKeyPair ( explicitGroupPublicKey , wrappers ) = message . kind else {
return
}
let groupPublicKey : String = ( explicitGroupPublicKey ? . toHexString ( ) ? ? threadId )
guard let userKeyPair : Box . KeyPair = Identity . fetchUserKeyPair ( db ) else {
return SNLog ( " Couldn't find user X25519 key pair. " )
}
guard let thread : SessionThread = try ? SessionThread . fetchOne ( db , id : groupPublicKey ) else {
guard let closedGroup: ClosedGroup = try ? ClosedGroup . fetchOne ( db , id : groupPublicKey ) else {
return SNLog ( " Ignoring closed group encryption key pair for nonexistent group. " )
}
guard let closedGroup : ClosedGroup = try ? thread . closedGroup . fetchOne ( db ) else { return }
guard let groupAdmins : [ GroupMember ] = try ? closedGroup . admins . fetchAll ( db ) else { return }
guard let sender : String = message . sender , groupAdmins . contains ( where : { $0 . profileId = = sender } ) else {
return SNLog ( " Ignoring closed group encryption key pair from non-admin. " )
@ -203,368 +275,355 @@ extension MessageReceiver {
SNLog ( " Received a new closed group encryption key pair. " )
}
private static func handleClosedGroupNameChanged ( _ db : Database , message : ClosedGroupControlMessage ) throws {
guard case let . nameChange ( name ) = message . kind else { return }
private static func handleClosedGroupNameChanged (
_ db : Database ,
threadId : String ,
threadVariant : SessionThread . Variant ,
message : ClosedGroupControlMessage
) throws {
guard
let messageKind : ClosedGroupControlMessage . Kind = message . kind ,
case let . nameChange ( name ) = message . kind
else { return }
try performIfValid ( db , message : message ) { id , sender , thread , closedGroup in
_ = try ClosedGroup
. filter ( id : id )
. updateAll ( db , ClosedGroup . Columns . name . set ( to : name ) )
// N o t i f y t h e u s e r i f n e e d e d
guard name != closedGroup . name else { return }
_ = try Interaction (
serverHash : message . serverHash ,
threadId : thread . id ,
authorId : sender ,
variant : . infoClosedGroupUpdated ,
body : ClosedGroupControlMessage . Kind
. nameChange ( name : name )
. infoMessage ( db , sender : sender ) ,
timestampMs : (
message . sentTimestamp . map { Int64 ( $0 ) } ? ?
SnodeAPI . currentOffsetTimestampMs ( )
try processIfValid (
db ,
threadId : threadId ,
threadVariant : threadVariant ,
message : message ,
messageKind : messageKind ,
infoMessageVariant : . infoClosedGroupUpdated ,
legacyGroupChanges : { sender , closedGroup , allMembers in
// U p d a t e l i b S e s s i o n
try ? SessionUtil . update (
db ,
groupPublicKey : threadId ,
name : name
)
) . inserted ( db )
// U p d a t e l i b S e s s i o n
try ? SessionUtil . update (
db ,
groupPublicKey : id ,
name : name
)
}
_ = try ClosedGroup
. filter ( id : threadId )
. updateAll ( // E x p l i c i t c o n f i g u p d a t e s o n o n e e d t o u s e ' u p d a t e A l l A n d C o n f i g '
db ,
ClosedGroup . Columns . name . set ( to : name )
)
}
)
}
private static func handleClosedGroupMembersAdded ( _ db : Database , message : ClosedGroupControlMessage ) throws {
guard case let . membersAdded ( membersAsData ) = message . kind else { return }
try performIfValid ( db , message : message ) { id , sender , thread , closedGroup in
guard let allGroupMembers : [ GroupMember ] = try ? closedGroup . allMembers . fetchAll ( db ) else {
return
}
// U p d a t e t h e g r o u p
let addedMembers : [ String ] = membersAsData . map { $0 . toHexString ( ) }
let currentMemberIds : Set < String > = allGroupMembers
. filter { $0 . role = = . standard }
. map { $0 . profileId }
. asSet ( )
let members : Set < String > = currentMemberIds . union ( addedMembers )
private static func handleClosedGroupMembersAdded (
_ db : Database ,
threadId : String ,
threadVariant : SessionThread . Variant ,
message : ClosedGroupControlMessage
) throws {
guard
let messageKind : ClosedGroupControlMessage . Kind = message . kind ,
case let . membersAdded ( membersAsData ) = message . kind
else { return }
// C r e a t e r e c o r d s f o r a n y n e w m e m b e r s
try addedMembers
. filter { ! currentMemberIds . contains ( $0 ) }
. forEach { memberId in
try GroupMember (
groupId : id ,
profileId : memberId ,
role : . standard ,
isHidden : false
) . insert ( db )
}
// S e n d t h e l a t e s t e n c r y p t i o n k e y p a i r t o t h e a d d e d m e m b e r s i f t h e c u r r e n t u s e r i s
// t h e a d m i n o f t h e g r o u p
//
// T h i s f i x e s a r a c e c o n d i t i o n w h e r e :
// • A m e m b e r r e m o v e s a n o t h e r m e m b e r .
// • A m e m b e r a d d s s o m e o n e t o t h e g r o u p a n d s e n d s t h e m t h e l a t e s t g r o u p k e y p a i r .
// • T h e a d m i n i s o f f l i n e d u r i n g a l l o f t h i s .
// • W h e n t h e a d m i n c o m e s b a c k o n l i n e t h e y s e e t h e m e m b e r r e m o v e d m e s s a g e a n d g e n e r a t e +
// d i s t r i b u t e a n e w k e y p a i r , b u t t h e y d o n ' t k n o w a b o u t t h e a d d e d m e m b e r y e t .
// • N o w t h e y s e e t h e m e m b e r a d d e d m e s s a g e .
//
// W i t h o u t t h e c o d e b e l o w , t h e a d d e d m e m b e r ( s ) w o u l d n e v e r g e t t h e k e y p a i r t h a t w a s
// g e n e r a t e d b y t h e a d m i n w h e n t h e y s a w t h e m e m b e r r e m o v e d m e s s a g e .
let userPublicKey : String = getUserHexEncodedPublicKey ( db )
if allGroupMembers . contains ( where : { $0 . role = = . admin && $0 . profileId = = userPublicKey } ) {
addedMembers . forEach { memberId in
MessageSender . sendLatestEncryptionKeyPair ( db , to : memberId , for : id )
}
}
// R e m o v e a n y ' z o m b i e ' v e r s i o n s o f t h e a d d e d m e m b e r s ( i n c a s e t h e y w e r e r e - a d d e d )
_ = try GroupMember
. filter ( GroupMember . Columns . groupId = = id )
. filter ( GroupMember . Columns . role = = GroupMember . Role . zombie )
. filter ( addedMembers . contains ( GroupMember . Columns . profileId ) )
. deleteAll ( db )
// N o t i f y t h e u s e r i f n e e d e d
guard members != currentMemberIds else { return }
_ = try Interaction (
serverHash : message . serverHash ,
threadId : thread . id ,
authorId : sender ,
variant : . infoClosedGroupUpdated ,
body : ClosedGroupControlMessage . Kind
. membersAdded (
members : addedMembers
. asSet ( )
. subtracting ( currentMemberIds )
. map { Data ( hex : $0 ) }
)
. infoMessage ( db , sender : sender ) ,
timestampMs : (
message . sentTimestamp . map { Int64 ( $0 ) } ? ?
SnodeAPI . currentOffsetTimestampMs ( )
)
) . inserted ( db )
// U p d a t e l i b S e s s i o n
try ? SessionUtil . update (
db ,
groupPublicKey : id ,
members : allGroupMembers
. filter { $0 . role = = . standard || $0 . role = = . zombie }
. map { $0 . profileId }
. asSet ( )
. union ( addedMembers ) ,
admins : allGroupMembers
. filter { $0 . role = = . admin }
try processIfValid (
db ,
threadId : threadId ,
threadVariant : threadVariant ,
message : message ,
messageKind : messageKind ,
infoMessageVariant : . infoClosedGroupUpdated ,
legacyGroupChanges : { sender , closedGroup , allMembers in
// U p d a t e t h e g r o u p
let addedMembers : [ String ] = membersAsData . map { $0 . toHexString ( ) }
let currentMemberIds : Set < String > = allMembers
. filter { $0 . role = = . standard }
. map { $0 . profileId }
. asSet ( )
)
}
// U p d a t e l i b S e s s i o n
try ? SessionUtil . update (
db ,
groupPublicKey : threadId ,
members : allMembers
. filter { $0 . role = = . standard || $0 . role = = . zombie }
. map { $0 . profileId }
. asSet ( )
. union ( addedMembers ) ,
admins : allMembers
. filter { $0 . role = = . admin }
. map { $0 . profileId }
. asSet ( )
)
// C r e a t e r e c o r d s f o r a n y n e w m e m b e r s
try addedMembers
. filter { ! currentMemberIds . contains ( $0 ) }
. forEach { memberId in
try GroupMember (
groupId : threadId ,
profileId : memberId ,
role : . standard ,
isHidden : false
) . save ( db )
}
// S e n d t h e l a t e s t e n c r y p t i o n k e y p a i r t o t h e a d d e d m e m b e r s i f t h e c u r r e n t u s e r i s
// t h e a d m i n o f t h e g r o u p
//
// T h i s f i x e s a r a c e c o n d i t i o n w h e r e :
// • A m e m b e r r e m o v e s a n o t h e r m e m b e r .
// • A m e m b e r a d d s s o m e o n e t o t h e g r o u p a n d s e n d s t h e m t h e l a t e s t g r o u p k e y p a i r .
// • T h e a d m i n i s o f f l i n e d u r i n g a l l o f t h i s .
// • W h e n t h e a d m i n c o m e s b a c k o n l i n e t h e y s e e t h e m e m b e r r e m o v e d m e s s a g e a n d g e n e r a t e +
// d i s t r i b u t e a n e w k e y p a i r , b u t t h e y d o n ' t k n o w a b o u t t h e a d d e d m e m b e r y e t .
// • N o w t h e y s e e t h e m e m b e r a d d e d m e s s a g e .
//
// W i t h o u t t h e c o d e b e l o w , t h e a d d e d m e m b e r ( s ) w o u l d n e v e r g e t t h e k e y p a i r t h a t w a s
// g e n e r a t e d b y t h e a d m i n w h e n t h e y s a w t h e m e m b e r r e m o v e d m e s s a g e .
let userPublicKey : String = getUserHexEncodedPublicKey ( db )
if allMembers . contains ( where : { $0 . role = = . admin && $0 . profileId = = userPublicKey } ) {
addedMembers . forEach { memberId in
MessageSender . sendLatestEncryptionKeyPair ( db , to : memberId , for : threadId )
}
}
// R e m o v e a n y ' z o m b i e ' v e r s i o n s o f t h e a d d e d m e m b e r s ( i n c a s e t h e y w e r e r e - a d d e d )
_ = try GroupMember
. filter ( GroupMember . Columns . groupId = = threadId )
. filter ( GroupMember . Columns . role = = GroupMember . Role . zombie )
. filter ( addedMembers . contains ( GroupMember . Columns . profileId ) )
. deleteAll ( db )
}
)
}
// / R e m o v e s t h e g i v e n m e m b e r s f r o m t h e g r o u p I F
// / • i t w a s n ' t t h e a d m i n t h a t w a s r e m o v e d ( t h a t s h o u l d h a p p e n t h r o u g h a ` M E M B E R _ L E F T ` m e s s a g e ) .
// / • t h e a d m i n s e n t t h e m e s s a g e ( o n l y t h e a d m i n c a n t r u l y r e m o v e m e m b e r s ) .
// / I f w e ' r e a m o n g t h e u s e r s t h a t w e r e r e m o v e d , d e l e t e a l l e n c r y p t i o n k e y p a i r s a n d t h e g r o u p p u b l i c k e y , u n s u b s c r i b e
// / f r o m p u s h n o t i f i c a t i o n s f o r t h i s c l o s e d g r o u p , a n d r e m o v e t h e g i v e n m e m b e r s f r o m t h e z o m b i e l i s t f o r t h i s g r o u p .
private static func handleClosedGroupMembersRemoved ( _ db : Database , message : ClosedGroupControlMessage ) throws {
guard case let . membersRemoved ( membersAsData ) = message . kind else { return }
private static func handleClosedGroupMembersRemoved (
_ db : Database ,
threadId : String ,
threadVariant : SessionThread . Variant ,
message : ClosedGroupControlMessage
) throws {
guard
let messageKind : ClosedGroupControlMessage . Kind = message . kind ,
case let . membersRemoved ( membersAsData ) = messageKind
else { return }
try performIfValid ( db , message : message ) { id , sender , thread , closedGroup in
// C h e c k t h a t t h e a d m i n w a s n ' t r e m o v e d
guard let allGroupMembers : [ GroupMember ] = try ? closedGroup . allMembers . fetchAll ( db ) else {
return
}
let removedMembers = membersAsData . map { $0 . toHexString ( ) }
let currentMemberIds : Set < String > = allGroupMembers
. filter { $0 . role = = . standard }
. map { $0 . profileId }
. asSet ( )
let members = currentMemberIds . subtracting ( removedMembers )
guard let firstAdminId : String = allGroupMembers . filter ( { $0 . role = = . admin } ) . first ? . profileId , members . contains ( firstAdminId ) else {
return SNLog ( " Ignoring invalid closed group update. " )
}
// C h e c k t h a t t h e m e s s a g e w a s s e n t b y t h e g r o u p a d m i n
guard allGroupMembers . filter ( { $0 . role = = . admin } ) . contains ( where : { $0 . profileId = = sender } ) else {
return SNLog ( " Ignoring invalid closed group update. " )
}
// D e l e t e t h e r e m o v e d m e m b e r s
try GroupMember
. filter ( GroupMember . Columns . groupId = = id )
. filter ( removedMembers . contains ( GroupMember . Columns . profileId ) )
. filter ( [ GroupMember . Role . standard , GroupMember . Role . zombie ] . contains ( GroupMember . Columns . role ) )
. deleteAll ( db )
// I f t h e c u r r e n t u s e r w a s r e m o v e d :
// • S t o p p o l l i n g f o r t h e g r o u p
// • R e m o v e t h e k e y p a i r s a s s o c i a t e d w i t h t h e g r o u p
// • N o t i f y t h e P N s e r v e r
let userPublicKey : String = getUserHexEncodedPublicKey ( db )
let wasCurrentUserRemoved : Bool = ! members . contains ( userPublicKey )
if wasCurrentUserRemoved {
ClosedGroupPoller . shared . stopPolling ( for : id )
let userPublicKey : String = getUserHexEncodedPublicKey ( db )
let removedMemberIds : [ String ] = membersAsData . map { $0 . toHexString ( ) }
try processIfValid (
db ,
threadId : threadId ,
threadVariant : threadVariant ,
message : message ,
messageKind : messageKind ,
infoMessageVariant : ( removedMemberIds . contains ( userPublicKey ) ?
. infoClosedGroupCurrentUserLeft :
. infoClosedGroupUpdated
) ,
legacyGroupChanges : { sender , closedGroup , allMembers in
let removedMembers = membersAsData . map { $0 . toHexString ( ) }
let currentMemberIds : Set < String > = allMembers
. filter { $0 . role = = . standard }
. map { $0 . profileId }
. asSet ( )
let members = currentMemberIds . subtracting ( removedMembers )
_ = try closedGroup
. keyPairs
. deleteAll ( db )
// C h e c k t h a t t h e g r o u p c r e a t o r i s s t i l l a m e m b e r a n d t h a t t h e m e s s a g e w a s
// s e n t b y a g r o u p a d m i n
guard
let firstAdminId : String = allMembers . filter ( { $0 . role = = . admin } )
. first ?
. profileId ,
members . contains ( firstAdminId ) ,
allMembers
. filter ( { $0 . role = = . admin } )
. contains ( where : { $0 . profileId = = sender } )
else { return SNLog ( " Ignoring invalid closed group update. " ) }
let _ = PushNotificationAPI . performOperation (
. unsubscribe ,
for : id ,
publicKey : userPublicKey
// U p d a t e l i b S e s s i o n
try ? SessionUtil . update (
db ,
groupPublicKey : threadId ,
members : allMembers
. filter { $0 . role = = . standard || $0 . role = = . zombie }
. map { $0 . profileId }
. asSet ( )
. subtracting ( removedMembers ) ,
admins : allMembers
. filter { $0 . role = = . admin }
. map { $0 . profileId }
. asSet ( )
)
}
// N o t i f y t h e u s e r i f n e e d e d
guard members != currentMemberIds else { return }
_ = try Interaction (
serverHash : message . serverHash ,
threadId : thread . id ,
authorId : sender ,
variant : ( wasCurrentUserRemoved ? . infoClosedGroupCurrentUserLeft : . infoClosedGroupUpdated ) ,
body : ClosedGroupControlMessage . Kind
. membersRemoved (
members : removedMembers
. asSet ( )
. intersection ( currentMemberIds )
. map { Data ( hex : $0 ) }
// D e l e t e t h e r e m o v e d m e m b e r s
try GroupMember
. filter ( GroupMember . Columns . groupId = = threadId )
. filter ( removedMembers . contains ( GroupMember . Columns . profileId ) )
. filter ( [ GroupMember . Role . standard , GroupMember . Role . zombie ] . contains ( GroupMember . Columns . role ) )
. deleteAll ( db )
// I f t h e c u r r e n t u s e r w a s r e m o v e d :
// • S t o p p o l l i n g f o r t h e g r o u p
// • R e m o v e t h e k e y p a i r s a s s o c i a t e d w i t h t h e g r o u p
// • N o t i f y t h e P N s e r v e r
let wasCurrentUserRemoved : Bool = ! members . contains ( userPublicKey )
if wasCurrentUserRemoved {
ClosedGroupPoller . shared . stopPolling ( for : threadId )
_ = try closedGroup
. keyPairs
. deleteAll ( db )
let _ = PushNotificationAPI . performOperation (
. unsubscribe ,
for : threadId ,
publicKey : userPublicKey
)
. infoMessage ( db , sender : sender ) ,
timestampMs : (
message . sentTimestamp . map { Int64 ( $0 ) } ? ?
SnodeAPI . currentOffsetTimestampMs ( )
)
) . inserted ( db )
// U p d a t e l i b S e s s i o n
try ? SessionUtil . update (
db ,
groupPublicKey : id ,
members : allGroupMembers
. filter { $0 . role = = . standard || $0 . role = = . zombie }
. map { $0 . profileId }
. asSet ( )
. subtracting ( removedMembers ) ,
admins : allGroupMembers
. filter { $0 . role = = . admin }
. map { $0 . profileId }
. asSet ( )
)
}
}
}
)
}
// / I f a r e g u l a r m e m b e r l e f t :
// / • M a r k t h e m a s a z o m b i e ( t o b e r e m o v e d b y t h e a d m i n l a t e r ) .
// / I f t h e a d m i n l e f t :
// / • U n s u b s c r i b e f r o m P N s , d e l e t e t h e g r o u p p u b l i c k e y , e t c . a s t h e g r o u p w i l l b e d i s b a n d e d .
private static func handleClosedGroupMemberLeft ( _ db : Database , message : ClosedGroupControlMessage ) throws {
guard case . memberLeft = message . kind else { return }
private static func handleClosedGroupMemberLeft (
_ db : Database ,
threadId : String ,
threadVariant : SessionThread . Variant ,
message : ClosedGroupControlMessage
) throws {
guard
let messageKind : ClosedGroupControlMessage . Kind = message . kind ,
case . memberLeft = messageKind
else { return }
try performIfValid ( db , message : message ) { id , sender , thread , closedGroup in
guard let allGroupMembers : [ GroupMember ] = try ? closedGroup . allMembers . fetchAll ( db ) else {
return
}
let userPublicKey : String = getUserHexEncodedPublicKey ( db )
let didAdminLeave : Bool = allGroupMembers . contains ( where : { member in
member . role = = . admin && member . profileId = = sender
} )
let members : [ GroupMember ] = allGroupMembers . filter { $0 . role = = . standard }
let membersToRemove : [ GroupMember ] = members
. filter { member in
didAdminLeave || // I f t h e a d m i n l e a v e s t h e g r o u p i s d i s b a n d e d
member . profileId = = sender
}
let updatedMemberIds : Set < String > = members
. map { $0 . profileId }
. asSet ( )
. subtracting ( membersToRemove . map { $0 . profileId } )
// D e l e t e t h e m e m b e r s t o r e m o v e
try GroupMember
. filter ( GroupMember . Columns . groupId = = id )
. filter ( updatedMemberIds . contains ( GroupMember . Columns . profileId ) )
. deleteAll ( db )
if didAdminLeave || sender = = userPublicKey {
// R e m o v e t h e g r o u p f r o m t h e d a t a b a s e a n d u n s u b s c r i b e f r o m P N s
ClosedGroupPoller . shared . stopPolling ( for : id )
_ = try closedGroup
. keyPairs
. deleteAll ( db )
let _ = PushNotificationAPI . performOperation (
. unsubscribe ,
for : id ,
publicKey : userPublicKey
)
}
// R e - a d d t h e r e m o v e d m e m b e r a s a z o m b i e ( u n l e s s t h e a d m i n l e f t w h i c h d i s b a n d s t h e
// g r o u p )
if ! didAdminLeave {
try GroupMember (
groupId : id ,
profileId : sender ,
role : . zombie ,
isHidden : false
) . insert ( db )
}
// N o t i f y t h e u s e r i f n e e d e d
guard updatedMemberIds != Set ( members . map { $0 . profileId } ) else { return }
_ = try Interaction (
serverHash : message . serverHash ,
threadId : thread . id ,
authorId : sender ,
variant : . infoClosedGroupUpdated ,
body : ClosedGroupControlMessage . Kind
. memberLeft
. infoMessage ( db , sender : sender ) ,
timestampMs : (
message . sentTimestamp . map { Int64 ( $0 ) } ? ?
SnodeAPI . currentOffsetTimestampMs ( )
)
) . inserted ( db )
// U p d a t e l i b S e s s i o n
try ? SessionUtil . update (
db ,
groupPublicKey : id ,
members : allGroupMembers
. filter {
( $0 . role = = . standard || $0 . role = = . zombie ) &&
! membersToRemove . contains ( $0 )
try processIfValid (
db ,
threadId : threadId ,
threadVariant : threadVariant ,
message : message ,
messageKind : messageKind ,
infoMessageVariant : . infoClosedGroupUpdated ,
legacyGroupChanges : { sender , closedGroup , allMembers in
let userPublicKey : String = getUserHexEncodedPublicKey ( db )
let didAdminLeave : Bool = allMembers . contains ( where : { member in
member . role = = . admin && member . profileId = = sender
} )
let members : [ GroupMember ] = allMembers . filter { $0 . role = = . standard }
let membersToRemove : [ GroupMember ] = members
. filter { member in
didAdminLeave || // I f t h e a d m i n l e a v e s t h e g r o u p i s d i s b a n d e d
member . profileId = = sender
}
. map { $0 . profileId }
. asSet ( ) ,
admins : allGroupMembers
. filter { $0 . role = = . admin }
let updatedMemberIds : Set < String > = members
. map { $0 . profileId }
. asSet ( )
)
}
}
private static func handleClosedGroupEncryptionKeyPairRequest ( _ db : Database , message : ClosedGroupControlMessage ) {
/*
guard case . encryptionKeyPairRequest = message . kind else { return }
let transaction = transaction as ! YapDatabaseReadWriteTransaction
guard let groupPublicKey = message . groupPublicKey else { return }
performIfValid ( for : message , using : transaction ) { groupID , _ , group in
let publicKey = message . sender !
// G u a r d a g a i n s t s e l f - s e n d s
guard publicKey != getUserHexEncodedPublicKey ( ) else {
return SNLog ( " Ignoring invalid closed group update. " )
. subtracting ( membersToRemove . map { $0 . profileId } )
// U p d a t e l i b S e s s i o n
try ? SessionUtil . update (
db ,
groupPublicKey : threadId ,
members : allMembers
. filter {
( $0 . role = = . standard || $0 . role = = . zombie ) &&
! membersToRemove . contains ( $0 )
}
. map { $0 . profileId }
. asSet ( ) ,
admins : allMembers
. filter { $0 . role = = . admin }
. map { $0 . profileId }
. asSet ( )
)
// D e l e t e t h e m e m b e r s t o r e m o v e
try GroupMember
. filter ( GroupMember . Columns . groupId = = threadId )
. filter ( updatedMemberIds . contains ( GroupMember . Columns . profileId ) )
. deleteAll ( db )
if didAdminLeave || sender = = userPublicKey {
try ClosedGroup . removeKeysAndUnsubscribe (
db ,
threadId : threadId ,
removeGroupData : false ,
calledFromConfigHandling : false
)
}
// R e - a d d t h e r e m o v e d m e m b e r a s a z o m b i e ( u n l e s s t h e a d m i n l e f t w h i c h d i s b a n d s t h e
// g r o u p )
if ! didAdminLeave {
try GroupMember (
groupId : threadId ,
profileId : sender ,
role : . zombie ,
isHidden : false
) . save ( db )
}
}
MessageSender . sendLatestEncryptionKeyPair ( to : publicKey , for : groupPublicKey , using : transaction )
}
*/
)
}
// MARK: - C o n v e n i e n c e
private static func performIfValid (
private static func processIfValid (
_ db : Database ,
threadId : String ,
threadVariant : SessionThread . Variant ,
message : ClosedGroupControlMessage ,
_ update : ( String , String , SessionThread , ClosedGroup
) throws -> Void ) throws {
guard let groupPublicKey : String = message . groupPublicKey else { return }
guard let thread : SessionThread = try ? SessionThread . fetchOne ( db , id : groupPublicKey ) else {
return SNLog ( " Ignoring closed group update for nonexistent group. " )
}
guard let closedGroup : ClosedGroup = try ? thread . closedGroup . fetchOne ( db ) else { return }
// C h e c k t h a t t h e m e s s a g e i s n ' t f r o m b e f o r e t h e g r o u p w a s c r e a t e d
guard Double ( message . sentTimestamp ? ? 0 ) > closedGroup . formationTimestamp else {
return SNLog ( " Ignoring closed group update from before thread was created. " )
}
messageKind : ClosedGroupControlMessage . Kind ,
infoMessageVariant : Interaction . Variant ,
legacyGroupChanges : ( String , ClosedGroup , [ GroupMember ] ) throws -> ( )
) throws {
guard let sender : String = message . sender else { return }
guard let members : [ GroupMember ] = try ? closedGroup . members . fetchAll ( db ) else { return }
guard let closedGroup : ClosedGroup = try ? ClosedGroup . fetchOne ( db , id : threadId ) else {
return SNLog ( " Ignoring group update for nonexistent group. " )
}
// C h e c k t h a t t h e s e n d e r i s a m e m b e r o f t h e g r o u p
guard members . contains ( where : { $0 . profileId = = sender } ) else {
return SNLog ( " Ignoring closed group update from non-member. " )
// L e g a c y g r o u p s u s e d t h e s e c o n t r o l m e s s a g e s f o r m a k i n g c h a n g e s , n e w g r o u p s o n l y u s e t h e m
// f o r i n f o r m a t i o n p u r p o s e s
switch threadVariant {
case . legacyGroup :
// C h e c k t h a t t h e m e s s a g e i s n ' t f r o m b e f o r e t h e g r o u p w a s c r e a t e d
guard Double ( message . sentTimestamp ? ? 0 ) > closedGroup . formationTimestamp else {
return SNLog ( " Ignoring legacy group update from before thread was created. " )
}
// I f t h e s e v a l u e s a r e m i s s i n g t h e n w e p r o b a b l y w o n ' t b e a b l e t o v a l i d l y h a n d l e t h e m e s s a g e
guard
let allMembers : [ GroupMember ] = try ? closedGroup . allMembers . fetchAll ( db ) ,
allMembers . contains ( where : { $0 . profileId = = sender } )
else { return SNLog ( " Ignoring legacy group update from non-member. " ) }
try legacyGroupChanges ( sender , closedGroup , allMembers )
case . group :
break
default : return // I g n o r e a s i n v a l i d
}
try update ( groupPublicKey , sender , thread , closedGroup )
// I n s e r t t h e i n f o m e s s a g e f o r t h i s g r o u p c o n t r o l m e s s a g e
_ = try Interaction (
serverHash : message . serverHash ,
threadId : threadId ,
authorId : sender ,
variant : infoMessageVariant ,
body : messageKind
. infoMessage ( db , sender : sender ) ,
timestampMs : (
message . sentTimestamp . map { Int64 ( $0 ) } ? ?
SnodeAPI . currentOffsetTimestampMs ( )
)
) . inserted ( db )
}
}