Fixed a few bugs and continued work on fixing unit tests

Fixed a bug where notifications might not work for messages
Fixed a bug where auto-playing audio messages wouldn't update the states correctly
Fixed a bug where a user wouldn't be able to join an open group with blinding enabled
This commit is contained in:
Morgan Pretty 2022-06-21 17:43:27 +10:00
parent 3261f12ea7
commit 153880cf4d
18 changed files with 894 additions and 541 deletions

View File

@ -1150,7 +1150,7 @@ final class ConversationVC: BaseVC, OWSConversationSettingsViewDelegate, Convers
OWSAlerts.showErrorAlert(message: "INVALID_AUDIO_FILE_ALERT_ERROR_MESSAGE".localized()) OWSAlerts.showErrorAlert(message: "INVALID_AUDIO_FILE_ALERT_ERROR_MESSAGE".localized())
return return
} }
// TODO: Looks like the 'play/pause' icon isn't swapping when it auto-plays to the next item)
cell.dynamicUpdate(with: cellViewModel, playbackInfo: updatedInfo) cell.dynamicUpdate(with: cellViewModel, playbackInfo: updatedInfo)
} }
}, },

View File

@ -511,6 +511,11 @@ public class ConversationViewModel: OWSAudioPlayerDelegate {
currentPlayingInteraction.mutate { $0 = viewModel.id } currentPlayingInteraction.mutate { $0 = viewModel.id }
audioPlayer.mutate { [weak self] player in audioPlayer.mutate { [weak self] player in
// Note: We clear the delegate and explicitly set to nil here as when the OWSAudioPlayer
// gets deallocated it triggers state changes which cause UI bugs when auto-playing
player?.delegate = nil
player = nil
let audioPlayer: OWSAudioPlayer = OWSAudioPlayer( let audioPlayer: OWSAudioPlayer = OWSAudioPlayer(
mediaUrl: URL(fileURLWithPath: originalFilePath), mediaUrl: URL(fileURLWithPath: originalFilePath),
audioBehavior: .audioMessagePlayback, audioBehavior: .audioMessagePlayback,
@ -543,7 +548,12 @@ public class ConversationViewModel: OWSAudioPlayerDelegate {
audioPlayer.wrappedValue?.stop() audioPlayer.wrappedValue?.stop()
currentPlayingInteraction.mutate { $0 = nil } currentPlayingInteraction.mutate { $0 = nil }
audioPlayer.mutate { $0 = nil } audioPlayer.mutate {
// Note: We clear the delegate and explicitly set to nil here as when the OWSAudioPlayer
// gets deallocated it triggers state changes which cause UI bugs when auto-playing
$0?.delegate = nil
$0 = nil
}
} }
// MARK: - OWSAudioPlayerDelegate // MARK: - OWSAudioPlayerDelegate
@ -591,7 +601,12 @@ public class ConversationViewModel: OWSAudioPlayerDelegate {
// Clear out the currently playing record // Clear out the currently playing record
currentPlayingInteraction.mutate { $0 = nil } currentPlayingInteraction.mutate { $0 = nil }
audioPlayer.mutate { $0 = nil } audioPlayer.mutate {
// Note: We clear the delegate and explicitly set to nil here as when the OWSAudioPlayer
// gets deallocated it triggers state changes which cause UI bugs when auto-playing
$0?.delegate = nil
$0 = nil
}
// If the next interaction is another voice message then autoplay it // If the next interaction is another voice message then autoplay it
guard guard

View File

@ -200,9 +200,6 @@ final class HomeVC: BaseVC, UITableViewDataSource, UITableViewDelegate, NewConve
DispatchQueue.global(qos: .utility).sync { DispatchQueue.global(qos: .utility).sync {
let _ = IP2Country.shared.populateCacheIfNeeded() let _ = IP2Country.shared.populateCacheIfNeeded()
} }
// Get default open group rooms if needed
// OpenGroupManager.getDefaultRoomsIfNeeded() // TODO: Needed???
} }
override func viewWillAppear(_ animated: Bool) { override func viewWillAppear(_ animated: Bool) {
@ -411,13 +408,8 @@ final class HomeVC: BaseVC, UITableViewDataSource, UITableViewDelegate, NewConve
switch section.model { switch section.model {
case .messageRequests: case .messageRequests:
let hide = UITableViewRowAction(style: .destructive, title: "TXT_HIDE_TITLE".localized()) { [weak self] _, _ in let hide = UITableViewRowAction(style: .destructive, title: "TXT_HIDE_TITLE".localized()) { _, _ in
GRDBStorage.shared.write { db in db[.hasHiddenMessageRequests] = true } GRDBStorage.shared.write { db in db[.hasHiddenMessageRequests] = true }
// Animate the row removal
self?.tableView.beginUpdates()
self?.tableView.deleteRows(at: [indexPath], with: .automatic)
self?.tableView.endUpdates()
} }
hide.backgroundColor = Colors.destructive hide.backgroundColor = Colors.destructive
@ -443,7 +435,7 @@ final class HomeVC: BaseVC, UITableViewDataSource, UITableViewDelegate, NewConve
title: "TXT_DELETE_TITLE".localized(), title: "TXT_DELETE_TITLE".localized(),
style: .destructive style: .destructive
) { _ in ) { _ in
GRDBStorage.shared.write { db in GRDBStorage.shared.writeAsync { db in
switch cellViewModel.threadVariant { switch cellViewModel.threadVariant {
case .closedGroup: case .closedGroup:
try MessageSender try MessageSender
@ -477,7 +469,7 @@ final class HomeVC: BaseVC, UITableViewDataSource, UITableViewDelegate, NewConve
"PIN_BUTTON_TEXT".localized() "PIN_BUTTON_TEXT".localized()
) )
) { _, _ in ) { _, _ in
GRDBStorage.shared.write { db in GRDBStorage.shared.writeAsync { db in
try SessionThread try SessionThread
.filter(id: cellViewModel.threadId) .filter(id: cellViewModel.threadId)
.updateAll(db, SessionThread.Columns.isPinned.set(to: !cellViewModel.threadIsPinned)) .updateAll(db, SessionThread.Columns.isPinned.set(to: !cellViewModel.threadIsPinned))
@ -495,7 +487,7 @@ final class HomeVC: BaseVC, UITableViewDataSource, UITableViewDelegate, NewConve
"BLOCK_LIST_BLOCK_BUTTON".localized() "BLOCK_LIST_BLOCK_BUTTON".localized()
) )
) { _, _ in ) { _, _ in
GRDBStorage.shared.write { db in GRDBStorage.shared.writeAsync { db in
try Contact try Contact
.filter(id: cellViewModel.threadId) .filter(id: cellViewModel.threadId)
.updateAll( .updateAll(

View File

@ -73,9 +73,8 @@ public class HomeViewModel {
let hasViewedSeed: Bool = db[.hasViewedSeed] let hasViewedSeed: Bool = db[.hasViewedSeed]
let userPublicKey: String = getUserHexEncodedPublicKey(db) let userPublicKey: String = getUserHexEncodedPublicKey(db)
let unreadMessageRequestCount: Int = try SessionThread let unreadMessageRequestCount: Int = try SessionThread
.unreadMessageRequestsCountQuery(userPublicKey: userPublicKey) .unreadMessageRequestsQuery(userPublicKey: userPublicKey)
.fetchOne(db) .fetchCount(db)
.defaulting(to: 0)
let finalUnreadMessageRequestCount: Int = (db[.hasHiddenMessageRequests] ? 0 : unreadMessageRequestCount) let finalUnreadMessageRequestCount: Int = (db[.hasHiddenMessageRequests] ? 0 : unreadMessageRequestCount)
let threads: [SessionThreadViewModel] = try SessionThreadViewModel let threads: [SessionThreadViewModel] = try SessionThreadViewModel
.homeQuery(userPublicKey: userPublicKey) .homeQuery(userPublicKey: userPublicKey)

View File

@ -142,10 +142,10 @@ public class NotificationPresenter: NSObject, NotificationsProtocol {
} }
public func notifyUser(_ db: Database, for interaction: Interaction, in thread: SessionThread, isBackgroundPoll: Bool) { public func notifyUser(_ db: Database, for interaction: Interaction, in thread: SessionThread, isBackgroundPoll: Bool) {
guard Date().timeIntervalSince1970 < (thread.mutedUntilTimestamp ?? 0) else { return } guard Date().timeIntervalSince1970 > (thread.mutedUntilTimestamp ?? 0) else { return }
let userPublicKey: String = getUserHexEncodedPublicKey(db) let userPublicKey: String = getUserHexEncodedPublicKey(db)
let isMessageRequest: Bool = thread.isMessageRequest(db) let isMessageRequest: Bool = thread.isMessageRequest(db, includeNonVisible: true)
// If the thread is a message request and the user hasn't hidden message requests then we need // If the thread is a message request and the user hasn't hidden message requests then we need
// to check if this is the only message request thread (group threads can't be message requests // to check if this is the only message request thread (group threads can't be message requests
@ -153,13 +153,13 @@ public class NotificationPresenter: NSObject, NotificationsProtocol {
// notification regardless of how many message requests there are) // notification regardless of how many message requests there are)
if thread.variant == .contact { if thread.variant == .contact {
if isMessageRequest && !db[.hasHiddenMessageRequests] { if isMessageRequest && !db[.hasHiddenMessageRequests] {
let numMessageRequestThreads: Int? = (try? SessionThread let numMessageRequestThreads: Int = (try? SessionThread
.messageRequestsCountQuery(userPublicKey: userPublicKey) .messageRequestsQuery(userPublicKey: userPublicKey, includeNonVisible: true)
.fetchOne(db)) .fetchCount(db))
.defaulting(to: 0) .defaulting(to: 0)
// Allow this to show a notification if there are no message requests (ie. this is the first one) // Allow this to show a notification if there are no message requests (ie. this is the first one)
guard (numMessageRequestThreads ?? 0) == 0 else { return } guard numMessageRequestThreads == 0 else { return }
} }
else if isMessageRequest && db[.hasHiddenMessageRequests] { else if isMessageRequest && db[.hasHiddenMessageRequests] {
// If there are other interactions on this thread already then don't show the notification // If there are other interactions on this thread already then don't show the notification

View File

@ -93,7 +93,6 @@ enum MockDataGenerator {
let cgRandomSeed: Int = 2222 let cgRandomSeed: Int = 2222
let ogRandomSeed: Int = 3333 let ogRandomSeed: Int = 3333
let chunkSize: Int = 1000 // Chunk up the thread writing to prevent memory issues let chunkSize: Int = 1000 // Chunk up the thread writing to prevent memory issues
let openGroupBaseUrl: String = "https://chat.lokinet.dev"
let stringContent: [String] = "abcdefghijklmnopqrstuvwxyz ABCDEFGHIJKLMNOPQRSTUVWXYZ 0123456789 ".map { String($0) } let stringContent: [String] = "abcdefghijklmnopqrstuvwxyz ABCDEFGHIJKLMNOPQRSTUVWXYZ 0123456789 ".map { String($0) }
let wordContent: [String] = ["alias", "consequatur", "aut", "perferendis", "sit", "voluptatem", "accusantium", "doloremque", "aperiam", "eaque", "ipsa", "quae", "ab", "illo", "inventore", "veritatis", "et", "quasi", "architecto", "beatae", "vitae", "dicta", "sunt", "explicabo", "aspernatur", "aut", "odit", "aut", "fugit", "sed", "quia", "consequuntur", "magni", "dolores", "eos", "qui", "ratione", "voluptatem", "sequi", "nesciunt", "neque", "dolorem", "ipsum", "quia", "dolor", "sit", "amet", "consectetur", "adipisci", "velit", "sed", "quia", "non", "numquam", "eius", "modi", "tempora", "incidunt", "ut", "labore", "et", "dolore", "magnam", "aliquam", "quaerat", "voluptatem", "ut", "enim", "ad", "minima", "veniam", "quis", "nostrum", "exercitationem", "ullam", "corporis", "nemo", "enim", "ipsam", "voluptatem", "quia", "voluptas", "sit", "suscipit", "laboriosam", "nisi", "ut", "aliquid", "ex", "ea", "commodi", "consequatur", "quis", "autem", "vel", "eum", "iure", "reprehenderit", "qui", "in", "ea", "voluptate", "velit", "esse", "quam", "nihil", "molestiae", "et", "iusto", "odio", "dignissimos", "ducimus", "qui", "blanditiis", "praesentium", "laudantium", "totam", "rem", "voluptatum", "deleniti", "atque", "corrupti", "quos", "dolores", "et", "quas", "molestias", "excepturi", "sint", "occaecati", "cupiditate", "non", "provident", "sed", "ut", "perspiciatis", "unde", "omnis", "iste", "natus", "error", "similique", "sunt", "in", "culpa", "qui", "officia", "deserunt", "mollitia", "animi", "id", "est", "laborum", "et", "dolorum", "fuga", "et", "harum", "quidem", "rerum", "facilis", "est", "et", "expedita", "distinctio", "nam", "libero", "tempore", "cum", "soluta", "nobis", "est", "eligendi", "optio", "cumque", "nihil", "impedit", "quo", "porro", "quisquam", "est", "qui", "minus", "id", "quod", "maxime", "placeat", "facere", "possimus", "omnis", "voluptas", "assumenda", "est", "omnis", "dolor", "repellendus", "temporibus", "autem", "quibusdam", "et", "aut", "consequatur", "vel", "illum", "qui", "dolorem", "eum", "fugiat", "quo", "voluptas", "nulla", "pariatur", "at", "vero", "eos", "et", "accusamus", "officiis", "debitis", "aut", "rerum", "necessitatibus", "saepe", "eveniet", "ut", "et", "voluptates", "repudiandae", "sint", "et", "molestiae", "non", "recusandae", "itaque", "earum", "rerum", "hic", "tenetur", "a", "sapiente", "delectus", "ut", "aut", "reiciendis", "voluptatibus", "maiores", "doloribus", "asperiores", "repellat"] let wordContent: [String] = ["alias", "consequatur", "aut", "perferendis", "sit", "voluptatem", "accusantium", "doloremque", "aperiam", "eaque", "ipsa", "quae", "ab", "illo", "inventore", "veritatis", "et", "quasi", "architecto", "beatae", "vitae", "dicta", "sunt", "explicabo", "aspernatur", "aut", "odit", "aut", "fugit", "sed", "quia", "consequuntur", "magni", "dolores", "eos", "qui", "ratione", "voluptatem", "sequi", "nesciunt", "neque", "dolorem", "ipsum", "quia", "dolor", "sit", "amet", "consectetur", "adipisci", "velit", "sed", "quia", "non", "numquam", "eius", "modi", "tempora", "incidunt", "ut", "labore", "et", "dolore", "magnam", "aliquam", "quaerat", "voluptatem", "ut", "enim", "ad", "minima", "veniam", "quis", "nostrum", "exercitationem", "ullam", "corporis", "nemo", "enim", "ipsam", "voluptatem", "quia", "voluptas", "sit", "suscipit", "laboriosam", "nisi", "ut", "aliquid", "ex", "ea", "commodi", "consequatur", "quis", "autem", "vel", "eum", "iure", "reprehenderit", "qui", "in", "ea", "voluptate", "velit", "esse", "quam", "nihil", "molestiae", "et", "iusto", "odio", "dignissimos", "ducimus", "qui", "blanditiis", "praesentium", "laudantium", "totam", "rem", "voluptatum", "deleniti", "atque", "corrupti", "quos", "dolores", "et", "quas", "molestias", "excepturi", "sint", "occaecati", "cupiditate", "non", "provident", "sed", "ut", "perspiciatis", "unde", "omnis", "iste", "natus", "error", "similique", "sunt", "in", "culpa", "qui", "officia", "deserunt", "mollitia", "animi", "id", "est", "laborum", "et", "dolorum", "fuga", "et", "harum", "quidem", "rerum", "facilis", "est", "et", "expedita", "distinctio", "nam", "libero", "tempore", "cum", "soluta", "nobis", "est", "eligendi", "optio", "cumque", "nihil", "impedit", "quo", "porro", "quisquam", "est", "qui", "minus", "id", "quod", "maxime", "placeat", "facere", "possimus", "omnis", "voluptas", "assumenda", "est", "omnis", "dolor", "repellendus", "temporibus", "autem", "quibusdam", "et", "aut", "consequatur", "vel", "illum", "qui", "dolorem", "eum", "fugiat", "quo", "voluptas", "nulla", "pariatur", "at", "vero", "eos", "et", "accusamus", "officiis", "debitis", "aut", "rerum", "necessitatibus", "saepe", "eveniet", "ut", "et", "voluptates", "repudiandae", "sint", "et", "molestiae", "non", "recusandae", "itaque", "earum", "rerum", "hic", "tenetur", "a", "sapiente", "delectus", "ut", "aut", "reiciendis", "voluptatibus", "maiores", "doloribus", "asperiores", "repellat"]
let timestampNow: TimeInterval = Date().timeIntervalSince1970 let timestampNow: TimeInterval = Date().timeIntervalSince1970

View File

@ -21,16 +21,12 @@ public enum SMKLegacy {
public static let threadCollection = "TSThread" public static let threadCollection = "TSThread"
internal static let disappearingMessagesCollection = "OWSDisappearingMessagesConfiguration" internal static let disappearingMessagesCollection = "OWSDisappearingMessagesConfiguration"
internal static let closedGroupPublicKeyCollection = "SNClosedGroupPublicKeyCollection"
internal static let closedGroupFormationTimestampCollection = "SNClosedGroupFormationTimestampCollection" internal static let closedGroupFormationTimestampCollection = "SNClosedGroupFormationTimestampCollection"
internal static let closedGroupZombieMembersCollection = "SNClosedGroupZombieMembersCollection" internal static let closedGroupZombieMembersCollection = "SNClosedGroupZombieMembersCollection"
internal static let openGroupCollection = "SNOpenGroupCollection" internal static let openGroupCollection = "SNOpenGroupCollection"
internal static let openGroupUserCountCollection = "SNOpenGroupUserCountCollection" internal static let openGroupUserCountCollection = "SNOpenGroupUserCountCollection"
internal static let openGroupImageCollection = "SNOpenGroupImageCollection" internal static let openGroupImageCollection = "SNOpenGroupImageCollection"
internal static let openGroupLastMessageServerIDCollection = "SNLastMessageServerIDCollection"
internal static let openGroupLastDeletionServerIDCollection = "SNLastDeletionServerIDCollection"
internal static let openGroupServerIdToUniqueIdLookupCollection = "SNOpenGroupServerIdToUniqueIdLookup"
public static let messageDatabaseViewExtensionName = "TSMessageDatabaseViewExtensionName_Monotonic" public static let messageDatabaseViewExtensionName = "TSMessageDatabaseViewExtensionName_Monotonic"
internal static let interactionCollection = "TSInteraction" internal static let interactionCollection = "TSInteraction"

View File

@ -45,8 +45,6 @@ enum _003_YDBToGRDBMigration: Migration {
var openGroupInfo: [String: SMKLegacy._OpenGroup] = [:] var openGroupInfo: [String: SMKLegacy._OpenGroup] = [:]
var openGroupUserCount: [String: Int64] = [:] var openGroupUserCount: [String: Int64] = [:]
var openGroupImage: [String: Data] = [:] var openGroupImage: [String: Data] = [:]
var openGroupLastMessageServerId: [String: Int64] = [:] // Optional
var openGroupLastDeletionServerId: [String: Int64] = [:] // Optional
var interactions: [String: [SMKLegacy._DBInteraction]] = [:] var interactions: [String: [SMKLegacy._DBInteraction]] = [:]
var attachments: [String: SMKLegacy._Attachment] = [:] var attachments: [String: SMKLegacy._Attachment] = [:]
@ -180,8 +178,6 @@ enum _003_YDBToGRDBMigration: Migration {
openGroupInfo[thread.uniqueId] = openGroup openGroupInfo[thread.uniqueId] = openGroup
openGroupUserCount[thread.uniqueId] = ((transaction.object(forKey: openGroup.id, inCollection: SMKLegacy.openGroupUserCountCollection) as? Int64) ?? 0) openGroupUserCount[thread.uniqueId] = ((transaction.object(forKey: openGroup.id, inCollection: SMKLegacy.openGroupUserCountCollection) as? Int64) ?? 0)
openGroupImage[thread.uniqueId] = transaction.object(forKey: openGroup.id, inCollection: SMKLegacy.openGroupImageCollection) as? Data openGroupImage[thread.uniqueId] = transaction.object(forKey: openGroup.id, inCollection: SMKLegacy.openGroupImageCollection) as? Data
openGroupLastMessageServerId[thread.uniqueId] = transaction.object(forKey: openGroup.id, inCollection: SMKLegacy.openGroupLastMessageServerIDCollection) as? Int64
openGroupLastDeletionServerId[thread.uniqueId] = transaction.object(forKey: openGroup.id, inCollection: SMKLegacy.openGroupLastDeletionServerIDCollection) as? Int64
} }
} }
GRDBStorage.shared.update(progress: 0.04, for: self, in: target) GRDBStorage.shared.update(progress: 0.04, for: self, in: target)
@ -787,10 +783,6 @@ enum _003_YDBToGRDBMigration: Migration {
case .mediaSavedNotification: variant = .infoMediaSavedNotification case .mediaSavedNotification: variant = .infoMediaSavedNotification
case .call: variant = .infoCall case .call: variant = .infoCall
case .messageRequestAccepted: variant = .infoMessageRequestAccepted case .messageRequestAccepted: variant = .infoMessageRequestAccepted
@unknown default:
SNLog("[Migration Error] Unsupported info message type")
throw StorageError.migrationFailed
} }
default: default:
@ -1100,8 +1092,6 @@ enum _003_YDBToGRDBMigration: Migration {
openGroupInfo = [:] openGroupInfo = [:]
openGroupUserCount = [:] openGroupUserCount = [:]
openGroupImage = [:] openGroupImage = [:]
openGroupLastMessageServerId = [:]
openGroupLastDeletionServerId = [:]
interactions = [:] interactions = [:]
attachments = [:] attachments = [:]
@ -1507,7 +1497,6 @@ enum _003_YDBToGRDBMigration: Migration {
return (true, nil) return (true, nil)
}() }()
_ = try Attachment( _ = try Attachment(
// Note: The legacy attachment object used a UUID string for it's id as well // Note: The legacy attachment object used a UUID string for it's id as well
// and saved files using these id's so just used the existing id so we don't // and saved files using these id's so just used the existing id so we don't

View File

@ -38,7 +38,7 @@ public struct Capability: Codable, FetchableRecord, PersistableRecord, TableReco
public init(from valueString: String) { public init(from valueString: String) {
let maybeValue: Variant? = Variant.allCases.first { $0.rawValue == valueString } let maybeValue: Variant? = Variant.allCases.first { $0.rawValue == valueString }
self = (maybeValue ?? .unsupported(valueString)) self = (maybeValue ?? .unsupported(valueString))
} }
} }

View File

@ -169,9 +169,9 @@ public extension SessionThread {
return existingThread return existingThread
} }
func isMessageRequest(_ db: Database) -> Bool { func isMessageRequest(_ db: Database, includeNonVisible: Bool = false) -> Bool {
return ( return (
shouldBeVisible && (includeNonVisible || shouldBeVisible) &&
variant == .contact && variant == .contact &&
id != getUserHexEncodedPublicKey(db) && // Note to self id != getUserHexEncodedPublicKey(db) && // Note to self
(try? Contact.fetchOne(db, id: id))?.isApproved != true (try? Contact.fetchOne(db, id: id))?.isApproved != true
@ -182,27 +182,27 @@ public extension SessionThread {
// MARK: - Convenience // MARK: - Convenience
public extension SessionThread { public extension SessionThread {
static func messageRequestsCountQuery(userPublicKey: String) -> SQLRequest<Int> { static func messageRequestsQuery(userPublicKey: String, includeNonVisible: Bool = false) -> SQLRequest<SessionThread> {
let thread: TypedTableAlias<SessionThread> = TypedTableAlias() let thread: TypedTableAlias<SessionThread> = TypedTableAlias()
let contact: TypedTableAlias<Contact> = TypedTableAlias() let contact: TypedTableAlias<Contact> = TypedTableAlias()
return """ return """
SELECT COUNT(\(thread[.id])) SELECT \(thread.allColumns())
FROM \(SessionThread.self) FROM \(SessionThread.self)
LEFT JOIN \(Contact.self) ON \(contact[.id]) = \(thread[.id]) LEFT JOIN \(Contact.self) ON \(contact[.id]) = \(thread[.id])
WHERE ( WHERE (
\(SessionThread.isMessageRequest(userPublicKey: userPublicKey)) \(SessionThread.isMessageRequest(userPublicKey: userPublicKey, includeNonVisible: includeNonVisible))
) )
""" """
} }
static func unreadMessageRequestsCountQuery(userPublicKey: String) -> SQLRequest<Int> { static func unreadMessageRequestsQuery(userPublicKey: String) -> SQLRequest<SessionThread> {
let thread: TypedTableAlias<SessionThread> = TypedTableAlias() let thread: TypedTableAlias<SessionThread> = TypedTableAlias()
let contact: TypedTableAlias<Contact> = TypedTableAlias() let contact: TypedTableAlias<Contact> = TypedTableAlias()
let interaction: TypedTableAlias<Interaction> = TypedTableAlias() let interaction: TypedTableAlias<Interaction> = TypedTableAlias()
return """ return """
SELECT COUNT(\(thread[.id])) SELECT \(thread.allColumns())
FROM \(SessionThread.self) FROM \(SessionThread.self)
JOIN ( JOIN (
SELECT SELECT
@ -225,13 +225,17 @@ public extension SessionThread {
/// ///
/// **Note:** In order to use this filter you **MUST** have a `joining(required/optional:)` to the /// **Note:** In order to use this filter you **MUST** have a `joining(required/optional:)` to the
/// `SessionThread.contact` association or it won't work /// `SessionThread.contact` association or it won't work
static func isMessageRequest(userPublicKey: String) -> SQLSpecificExpressible { static func isMessageRequest(userPublicKey: String, includeNonVisible: Bool = false) -> SQLSpecificExpressible {
let thread: TypedTableAlias<SessionThread> = TypedTableAlias() let thread: TypedTableAlias<SessionThread> = TypedTableAlias()
let contact: TypedTableAlias<Contact> = TypedTableAlias() let contact: TypedTableAlias<Contact> = TypedTableAlias()
let shouldBeVisibleSQL: SQL = (includeNonVisible ?
SQL(stringLiteral: "true") :
SQL("\(thread[.shouldBeVisible]) = true")
)
return SQL( return SQL(
""" """
\(thread[.shouldBeVisible]) = true AND \(shouldBeVisibleSQL) AND
\(SQL("\(thread[.variant]) = \(SessionThread.Variant.contact)")) AND \(SQL("\(thread[.variant]) = \(SessionThread.Variant.contact)")) AND
\(SQL("\(thread[.id]) != \(userPublicKey)")) AND \(SQL("\(thread[.id]) != \(userPublicKey)")) AND
IFNULL(\(contact[.isApproved]), false) = false IFNULL(\(contact[.isApproved]), false) = false

View File

@ -184,6 +184,7 @@ public enum OpenGroupAPI {
_ db: Database, _ db: Database,
server: String, server: String,
requests: [BatchRequestInfoType], requests: [BatchRequestInfoType],
authenticated: Bool = true,
using dependencies: Dependencies = Dependencies() using dependencies: Dependencies = Dependencies()
) -> Promise<[Endpoint: (OnionRequestResponseInfoType, Codable?)]> { ) -> Promise<[Endpoint: (OnionRequestResponseInfoType, Codable?)]> {
let requestBody: BatchRequest = requests.map { $0.toSubRequest() } let requestBody: BatchRequest = requests.map { $0.toSubRequest() }
@ -198,6 +199,7 @@ public enum OpenGroupAPI {
endpoint: Endpoint.sequence, endpoint: Endpoint.sequence,
body: requestBody body: requestBody
), ),
authenticated: authenticated,
using: dependencies using: dependencies
) )
.decoded(as: responseTypes, on: OpenGroupAPI.workQueue, using: dependencies) .decoded(as: responseTypes, on: OpenGroupAPI.workQueue, using: dependencies)
@ -220,7 +222,7 @@ public enum OpenGroupAPI {
/// could return: `{"capabilities": ["sogs", "batch"], "missing": ["magic"]}` /// could return: `{"capabilities": ["sogs", "batch"], "missing": ["magic"]}`
public static func capabilities( public static func capabilities(
_ db: Database, _ db: Database,
on server: String, server: String,
using dependencies: Dependencies = Dependencies() using dependencies: Dependencies = Dependencies()
) -> Promise<(OnionRequestResponseInfoType, Capabilities)> { ) -> Promise<(OnionRequestResponseInfoType, Capabilities)> {
return OpenGroupAPI return OpenGroupAPI
@ -242,7 +244,7 @@ public enum OpenGroupAPI {
/// Rooms to which the user does not have access (e.g. because they are banned, or the room has restricted access permissions) are not included /// Rooms to which the user does not have access (e.g. because they are banned, or the room has restricted access permissions) are not included
public static func rooms( public static func rooms(
_ db: Database, _ db: Database,
for server: String, server: String,
using dependencies: Dependencies = Dependencies() using dependencies: Dependencies = Dependencies()
) -> Promise<(OnionRequestResponseInfoType, [Room])> { ) -> Promise<(OnionRequestResponseInfoType, [Room])> {
return OpenGroupAPI return OpenGroupAPI
@ -315,6 +317,7 @@ public enum OpenGroupAPI {
_ db: Database, _ db: Database,
for roomToken: String, for roomToken: String,
on server: String, on server: String,
authenticated: Bool = true,
using dependencies: Dependencies = Dependencies() using dependencies: Dependencies = Dependencies()
) -> Promise<(capabilities: (info: OnionRequestResponseInfoType, data: Capabilities), room: (info: OnionRequestResponseInfoType, data: Room))> { ) -> Promise<(capabilities: (info: OnionRequestResponseInfoType, data: Capabilities), room: (info: OnionRequestResponseInfoType, data: Room))> {
let requestResponseType: [BatchRequestInfoType] = [ let requestResponseType: [BatchRequestInfoType] = [
@ -342,6 +345,7 @@ public enum OpenGroupAPI {
db, db,
server: server, server: server,
requests: requestResponseType, requests: requestResponseType,
authenticated: authenticated,
using: dependencies using: dependencies
) )
.map { (response: [Endpoint: (OnionRequestResponseInfoType, Codable?)]) -> (capabilities: (OnionRequestResponseInfoType, Capabilities), room: (OnionRequestResponseInfoType, Room)) in .map { (response: [Endpoint: (OnionRequestResponseInfoType, Codable?)]) -> (capabilities: (OnionRequestResponseInfoType, Capabilities), room: (OnionRequestResponseInfoType, Room)) in
@ -1199,6 +1203,7 @@ public enum OpenGroupAPI {
private static func send<T: Encodable>( private static func send<T: Encodable>(
_ db: Database, _ db: Database,
request: Request<T, Endpoint>, request: Request<T, Endpoint>,
authenticated: Bool = true,
using dependencies: Dependencies = Dependencies() using dependencies: Dependencies = Dependencies()
) -> Promise<(OnionRequestResponseInfoType, Data?)> { ) -> Promise<(OnionRequestResponseInfoType, Data?)> {
let urlRequest: URLRequest let urlRequest: URLRequest
@ -1218,6 +1223,11 @@ public enum OpenGroupAPI {
guard let publicKey: String = maybePublicKey else { return Promise(error: Error.noPublicKey) } guard let publicKey: String = maybePublicKey else { return Promise(error: Error.noPublicKey) }
// If we don't want to authenticate the request then send it immediately
guard authenticated else {
return dependencies.onionApi.sendOnionRequest(urlRequest, to: request.server, with: publicKey)
}
// Attempt to sign the request with the new auth // Attempt to sign the request with the new auth
guard let signedRequest: URLRequest = sign(db, request: urlRequest, for: request.server, with: publicKey, using: dependencies) else { guard let signedRequest: URLRequest = sign(db, request: urlRequest, for: request.server, with: publicKey, using: dependencies) else {
return Promise(error: Error.signingFailed) return Promise(error: Error.signingFailed)

View File

@ -195,11 +195,16 @@ public final class OpenGroupManager: NSObject {
OpenGroupAPI.workQueue.async { OpenGroupAPI.workQueue.async {
dependencies.storage dependencies.storage
.writeAsync { db in .writeAsync { db in
// Note: The initial request for room info and it's capabilities should NOT be
// authenticated (this is because if the server requires blinding and the auth
// headers aren't blinded it will error - these endpoints do support unauthenticated
// retrieval so doing so prevents the error)
OpenGroupAPI OpenGroupAPI
.capabilitiesAndRoom( .capabilitiesAndRoom(
db, db,
for: roomToken, for: roomToken,
on: server, on: server,
authenticated: false,
using: dependencies using: dependencies
) )
} }
@ -704,7 +709,7 @@ public final class OpenGroupManager: NSObject {
// Try to retrieve the default rooms 8 times // Try to retrieve the default rooms 8 times
attempt(maxRetryCount: 8, recoveringOn: DispatchQueue.main) { attempt(maxRetryCount: 8, recoveringOn: DispatchQueue.main) {
dependencies.storage.read { db in dependencies.storage.read { db in
OpenGroupAPI.rooms(db, for: OpenGroupAPI.defaultServer, using: dependencies) OpenGroupAPI.rooms(db, server: OpenGroupAPI.defaultServer, using: dependencies)
} }
.map { _, data in data } .map { _, data in data }
} }

View File

@ -37,7 +37,7 @@ typedef NS_ENUM(NSUInteger, OWSAudioBehavior) {
@interface OWSAudioPlayer : NSObject @interface OWSAudioPlayer : NSObject
@property (nonatomic, readonly, weak) id<OWSAudioPlayerDelegate> delegate; @property (nonatomic, weak) id<OWSAudioPlayerDelegate> delegate;
// This property can be used to associate instances of the player with view or model objects. // This property can be used to associate instances of the player with view or model objects.
@property (nonatomic, weak) id owner; @property (nonatomic, weak) id owner;
@property (nonatomic) BOOL isLooping; @property (nonatomic) BOOL isLooping;

File diff suppressed because it is too large Load Diff

View File

@ -113,7 +113,6 @@ class OpenGroupManagerSpec: QuickSpec {
dependencies = OpenGroupManager.OGMDependencies( dependencies = OpenGroupManager.OGMDependencies(
cache: Atomic(mockOGMCache), cache: Atomic(mockOGMCache),
onionApi: TestCapabilitiesAndRoomApi.self, onionApi: TestCapabilitiesAndRoomApi.self,
identityManager: mockIdentityManager,
generalCache: Atomic(mockGeneralCache), generalCache: Atomic(mockGeneralCache),
storage: mockStorage, storage: mockStorage,
sodium: mockSodium, sodium: mockSodium,

View File

@ -2,6 +2,8 @@
import Foundation import Foundation
import Sodium import Sodium
import GRDB
import SessionUtilitiesKit
import Quick import Quick
import Nimble import Nimble
@ -12,7 +14,7 @@ class MessageReceiverDecryptionSpec: QuickSpec {
// MARK: - Spec // MARK: - Spec
override func spec() { override func spec() {
var mockStorage: MockStorage! var mockStorage: GRDBStorage!
var mockSodium: MockSodium! var mockSodium: MockSodium!
var mockBox: MockBox! var mockBox: MockBox!
var mockGenericHash: MockGenericHash! var mockGenericHash: MockGenericHash!
@ -23,7 +25,7 @@ class MessageReceiverDecryptionSpec: QuickSpec {
describe("a MessageReceiver") { describe("a MessageReceiver") {
beforeEach { beforeEach {
mockStorage = MockStorage() mockStorage = GRDBStorage(customWriter: DatabaseQueue())
mockSodium = MockSodium() mockSodium = MockSodium()
mockBox = MockBox() mockBox = MockBox()
mockGenericHash = MockGenericHash() mockGenericHash = MockGenericHash()
@ -45,14 +47,10 @@ class MessageReceiverDecryptionSpec: QuickSpec {
nonceGenerator24: mockNonce24Generator nonceGenerator24: mockNonce24Generator
) )
mockStorage mockStorage.write { db in
.when { $0.getUserED25519KeyPair() } try Identity(variant: .ed25519PublicKey, data: Data(hex: TestConstants.edPublicKey)).insert(db)
.thenReturn( try Identity(variant: .ed25519SecretKey, data: Data(hex: TestConstants.edSecretKey)).insert(db)
Box.KeyPair( }
publicKey: Data(hex: TestConstants.edPublicKey).bytes,
secretKey: Data(hex: TestConstants.edSecretKey).bytes
)
)
mockBox mockBox
.when { .when {
$0.open( $0.open(
@ -109,9 +107,9 @@ class MessageReceiverDecryptionSpec: QuickSpec {
"sFMhE5G4PbRtQFey1hsxLl221Qivc3ayaX2Mm/X89Dl8e45BC+Lb/KU9EdesxIK4pVgYXs9XrMtX3v8" + "sFMhE5G4PbRtQFey1hsxLl221Qivc3ayaX2Mm/X89Dl8e45BC+Lb/KU9EdesxIK4pVgYXs9XrMtX3v8" +
"dt0eBaXneOBfr7qB8pHwwMZjtkOu1ED07T9nszgbWabBphUfWXe2U9K3PTRisSCI=" "dt0eBaXneOBfr7qB8pHwwMZjtkOu1ED07T9nszgbWabBphUfWXe2U9K3PTRisSCI="
)!, )!,
using: try! ECKeyPair( using: Box.KeyPair(
publicKeyData: Data.data(fromHex: TestConstants.publicKey)!, publicKey: Data.data(fromHex: TestConstants.publicKey)!.bytes,
privateKeyData: Data.data(fromHex: TestConstants.privateKey)! secretKey: Data.data(fromHex: TestConstants.privateKey)!.bytes
), ),
dependencies: Dependencies() dependencies: Dependencies()
) )
@ -135,14 +133,14 @@ class MessageReceiverDecryptionSpec: QuickSpec {
expect { expect {
try MessageReceiver.decryptWithSessionProtocol( try MessageReceiver.decryptWithSessionProtocol(
ciphertext: "TestMessage".data(using: .utf8)!, ciphertext: "TestMessage".data(using: .utf8)!,
using: try! ECKeyPair( using: Box.KeyPair(
publicKeyData: Data.data(fromHex: TestConstants.publicKey)!, publicKey: Data.data(fromHex: TestConstants.publicKey)!.bytes,
privateKeyData: Data.data(fromHex: TestConstants.privateKey)! secretKey: Data.data(fromHex: TestConstants.privateKey)!.bytes
), ),
dependencies: dependencies dependencies: dependencies
) )
} }
.to(throwError(MessageReceiver.Error.decryptionFailed)) .to(throwError(MessageReceiverError.decryptionFailed))
} }
it("throws an error if the open message is too short") { it("throws an error if the open message is too short") {
@ -159,14 +157,14 @@ class MessageReceiverDecryptionSpec: QuickSpec {
expect { expect {
try MessageReceiver.decryptWithSessionProtocol( try MessageReceiver.decryptWithSessionProtocol(
ciphertext: "TestMessage".data(using: .utf8)!, ciphertext: "TestMessage".data(using: .utf8)!,
using: try! ECKeyPair( using: Box.KeyPair(
publicKeyData: Data.data(fromHex: TestConstants.publicKey)!, publicKey: Data.data(fromHex: TestConstants.publicKey)!.bytes,
privateKeyData: Data.data(fromHex: TestConstants.privateKey)! secretKey: Data.data(fromHex: TestConstants.privateKey)!.bytes
), ),
dependencies: dependencies dependencies: dependencies
) )
} }
.to(throwError(MessageReceiver.Error.decryptionFailed)) .to(throwError(MessageReceiverError.decryptionFailed))
} }
it("throws an error if it cannot verify the message") { it("throws an error if it cannot verify the message") {
@ -177,14 +175,14 @@ class MessageReceiverDecryptionSpec: QuickSpec {
expect { expect {
try MessageReceiver.decryptWithSessionProtocol( try MessageReceiver.decryptWithSessionProtocol(
ciphertext: "TestMessage".data(using: .utf8)!, ciphertext: "TestMessage".data(using: .utf8)!,
using: try! ECKeyPair( using: Box.KeyPair(
publicKeyData: Data.data(fromHex: TestConstants.publicKey)!, publicKey: Data.data(fromHex: TestConstants.publicKey)!.bytes,
privateKeyData: Data.data(fromHex: TestConstants.privateKey)! secretKey: Data.data(fromHex: TestConstants.privateKey)!.bytes
), ),
dependencies: dependencies dependencies: dependencies
) )
} }
.to(throwError(MessageReceiver.Error.invalidSignature)) .to(throwError(MessageReceiverError.invalidSignature))
} }
it("throws an error if it cannot get the senders x25519 public key") { it("throws an error if it cannot get the senders x25519 public key") {
@ -193,14 +191,14 @@ class MessageReceiverDecryptionSpec: QuickSpec {
expect { expect {
try MessageReceiver.decryptWithSessionProtocol( try MessageReceiver.decryptWithSessionProtocol(
ciphertext: "TestMessage".data(using: .utf8)!, ciphertext: "TestMessage".data(using: .utf8)!,
using: try! ECKeyPair( using: Box.KeyPair(
publicKeyData: Data.data(fromHex: TestConstants.publicKey)!, publicKey: Data.data(fromHex: TestConstants.publicKey)!.bytes,
privateKeyData: Data.data(fromHex: TestConstants.privateKey)! secretKey: Data.data(fromHex: TestConstants.privateKey)!.bytes
), ),
dependencies: dependencies dependencies: dependencies
) )
} }
.to(throwError(MessageReceiver.Error.decryptionFailed)) .to(throwError(MessageReceiverError.decryptionFailed))
} }
} }
@ -263,7 +261,7 @@ class MessageReceiverDecryptionSpec: QuickSpec {
using: dependencies using: dependencies
) )
} }
.to(throwError(MessageReceiver.Error.decryptionFailed)) .to(throwError(MessageReceiverError.decryptionFailed))
} }
it("throws an error if it cannot get the blinded keyPair") { it("throws an error if it cannot get the blinded keyPair") {
@ -288,7 +286,7 @@ class MessageReceiverDecryptionSpec: QuickSpec {
using: dependencies using: dependencies
) )
} }
.to(throwError(MessageReceiver.Error.decryptionFailed)) .to(throwError(MessageReceiverError.decryptionFailed))
} }
it("throws an error if it cannot get the decryption key") { it("throws an error if it cannot get the decryption key") {
@ -321,7 +319,7 @@ class MessageReceiverDecryptionSpec: QuickSpec {
using: dependencies using: dependencies
) )
} }
.to(throwError(MessageReceiver.Error.decryptionFailed)) .to(throwError(MessageReceiverError.decryptionFailed))
} }
it("throws an error if the data version is not 0") { it("throws an error if the data version is not 0") {
@ -342,7 +340,7 @@ class MessageReceiverDecryptionSpec: QuickSpec {
using: dependencies using: dependencies
) )
} }
.to(throwError(MessageReceiver.Error.decryptionFailed)) .to(throwError(MessageReceiverError.decryptionFailed))
} }
it("throws an error if it cannot decrypt the data") { it("throws an error if it cannot decrypt the data") {
@ -367,7 +365,7 @@ class MessageReceiverDecryptionSpec: QuickSpec {
using: dependencies using: dependencies
) )
} }
.to(throwError(MessageReceiver.Error.decryptionFailed)) .to(throwError(MessageReceiverError.decryptionFailed))
} }
it("throws an error if the inner bytes are too short") { it("throws an error if the inner bytes are too short") {
@ -392,7 +390,7 @@ class MessageReceiverDecryptionSpec: QuickSpec {
using: dependencies using: dependencies
) )
} }
.to(throwError(MessageReceiver.Error.decryptionFailed)) .to(throwError(MessageReceiverError.decryptionFailed))
} }
it("throws an error if it cannot generate the blinding factor") { it("throws an error if it cannot generate the blinding factor") {
@ -417,7 +415,7 @@ class MessageReceiverDecryptionSpec: QuickSpec {
using: dependencies using: dependencies
) )
} }
.to(throwError(MessageReceiver.Error.invalidSignature)) .to(throwError(MessageReceiverError.invalidSignature))
} }
it("throws an error if it cannot generate the combined key") { it("throws an error if it cannot generate the combined key") {
@ -442,7 +440,7 @@ class MessageReceiverDecryptionSpec: QuickSpec {
using: dependencies using: dependencies
) )
} }
.to(throwError(MessageReceiver.Error.invalidSignature)) .to(throwError(MessageReceiverError.invalidSignature))
} }
it("throws an error if the combined key does not match kA") { it("throws an error if the combined key does not match kA") {
@ -467,7 +465,7 @@ class MessageReceiverDecryptionSpec: QuickSpec {
using: dependencies using: dependencies
) )
} }
.to(throwError(MessageReceiver.Error.invalidSignature)) .to(throwError(MessageReceiverError.invalidSignature))
} }
it("throws an error if it cannot get the senders x25519 public key") { it("throws an error if it cannot get the senders x25519 public key") {
@ -492,7 +490,7 @@ class MessageReceiverDecryptionSpec: QuickSpec {
using: dependencies using: dependencies
) )
} }
.to(throwError(MessageReceiver.Error.decryptionFailed)) .to(throwError(MessageReceiverError.decryptionFailed))
} }
} }
} }

View File

@ -8,8 +8,8 @@ extension OpenGroup: Mocked {
server: any(), server: any(),
roomToken: any(), roomToken: any(),
publicKey: TestConstants.publicKey, publicKey: TestConstants.publicKey,
name: any(),
isActive: any(), isActive: any(),
name: any(),
roomDescription: any(), roomDescription: any(),
imageId: any(), imageId: any(),
imageData: any(), imageData: any(),
@ -22,7 +22,7 @@ extension OpenGroup: Mocked {
} }
extension VisibleMessage: Mocked { extension VisibleMessage: Mocked {
static var mockValue: VisibleMessage = VisibleMessage() static var mockValue: VisibleMessage = VisibleMessage(text: "")
} }
extension BlindedIdMapping: Mocked { extension BlindedIdMapping: Mocked {

View File

@ -13,7 +13,7 @@ public class NSENotificationPresenter: NSObject, NotificationsProtocol {
guard Date().timeIntervalSince1970 > (thread.mutedUntilTimestamp ?? 0) else { return } guard Date().timeIntervalSince1970 > (thread.mutedUntilTimestamp ?? 0) else { return }
let userPublicKey: String = getUserHexEncodedPublicKey(db) let userPublicKey: String = getUserHexEncodedPublicKey(db)
let isMessageRequest: Bool = thread.isMessageRequest(db) let isMessageRequest: Bool = thread.isMessageRequest(db, includeNonVisible: true)
// If the thread is a message request and the user hasn't hidden message requests then we need // If the thread is a message request and the user hasn't hidden message requests then we need
// to check if this is the only message request thread (group threads can't be message requests // to check if this is the only message request thread (group threads can't be message requests
@ -21,13 +21,13 @@ public class NSENotificationPresenter: NSObject, NotificationsProtocol {
// notification regardless of how many message requests there are) // notification regardless of how many message requests there are)
if thread.variant == .contact { if thread.variant == .contact {
if isMessageRequest && !db[.hasHiddenMessageRequests] { if isMessageRequest && !db[.hasHiddenMessageRequests] {
let numMessageRequestThreads: Int? = (try? SessionThread let numMessageRequestThreads: Int = (try? SessionThread
.messageRequestsCountQuery(userPublicKey: userPublicKey) .messageRequestsQuery(userPublicKey: userPublicKey, includeNonVisible: true)
.fetchOne(db)) .fetchCount(db))
.defaulting(to: 0) .defaulting(to: 0)
// Allow this to show a notification if there are no message requests (ie. this is the first one) // Allow this to show a notification if there are no message requests (ie. this is the first one)
guard (numMessageRequestThreads ?? 0) == 0 else { return } guard numMessageRequestThreads == 0 else { return }
} }
else if isMessageRequest && db[.hasHiddenMessageRequests] { else if isMessageRequest && db[.hasHiddenMessageRequests] {
// If there are other interactions on this thread already then don't show the notification // If there are other interactions on this thread already then don't show the notification