Fixed a few bugs and resolved some TODOs

Added additional properties to the Group to simplify the code
Added the roleStatus to the GroupMember for future functionality
Fixed a bug where the input field might not appear if a group becomes valid while it's open
Fixed a bug where updated groups might not get their states loaded into memory under certain conditions
Removed some duplicate code
pull/941/head
Morgan Pretty 2 years ago
parent 8e04944af0
commit 67713ca498

@ -348,7 +348,7 @@ final class EditClosedGroupVC: BaseVC, UITableViewDataSource, UITableViewDelegat
guard !updatedName.isEmpty else { guard !updatedName.isEmpty else {
return showError(title: "vc_create_closed_group_group_name_missing_error".localized()) return showError(title: "vc_create_closed_group_group_name_missing_error".localized())
} }
guard updatedName.utf8CString.count < SessionUtil.libSessionMaxGroupNameByteLength else { guard updatedName.utf8CString.count < SessionUtil.sizeMaxGroupNameBytes else {
return showError(title: "vc_create_closed_group_group_name_too_long_error".localized()) return showError(title: "vc_create_closed_group_group_name_too_long_error".localized())
} }

@ -320,7 +320,7 @@ final class NewClosedGroupVC: BaseVC, UITableViewDataSource, UITableViewDelegate
else { else {
return showError(title: "vc_create_closed_group_group_name_missing_error".localized()) return showError(title: "vc_create_closed_group_group_name_missing_error".localized())
} }
guard name.utf8CString.count < SessionUtil.libSessionMaxGroupNameByteLength else { guard name.utf8CString.count < SessionUtil.sizeMaxGroupNameBytes else {
return showError(title: "vc_create_closed_group_group_name_too_long_error".localized()) return showError(title: "vc_create_closed_group_group_name_too_long_error".localized())
} }
guard selectedProfiles.count >= 1 else { guard selectedProfiles.count >= 1 else {

@ -895,10 +895,12 @@ final class ConversationVC: BaseVC, SessionUtilRespondingViewController, Convers
} }
// Now we have done all the needed diffs update the viewModel with the latest data // Now we have done all the needed diffs update the viewModel with the latest data
let oldCanWrite: Bool = viewModel.threadData.canWrite
self.viewModel.updateThreadData(updatedThreadData) self.viewModel.updateThreadData(updatedThreadData)
/// **Note:** This needs to happen **after** we have update the viewModel's thread data /// **Note:** This needs to happen **after** we have update the viewModel's thread data (otherwise the `inputAccessoryView`
if initialLoad || viewModel.threadData.currentUserIsClosedGroupMember != updatedThreadData.currentUserIsClosedGroupMember { /// won't be generated correctly)
if initialLoad || oldCanWrite != updatedThreadData.canWrite {
if !self.isFirstResponder { if !self.isFirstResponder {
self.becomeFirstResponder() self.becomeFirstResponder()
} }

@ -197,6 +197,7 @@ enum MockDataGenerator {
threadId: randomLegacyGroupPublicKey, threadId: randomLegacyGroupPublicKey,
name: groupName, name: groupName,
formationTimestamp: timestampNow, formationTimestamp: timestampNow,
shouldPoll: true,
invited: false invited: false
) )
.saved(db) .saved(db)
@ -206,6 +207,7 @@ enum MockDataGenerator {
groupId: randomLegacyGroupPublicKey, groupId: randomLegacyGroupPublicKey,
profileId: memberId, profileId: memberId,
role: .standard, role: .standard,
roleStatus: .accepted, // Legacy group members don't have role statuses
isHidden: false isHidden: false
) )
.save(db) .save(db)
@ -215,6 +217,7 @@ enum MockDataGenerator {
groupId: randomLegacyGroupPublicKey, groupId: randomLegacyGroupPublicKey,
profileId: adminId, profileId: adminId,
role: .admin, role: .admin,
roleStatus: .accepted, // Legacy group members don't have role statuses
isHidden: false isHidden: false
) )
.save(db) .save(db)

@ -10,20 +10,41 @@ enum _018_GroupsRebuildChanges: Migration {
static let needsConfigSync: Bool = false static let needsConfigSync: Bool = false
static let minExpectedRunDuration: TimeInterval = 0.1 static let minExpectedRunDuration: TimeInterval = 0.1
static var requirements: [MigrationRequirement] = [.sessionUtilStateLoaded] static var requirements: [MigrationRequirement] = [.sessionUtilStateLoaded]
static var fetchedTables: [(FetchableRecord & TableRecord).Type] = [] static var fetchedTables: [(FetchableRecord & TableRecord).Type] = [Identity.self]
static var createdOrAlteredTables: [(FetchableRecord & TableRecord).Type] = [ClosedGroup.self] static var createdOrAlteredTables: [(FetchableRecord & TableRecord).Type] = [
ClosedGroup.self, GroupMember.self
]
static func migrate(_ db: Database, using dependencies: Dependencies) throws { static func migrate(_ db: Database, using dependencies: Dependencies) throws {
try db.alter(table: ClosedGroup.self) { t in try db.alter(table: ClosedGroup.self) { t in
t.add(.groupDescription, .text)
t.add(.displayPictureUrl, .text) t.add(.displayPictureUrl, .text)
t.add(.displayPictureFilename, .text) t.add(.displayPictureFilename, .text)
t.add(.displayPictureEncryptionKey, .blob) t.add(.displayPictureEncryptionKey, .blob)
t.add(.lastDisplayPictureUpdate, .integer).defaults(to: 0) t.add(.lastDisplayPictureUpdate, .integer).defaults(to: 0)
t.add(.shouldPoll, .boolean).defaults(to: false)
t.add(.groupIdentityPrivateKey, .blob) t.add(.groupIdentityPrivateKey, .blob)
t.add(.authData, .blob) t.add(.authData, .blob)
t.add(.invited, .boolean).defaults(to: false) t.add(.invited, .boolean).defaults(to: false)
} }
try db.alter(table: GroupMember.self) { t in
t.add(.roleStatus, .integer)
.notNull()
.defaults(to: GroupMember.RoleStatus.accepted)
}
// Update existing groups where the current user is a member to have `shouldPoll` as `true`
try ClosedGroup
.joining(
required: ClosedGroup.members
.filter(GroupMember.Columns.profileId == getUserSessionId(db, using: dependencies).hexString)
)
.updateAll(
db,
ClosedGroup.Columns.shouldPoll.set(to: true)
)
Storage.update(progress: 1, for: self, in: target, using: dependencies) Storage.update(progress: 1, for: self, in: target, using: dependencies)
} }
} }

@ -20,6 +20,7 @@ public struct ClosedGroup: Codable, Identifiable, FetchableRecord, PersistableRe
public enum CodingKeys: String, CodingKey, ColumnExpression { public enum CodingKeys: String, CodingKey, ColumnExpression {
case threadId case threadId
case name case name
case groupDescription
case formationTimestamp case formationTimestamp
case displayPictureUrl case displayPictureUrl
@ -27,6 +28,7 @@ public struct ClosedGroup: Codable, Identifiable, FetchableRecord, PersistableRe
case displayPictureEncryptionKey case displayPictureEncryptionKey
case lastDisplayPictureUpdate case lastDisplayPictureUpdate
case shouldPoll
case groupIdentityPrivateKey case groupIdentityPrivateKey
case authData case authData
case invited case invited
@ -40,6 +42,7 @@ public struct ClosedGroup: Codable, Identifiable, FetchableRecord, PersistableRe
/// **Note:** This value will always be publicKey for the closed group /// **Note:** This value will always be publicKey for the closed group
public let threadId: String public let threadId: String
public let name: String public let name: String
public let groupDescription: String?
public let formationTimestamp: TimeInterval public let formationTimestamp: TimeInterval
/// The URL from which to fetch the groups's display picture. /// The URL from which to fetch the groups's display picture.
@ -54,6 +57,9 @@ public struct ClosedGroup: Codable, Identifiable, FetchableRecord, PersistableRe
/// The timestamp (in seconds since epoch) that the display picture was last updated /// The timestamp (in seconds since epoch) that the display picture was last updated
public let lastDisplayPictureUpdate: TimeInterval? public let lastDisplayPictureUpdate: TimeInterval?
/// A flag indicating whether we should poll for messages in this group
public let shouldPoll: Bool?
/// The private key for performing admin actions on this group /// The private key for performing admin actions on this group
public let groupIdentityPrivateKey: Data? public let groupIdentityPrivateKey: Data?
@ -104,22 +110,26 @@ public struct ClosedGroup: Codable, Identifiable, FetchableRecord, PersistableRe
public init( public init(
threadId: String, threadId: String,
name: String, name: String,
groupDescription: String? = nil,
formationTimestamp: TimeInterval, formationTimestamp: TimeInterval,
displayPictureUrl: String? = nil, displayPictureUrl: String? = nil,
displayPictureFilename: String? = nil, displayPictureFilename: String? = nil,
displayPictureEncryptionKey: Data? = nil, displayPictureEncryptionKey: Data? = nil,
lastDisplayPictureUpdate: TimeInterval? = nil, lastDisplayPictureUpdate: TimeInterval? = nil,
shouldPoll: Bool?,
groupIdentityPrivateKey: Data? = nil, groupIdentityPrivateKey: Data? = nil,
authData: Data? = nil, authData: Data? = nil,
invited: Bool? invited: Bool?
) { ) {
self.threadId = threadId self.threadId = threadId
self.name = name self.name = name
self.groupDescription = groupDescription
self.formationTimestamp = formationTimestamp self.formationTimestamp = formationTimestamp
self.displayPictureUrl = displayPictureUrl self.displayPictureUrl = displayPictureUrl
self.displayPictureFilename = displayPictureFilename self.displayPictureFilename = displayPictureFilename
self.displayPictureEncryptionKey = displayPictureEncryptionKey self.displayPictureEncryptionKey = displayPictureEncryptionKey
self.lastDisplayPictureUpdate = lastDisplayPictureUpdate self.lastDisplayPictureUpdate = lastDisplayPictureUpdate
self.shouldPoll = shouldPoll
self.groupIdentityPrivateKey = groupIdentityPrivateKey self.groupIdentityPrivateKey = groupIdentityPrivateKey
self.authData = authData self.authData = authData
self.invited = invited self.invited = invited
@ -158,27 +168,6 @@ public extension ClosedGroup {
case forced case forced
} }
/// The Group public key takes up 32 bytes
static func pubKeyByteLength(for variant: SessionThread.Variant) -> Int {
return 32
}
/// The Group secret key size differs between legacy and updated groups
static func secretKeyByteLength(for variant: SessionThread.Variant) -> Int {
switch variant {
case .group: return 64
default: return 32
}
}
/// The Group authData size differs between legacy and updated groups
static func authDataByteLength(for variant: SessionThread.Variant) -> Int {
switch variant {
case .group: return 100
default: return 0
}
}
static func approveGroup( static func approveGroup(
_ db: Database, _ db: Database,
group: ClosedGroup, group: ClosedGroup,
@ -189,12 +178,13 @@ public extension ClosedGroup {
throw MessageReceiverError.noUserED25519KeyPair throw MessageReceiverError.noUserED25519KeyPair
} }
if group.invited == true { if group.invited == true || group.shouldPoll != true {
try ClosedGroup try ClosedGroup
.filter(id: group.id) .filter(id: group.id)
.updateAllAndConfig( .updateAllAndConfig(
db, db,
ClosedGroup.Columns.invited.set(to: false), ClosedGroup.Columns.invited.set(to: false),
ClosedGroup.Columns.shouldPoll.set(to: true),
calledFromConfig: calledFromConfigHandling, calledFromConfig: calledFromConfigHandling,
using: dependencies using: dependencies
) )
@ -205,6 +195,7 @@ public extension ClosedGroup {
userED25519KeyPair: userED25519KeyPair, userED25519KeyPair: userED25519KeyPair,
groupIdentityPrivateKey: group.groupIdentityPrivateKey, groupIdentityPrivateKey: group.groupIdentityPrivateKey,
authData: group.authData, authData: group.authData,
shouldLoadState: true,
using: dependencies using: dependencies
) )

@ -17,6 +17,7 @@ public struct GroupMember: Codable, Equatable, Hashable, FetchableRecord, Persis
case groupId case groupId
case profileId case profileId
case role case role
case roleStatus
case isHidden case isHidden
} }
@ -27,9 +28,16 @@ public struct GroupMember: Codable, Equatable, Hashable, FetchableRecord, Persis
case admin case admin
} }
public enum RoleStatus: Int, Codable, DatabaseValueConvertible {
case accepted
case pending
case failed
}
public let groupId: String public let groupId: String
public let profileId: String public let profileId: String
public let role: Role public let role: Role
public let roleStatus: RoleStatus
public let isHidden: Bool public let isHidden: Bool
// MARK: - Relationships // MARK: - Relationships
@ -52,11 +60,31 @@ public struct GroupMember: Codable, Equatable, Hashable, FetchableRecord, Persis
groupId: String, groupId: String,
profileId: String, profileId: String,
role: Role, role: Role,
roleStatus: RoleStatus,
isHidden: Bool isHidden: Bool
) { ) {
self.groupId = groupId self.groupId = groupId
self.profileId = profileId self.profileId = profileId
self.role = role self.role = role
self.roleStatus = roleStatus
self.isHidden = isHidden self.isHidden = isHidden
} }
} }
// MARK: - Decoding
extension GroupMember {
public init(from decoder: Decoder) throws {
let container: KeyedDecodingContainer<CodingKeys> = try decoder.container(keyedBy: CodingKeys.self)
self = GroupMember(
groupId: try container.decode(String.self, forKey: .groupId),
profileId: try container.decode(String.self, forKey: .profileId),
role: try container.decode(Role.self, forKey: .role),
// Added in `_018_GroupsRebuildChanges`
roleStatus: ((try? container.decode(RoleStatus.self, forKey: .roleStatus)) ?? .accepted),
// Added in `_006_FixHiddenModAdminSupport`
isHidden: ((try? container.decode(Bool.self, forKey: .isHidden)) ?? false)
)
}
}

@ -63,9 +63,6 @@ public struct OpenGroup: Codable, Identifiable, FetchableRecord, PersistableReco
static let all: Permissions = [ .read, .write, .upload ] static let all: Permissions = [ .read, .write, .upload ]
} }
/// The Community public key takes up 32 bytes
static let pubkeyByteLength: Int = 32
public var id: String { threadId } // Identifiable public var id: String { threadId } // Identifiable
/// The id for the thread this open group belongs to /// The id for the thread this open group belongs to

@ -471,6 +471,7 @@ public final class OpenGroupManager {
groupId: threadId, groupId: threadId,
profileId: adminId, profileId: adminId,
role: .admin, role: .admin,
roleStatus: .accepted, // Community members don't have role statuses
isHidden: false isHidden: false
).save(db) ).save(db)
} }
@ -482,6 +483,7 @@ public final class OpenGroupManager {
groupId: threadId, groupId: threadId,
profileId: adminId, profileId: adminId,
role: .admin, role: .admin,
roleStatus: .accepted, // Community members don't have role statuses
isHidden: true isHidden: true
).save(db) ).save(db)
} }
@ -491,6 +493,7 @@ public final class OpenGroupManager {
groupId: threadId, groupId: threadId,
profileId: moderatorId, profileId: moderatorId,
role: .moderator, role: .moderator,
roleStatus: .accepted, // Community members don't have role statuses
isHidden: false isHidden: false
).save(db) ).save(db)
} }
@ -502,6 +505,7 @@ public final class OpenGroupManager {
groupId: threadId, groupId: threadId,
profileId: moderatorId, profileId: moderatorId,
role: .moderator, role: .moderator,
roleStatus: .accepted, // Community members don't have role statuses
isHidden: true isHidden: true
).save(db) ).save(db)
} }

@ -37,6 +37,7 @@ extension MessageReceiver {
threadId: groupSessionId, threadId: groupSessionId,
name: (name ?? "GROUP_TITLE_FALLBACK".localized()), name: (name ?? "GROUP_TITLE_FALLBACK".localized()),
formationTimestamp: TimeInterval(joinedAt), formationTimestamp: TimeInterval(joinedAt),
shouldPoll: false,
groupIdentityPrivateKey: groupIdentityPrivateKey, groupIdentityPrivateKey: groupIdentityPrivateKey,
authData: authData, authData: authData,
invited: invited invited: invited

@ -163,6 +163,7 @@ extension MessageReceiver {
threadId: legacyGroupSessionId, threadId: legacyGroupSessionId,
name: name, name: name,
formationTimestamp: (TimeInterval(formationTimestampMs) / 1000), formationTimestamp: (TimeInterval(formationTimestampMs) / 1000),
shouldPoll: true, // Legacy groups should always poll
invited: false // Legacy groups are never in the "invite" state invited: false // Legacy groups are never in the "invite" state
).saved(db) ).saved(db)
@ -177,6 +178,7 @@ extension MessageReceiver {
groupId: legacyGroupSessionId, groupId: legacyGroupSessionId,
profileId: memberId, profileId: memberId,
role: .standard, role: .standard,
roleStatus: .accepted, // Legacy group members don't have role statuses
isHidden: false isHidden: false
).save(db) ).save(db)
} }
@ -186,6 +188,7 @@ extension MessageReceiver {
groupId: legacyGroupSessionId, groupId: legacyGroupSessionId,
profileId: adminId, profileId: adminId,
role: .admin, role: .admin,
roleStatus: .accepted, // Legacy group members don't have role statuses
isHidden: false isHidden: false
).save(db) ).save(db)
} }
@ -431,6 +434,7 @@ extension MessageReceiver {
groupId: threadId, groupId: threadId,
profileId: memberId, profileId: memberId,
role: .standard, role: .standard,
roleStatus: .accepted, // Legacy group members don't have role statuses
isHidden: false isHidden: false
).save(db) ).save(db)
} }
@ -633,6 +637,7 @@ extension MessageReceiver {
groupId: threadId, groupId: threadId,
profileId: sender, profileId: sender,
role: .zombie, role: .zombie,
roleStatus: .accepted, // Legacy group members don't have role statuses
isHidden: false isHidden: false
).save(db) ).save(db)
} }

@ -42,6 +42,7 @@ extension MessageSender {
threadId: legacyGroupSessionId, threadId: legacyGroupSessionId,
name: name, name: name,
formationTimestamp: formationTimestamp, formationTimestamp: formationTimestamp,
shouldPoll: true, // Legacy groups should always poll
invited: false // Legacy groups are never in the "invite" state invited: false // Legacy groups are never in the "invite" state
).insert(db) ).insert(db)
@ -60,6 +61,7 @@ extension MessageSender {
groupId: legacyGroupSessionId, groupId: legacyGroupSessionId,
profileId: adminId, profileId: adminId,
role: .admin, role: .admin,
roleStatus: .accepted, // Legacy group members don't have role statuses
isHidden: false isHidden: false
).save(db) ).save(db)
} }
@ -69,6 +71,7 @@ extension MessageSender {
groupId: legacyGroupSessionId, groupId: legacyGroupSessionId,
profileId: memberId, profileId: memberId,
role: .standard, role: .standard,
roleStatus: .accepted, // Legacy group members don't have role statuses
isHidden: false isHidden: false
).save(db) ).save(db)
} }
@ -483,6 +486,7 @@ extension MessageSender {
groupId: closedGroup.id, groupId: closedGroup.id,
profileId: member, profileId: member,
role: .standard, role: .standard,
roleStatus: .accepted, // Legacy group members don't have role statuses
isHidden: false isHidden: false
).save(db) ).save(db)
} }

@ -50,11 +50,7 @@ public final class ClosedGroupPoller: Poller {
.read { db -> Set<String> in .read { db -> Set<String> in
try ClosedGroup try ClosedGroup
.select(.threadId) .select(.threadId)
.filter(ClosedGroup.Columns.invited == false) .filter(ClosedGroup.Columns.shouldPoll == true)
.joining(
required: ClosedGroup.members
.filter(GroupMember.Columns.profileId == getUserSessionId(db, using: dependencies).hexString)
)
.asRequest(of: String.self) .asRequest(of: String.self)
.fetchSet(db) .fetchSet(db)
} }

@ -165,7 +165,7 @@ public class Poller {
let pollerName: String = ( let pollerName: String = (
poller?.pollerName(for: publicKey) ?? poller?.pollerName(for: publicKey) ??
"poller with public key \(publicKey)" "poller with public key \(publicKey)" // stringlint:disable
) )
let configHashes: [String] = SessionUtil.configHashes(for: publicKey, using: dependencies) let configHashes: [String] = SessionUtil.configHashes(for: publicKey, using: dependencies)

@ -8,9 +8,9 @@ import SessionUtilitiesKit
// MARK: - Size Restrictions // MARK: - Size Restrictions
public extension SessionUtil { public extension SessionUtil {
static var libSessionMaxNameByteLength: Int { CONTACT_MAX_NAME_LENGTH } static var sizeMaxNameBytes: Int { CONTACT_MAX_NAME_LENGTH }
static var libSessionMaxNicknameByteLength: Int { CONTACT_MAX_NAME_LENGTH } static var sizeMaxNicknameBytes: Int { CONTACT_MAX_NAME_LENGTH }
static var libSessionMaxProfileUrlByteLength: Int { PROFILE_PIC_MAX_URL_LENGTH } static var sizeMaxProfileUrlBytes: Int { PROFILE_PIC_MAX_URL_LENGTH }
} }
// MARK: - Contacts Handling // MARK: - Contacts Handling

@ -623,7 +623,7 @@ public extension SessionUtil {
let roomToken: String = String(libSessionVal: community.room) let roomToken: String = String(libSessionVal: community.room)
let publicKey: String = Data( let publicKey: String = Data(
libSessionVal: community.pubkey, libSessionVal: community.pubkey,
count: OpenGroup.pubkeyByteLength count: SessionUtil.sizeCommunityPubkeyBytes
).toHexString() ).toHexString()
result.append( result.append(

@ -71,12 +71,18 @@ internal extension SessionUtil {
.fetchSet(db)) .fetchSet(db))
.defaulting(to: []) .defaulting(to: [])
let updatedMembers: Set<GroupMember> = result let updatedMembers: Set<GroupMember> = result
.map { .map { data in
GroupMember( GroupMember(
groupId: groupSessionId.hexString, groupId: groupSessionId.hexString,
profileId: $0.memberId, profileId: data.memberId,
role: ($0.admin ? .admin : .standard), role: (data.admin || (data.promoted > 0) ? .admin : .standard),
// TODO: Other properties roleStatus: {
switch (data.invited, data.promoted, data.admin) {
case (2, _, _), (_, 2, false): return .failed // Explicitly failed
case (1..., _, _), (_, 1..., false): return .pending // Pending if not accepted
default: return .accepted // Otherwise it's accepted
}
}(),
isHidden: false isHidden: false
) )
} }

@ -33,11 +33,7 @@ internal extension SessionUtil {
let userED25519KeyPair: KeyPair = Identity.fetchUserEd25519KeyPair(db, using: dependencies) let userED25519KeyPair: KeyPair = Identity.fetchUserEd25519KeyPair(db, using: dependencies)
else { throw MessageSenderError.noKeyPair } else { throw MessageSenderError.noKeyPair }
// There will probably be custom init functions, will need a way to save the conf into // Prep the relevant details
// the in-memory state after init though
var secretKey: [UInt8] = userED25519KeyPair.secretKey
var groupIdentityPublicKey: [UInt8] = groupIdentityKeyPair.publicKey
var groupIdentityPrivateKey: [UInt8] = groupIdentityKeyPair.secretKey
let groupSessionId: SessionId = SessionId(.group, publicKey: groupIdentityKeyPair.publicKey) let groupSessionId: SessionId = SessionId(.group, publicKey: groupIdentityKeyPair.publicKey)
let creationTimestamp: TimeInterval = TimeInterval( let creationTimestamp: TimeInterval = TimeInterval(
SnodeAPI.currentOffsetTimestampMs(using: dependencies) / 1000 SnodeAPI.currentOffsetTimestampMs(using: dependencies) / 1000
@ -46,43 +42,17 @@ internal extension SessionUtil {
let currentUserProfile: Profile? = Profile.fetchOrCreateCurrentUser(db, using: dependencies) let currentUserProfile: Profile? = Profile.fetchOrCreateCurrentUser(db, using: dependencies)
// Create the new config objects // Create the new config objects
var groupKeysConf: UnsafeMutablePointer<config_group_keys>? = nil let groupState: [ConfigDump.Variant: Config] = try createGroupState(
var groupInfoConf: UnsafeMutablePointer<config_object>? = nil groupSessionId: groupSessionId,
var groupMembersConf: UnsafeMutablePointer<config_object>? = nil userED25519KeyPair: userED25519KeyPair,
var error: [CChar] = [CChar](repeating: 0, count: 256) groupIdentityPrivateKey: Data(groupIdentityKeyPair.secretKey),
try groups_info_init( authData: nil,
&groupInfoConf, shouldLoadState: false, // We manually load the state after populating the configs
&groupIdentityPublicKey, using: dependencies
&groupIdentityPrivateKey, )
nil,
0,
&error
).orThrow(error: error)
try groups_members_init(
&groupMembersConf,
&groupIdentityPublicKey,
&groupIdentityPrivateKey,
nil,
0,
&error
).orThrow(error: error)
try groups_keys_init(
&groupKeysConf,
&secretKey,
&groupIdentityPublicKey,
&groupIdentityPrivateKey,
groupInfoConf,
groupMembersConf,
nil,
0,
&error
).orThrow(error: error)
guard // Extract the conf objects from the state to load in the initial data
let keysConf: UnsafeMutablePointer<config_group_keys> = groupKeysConf, guard case .groupKeys(_, let groupInfoConf, let membersConf) = groupState[.groupKeys] else {
let infoConf: UnsafeMutablePointer<config_object> = groupInfoConf,
let membersConf: UnsafeMutablePointer<config_object> = groupMembersConf
else {
SNLog("[SessionUtil Error] Group config objects were null") SNLog("[SessionUtil Error] Group config objects were null")
throw SessionUtilError.unableToCreateConfigObject throw SessionUtilError.unableToCreateConfigObject
} }
@ -133,13 +103,8 @@ internal extension SessionUtil {
groups_members_set(membersConf, &member) groups_members_set(membersConf, &member)
} }
} }
// Define the config state map and load it into memory
let groupState: [ConfigDump.Variant: Config] = [
.groupKeys: .groupKeys(keysConf, info: infoConf, members: membersConf),
.groupInfo: .object(infoConf),
.groupMembers: .object(membersConf),
]
// Now that everything has been populated correctly we can load the state into memory
dependencies.mutate(cache: .sessionUtil) { cache in dependencies.mutate(cache: .sessionUtil) { cache in
groupState.forEach { variant, config in groupState.forEach { variant, config in
cache.setConfig(for: variant, sessionId: groupSessionId, to: config) cache.setConfig(for: variant, sessionId: groupSessionId, to: config)
@ -147,7 +112,7 @@ internal extension SessionUtil {
} }
return ( return (
SessionId(.group, publicKey: groupIdentityPublicKey), groupSessionId,
groupIdentityKeyPair, groupIdentityKeyPair,
groupState, groupState,
ClosedGroup( ClosedGroup(
@ -158,7 +123,8 @@ internal extension SessionUtil {
displayPictureFilename: displayPictureFilename, displayPictureFilename: displayPictureFilename,
displayPictureEncryptionKey: displayPictureEncryptionKey, displayPictureEncryptionKey: displayPictureEncryptionKey,
lastDisplayPictureUpdate: creationTimestamp, lastDisplayPictureUpdate: creationTimestamp,
groupIdentityPrivateKey: Data(groupIdentityPrivateKey), shouldPoll: true,
groupIdentityPrivateKey: Data(groupIdentityKeyPair.secretKey),
invited: false invited: false
), ),
finalMembers.map { memberId, info -> GroupMember in finalMembers.map { memberId, info -> GroupMember in
@ -166,6 +132,7 @@ internal extension SessionUtil {
groupId: groupSessionId.hexString, groupId: groupSessionId.hexString,
profileId: memberId, profileId: memberId,
role: (info.isAdmin ? .admin : .standard), role: (info.isAdmin ? .admin : .standard),
roleStatus: (memberId == userSessionId.hexString ? .accepted : .pending),
isHidden: false isHidden: false
) )
} }
@ -222,6 +189,7 @@ internal extension SessionUtil {
userED25519KeyPair: KeyPair, userED25519KeyPair: KeyPair,
groupIdentityPrivateKey: Data?, groupIdentityPrivateKey: Data?,
authData: Data?, authData: Data?,
shouldLoadState: Bool,
using dependencies: Dependencies using dependencies: Dependencies
) throws -> [ConfigDump.Variant: Config] { ) throws -> [ConfigDump.Variant: Config] {
var secretKey: [UInt8] = userED25519KeyPair.secretKey var secretKey: [UInt8] = userED25519KeyPair.secretKey
@ -277,11 +245,16 @@ internal extension SessionUtil {
.groupMembers: .object(membersConf), .groupMembers: .object(membersConf),
] ]
// Only load the state if specified (during initial group creation we want to
// load the state after populating the different configs incase invalid data
// was provided)
if shouldLoadState {
dependencies.mutate(cache: .sessionUtil) { cache in dependencies.mutate(cache: .sessionUtil) { cache in
groupState.forEach { variant, config in groupState.forEach { variant, config in
cache.setConfig(for: variant, sessionId: groupSessionId, to: config) cache.setConfig(for: variant, sessionId: groupSessionId, to: config)
} }
} }
}
return groupState return groupState
} }

@ -10,10 +10,16 @@ import SessionSnodeKit
// MARK: - Size Restrictions // MARK: - Size Restrictions
public extension SessionUtil { public extension SessionUtil {
static var libSessionMaxGroupNameByteLength: Int { GROUP_NAME_MAX_LENGTH } static var sizeMaxGroupNameBytes: Int { GROUP_NAME_MAX_LENGTH }
static var libSessionMaxGroupBaseUrlByteLength: Int { COMMUNITY_BASE_URL_MAX_LENGTH } static var sizeMaxCommunityBaseUrlBytes: Int { COMMUNITY_BASE_URL_MAX_LENGTH }
static var libSessionMaxGroupFullUrlByteLength: Int { COMMUNITY_FULL_URL_MAX_LENGTH } static var sizeMaxCommunityFullUrlBytes: Int { COMMUNITY_FULL_URL_MAX_LENGTH }
static var libSessionMaxCommunityRoomByteLength: Int { COMMUNITY_ROOM_MAX_LENGTH } static var sizeMaxCommunityRoomBytes: Int { COMMUNITY_ROOM_MAX_LENGTH }
static var sizeCommunityPubkeyBytes: Int { 32 }
static var sizeLegacyGroupPubkeyBytes: Int { 32 }
static var sizeLegacyGroupSecretKeyBytes: Int { 32 }
static var sizeGroupSecretKeyBytes: Int { 64 }
static var sizeGroupAuthDataBytes: Int { 100 }
} }
// MARK: - UserGroups Handling // MARK: - UserGroups Handling
@ -58,7 +64,7 @@ internal extension SessionUtil {
roomToken: roomToken, roomToken: roomToken,
publicKey: Data( publicKey: Data(
libSessionVal: community.pubkey, libSessionVal: community.pubkey,
count: OpenGroup.pubkeyByteLength count: SessionUtil.sizeCommunityPubkeyBytes
).toHexString() ).toHexString()
), ),
priority: community.priority priority: community.priority
@ -77,11 +83,11 @@ internal extension SessionUtil {
threadId: groupId, threadId: groupId,
publicKey: Data( publicKey: Data(
libSessionVal: legacyGroup.enc_pubkey, libSessionVal: legacyGroup.enc_pubkey,
count: ClosedGroup.pubKeyByteLength(for: .legacyGroup) count: SessionUtil.sizeLegacyGroupPubkeyBytes
), ),
secretKey: Data( secretKey: Data(
libSessionVal: legacyGroup.enc_seckey, libSessionVal: legacyGroup.enc_seckey,
count: ClosedGroup.secretKeyByteLength(for: .legacyGroup) count: SessionUtil.sizeLegacyGroupSecretKeyBytes
), ),
receivedTimestamp: (TimeInterval(SnodeAPI.currentOffsetTimestampMs(using: dependencies)) / 1000) receivedTimestamp: (TimeInterval(SnodeAPI.currentOffsetTimestampMs(using: dependencies)) / 1000)
), ),
@ -99,6 +105,7 @@ internal extension SessionUtil {
groupId: groupId, groupId: groupId,
profileId: memberId, profileId: memberId,
role: .standard, role: .standard,
roleStatus: .accepted, // Legacy group members don't have role statuses
isHidden: false isHidden: false
) )
}, },
@ -109,6 +116,7 @@ internal extension SessionUtil {
groupId: groupId, groupId: groupId,
profileId: memberId, profileId: memberId,
role: .admin, role: .admin,
roleStatus: .accepted, // Legacy group members don't have role statuses
isHidden: false isHidden: false
) )
}, },
@ -126,7 +134,7 @@ internal extension SessionUtil {
groupIdentityPrivateKey: (!group.have_secretkey ? nil : groupIdentityPrivateKey: (!group.have_secretkey ? nil :
Data( Data(
libSessionVal: group.secretkey, libSessionVal: group.secretkey,
count: ClosedGroup.secretKeyByteLength(for: .group), count: SessionUtil.sizeGroupSecretKeyBytes,
nullIfEmpty: true nullIfEmpty: true
) )
), ),
@ -134,7 +142,7 @@ internal extension SessionUtil {
authData: (!group.have_auth_data ? nil : authData: (!group.have_auth_data ? nil :
Data( Data(
libSessionVal: group.auth_data, libSessionVal: group.auth_data,
count: ClosedGroup.authDataByteLength(for: .group), count: SessionUtil.sizeGroupAuthDataBytes,
nullIfEmpty: true nullIfEmpty: true
) )
), ),
@ -334,6 +342,7 @@ internal extension SessionUtil {
groupId: admin.groupId, groupId: admin.groupId,
profileId: admin.profileId, profileId: admin.profileId,
role: .standard, role: .standard,
roleStatus: .accepted, // Legacy group members don't have role statuses
isHidden: false isHidden: false
) )
} }
@ -842,6 +851,7 @@ public extension SessionUtil {
groupId: legacyGroupSessionId, groupId: legacyGroupSessionId,
profileId: memberId, profileId: memberId,
role: .standard, role: .standard,
roleStatus: .accepted, // Legacy group members don't have role statuses
isHidden: false isHidden: false
) )
}, },
@ -851,6 +861,7 @@ public extension SessionUtil {
groupId: legacyGroupSessionId, groupId: legacyGroupSessionId,
profileId: memberId, profileId: memberId,
role: .admin, role: .admin,
roleStatus: .accepted, // Legacy group members don't have role statuses
isHidden: false isHidden: false
) )
}, },
@ -891,6 +902,7 @@ public extension SessionUtil {
groupId: legacyGroupSessionId, groupId: legacyGroupSessionId,
profileId: memberId, profileId: memberId,
role: .standard, role: .standard,
roleStatus: .accepted, // Legacy group members don't have role statuses
isHidden: false isHidden: false
) )
}, },
@ -900,6 +912,7 @@ public extension SessionUtil {
groupId: legacyGroupSessionId, groupId: legacyGroupSessionId,
profileId: memberId, profileId: memberId,
role: .admin, role: .admin,
roleStatus: .accepted, // Legacy group members don't have role statuses
isHidden: false isHidden: false
) )
} }

@ -45,7 +45,7 @@ public enum SessionUtil {
// Ensure we have the ed25519 key and that we haven't already loaded the state before // Ensure we have the ed25519 key and that we haven't already loaded the state before
// we continue // we continue
guard guard
let ed25519SecretKey: [UInt8] = Identity.fetchUserEd25519KeyPair(db, using: dependencies)?.secretKey, let ed25519KeyPair: KeyPair = Identity.fetchUserEd25519KeyPair(db, using: dependencies),
dependencies[cache: .sessionUtil].isEmpty dependencies[cache: .sessionUtil].isEmpty
else { return SNLog("[SessionUtil] Ignoring loadState due to existing state") } else { return SNLog("[SessionUtil] Ignoring loadState due to existing state") }
@ -58,11 +58,14 @@ public enum SessionUtil {
.asSet() .asSet()
let missingRequiredVariants: Set<ConfigDump.Variant> = ConfigDump.Variant.userVariants let missingRequiredVariants: Set<ConfigDump.Variant> = ConfigDump.Variant.userVariants
.subtracting(existingDumpVariants) .subtracting(existingDumpVariants)
let groupsByKey: [String: Data] = (try? ClosedGroup let groupsByKey: [String: ClosedGroup] = (try? ClosedGroup
.filter(ids: existingDumps.map { $0.sessionId.hexString }) .filter(ClosedGroup.Columns.threadId.like("\(SessionId.Prefix.group.rawValue)%"))
.fetchAll(db) .fetchAll(db)
.reduce(into: [:]) { result, next in result[next.threadId] = next.groupIdentityPrivateKey }) .reduce(into: [:]) { result, next in result[next.threadId] = next })
.defaulting(to: [:]) .defaulting(to: [:])
let groupsWithNoDumps: [ClosedGroup] = groupsByKey
.values
.filter { group in !existingDumps.contains(where: { $0.sessionId.hexString == group.id }) }
// Create the config records for each dump // Create the config records for each dump
dependencies.mutate(cache: .sessionUtil) { cache in dependencies.mutate(cache: .sessionUtil) { cache in
@ -74,8 +77,10 @@ public enum SessionUtil {
.loadState( .loadState(
for: dump.variant, for: dump.variant,
sessionId: dump.sessionId, sessionId: dump.sessionId,
userEd25519SecretKey: ed25519SecretKey, userEd25519SecretKey: ed25519KeyPair.secretKey,
groupEd25519SecretKey: groupsByKey[dump.sessionId.hexString].map { Array($0) }, groupEd25519SecretKey: groupsByKey[dump.sessionId.hexString]?
.groupIdentityPrivateKey
.map { Array($0) },
cachedData: dump.data, cachedData: dump.data,
cache: cache cache: cache
) )
@ -83,6 +88,8 @@ public enum SessionUtil {
) )
} }
/// It's possible for there to not be dumps for all of the user configs so we load any missing ones to ensure funcitonality
/// works smoothly
missingRequiredVariants.forEach { variant in missingRequiredVariants.forEach { variant in
cache.setConfig( cache.setConfig(
for: variant, for: variant,
@ -91,7 +98,7 @@ public enum SessionUtil {
.loadState( .loadState(
for: variant, for: variant,
sessionId: userSessionId, sessionId: userSessionId,
userEd25519SecretKey: ed25519SecretKey, userEd25519SecretKey: ed25519KeyPair.secretKey,
groupEd25519SecretKey: nil, groupEd25519SecretKey: nil,
cachedData: nil, cachedData: nil,
cache: cache cache: cache
@ -101,6 +108,22 @@ public enum SessionUtil {
} }
} }
/// It's possible for a group to get created but for a dump to not be created (eg. when a crash happens at the right time), to
/// handle this we also load the state of any groups which don't have dumps if they aren't in the `invited` state (those in
/// the `invited` state will have their state loaded if the invite is accepted)
groupsWithNoDumps
.filter { $0.invited != true }
.forEach { group in
_ = try? SessionUtil.createGroupState(
groupSessionId: SessionId(.group, hex: group.id),
userED25519KeyPair: ed25519KeyPair,
groupIdentityPrivateKey: group.groupIdentityPrivateKey,
authData: group.authData,
shouldLoadState: true,
using: dependencies
)
}
SNLog("[SessionUtil] Completed loadState") SNLog("[SessionUtil] Completed loadState")
} }
@ -498,9 +521,9 @@ public enum SessionUtil {
public extension SessionUtil { public extension SessionUtil {
static func parseCommunity(url: String) -> (room: String, server: String, publicKey: String)? { static func parseCommunity(url: String) -> (room: String, server: String, publicKey: String)? {
var cFullUrl: [CChar] = url.cArray.nullTerminated() var cFullUrl: [CChar] = url.cArray.nullTerminated()
var cBaseUrl: [CChar] = [CChar](repeating: 0, count: COMMUNITY_BASE_URL_MAX_LENGTH) var cBaseUrl: [CChar] = [CChar](repeating: 0, count: SessionUtil.sizeMaxCommunityBaseUrlBytes)
var cRoom: [CChar] = [CChar](repeating: 0, count: COMMUNITY_ROOM_MAX_LENGTH) var cRoom: [CChar] = [CChar](repeating: 0, count: SessionUtil.sizeMaxCommunityRoomBytes)
var cPubkey: [UInt8] = [UInt8](repeating: 0, count: OpenGroup.pubkeyByteLength) var cPubkey: [UInt8] = [UInt8](repeating: 0, count: SessionUtil.sizeCommunityPubkeyBytes)
guard guard
community_parse_full_url(&cFullUrl, &cBaseUrl, &cRoom, &cPubkey) && community_parse_full_url(&cFullUrl, &cBaseUrl, &cRoom, &cPubkey) &&

@ -38,11 +38,11 @@ public struct ProfileManager {
// MARK: - Functions // MARK: - Functions
public static func isToLong(profileName: String) -> Bool { public static func isToLong(profileName: String) -> Bool {
return (profileName.utf8CString.count > SessionUtil.libSessionMaxNameByteLength) return (profileName.utf8CString.count > SessionUtil.sizeMaxNameBytes)
} }
public static func isToLong(profileUrl: String) -> Bool { public static func isToLong(profileUrl: String) -> Bool {
return (profileUrl.utf8CString.count > SessionUtil.libSessionMaxProfileUrlByteLength) return (profileUrl.utf8CString.count > SessionUtil.sizeMaxProfileUrlBytes)
} }
public static func profileAvatar( public static func profileAvatar(

Loading…
Cancel
Save