mirror of https://github.com/oxen-io/session-ios
Updated to the latest libSession, fixed a few bugs
Added the logic to sync the last read state for a conversation Added the legacyClosedGroup thread variant Updated the config handling to be able to update the 'mergeResult' and require a dump/push due to local changes Fixed an issue where the name on the CallVC could go off the screen Fixed an issue where OpenGroup info could sometimes incorrectly get deleted Fixed an issue where the ConfirmationModal on a SessionTableViewController wouldn't trigger it's action Fixed an issue where the config handling could incorrectly trigger a contacts update when there were no changespull/856/head
parent
4f8fb63f2c
commit
07046db4b6
@ -0,0 +1,614 @@
|
||||
// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved.
|
||||
|
||||
import Foundation
|
||||
import GRDB
|
||||
import SessionUtil
|
||||
import SessionUtilitiesKit
|
||||
|
||||
internal extension SessionUtil {
|
||||
// MARK: - Incoming Changes
|
||||
|
||||
static func handleConvoInfoVolatileUpdate(
|
||||
_ db: Database,
|
||||
in atomicConf: Atomic<UnsafeMutablePointer<config_object>?>,
|
||||
mergeResult: ConfResult
|
||||
) throws -> ConfResult {
|
||||
guard mergeResult.needsDump else { return mergeResult }
|
||||
guard atomicConf.wrappedValue != nil else { throw SessionUtilError.nilConfigObject }
|
||||
|
||||
// Since we are doing direct memory manipulation we are using an `Atomic` type which has
|
||||
// blocking access in it's `mutate` closure
|
||||
let volatileThreadInfo: [VolatileThreadInfo] = atomicConf.mutate { conf -> [VolatileThreadInfo] in
|
||||
var volatileThreadInfo: [VolatileThreadInfo] = []
|
||||
var oneToOne: convo_info_volatile_1to1 = convo_info_volatile_1to1()
|
||||
var openGroup: convo_info_volatile_open = convo_info_volatile_open()
|
||||
var legacyClosedGroup: convo_info_volatile_legacy_closed = convo_info_volatile_legacy_closed()
|
||||
let convoIterator: OpaquePointer = convo_info_volatile_iterator_new(conf)
|
||||
|
||||
while !convo_info_volatile_iterator_done(convoIterator) {
|
||||
if convo_info_volatile_it_is_1to1(convoIterator, &oneToOne) {
|
||||
let sessionId: String = String(cString: withUnsafeBytes(of: oneToOne.session_id) { [UInt8]($0) }
|
||||
.map { CChar($0) }
|
||||
.nullTerminated()
|
||||
)
|
||||
|
||||
volatileThreadInfo.append(
|
||||
VolatileThreadInfo(
|
||||
threadId: sessionId,
|
||||
variant: .contact,
|
||||
changes: [
|
||||
.markedAsUnread(oneToOne.unread),
|
||||
.lastReadTimestampMs(oneToOne.last_read)
|
||||
]
|
||||
)
|
||||
)
|
||||
}
|
||||
else if convo_info_volatile_it_is_open(convoIterator, &openGroup) {
|
||||
let server: String = String(cString: withUnsafeBytes(of: openGroup.base_url) { [UInt8]($0) }
|
||||
.map { CChar($0) }
|
||||
.nullTerminated()
|
||||
)
|
||||
let roomToken: String = String(cString: withUnsafeBytes(of: openGroup.room) { [UInt8]($0) }
|
||||
.map { CChar($0) }
|
||||
.nullTerminated()
|
||||
)
|
||||
let publicKey: String = String(cString: withUnsafeBytes(of: openGroup.pubkey) { [UInt8]($0) }
|
||||
.map { CChar($0) }
|
||||
.nullTerminated()
|
||||
)
|
||||
|
||||
// Note: A normal 'openGroupId' isn't lowercased but the volatile conversation
|
||||
// info will always be lowercase so we force everything to lowercase to simplify
|
||||
// the code
|
||||
volatileThreadInfo.append(
|
||||
VolatileThreadInfo(
|
||||
threadId: OpenGroup.idFor(roomToken: roomToken, server: server),
|
||||
variant: .openGroup,
|
||||
openGroupUrlInfo: VolatileThreadInfo.OpenGroupUrlInfo(
|
||||
threadId: OpenGroup.idFor(roomToken: roomToken, server: server),
|
||||
server: server,
|
||||
roomToken: roomToken,
|
||||
publicKey: publicKey
|
||||
),
|
||||
changes: [
|
||||
.markedAsUnread(openGroup.unread),
|
||||
.lastReadTimestampMs(openGroup.last_read)
|
||||
]
|
||||
)
|
||||
)
|
||||
}
|
||||
else if convo_info_volatile_it_is_legacy_closed(convoIterator, &legacyClosedGroup) {
|
||||
let groupId: String = String(cString: withUnsafeBytes(of: legacyClosedGroup.group_id) { [UInt8]($0) }
|
||||
.map { CChar($0) }
|
||||
.nullTerminated()
|
||||
)
|
||||
|
||||
volatileThreadInfo.append(
|
||||
VolatileThreadInfo(
|
||||
threadId: groupId,
|
||||
variant: .legacyClosedGroup,
|
||||
changes: [
|
||||
.markedAsUnread(legacyClosedGroup.unread),
|
||||
.lastReadTimestampMs(legacyClosedGroup.last_read)
|
||||
]
|
||||
)
|
||||
)
|
||||
}
|
||||
else {
|
||||
SNLog("Ignoring unknown conversation type when iterating through volatile conversation info update")
|
||||
}
|
||||
|
||||
convo_info_volatile_iterator_advance(convoIterator)
|
||||
}
|
||||
convo_info_volatile_iterator_free(convoIterator) // Need to free the iterator
|
||||
|
||||
return volatileThreadInfo
|
||||
}
|
||||
|
||||
// If we don't have any conversations then no need to continue
|
||||
guard !volatileThreadInfo.isEmpty else { return mergeResult }
|
||||
|
||||
// Get the local volatile thread info from all conversations
|
||||
let localVolatileThreadInfo: [String: VolatileThreadInfo] = VolatileThreadInfo.fetchAll(db)
|
||||
.reduce(into: [:]) { result, next in result[next.threadId] = next }
|
||||
|
||||
// Map the volatileThreadInfo, upserting any changes and returning a list of local changes
|
||||
// which should override any synced changes (eg. 'lastReadTimestampMs')
|
||||
let newerLocalChanges: [VolatileThreadInfo] = try volatileThreadInfo
|
||||
.compactMap { threadInfo -> VolatileThreadInfo? in
|
||||
// Fetch the "proper" threadId (we need the correct casing for updating the database)
|
||||
guard
|
||||
let threadId: String = try? SessionThread
|
||||
.select(.id)
|
||||
.filter(SessionThread.Columns.id.lowercased == threadInfo.threadId)
|
||||
.asRequest(of: String.self)
|
||||
.fetchOne(db)
|
||||
else { return nil }
|
||||
|
||||
|
||||
// Get the existing local state for the thread
|
||||
let localThreadInfo: VolatileThreadInfo? = localVolatileThreadInfo[threadId]
|
||||
|
||||
// Update the thread 'markedAsUnread' state
|
||||
if
|
||||
let markedAsUnread: Bool = threadInfo.changes.markedAsUnread,
|
||||
markedAsUnread != (localThreadInfo?.changes.markedAsUnread ?? false)
|
||||
{
|
||||
try SessionThread
|
||||
.filter(id: threadId)
|
||||
.updateAll( // Handling a config update so don't use `updateAllAndConfig`
|
||||
db,
|
||||
SessionThread.Columns.markedAsUnread.set(to: markedAsUnread)
|
||||
)
|
||||
}
|
||||
|
||||
// If the device has a more recent read interaction then return the info so we can
|
||||
// update the cached config state accordingly
|
||||
guard
|
||||
let lastReadTimestampMs: Int64 = threadInfo.changes.lastReadTimestampMs,
|
||||
lastReadTimestampMs > (localThreadInfo?.changes.lastReadTimestampMs ?? 0)
|
||||
else {
|
||||
// We only want to return the 'lastReadTimestampMs' change, since the local state
|
||||
// should win in that case, so ignore all others
|
||||
return localThreadInfo?
|
||||
.filterChanges { change in
|
||||
switch change {
|
||||
case .lastReadTimestampMs: return true
|
||||
default: return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Mark all older interactions as read
|
||||
try Interaction
|
||||
.filter(
|
||||
Interaction.Columns.threadId == threadId &&
|
||||
Interaction.Columns.timestampMs <= lastReadTimestampMs
|
||||
)
|
||||
.updateAll( // Handling a config update so don't use `updateAllAndConfig`
|
||||
db,
|
||||
Interaction.Columns.wasRead.set(to: true)
|
||||
)
|
||||
return nil
|
||||
}
|
||||
|
||||
// If there are no newer local last read timestamps then just return the mergeResult
|
||||
guard !newerLocalChanges.isEmpty else { return mergeResult }
|
||||
|
||||
return try upsert(
|
||||
convoInfoVolatileChanges: newerLocalChanges,
|
||||
in: atomicConf
|
||||
)
|
||||
}
|
||||
|
||||
static func upsert(
|
||||
convoInfoVolatileChanges: [VolatileThreadInfo],
|
||||
in atomicConf: Atomic<UnsafeMutablePointer<config_object>?>
|
||||
) throws -> ConfResult {
|
||||
// Since we are doing direct memory manipulation we are using an `Atomic` type which has
|
||||
// blocking access in it's `mutate` closure
|
||||
return atomicConf.mutate { conf in
|
||||
convoInfoVolatileChanges.forEach { threadInfo in
|
||||
var cThreadId: [CChar] = threadInfo.cThreadId
|
||||
|
||||
switch threadInfo.variant {
|
||||
case .contact:
|
||||
var oneToOne: convo_info_volatile_1to1 = convo_info_volatile_1to1()
|
||||
|
||||
guard convo_info_volatile_get_or_construct_1to1(conf, &oneToOne, &cThreadId) else {
|
||||
SNLog("Unable to create contact conversation when updating last read timestamp")
|
||||
return
|
||||
}
|
||||
|
||||
threadInfo.changes.forEach { change in
|
||||
switch change {
|
||||
case .lastReadTimestampMs(let lastReadMs):
|
||||
oneToOne.last_read = lastReadMs
|
||||
|
||||
case .markedAsUnread(let unread):
|
||||
oneToOne.unread = unread
|
||||
}
|
||||
}
|
||||
convo_info_volatile_set_1to1(conf, &oneToOne)
|
||||
|
||||
case .legacyClosedGroup:
|
||||
var legacyClosedGroup: convo_info_volatile_legacy_closed = convo_info_volatile_legacy_closed()
|
||||
|
||||
guard convo_info_volatile_get_or_construct_legacy_closed(conf, &legacyClosedGroup, &cThreadId) else {
|
||||
SNLog("Unable to create legacy group conversation when updating last read timestamp")
|
||||
return
|
||||
}
|
||||
|
||||
threadInfo.changes.forEach { change in
|
||||
switch change {
|
||||
case .lastReadTimestampMs(let lastReadMs):
|
||||
legacyClosedGroup.last_read = lastReadMs
|
||||
|
||||
case .markedAsUnread(let unread):
|
||||
legacyClosedGroup.unread = unread
|
||||
}
|
||||
}
|
||||
convo_info_volatile_set_legacy_closed(conf, &legacyClosedGroup)
|
||||
|
||||
case .openGroup:
|
||||
guard
|
||||
var cBaseUrl: [CChar] = threadInfo.cBaseUrl,
|
||||
var cRoomToken: [CChar] = threadInfo.cRoomToken,
|
||||
var cPubkey: [CChar] = threadInfo.cPubkey
|
||||
else {
|
||||
SNLog("Unable to create community conversation when updating last read timestamp due to missing URL info")
|
||||
return
|
||||
}
|
||||
|
||||
var openGroup: convo_info_volatile_open = convo_info_volatile_open()
|
||||
|
||||
guard convo_info_volatile_get_or_construct_open(conf, &openGroup, &cBaseUrl, &cRoomToken, &cPubkey) else {
|
||||
SNLog("Unable to create legacy group conversation when updating last read timestamp")
|
||||
return
|
||||
}
|
||||
|
||||
threadInfo.changes.forEach { change in
|
||||
switch change {
|
||||
case .lastReadTimestampMs(let lastReadMs):
|
||||
openGroup.last_read = lastReadMs
|
||||
|
||||
case .markedAsUnread(let unread):
|
||||
openGroup.unread = unread
|
||||
}
|
||||
}
|
||||
convo_info_volatile_set_open(conf, &openGroup)
|
||||
|
||||
case .closedGroup: return // TODO: Need to add when the type is added to the lib
|
||||
}
|
||||
}
|
||||
|
||||
return ConfResult(
|
||||
needsPush: config_needs_push(conf),
|
||||
needsDump: config_needs_dump(conf)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Convenience
|
||||
|
||||
internal extension SessionUtil {
|
||||
static func updatingThreads<T>(_ db: Database, _ updated: [T]) throws -> [T] {
|
||||
guard let updatedThreads: [SessionThread] = updated as? [SessionThread] else {
|
||||
throw StorageError.generic
|
||||
}
|
||||
|
||||
// If we have no updated threads then no need to continue
|
||||
guard !updatedThreads.isEmpty else { return updated }
|
||||
|
||||
let userPublicKey: String = getUserHexEncodedPublicKey(db)
|
||||
let changes: [VolatileThreadInfo] = try updatedThreads.map { thread in
|
||||
VolatileThreadInfo(
|
||||
threadId: thread.id,
|
||||
variant: thread.variant,
|
||||
openGroupUrlInfo: (thread.variant != .openGroup ? nil :
|
||||
try VolatileThreadInfo.OpenGroupUrlInfo.fetchOne(db, id: thread.id)
|
||||
),
|
||||
changes: [.markedAsUnread(thread.markedAsUnread ?? false)]
|
||||
)
|
||||
}
|
||||
|
||||
db.afterNextTransaction { db in
|
||||
do {
|
||||
let atomicConf: Atomic<UnsafeMutablePointer<config_object>?> = SessionUtil.config(
|
||||
for: .convoInfoVolatile,
|
||||
publicKey: userPublicKey
|
||||
)
|
||||
let result: ConfResult = try upsert(
|
||||
convoInfoVolatileChanges: changes,
|
||||
in: atomicConf
|
||||
)
|
||||
|
||||
// If we don't need to dump the data the we can finish early
|
||||
guard result.needsDump else { return }
|
||||
|
||||
try SessionUtil.saveState(
|
||||
db,
|
||||
keepingExistingMessageHashes: true,
|
||||
configDump: try atomicConf.mutate { conf in
|
||||
try SessionUtil.createDump(
|
||||
conf: conf,
|
||||
for: .convoInfoVolatile,
|
||||
publicKey: userPublicKey,
|
||||
messageHashes: nil
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
catch {
|
||||
SNLog("[libSession-util] Failed to dump updated data")
|
||||
}
|
||||
}
|
||||
|
||||
return updated
|
||||
}
|
||||
|
||||
static func syncThreadLastReadIfNeeded(
|
||||
_ db: Database,
|
||||
threadId: String,
|
||||
threadVariant: SessionThread.Variant,
|
||||
lastReadTimestampMs: Int64
|
||||
) throws {
|
||||
let userPublicKey: String = getUserHexEncodedPublicKey(db)
|
||||
let atomicConf: Atomic<UnsafeMutablePointer<config_object>?> = SessionUtil.config(
|
||||
for: .convoInfoVolatile,
|
||||
publicKey: userPublicKey
|
||||
)
|
||||
let change: VolatileThreadInfo = VolatileThreadInfo(
|
||||
threadId: threadId,
|
||||
variant: threadVariant,
|
||||
openGroupUrlInfo: (threadVariant != .openGroup ? nil :
|
||||
try VolatileThreadInfo.OpenGroupUrlInfo.fetchOne(db, id: threadId)
|
||||
),
|
||||
changes: [.lastReadTimestampMs(lastReadTimestampMs)]
|
||||
)
|
||||
|
||||
// Update the conf
|
||||
let result: ConfResult = try upsert(
|
||||
convoInfoVolatileChanges: [change],
|
||||
in: atomicConf
|
||||
)
|
||||
|
||||
// If we need to dump then do so here
|
||||
if result.needsDump {
|
||||
try SessionUtil.saveState(
|
||||
db,
|
||||
keepingExistingMessageHashes: true,
|
||||
configDump: try atomicConf.mutate { conf in
|
||||
try SessionUtil.createDump(
|
||||
conf: conf,
|
||||
for: .contacts,
|
||||
publicKey: userPublicKey,
|
||||
messageHashes: nil
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// If we need to push then enqueue a 'ConfigurationSyncJob'
|
||||
if result.needsPush {
|
||||
ConfigurationSyncJob.enqueue(db)
|
||||
}
|
||||
}
|
||||
|
||||
static func timestampAlreadyRead(
|
||||
threadId: String,
|
||||
threadVariant: SessionThread.Variant,
|
||||
timestampMs: Int64,
|
||||
userPublicKey: String,
|
||||
openGroup: OpenGroup?
|
||||
) -> Bool {
|
||||
let atomicConf: Atomic<UnsafeMutablePointer<config_object>?> = SessionUtil.config(
|
||||
for: .convoInfoVolatile,
|
||||
publicKey: userPublicKey
|
||||
)
|
||||
|
||||
// If we don't have a config then just assume it's unread
|
||||
guard atomicConf.wrappedValue != nil else { return false }
|
||||
|
||||
// Since we are doing direct memory manipulation we are using an `Atomic` type which has
|
||||
// blocking access in it's `mutate` closure
|
||||
return atomicConf.mutate { conf in
|
||||
switch threadVariant {
|
||||
case .contact:
|
||||
var cThreadId: [CChar] = threadId
|
||||
.bytes
|
||||
.map { CChar(bitPattern: $0) }
|
||||
var oneToOne: convo_info_volatile_1to1 = convo_info_volatile_1to1()
|
||||
guard convo_info_volatile_get_1to1(conf, &oneToOne, &cThreadId) else { return false }
|
||||
|
||||
return (oneToOne.last_read > timestampMs)
|
||||
|
||||
case .legacyClosedGroup:
|
||||
var cThreadId: [CChar] = threadId
|
||||
.bytes
|
||||
.map { CChar(bitPattern: $0) }
|
||||
var legacyClosedGroup: convo_info_volatile_legacy_closed = convo_info_volatile_legacy_closed()
|
||||
|
||||
guard convo_info_volatile_get_legacy_closed(conf, &legacyClosedGroup, &cThreadId) else {
|
||||
return false
|
||||
}
|
||||
|
||||
return (legacyClosedGroup.last_read > timestampMs)
|
||||
|
||||
case .openGroup:
|
||||
guard let openGroup: OpenGroup = openGroup else { return false }
|
||||
|
||||
var cBaseUrl: [CChar] = openGroup.server
|
||||
.bytes
|
||||
.map { CChar(bitPattern: $0) }
|
||||
var cRoomToken: [CChar] = openGroup.roomToken
|
||||
.bytes
|
||||
.map { CChar(bitPattern: $0) }
|
||||
var cPubKey: [CChar] = openGroup.publicKey
|
||||
.bytes
|
||||
.map { CChar(bitPattern: $0) }
|
||||
var convoOpenGroup: convo_info_volatile_open = convo_info_volatile_open()
|
||||
|
||||
guard convo_info_volatile_get_open(conf, &convoOpenGroup, &cBaseUrl, &cRoomToken, &cPubKey) else {
|
||||
return false
|
||||
}
|
||||
|
||||
return (convoOpenGroup.last_read > timestampMs)
|
||||
|
||||
case .closedGroup: return false // TODO: Need to add when the type is added to the lib
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - VolatileThreadInfo
|
||||
|
||||
public extension SessionUtil {
|
||||
struct VolatileThreadInfo {
|
||||
enum Change {
|
||||
case markedAsUnread(Bool)
|
||||
case lastReadTimestampMs(Int64)
|
||||
}
|
||||
|
||||
fileprivate struct OpenGroupUrlInfo: FetchableRecord, Codable, Hashable {
|
||||
let threadId: String
|
||||
let server: String
|
||||
let roomToken: String
|
||||
let publicKey: String
|
||||
|
||||
static func fetchOne(_ db: Database, id: String) throws -> OpenGroupUrlInfo? {
|
||||
return try OpenGroup
|
||||
.filter(id: id)
|
||||
.select(.threadId, .server, .roomToken, .publicKey)
|
||||
.asRequest(of: OpenGroupUrlInfo.self)
|
||||
.fetchOne(db)
|
||||
}
|
||||
}
|
||||
|
||||
let threadId: String
|
||||
let variant: SessionThread.Variant
|
||||
private let openGroupUrlInfo: OpenGroupUrlInfo?
|
||||
let changes: [Change]
|
||||
|
||||
var cThreadId: [CChar] {
|
||||
threadId.bytes.map { CChar(bitPattern: $0) }
|
||||
}
|
||||
var cBaseUrl: [CChar]? {
|
||||
(openGroupUrlInfo?.server).map {
|
||||
$0.bytes.map { CChar(bitPattern: $0) }
|
||||
}
|
||||
}
|
||||
var cRoomToken: [CChar]? {
|
||||
(openGroupUrlInfo?.roomToken).map {
|
||||
$0.bytes.map { CChar(bitPattern: $0) }
|
||||
}
|
||||
}
|
||||
var cPubkey: [CChar]? {
|
||||
(openGroupUrlInfo?.publicKey).map {
|
||||
$0.bytes.map { CChar(bitPattern: $0) }
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate init(
|
||||
threadId: String,
|
||||
variant: SessionThread.Variant,
|
||||
openGroupUrlInfo: OpenGroupUrlInfo? = nil,
|
||||
changes: [Change]
|
||||
) {
|
||||
self.threadId = threadId
|
||||
self.variant = variant
|
||||
self.openGroupUrlInfo = openGroupUrlInfo
|
||||
self.changes = changes
|
||||
}
|
||||
|
||||
// MARK: - Convenience
|
||||
|
||||
func filterChanges(isIncluded: (Change) -> Bool) -> VolatileThreadInfo {
|
||||
return VolatileThreadInfo(
|
||||
threadId: threadId,
|
||||
variant: variant,
|
||||
openGroupUrlInfo: openGroupUrlInfo,
|
||||
changes: changes.filter(isIncluded)
|
||||
)
|
||||
}
|
||||
|
||||
static func fetchAll(_ db: Database? = nil, ids: [String]? = nil) -> [VolatileThreadInfo] {
|
||||
guard let db: Database = db else {
|
||||
return Storage.shared
|
||||
.read { db in fetchAll(db, ids: ids) }
|
||||
.defaulting(to: [])
|
||||
}
|
||||
|
||||
struct FetchedInfo: FetchableRecord, Codable, Hashable {
|
||||
let id: String
|
||||
let variant: SessionThread.Variant
|
||||
let markedAsUnread: Bool?
|
||||
let timestampMs: Int64?
|
||||
let server: String?
|
||||
let roomToken: String?
|
||||
let publicKey: String?
|
||||
}
|
||||
|
||||
let thread: TypedTableAlias<SessionThread> = TypedTableAlias()
|
||||
let interaction: TypedTableAlias<Interaction> = TypedTableAlias()
|
||||
let openGroup: TypedTableAlias<OpenGroup> = TypedTableAlias()
|
||||
let request: SQLRequest<FetchedInfo> = """
|
||||
SELECT
|
||||
\(thread[.id]),
|
||||
\(thread[.variant]),
|
||||
\(thread[.markedAsUnread]),
|
||||
MAX(\(interaction[.timestampMs])),
|
||||
\(openGroup[.server]),
|
||||
\(openGroup[.roomToken]),
|
||||
\(openGroup[.publicKey])
|
||||
|
||||
FROM \(SessionThread.self)
|
||||
LEFT JOIN \(Interaction.self) ON (
|
||||
\(interaction[.threadId]) = \(thread[.id]) AND
|
||||
\(interaction[.wasRead]) = true AND
|
||||
-- Note: Due to the complexity of how call messages are handled and the short
|
||||
-- duration they exist in the swarm, we have decided to exclude trying to
|
||||
-- include them when syncing the read status of conversations (they are also
|
||||
-- implemented differently between platforms so including them could be a
|
||||
-- significant amount of work)
|
||||
\(SQL("\(interaction[.variant]) IN \(Interaction.Variant.variantsToIncrementUnreadCount.filter { $0 != .infoCall })"))
|
||||
)
|
||||
LEFT JOIN \(OpenGroup.self) ON \(openGroup[.threadId]) = \(thread[.id])
|
||||
\(ids == nil ? SQL("") :
|
||||
"WHERE \(SQL("\(thread[.id]) IN \(ids ?? [])"))"
|
||||
)
|
||||
"""
|
||||
|
||||
return ((try? request.fetchAll(db)) ?? [])
|
||||
.map { threadInfo in
|
||||
VolatileThreadInfo(
|
||||
threadId: threadInfo.id,
|
||||
variant: threadInfo.variant,
|
||||
openGroupUrlInfo: {
|
||||
guard
|
||||
let server: String = threadInfo.server,
|
||||
let roomToken: String = threadInfo.roomToken,
|
||||
let publicKey: String = threadInfo.publicKey
|
||||
else { return nil }
|
||||
|
||||
return VolatileThreadInfo.OpenGroupUrlInfo(
|
||||
threadId: threadInfo.id,
|
||||
server: server,
|
||||
roomToken: roomToken,
|
||||
publicKey: publicKey
|
||||
)
|
||||
}(),
|
||||
changes: [
|
||||
.markedAsUnread(threadInfo.markedAsUnread ?? false),
|
||||
.lastReadTimestampMs(threadInfo.timestampMs ?? 0)
|
||||
]
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate extension [SessionUtil.VolatileThreadInfo.Change] {
|
||||
var markedAsUnread: Bool? {
|
||||
for change in self {
|
||||
switch change {
|
||||
case .markedAsUnread(let value): return value
|
||||
default: continue
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
var lastReadTimestampMs: Int64? {
|
||||
for change in self {
|
||||
switch change {
|
||||
case .lastReadTimestampMs(let value): return value
|
||||
default: continue
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
@ -0,0 +1,19 @@
|
||||
// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved.
|
||||
|
||||
import Foundation
|
||||
import GRDB
|
||||
import SessionUtil
|
||||
import SessionUtilitiesKit
|
||||
|
||||
internal extension SessionUtil {
|
||||
// MARK: - Incoming Changes
|
||||
|
||||
static func handleGroupsUpdate(
|
||||
_ db: Database,
|
||||
in atomicConf: Atomic<UnsafeMutablePointer<config_object>?>,
|
||||
mergeResult: ConfResult
|
||||
) throws -> ConfResult {
|
||||
// TODO: This
|
||||
return mergeResult
|
||||
}
|
||||
}
|
Binary file not shown.
Binary file not shown.
@ -0,0 +1,219 @@
|
||||
#pragma once
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#include "base.h"
|
||||
#include "profile_pic.h"
|
||||
|
||||
typedef struct convo_info_volatile_1to1 {
|
||||
char session_id[67]; // in hex; 66 hex chars + null terminator.
|
||||
|
||||
int64_t last_read; // milliseconds since unix epoch
|
||||
bool unread; // true if the conversation is explicitly marked unread
|
||||
} convo_info_volatile_1to1;
|
||||
|
||||
typedef struct convo_info_volatile_open {
|
||||
char base_url[268]; // null-terminated (max length 267), normalized (i.e. always lower-case,
|
||||
// only has port if non-default, has trailing / removed)
|
||||
char room[65]; // null-terminated (max length 64), normalized (always lower-case)
|
||||
unsigned char pubkey[32]; // 32 bytes (not terminated, can contain nulls)
|
||||
|
||||
int64_t last_read; // ms since unix epoch
|
||||
bool unread; // true if marked unread
|
||||
} convo_info_volatile_open;
|
||||
|
||||
typedef struct convo_info_volatile_legacy_closed {
|
||||
char group_id[67]; // in hex; 66 hex chars + null terminator. Looks just like a Session ID,
|
||||
// though isn't really one.
|
||||
|
||||
int64_t last_read; // ms since unix epoch
|
||||
bool unread; // true if marked unread
|
||||
} convo_info_volatile_legacy_closed;
|
||||
|
||||
/// Constructs a conversations config object and sets a pointer to it in `conf`.
|
||||
///
|
||||
/// \param ed25519_secretkey must be the 32-byte secret key seed value. (You can also pass the
|
||||
/// pointer to the beginning of the 64-byte value libsodium calls the "secret key" as the first 32
|
||||
/// bytes of that are the seed). This field cannot be null.
|
||||
///
|
||||
/// \param dump - if non-NULL this restores the state from the dumped byte string produced by a past
|
||||
/// instantiation's call to `dump()`. To construct a new, empty object this should be NULL.
|
||||
///
|
||||
/// \param dumplen - the length of `dump` when restoring from a dump, or 0 when `dump` is NULL.
|
||||
///
|
||||
/// \param error - the pointer to a buffer in which we will write an error string if an error
|
||||
/// occurs; error messages are discarded if this is given as NULL. If non-NULL this must be a
|
||||
/// buffer of at least 256 bytes.
|
||||
///
|
||||
/// Returns 0 on success; returns a non-zero error code and write the exception message as a
|
||||
/// C-string into `error` (if not NULL) on failure.
|
||||
///
|
||||
/// When done with the object the `config_object` must be destroyed by passing the pointer to
|
||||
/// config_free() (in `session/config/base.h`).
|
||||
int convo_info_volatile_init(
|
||||
config_object** conf,
|
||||
const unsigned char* ed25519_secretkey,
|
||||
const unsigned char* dump,
|
||||
size_t dumplen,
|
||||
char* error) __attribute__((warn_unused_result));
|
||||
|
||||
/// Fills `convo` with the conversation info given a session ID (specified as a null-terminated hex
|
||||
/// string), if the conversation exists, and returns true. If the conversation does not exist then
|
||||
/// `convo` is left unchanged and false is returned.
|
||||
bool convo_info_volatile_get_1to1(
|
||||
const config_object* conf, convo_info_volatile_1to1* convo, const char* session_id)
|
||||
__attribute__((warn_unused_result));
|
||||
|
||||
/// Same as the above except that when the conversation does not exist, this sets all the convo
|
||||
/// fields to defaults and loads it with the given session_id.
|
||||
///
|
||||
/// Returns true as long as it is given a valid session_id. A false return is considered an error,
|
||||
/// and means the session_id was not a valid session_id.
|
||||
///
|
||||
/// This is the method that should usually be used to create or update a conversation, followed by
|
||||
/// setting fields in the convo, and then giving it to convo_info_volatile_set().
|
||||
bool convo_info_volatile_get_or_construct_1to1(
|
||||
const config_object* conf, convo_info_volatile_1to1* convo, const char* session_id)
|
||||
__attribute__((warn_unused_result));
|
||||
|
||||
/// open-group versions of the 1-to-1 functions:
|
||||
///
|
||||
/// Gets an open group convo info. `base_url` and `room` are null-terminated c strings; pubkey is
|
||||
/// 32 bytes. base_url and room will always be lower-cased (if not already).
|
||||
bool convo_info_volatile_get_open(
|
||||
const config_object* conf,
|
||||
convo_info_volatile_open* og,
|
||||
const char* base_url,
|
||||
const char* room,
|
||||
unsigned const char* pubkey) __attribute__((warn_unused_result));
|
||||
bool convo_info_volatile_get_or_construct_open(
|
||||
const config_object* conf,
|
||||
convo_info_volatile_open* convo,
|
||||
const char* base_url,
|
||||
const char* room,
|
||||
unsigned const char* pubkey) __attribute__((warn_unused_result));
|
||||
|
||||
/// Fills `convo` with the conversation info given a legacy closed group ID (specified as a
|
||||
/// null-terminated hex string), if the conversation exists, and returns true. If the conversation
|
||||
/// does not exist then `convo` is left unchanged and false is returned.
|
||||
bool convo_info_volatile_get_legacy_closed(
|
||||
const config_object* conf, convo_info_volatile_legacy_closed* convo, const char* id)
|
||||
__attribute__((warn_unused_result));
|
||||
|
||||
/// Same as the above except that when the conversation does not exist, this sets all the convo
|
||||
/// fields to defaults and loads it with the given id.
|
||||
///
|
||||
/// Returns true as long as it is given a valid legacy closed group id (i.e. same format as a
|
||||
/// session id). A false return is considered an error, and means the id was not a valid session
|
||||
/// id.
|
||||
///
|
||||
/// This is the method that should usually be used to create or update a conversation, followed by
|
||||
/// setting fields in the convo, and then giving it to convo_info_volatile_set().
|
||||
bool convo_info_volatile_get_or_construct_legacy_closed(
|
||||
const config_object* conf, convo_info_volatile_legacy_closed* convo, const char* id)
|
||||
__attribute__((warn_unused_result));
|
||||
|
||||
/// Adds or updates a conversation from the given convo info
|
||||
void convo_info_volatile_set_1to1(config_object* conf, const convo_info_volatile_1to1* convo);
|
||||
void convo_info_volatile_set_open(config_object* conf, const convo_info_volatile_open* convo);
|
||||
void convo_info_volatile_set_legacy_closed(
|
||||
config_object* conf, const convo_info_volatile_legacy_closed* convo);
|
||||
|
||||
/// Erases a conversation from the conversation list. Returns true if the conversation was found
|
||||
/// and removed, false if the conversation was not present. You must not call this during
|
||||
/// iteration; see details below.
|
||||
bool convo_info_volatile_erase_1to1(config_object* conf, const char* session_id);
|
||||
bool convo_info_volatile_erase_open(
|
||||
config_object* conf, const char* base_url, const char* room, unsigned const char* pubkey);
|
||||
bool convo_info_volatile_erase_legacy_closed(config_object* conf, const char* group_id);
|
||||
|
||||
/// Returns the number of conversations.
|
||||
size_t convo_info_volatile_size(const config_object* conf);
|
||||
/// Returns the number of conversations of the specific type.
|
||||
size_t convo_info_volatile_size_1to1(const config_object* conf);
|
||||
size_t convo_info_volatile_size_open(const config_object* conf);
|
||||
size_t convo_info_volatile_size_legacy_closed(const config_object* conf);
|
||||
|
||||
/// Functions for iterating through the entire conversation list. Intended use is:
|
||||
///
|
||||
/// convo_info_volatile_1to1 c1;
|
||||
/// convo_info_volatile_open c2;
|
||||
/// convo_info_volatile_legacy_closed c3;
|
||||
/// convo_info_volatile_iterator *it = convo_info_volatile_iterator_new(my_convos);
|
||||
/// for (; !convo_info_volatile_iterator_done(it); convo_info_volatile_iterator_advance(it)) {
|
||||
/// if (convo_info_volatile_it_is_1to1(it, &c1)) {
|
||||
/// // use c1.whatever
|
||||
/// } else if (convo_info_volatile_it_is_open(it, &c2)) {
|
||||
/// // use c2.whatever
|
||||
/// } else if (convo_info_volatile_it_is_legacy_closed(it, &c3)) {
|
||||
/// // use c3.whatever
|
||||
/// }
|
||||
/// }
|
||||
/// convo_info_volatile_iterator_free(it);
|
||||
///
|
||||
/// It is permitted to modify records (e.g. with a call to one of the `convo_info_volatile_set_*`
|
||||
/// functions) and add records while iterating.
|
||||
///
|
||||
/// If you need to remove while iterating then usage is slightly different: you must advance the
|
||||
/// iteration by calling either convo_info_volatile_iterator_advance if not deleting, or
|
||||
/// convo_info_volatile_iterator_erase to erase and advance. Usage looks like this:
|
||||
///
|
||||
/// convo_info_volatile_1to1 c1;
|
||||
/// convo_info_volatile_iterator *it = convo_info_volatile_iterator_new(my_convos);
|
||||
/// while (!convo_info_volatile_iterator_done(it)) {
|
||||
/// if (convo_it_is_1to1(it, &c1)) {
|
||||
/// bool should_delete = /* ... */;
|
||||
/// if (should_delete)
|
||||
/// convo_info_volatile_iterator_erase(it);
|
||||
/// else
|
||||
/// convo_info_volatile_iterator_advance(it);
|
||||
/// }
|
||||
/// }
|
||||
/// convo_info_volatile_iterator_free(it);
|
||||
///
|
||||
|
||||
typedef struct convo_info_volatile_iterator convo_info_volatile_iterator;
|
||||
|
||||
// Starts a new iterator that iterates over all conversations.
|
||||
convo_info_volatile_iterator* convo_info_volatile_iterator_new(const config_object* conf);
|
||||
|
||||
// The same as `convo_info_volatile_iterator_new` except that this iterates *only* over one type of
|
||||
// conversation. You still need to use `convo_info_volatile_it_is_1to1` (or the alternatives) to
|
||||
// load the data in each pass of the loop. (You can, however, safely ignore the bool return value
|
||||
// of the `it_is_whatever` function: it will always be true for the particular type being iterated
|
||||
// over).
|
||||
convo_info_volatile_iterator* convo_info_volatile_iterator_new_1to1(const config_object* conf);
|
||||
convo_info_volatile_iterator* convo_info_volatile_iterator_new_open(const config_object* conf);
|
||||
convo_info_volatile_iterator* convo_info_volatile_iterator_new_legacy_closed(
|
||||
const config_object* conf);
|
||||
|
||||
// Frees an iterator once no longer needed.
|
||||
void convo_info_volatile_iterator_free(convo_info_volatile_iterator* it);
|
||||
|
||||
// Returns true if iteration has reached the end.
|
||||
bool convo_info_volatile_iterator_done(convo_info_volatile_iterator* it);
|
||||
|
||||
// Advances the iterator.
|
||||
void convo_info_volatile_iterator_advance(convo_info_volatile_iterator* it);
|
||||
|
||||
// If the current iterator record is a 1-to-1 conversation this sets the details into `c` and
|
||||
// returns true. Otherwise it returns false.
|
||||
bool convo_info_volatile_it_is_1to1(convo_info_volatile_iterator* it, convo_info_volatile_1to1* c);
|
||||
|
||||
// If the current iterator record is an open group conversation this sets the details into `c` and
|
||||
// returns true. Otherwise it returns false.
|
||||
bool convo_info_volatile_it_is_open(convo_info_volatile_iterator* it, convo_info_volatile_open* c);
|
||||
|
||||
// If the current iterator record is a legacy closed group conversation this sets the details into
|
||||
// `c` and returns true. Otherwise it returns false.
|
||||
bool convo_info_volatile_it_is_legacy_closed(
|
||||
convo_info_volatile_iterator* it, convo_info_volatile_legacy_closed* c);
|
||||
|
||||
// Erases the current convo while advancing the iterator to the next convo in the iteration.
|
||||
void convo_info_volatile_iterator_erase(config_object* conf, convo_info_volatile_iterator* it);
|
||||
|
||||
#ifdef __cplusplus
|
||||
} // extern "C"
|
||||
#endif
|
@ -0,0 +1,380 @@
|
||||
#pragma once
|
||||
|
||||
#include <chrono>
|
||||
#include <cstddef>
|
||||
#include <iterator>
|
||||
#include <memory>
|
||||
#include <session/config.hpp>
|
||||
|
||||
#include "base.hpp"
|
||||
|
||||
extern "C" {
|
||||
struct convo_info_volatile_1to1;
|
||||
struct convo_info_volatile_open;
|
||||
struct convo_info_volatile_legacy_closed;
|
||||
}
|
||||
|
||||
namespace session::config {
|
||||
|
||||
class ConvoInfoVolatile;
|
||||
|
||||
/// keys used in this config, either currently or in the past (so that we don't reuse):
|
||||
///
|
||||
/// Note that this is a high-frequency object, intended only for properties that change frequently (
|
||||
/// (currently just the read timestamp for each conversation).
|
||||
///
|
||||
/// 1 - dict of one-to-one conversations. Each key is the Session ID of the contact (in hex).
|
||||
/// Values are dicts with keys:
|
||||
/// r - the unix timestamp (in integer milliseconds) of the last-read message. Always
|
||||
/// included, but will be 0 if no messages are read.
|
||||
/// u - will be present and set to 1 if this conversation is specifically marked unread.
|
||||
///
|
||||
/// o - open group conversations. Each key is: BASE_URL + '\0' + LC_ROOM_NAME + '\0' +
|
||||
/// SERVER_PUBKEY (in bytes). Note that room name is *always* lower-cased here (so that clients
|
||||
/// with the same room but with different cases will always set the same key). Values are dicts
|
||||
/// with keys:
|
||||
/// r - the unix timestamp (in integer milliseconds) of the last-read message. Always included,
|
||||
/// but will be 0 if no messages are read.
|
||||
/// u - will be present and set to 1 if this conversation is specifically marked unread.
|
||||
///
|
||||
/// C - legacy closed group conversations. The key is the closed group identifier (which looks
|
||||
/// indistinguishable from a Session ID, but isn't really a proper Session ID). Values are
|
||||
/// dicts with keys:
|
||||
/// r - the unix timestamp (integer milliseconds) of the last-read message. Always included,
|
||||
/// but will be 0 if no messages are read.
|
||||
/// u - will be present and set to 1 if this conversation is specifically marked unread.
|
||||
///
|
||||
/// c - reserved for future tracking of new closed group conversations.
|
||||
|
||||
namespace convo {
|
||||
|
||||
struct base {
|
||||
int64_t last_read = 0;
|
||||
bool unread = false;
|
||||
|
||||
protected:
|
||||
void load(const dict& info_dict);
|
||||
};
|
||||
|
||||
struct one_to_one : base {
|
||||
std::string session_id; // in hex
|
||||
|
||||
// Constructs an empty one_to_one from a session_id. Session ID can be either bytes (33) or
|
||||
// hex (66).
|
||||
explicit one_to_one(std::string&& session_id);
|
||||
explicit one_to_one(std::string_view session_id);
|
||||
|
||||
// Internal ctor/method for C API implementations:
|
||||
one_to_one(const struct convo_info_volatile_1to1& c); // From c struct
|
||||
void into(convo_info_volatile_1to1& c) const; // Into c struct
|
||||
|
||||
friend class session::config::ConvoInfoVolatile;
|
||||
};
|
||||
|
||||
struct open_group : base {
|
||||
// 267 = len('https://') + 253 (max valid DNS name length) + len(':XXXXX')
|
||||
static constexpr size_t MAX_URL = 267, MAX_ROOM = 64;
|
||||
|
||||
std::string_view base_url() const; // Accesses the base url (i.e. not including room or
|
||||
// pubkey). Always lower-case.
|
||||
std::string_view room()
|
||||
const; // Accesses the room name, always in lower-case. (Note that the
|
||||
// actual open group info might not be lower-case; it is just in
|
||||
// the open group convo where we force it lower-case).
|
||||
ustring_view pubkey() const; // Accesses the server pubkey (32 bytes).
|
||||
std::string pubkey_hex() const; // Accesses the server pubkey as hex (64 hex digits).
|
||||
|
||||
open_group() = default;
|
||||
|
||||
// Constructs an empty open_group convo struct from url, room, and pubkey. `base_url` and
|
||||
// `room` will be lower-cased if not already (they do not have to be passed lower-case).
|
||||
// pubkey is 32 bytes.
|
||||
open_group(std::string_view base_url, std::string_view room, ustring_view pubkey);
|
||||
|
||||
// Same as above, but takes pubkey as a hex string.
|
||||
open_group(std::string_view base_url, std::string_view room, std::string_view pubkey_hex);
|
||||
|
||||
// Takes a combined room URL (e.g. https://whatever.com/r/Room?public_key=01234....), either
|
||||
// new style (with /r/) or old style (without /r/). Note that the URL gets canonicalized so
|
||||
// the resulting `base_url()` and `room()` values may not be exactly equal to what is given.
|
||||
//
|
||||
// See also `parse_full_url` which does the same thing but returns it in pieces rather than
|
||||
// constructing a new `open_group` object.
|
||||
explicit open_group(std::string_view full_url);
|
||||
|
||||
// Internal ctor/method for C API implementations:
|
||||
open_group(const struct convo_info_volatile_open& c); // From c struct
|
||||
void into(convo_info_volatile_open& c) const; // Into c struct
|
||||
|
||||
// Replaces the baseurl/room/pubkey of this object. Note that changing this and then giving
|
||||
// it to `set` will end up inserting a *new* record but not removing the *old* one (you need
|
||||
// to erase first to do that).
|
||||
void set_server(std::string_view base_url, std::string_view room, ustring_view pubkey);
|
||||
void set_server(
|
||||
std::string_view base_url, std::string_view room, std::string_view pubkey_hex);
|
||||
void set_server(std::string_view full_url);
|
||||
|
||||
// Loads the baseurl/room/pubkey of this object from an encoded key. Throws
|
||||
// std::invalid_argument if the encoded key does not look right.
|
||||
void load_encoded_key(std::string key);
|
||||
|
||||
// Takes a base URL as input and returns it in canonical form. This involves doing things
|
||||
// like lower casing it and removing redundant ports (e.g. :80 when using http://).
|
||||
static std::string canonical_url(std::string_view url);
|
||||
|
||||
// Takes a full room URL, splits it up into canonical url (see above), lower-case room
|
||||
// token, and server pubkey. We take both the deprecated form (e.g.
|
||||
// https://example.com/SomeRoom?public_key=...) and new form
|
||||
// (https://example.com/r/SomeRoom?public_key=...). The public_key is typically specified
|
||||
// in hex (64 digits), but we also accept unpadded base64 (43 chars) and base32z (52 chars)
|
||||
// encodings (for slightly shorter URLs).
|
||||
static std::tuple<std::string, std::string, ustring> parse_full_url(
|
||||
std::string_view full_url);
|
||||
|
||||
private:
|
||||
std::string key;
|
||||
size_t url_size = 0;
|
||||
|
||||
friend class session::config::ConvoInfoVolatile;
|
||||
|
||||
// Returns the key value we use in the stored dict for this open group, i.e.
|
||||
// lc(URL) + lc(NAME) + PUBKEY_BYTES.
|
||||
static std::string make_key(
|
||||
std::string_view base_url, std::string_view room, std::string_view pubkey_hex);
|
||||
static std::string make_key(
|
||||
std::string_view base_url, std::string_view room, ustring_view pubkey);
|
||||
};
|
||||
|
||||
struct legacy_closed_group : base {
|
||||
std::string id; // in hex, indistinguishable from a Session ID
|
||||
|
||||
// Constructs an empty legacy_closed_group from a quasi-session_id
|
||||
explicit legacy_closed_group(std::string&& group_id);
|
||||
explicit legacy_closed_group(std::string_view group_id);
|
||||
|
||||
// Internal ctor/method for C API implementations:
|
||||
legacy_closed_group(const struct convo_info_volatile_legacy_closed& c); // From c struct
|
||||
void into(convo_info_volatile_legacy_closed& c) const; // Into c struct
|
||||
|
||||
private:
|
||||
friend class session::config::ConvoInfoVolatile;
|
||||
};
|
||||
|
||||
using any = std::variant<one_to_one, open_group, legacy_closed_group>;
|
||||
} // namespace convo
|
||||
|
||||
class ConvoInfoVolatile : public ConfigBase {
|
||||
|
||||
public:
|
||||
// No default constructor
|
||||
ConvoInfoVolatile() = delete;
|
||||
|
||||
/// Constructs a conversation list from existing data (stored from `dump()`) and the user's
|
||||
/// secret key for generating the data encryption key. To construct a blank list (i.e. with no
|
||||
/// pre-existing dumped data to load) pass `std::nullopt` as the second argument.
|
||||
///
|
||||
/// \param ed25519_secretkey - contains the libsodium secret key used to encrypt/decrypt the
|
||||
/// data when pushing/pulling from the swarm. This can either be the full 64-byte value (which
|
||||
/// is technically the 32-byte seed followed by the 32-byte pubkey), or just the 32-byte seed of
|
||||
/// the secret key.
|
||||
///
|
||||
/// \param dumped - either `std::nullopt` to construct a new, empty object; or binary state data
|
||||
/// that was previously dumped from an instance of this class by calling `dump()`.
|
||||
ConvoInfoVolatile(ustring_view ed25519_secretkey, std::optional<ustring_view> dumped);
|
||||
|
||||
Namespace storage_namespace() const override { return Namespace::ConvoInfoVolatile; }
|
||||
|
||||
const char* encryption_domain() const override { return "ConvoInfoVolatile"; }
|
||||
|
||||
/// Looks up and returns a contact by session ID (hex). Returns nullopt if the session ID was
|
||||
/// not found, otherwise returns a filled out `convo::one_to_one`.
|
||||
std::optional<convo::one_to_one> get_1to1(std::string_view session_id) const;
|
||||
|
||||
/// Looks up and returns an open group conversation. Takes the base URL, room name (case
|
||||
/// insensitive), and pubkey (in hex). Retuns nullopt if the open group was not found,
|
||||
/// otherwise a filled out `convo::open_group`.
|
||||
std::optional<convo::open_group> get_open(
|
||||
std::string_view base_url, std::string_view room, std::string_view pubkey_hex) const;
|
||||
|
||||
/// Same as above, but takes the pubkey as bytes instead of hex
|
||||
std::optional<convo::open_group> get_open(
|
||||
std::string_view base_url, std::string_view room, ustring_view pubkey) const;
|
||||
|
||||
/// Looks up and returns a legacy closed group conversation by ID. The ID looks like a hex
|
||||
/// Session ID, but isn't really a Session ID. Returns nullopt if there is no record of the
|
||||
/// closed group conversation.
|
||||
std::optional<convo::legacy_closed_group> get_legacy_closed(std::string_view pubkey_hex) const;
|
||||
|
||||
/// These are the same as the above methods (without "_or_construct" in the name), except that
|
||||
/// when the conversation doesn't exist a new one is created, prefilled with the pubkey/url/etc.
|
||||
convo::one_to_one get_or_construct_1to1(std::string_view session_id) const;
|
||||
convo::open_group get_or_construct_open(
|
||||
std::string_view base_url, std::string_view room, std::string_view pubkey_hex) const;
|
||||
convo::open_group get_or_construct_open(
|
||||
std::string_view base_url, std::string_view room, ustring_view pubkey) const;
|
||||
convo::legacy_closed_group get_or_construct_legacy_closed(std::string_view pubkey_hex) const;
|
||||
|
||||
/// Inserts or replaces existing conversation info. For example, to update a 1-to-1
|
||||
/// conversation last read time you would do:
|
||||
///
|
||||
/// auto info = conversations.get_or_construct_1to1(some_session_id);
|
||||
/// info.last_read = new_unix_timestamp;
|
||||
/// conversations.set(info);
|
||||
///
|
||||
void set(const convo::one_to_one& c);
|
||||
void set(const convo::legacy_closed_group& c);
|
||||
void set(const convo::open_group& c);
|
||||
|
||||
void set(const convo::any& c); // Variant which can be any of the above
|
||||
|
||||
protected:
|
||||
void set_base(const convo::base& c, DictFieldProxy& info);
|
||||
|
||||
public:
|
||||
/// Removes a one-to-one conversation. Returns true if found and removed, false if not present.
|
||||
bool erase_1to1(std::string_view pubkey);
|
||||
|
||||
/// Removes an open group conversation record. Returns true if found and removed, false if not
|
||||
/// present. Arguments are the same as `get_open`.
|
||||
bool erase_open(std::string_view base_url, std::string_view room, std::string_view pubkey_hex);
|
||||
bool erase_open(std::string_view base_url, std::string_view room, ustring_view pubkey);
|
||||
|
||||
/// Removes a legacy closed group conversation. Returns true if found and removed, false if not
|
||||
/// present.
|
||||
bool erase_legacy_closed(std::string_view pubkey_hex);
|
||||
|
||||
/// Removes a conversation taking the convo::whatever record (rather than the pubkey/url).
|
||||
bool erase(const convo::one_to_one& c);
|
||||
bool erase(const convo::open_group& c);
|
||||
bool erase(const convo::legacy_closed_group& c);
|
||||
|
||||
bool erase(const convo::any& c); // Variant of any of them
|
||||
|
||||
struct iterator;
|
||||
|
||||
/// This works like erase, but takes an iterator to the conversation to remove. The element is
|
||||
/// removed and the iterator to the next element after the removed one is returned. This is
|
||||
/// intended for use where elements are to be removed during iteration: see below for an
|
||||
/// example.
|
||||
iterator erase(iterator it);
|
||||
|
||||
/// Returns the number of conversations (of any type).
|
||||
size_t size() const;
|
||||
|
||||
/// Returns the number of 1-to-1, open group, and legacy closed group conversations,
|
||||
/// respectively.
|
||||
size_t size_1to1() const;
|
||||
size_t size_open() const;
|
||||
size_t size_legacy_closed() const;
|
||||
|
||||
/// Returns true if the conversation list is empty.
|
||||
bool empty() const { return size() == 0; }
|
||||
|
||||
/// Iterators for iterating through all conversations. Typically you access this implicit via a
|
||||
/// for loop over the `ConvoInfoVolatile` object:
|
||||
///
|
||||
/// for (auto& convo : conversations) {
|
||||
/// if (auto* dm = std::get_if<convo::one_to_one>(&convo)) {
|
||||
/// // use dm->session_id, dm->last_read, etc.
|
||||
/// } else if (auto* og = std::get_if<convo::open_group>(&convo)) {
|
||||
/// // use og->base_url, og->room, om->last_read, etc.
|
||||
/// } else if (auto* lcg = std::get_if<convo::legacy_closed_group>(&convo)) {
|
||||
/// // use lcg->id, lcg->last_read
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// This iterates through all conversations in sorted order (sorted first by convo type, then by
|
||||
/// id within the type).
|
||||
///
|
||||
/// It is permitted to modify and add records while iterating (e.g. by modifying one of the
|
||||
/// `dm`/`og`/`lcg` and then calling set()).
|
||||
///
|
||||
/// If you need to erase the current conversation during iteration then care is required: you
|
||||
/// need to advance the iterator via the iterator version of erase when erasing an element
|
||||
/// rather than incrementing it regularly. For example:
|
||||
///
|
||||
/// for (auto it = conversations.begin(); it != conversations.end(); ) {
|
||||
/// if (should_remove(*it))
|
||||
/// it = converations.erase(it);
|
||||
/// else
|
||||
/// ++it;
|
||||
/// }
|
||||
///
|
||||
/// Alternatively, you can use the first version with two loops: the first loop through all
|
||||
/// converations doesn't erase but just builds a vector of IDs to erase, then the second loops
|
||||
/// through that vector calling `erase_1to1()`/`erase_open()`/`erase_legacy_closed()` for each
|
||||
/// one.
|
||||
///
|
||||
iterator begin() const { return iterator{data}; }
|
||||
iterator end() const { return iterator{}; }
|
||||
|
||||
template <typename ConvoType>
|
||||
struct subtype_iterator;
|
||||
|
||||
/// Returns an iterator that iterates only through one type of conversations
|
||||
subtype_iterator<convo::one_to_one> begin_1to1() const { return {data}; }
|
||||
subtype_iterator<convo::open_group> begin_open() const { return {data}; }
|
||||
subtype_iterator<convo::legacy_closed_group> begin_legacy_closed() const { return {data}; }
|
||||
|
||||
using iterator_category = std::input_iterator_tag;
|
||||
using value_type =
|
||||
std::variant<convo::one_to_one, convo::open_group, convo::legacy_closed_group>;
|
||||
using reference = value_type&;
|
||||
using pointer = value_type*;
|
||||
using difference_type = std::ptrdiff_t;
|
||||
|
||||
struct iterator {
|
||||
protected:
|
||||
std::shared_ptr<convo::any> _val;
|
||||
std::optional<dict::const_iterator> _it_11, _end_11, _it_open, _end_open, _it_lclosed,
|
||||
_end_lclosed;
|
||||
void _load_val();
|
||||
iterator() = default; // Constructs an end tombstone
|
||||
explicit iterator(
|
||||
const DictFieldRoot& data,
|
||||
bool oneto1 = true,
|
||||
bool open = true,
|
||||
bool closed = true);
|
||||
friend class ConvoInfoVolatile;
|
||||
|
||||
public:
|
||||
bool operator==(const iterator& other) const;
|
||||
bool operator!=(const iterator& other) const { return !(*this == other); }
|
||||
bool done() const; // Equivalent to comparing against the end iterator
|
||||
convo::any& operator*() const { return *_val; }
|
||||
convo::any* operator->() const { return _val.get(); }
|
||||
iterator& operator++();
|
||||
iterator operator++(int) {
|
||||
auto copy{*this};
|
||||
++*this;
|
||||
return copy;
|
||||
}
|
||||
};
|
||||
|
||||
template <typename ConvoType>
|
||||
struct subtype_iterator : iterator {
|
||||
protected:
|
||||
subtype_iterator(const DictFieldRoot& data) :
|
||||
iterator(
|
||||
data,
|
||||
std::is_same_v<convo::one_to_one, ConvoType>,
|
||||
std::is_same_v<convo::open_group, ConvoType>,
|
||||
std::is_same_v<convo::legacy_closed_group, ConvoType>) {}
|
||||
friend class ConvoInfoVolatile;
|
||||
|
||||
public:
|
||||
ConvoType& operator*() const { return std::get<ConvoType>(*_val); }
|
||||
ConvoType* operator->() const { return &std::get<ConvoType>(*_val); }
|
||||
subtype_iterator& operator++() {
|
||||
iterator::operator++();
|
||||
return *this;
|
||||
}
|
||||
subtype_iterator operator++(int) {
|
||||
auto copy{*this};
|
||||
++*this;
|
||||
return copy;
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
} // namespace session::config
|
@ -0,0 +1,16 @@
|
||||
#pragma once
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#include <stdbool.h>
|
||||
|
||||
/// Returns true if session_id has the right form (66 hex digits). This is a quick check, not a
|
||||
/// robust one: it does not check the leading byte prefix, nor the cryptographic properties of the
|
||||
/// pubkey for actual validity.
|
||||
bool session_id_is_valid(const char* session_id);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
Loading…
Reference in New Issue