Merge pull request #362 from oxen-io/data-extraction-notifications

Data Extraction Notifications
pull/363/head
Niels Andriesse 4 years ago committed by GitHub
commit 6d179bf918
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -287,6 +287,8 @@
B8F5F54E25EC50A5003BF8D4 /* BlockListUIUtils.h in Headers */ = {isa = PBXBuildFile; fileRef = B8F5F52725EC4F6A003BF8D4 /* BlockListUIUtils.h */; settings = {ATTRIBUTES = (Public, ); }; };
B8F5F56525EC8453003BF8D4 /* Notification+Contacts.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8F5F56425EC8453003BF8D4 /* Notification+Contacts.swift */; };
B8F5F58325EC94A6003BF8D4 /* Collection+Subscripting.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8F5F58225EC94A6003BF8D4 /* Collection+Subscripting.swift */; };
B8F5F60325EDE16F003BF8D4 /* DataExtractionNotification.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8F5F60225EDE16F003BF8D4 /* DataExtractionNotification.swift */; };
B8F5F61B25EDE4BF003BF8D4 /* DataExtractionNotificationInfoMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8F5F61A25EDE4BF003BF8D4 /* DataExtractionNotificationInfoMessage.swift */; };
B8FF8DAE25C0D00F004D1F22 /* SessionMessagingKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C3C2A6F025539DE700C340D1 /* SessionMessagingKit.framework */; };
B8FF8DAF25C0D00F004D1F22 /* SessionUtilitiesKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C3C2A679255388CC00C340D1 /* SessionUtilitiesKit.framework */; };
B8FF8E6225C10DA5004D1F22 /* GeoLite2-Country-Blocks-IPv4 in Resources */ = {isa = PBXBuildFile; fileRef = B8FF8E6125C10DA5004D1F22 /* GeoLite2-Country-Blocks-IPv4 */; };
@ -1283,6 +1285,8 @@
B8F5F52825EC4F8A003BF8D4 /* BlockListUIUtils.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = BlockListUIUtils.m; sourceTree = "<group>"; };
B8F5F56425EC8453003BF8D4 /* Notification+Contacts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Notification+Contacts.swift"; sourceTree = "<group>"; };
B8F5F58225EC94A6003BF8D4 /* Collection+Subscripting.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Collection+Subscripting.swift"; sourceTree = "<group>"; };
B8F5F60225EDE16F003BF8D4 /* DataExtractionNotification.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataExtractionNotification.swift; sourceTree = "<group>"; };
B8F5F61A25EDE4BF003BF8D4 /* DataExtractionNotificationInfoMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataExtractionNotificationInfoMessage.swift; sourceTree = "<group>"; };
B8FF8E6125C10DA5004D1F22 /* GeoLite2-Country-Blocks-IPv4 */ = {isa = PBXFileReference; lastKnownFileType = file.bplist; name = "GeoLite2-Country-Blocks-IPv4"; path = "Countries/GeoLite2-Country-Blocks-IPv4"; sourceTree = "<group>"; };
B8FF8E7325C10FC3004D1F22 /* GeoLite2-Country-Locations-English */ = {isa = PBXFileReference; lastKnownFileType = file.bplist; name = "GeoLite2-Country-Locations-English"; path = "Countries/GeoLite2-Country-Locations-English"; sourceTree = "<group>"; };
B8FF8EA525C11FEF004D1F22 /* IPv4.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IPv4.swift; sourceTree = "<group>"; };
@ -2383,6 +2387,14 @@
path = Shared;
sourceTree = "<group>";
};
B8F5F61925EDE4B0003BF8D4 /* Data Extraction */ = {
isa = PBXGroup;
children = (
B8F5F61A25EDE4BF003BF8D4 /* DataExtractionNotificationInfoMessage.swift */,
);
path = "Data Extraction";
sourceTree = "<group>";
};
B8FF8E6025C10D8B004D1F22 /* Countries */ = {
isa = PBXGroup;
children = (
@ -2424,6 +2436,7 @@
C300A5BC2554B00D00555489 /* ReadReceipt.swift */,
C300A5D22554B05A00555489 /* TypingIndicator.swift */,
C34A977325A3E34A00852C71 /* ClosedGroupControlMessage.swift */,
B8F5F60225EDE16F003BF8D4 /* DataExtractionNotification.swift */,
C300A5E62554B07300555489 /* ExpirationTimerUpdate.swift */,
C3DA9C0625AE7396008F7C7E /* ConfigurationMessage.swift */,
);
@ -2435,6 +2448,7 @@
children = (
C3D9E3B52567685D0040E4F3 /* Attachments */,
C32C5B9E256DC720003C73A2 /* Blocking */,
B8F5F61925EDE4B0003BF8D4 /* Data Extraction */,
C32C5B01256DC054003C73A2 /* Expiration */,
C32C5D22256DD496003C73A2 /* Link Previews */,
C32C5D2D256DD4C4003C73A2 /* Mentions */,
@ -4748,6 +4762,7 @@
C3D9E52725677DF20040E4F3 /* OWSThumbnailService.swift in Sources */,
C32C5E75256DE020003C73A2 /* YapDatabaseTransaction+OWS.m in Sources */,
C3BBE0802554CDD70050F1E3 /* Storage.swift in Sources */,
B8F5F61B25EDE4BF003BF8D4 /* DataExtractionNotificationInfoMessage.swift in Sources */,
C379DCF4256735770002D4EB /* VisibleMessage+Attachment.swift in Sources */,
B8856D34256F1192001CE70E /* Environment.m in Sources */,
B8B320B7258C30D70020074B /* HTMLMetadata.swift in Sources */,
@ -4761,6 +4776,7 @@
C3A3A171256E1D25004D228D /* SSKReachabilityManager.swift in Sources */,
C3A721392558BDFA0043A11F /* OpenGroupAPI.swift in Sources */,
C3A71D0B2558989C0043A11F /* MessageWrapper.swift in Sources */,
B8F5F60325EDE16F003BF8D4 /* DataExtractionNotification.swift in Sources */,
C32C5D24256DD4C0003C73A2 /* MentionsManager.swift in Sources */,
C3A71D1E25589AC30043A11F /* WebSocketProto.swift in Sources */,
C3A721382558BDFA0043A11F /* OpenGroupMessage.swift in Sources */,

@ -487,6 +487,7 @@ extension ConversationVC : InputViewDelegate, MessageCellDelegate, ContextMenuAc
func save(_ viewItem: ConversationViewItem) {
guard viewItem.canSaveMedia() else { return }
viewItem.saveMediaAction()
sendMediaSavedNotificationIfNeeded(with: viewItem.interaction.timestamp)
}
func ban(_ viewItem: ConversationViewItem) {
@ -653,6 +654,31 @@ extension ConversationVC : InputViewDelegate, MessageCellDelegate, ContextMenuAc
audioRecorder?.stop()
audioSession.endAudioActivity(recordVoiceMessageActivity)
}
// MARK: Data Extraction Notifications
@objc func sendScreenshotNotificationIfNeeded() {
// Disabled until other platforms implement it as well
/*
guard thread is TSContactThread else { return }
let message = DataExtractionNotification()
message.kind = .screenshot
Storage.write { transaction in
MessageSender.send(message, in: self.thread, using: transaction)
}
*/
}
func sendMediaSavedNotificationIfNeeded(with timestamp: UInt64) {
// Disabled until other platforms implement it as well
/*
guard thread is TSContactThread else { return }
let message = DataExtractionNotification()
message.kind = .mediaSaved(timestamp: timestamp)
Storage.write { transaction in
MessageSender.send(message, in: self.thread, using: transaction)
}
*/
}
// MARK: Requesting Permission
func requestCameraPermissionIfNeeded() -> Bool {

@ -184,6 +184,7 @@ final class ConversationVC : BaseVC, ConversationViewModelDelegate, OWSConversat
notificationCenter.addObserver(self, selector: #selector(handleAudioDidFinishPlayingNotification(_:)), name: .SNAudioDidFinishPlaying, object: nil)
notificationCenter.addObserver(self, selector: #selector(addOrRemoveBlockedBanner), name: NSNotification.Name(rawValue: kNSNotificationName_BlockListDidChange), object: nil)
notificationCenter.addObserver(self, selector: #selector(handleGroupUpdatedNotification), name: .groupThreadUpdated, object: nil)
notificationCenter.addObserver(self, selector: #selector(sendScreenshotNotificationIfNeeded), name: UIApplication.userDidTakeScreenshotNotification, object: nil)
// Mentions
MentionsManager.populateUserPublicKeyCacheIfNeeded(for: thread.uniqueId!)
// Draft

@ -47,7 +47,7 @@ final class InfoMessageCell : MessageCell {
guard let message = viewItem?.interaction as? TSInfoMessage else { return }
let icon: UIImage?
switch message.messageType {
case .typeDisappearingMessagesUpdate:
case .disappearingMessagesUpdate:
var configuration: OWSDisappearingMessagesConfiguration?
Storage.read { transaction in
configuration = message.thread(with: transaction).disappearingMessagesConfiguration(with: transaction)
@ -57,6 +57,7 @@ final class InfoMessageCell : MessageCell {
} else {
icon = nil
}
case .mediaSavedNotification: icon = UIImage(named: "ic_download")
default: icon = nil
}
if let icon = icon {

@ -15,8 +15,6 @@ public final class ClosedGroupControlMessage : ControlMessage {
// MARK: Kind
public enum Kind : CustomStringConvertible {
case new(publicKey: Data, name: String, encryptionKeyPair: ECKeyPair, members: [Data], admins: [Data])
/// - Note: Deprecated in favor of more explicit group updates.
case update(name: String, members: [Data])
/// An encryption key pair encrypted for each member individually.
///
/// - Note: `publicKey` is only set when an encryption key pair is sent in a one-to-one context (i.e. not in a group).
@ -30,7 +28,6 @@ public final class ClosedGroupControlMessage : ControlMessage {
public var description: String {
switch self {
case .new: return "new"
case .update: return "update"
case .encryptionKeyPair: return "encryptionKeyPair"
case .nameChange: return "nameChange"
case .membersAdded: return "membersAdded"
@ -95,8 +92,6 @@ public final class ClosedGroupControlMessage : ControlMessage {
case .new(let publicKey, let name, let encryptionKeyPair, let members, let admins):
return !publicKey.isEmpty && !name.isEmpty && !encryptionKeyPair.publicKey.isEmpty
&& !encryptionKeyPair.privateKey.isEmpty && !members.isEmpty && !admins.isEmpty
case .update(let name, _):
return !name.isEmpty
case .encryptionKeyPair: return true
case .nameChange(let name): return !name.isEmpty
case .membersAdded(let members): return !members.isEmpty
@ -118,10 +113,6 @@ public final class ClosedGroupControlMessage : ControlMessage {
let members = coder.decodeObject(forKey: "members") as? [Data],
let admins = coder.decodeObject(forKey: "admins") as? [Data] else { return nil }
self.kind = .new(publicKey: publicKey, name: name, encryptionKeyPair: encryptionKeyPair, members: members, admins: admins)
case "update":
guard let name = coder.decodeObject(forKey: "name") as? String,
let members = coder.decodeObject(forKey: "members") as? [Data] else { return nil }
self.kind = .update(name: name, members: members)
case "encryptionKeyPair":
let publicKey = coder.decodeObject(forKey: "publicKey") as? Data
guard let wrappers = coder.decodeObject(forKey: "wrappers") as? [KeyPairWrapper] else { return nil }
@ -154,10 +145,6 @@ public final class ClosedGroupControlMessage : ControlMessage {
coder.encode(encryptionKeyPair, forKey: "encryptionKeyPair")
coder.encode(members, forKey: "members")
coder.encode(admins, forKey: "admins")
case .update(let name, let members):
coder.encode("update", forKey: "kind")
coder.encode(name, forKey: "name")
coder.encode(members, forKey: "members")
case .encryptionKeyPair(let publicKey, let wrappers):
coder.encode("encryptionKeyPair", forKey: "kind")
coder.encode(publicKey, forKey: "publicKey")
@ -194,9 +181,6 @@ public final class ClosedGroupControlMessage : ControlMessage {
SNLog("Couldn't parse key pair.")
return nil
}
case .update:
guard let name = closedGroupControlMessageProto.name else { return nil }
kind = .update(name: name, members: closedGroupControlMessageProto.members)
case .encryptionKeyPair:
let publicKey = closedGroupControlMessageProto.publicKey
let wrappers = closedGroupControlMessageProto.wrappers.compactMap { KeyPairWrapper.fromProto($0) }
@ -237,10 +221,6 @@ public final class ClosedGroupControlMessage : ControlMessage {
}
closedGroupControlMessage.setMembers(members)
closedGroupControlMessage.setAdmins(admins)
case .update(let name, let members):
closedGroupControlMessage = SNProtoDataMessageClosedGroupControlMessage.builder(type: .update)
closedGroupControlMessage.setName(name)
closedGroupControlMessage.setMembers(members)
case .encryptionKeyPair(let publicKey, let wrappers):
closedGroupControlMessage = SNProtoDataMessageClosedGroupControlMessage.builder(type: .encryptionKeyPair)
if let publicKey = publicKey {

@ -0,0 +1,106 @@
import SessionUtilitiesKit
public final class DataExtractionNotification : ControlMessage {
public var kind: Kind?
// MARK: Kind
public enum Kind : CustomStringConvertible {
case screenshot
case mediaSaved(timestamp: UInt64)
public var description: String {
switch self {
case .screenshot: return "screenshot"
case .mediaSaved: return "mediaSaved"
}
}
}
// MARK: Initialization
public override init() { super.init() }
internal init(kind: Kind) {
super.init()
self.kind = kind
}
// MARK: Validation
public override var isValid: Bool {
guard super.isValid, let kind = kind else { return false }
switch kind {
case .screenshot: return true
case .mediaSaved(let timestamp): return timestamp > 0
}
}
// MARK: Coding
public required init?(coder: NSCoder) {
super.init(coder: coder)
guard let rawKind = coder.decodeObject(forKey: "kind") as? String else { return nil }
switch rawKind {
case "screenshot":
self.kind = .screenshot
case "mediaSaved":
guard let timestamp = coder.decodeObject(forKey: "timestamp") as? UInt64 else { return nil }
self.kind = .mediaSaved(timestamp: timestamp)
default: return nil
}
}
public override func encode(with coder: NSCoder) {
super.encode(with: coder)
guard let kind = kind else { return }
switch kind {
case .screenshot:
coder.encode("screenshot", forKey: "kind")
case .mediaSaved(let timestamp):
coder.encode("mediaSaved", forKey: "kind")
coder.encode(timestamp, forKey: "timestamp")
}
}
// MARK: Proto Conversion
public override class func fromProto(_ proto: SNProtoContent) -> DataExtractionNotification? {
guard let dataExtractionNotification = proto.dataExtractionNotification else { return nil }
let kind: Kind
switch dataExtractionNotification.type {
case .screenshot: kind = .screenshot
case .mediaSaved:
let timestamp = dataExtractionNotification.hasTimestamp ? dataExtractionNotification.timestamp : 0
kind = .mediaSaved(timestamp: timestamp)
}
return DataExtractionNotification(kind: kind)
}
public override func toProto(using transaction: YapDatabaseReadWriteTransaction) -> SNProtoContent? {
guard let kind = kind else {
SNLog("Couldn't construct data extraction notification proto from: \(self).")
return nil
}
do {
let dataExtractionNotification: SNProtoDataExtractionNotification.SNProtoDataExtractionNotificationBuilder
switch kind {
case .screenshot:
dataExtractionNotification = SNProtoDataExtractionNotification.builder(type: .screenshot)
case .mediaSaved(let timestamp):
dataExtractionNotification = SNProtoDataExtractionNotification.builder(type: .mediaSaved)
dataExtractionNotification.setTimestamp(timestamp)
}
let contentProto = SNProtoContent.builder()
contentProto.setDataExtractionNotification(try dataExtractionNotification.build())
return try contentProto.build()
} catch {
SNLog("Couldn't construct data extraction notification proto from: \(self).")
return nil
}
}
// MARK: Description
public override var description: String {
"""
DataExtractionNotification(
kind: \(kind?.description ?? "null")
)
"""
}
}

@ -10,20 +10,13 @@ NS_ASSUME_NONNULL_BEGIN
@interface TSInfoMessage : TSMessage <OWSReadTracking>
typedef NS_ENUM(NSInteger, TSInfoMessageType) {
TSInfoMessageTypeSessionDidEnd,
TSInfoMessageUserNotRegistered,
// TSInfoMessageTypeUnsupportedMessage appears to be obsolete.
TSInfoMessageTypeUnsupportedMessage,
TSInfoMessageTypeGroupUpdate,
TSInfoMessageTypeGroupQuit,
TSInfoMessageTypeDisappearingMessagesUpdate,
TSInfoMessageAddToContactsOffer,
TSInfoMessageAddUserToProfileWhitelistOffer,
TSInfoMessageAddGroupToProfileWhitelistOffer
TSInfoMessageTypeScreenshotNotification,
TSInfoMessageTypeMediaSavedNotification
};
+ (instancetype)userNotRegisteredMessageInThread:(TSThread *)thread recipientId:(NSString *)recipientId;
@property (atomic, readonly) TSInfoMessageType messageType;
@property (atomic, readonly, nullable) NSString *customMessage;
@property (atomic, readonly, nullable) NSString *unregisteredRecipientId;

@ -95,15 +95,6 @@ NSUInteger TSInfoMessageSchemaVersion = 1;
return self;
}
+ (instancetype)userNotRegisteredMessageInThread:(TSThread *)thread recipientId:(NSString *)recipientId
{
// MJK TODO - remove senderTimestamp
return [[self alloc] initWithTimestamp:[NSDate millisecondTimestamp]
inThread:thread
messageType:TSInfoMessageUserNotRegistered
unregisteredRecipientId:recipientId];
}
- (OWSInteractionType)interactionType
{
return OWSInteractionType_Info;
@ -112,33 +103,10 @@ NSUInteger TSInfoMessageSchemaVersion = 1;
- (NSString *)previewTextWithTransaction:(YapDatabaseReadTransaction *)transaction
{
switch (_messageType) {
case TSInfoMessageTypeSessionDidEnd:
return NSLocalizedString(@"SECURE_SESSION_RESET", nil);
case TSInfoMessageTypeUnsupportedMessage:
return NSLocalizedString(@"UNSUPPORTED_ATTACHMENT", nil);
case TSInfoMessageUserNotRegistered:
if (self.unregisteredRecipientId.length > 0) {
NSString *recipientName = @"";
return [NSString stringWithFormat:NSLocalizedString(@"ERROR_UNREGISTERED_USER_FORMAT",
@"Format string for 'unregistered user' error. Embeds {{the "
@"unregistered user's name or signal id}}."),
recipientName];
} else {
return NSLocalizedString(@"CONTACT_DETAIL_COMM_TYPE_INSECURE", nil);
}
case TSInfoMessageTypeGroupQuit:
return NSLocalizedString(@"GROUP_YOU_LEFT", nil);
case TSInfoMessageTypeGroupUpdate:
return _customMessage != nil ? _customMessage : NSLocalizedString(@"GROUP_UPDATED", nil);
case TSInfoMessageAddToContactsOffer:
return NSLocalizedString(@"ADD_TO_CONTACTS_OFFER",
@"Message shown in conversation view that offers to add an unknown user to your phone's contacts.");
case TSInfoMessageAddUserToProfileWhitelistOffer:
return NSLocalizedString(@"ADD_USER_TO_PROFILE_WHITELIST_OFFER",
@"Message shown in conversation view that offers to share your profile with a user.");
case TSInfoMessageAddGroupToProfileWhitelistOffer:
return NSLocalizedString(@"ADD_GROUP_TO_PROFILE_WHITELIST_OFFER",
@"Message shown in conversation view that offers to share your profile with a group.");
default:
break;
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

@ -1,10 +1,9 @@
// DO NOT EDIT.
// swift-format-ignore-file
//
// Generated by the Swift generator plugin for the protocol buffer compiler.
// Source: WebSocketResources.proto
//
// For information on using the generated types, please see the documentation:
// For information on using the generated types, please see the documenation:
// https://github.com/apple/swift-protobuf/
//*
@ -21,7 +20,7 @@ import SwiftProtobuf
// If the compiler emits an error on this type, it is because this file
// was generated by a version of the `protoc` Swift plug-in that is
// incompatible with the version of SwiftProtobuf to which you are linking.
// Please ensure that you are building against the same version of the API
// Please ensure that your are building against the same version of the API
// that was used to generate this file.
fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAPIVersionCheck {
struct _2: SwiftProtobuf.ProtobufAPIVersion_2 {}
@ -146,31 +145,31 @@ struct WebSocketProtos_WebSocketMessage {
/// @required
var type: WebSocketProtos_WebSocketMessage.TypeEnum {
get {return _type ?? .unknown}
set {_type = newValue}
get {return _storage._type ?? .unknown}
set {_uniqueStorage()._type = newValue}
}
/// Returns true if `type` has been explicitly set.
var hasType: Bool {return self._type != nil}
var hasType: Bool {return _storage._type != nil}
/// Clears the value of `type`. Subsequent reads from it will return its default value.
mutating func clearType() {self._type = nil}
mutating func clearType() {_uniqueStorage()._type = nil}
var request: WebSocketProtos_WebSocketRequestMessage {
get {return _request ?? WebSocketProtos_WebSocketRequestMessage()}
set {_request = newValue}
get {return _storage._request ?? WebSocketProtos_WebSocketRequestMessage()}
set {_uniqueStorage()._request = newValue}
}
/// Returns true if `request` has been explicitly set.
var hasRequest: Bool {return self._request != nil}
var hasRequest: Bool {return _storage._request != nil}
/// Clears the value of `request`. Subsequent reads from it will return its default value.
mutating func clearRequest() {self._request = nil}
mutating func clearRequest() {_uniqueStorage()._request = nil}
var response: WebSocketProtos_WebSocketResponseMessage {
get {return _response ?? WebSocketProtos_WebSocketResponseMessage()}
set {_response = newValue}
get {return _storage._response ?? WebSocketProtos_WebSocketResponseMessage()}
set {_uniqueStorage()._response = newValue}
}
/// Returns true if `response` has been explicitly set.
var hasResponse: Bool {return self._response != nil}
var hasResponse: Bool {return _storage._response != nil}
/// Clears the value of `response`. Subsequent reads from it will return its default value.
mutating func clearResponse() {self._response = nil}
mutating func clearResponse() {_uniqueStorage()._response = nil}
var unknownFields = SwiftProtobuf.UnknownStorage()
@ -205,9 +204,7 @@ struct WebSocketProtos_WebSocketMessage {
init() {}
fileprivate var _type: WebSocketProtos_WebSocketMessage.TypeEnum? = nil
fileprivate var _request: WebSocketProtos_WebSocketRequestMessage? = nil
fileprivate var _response: WebSocketProtos_WebSocketResponseMessage? = nil
fileprivate var _storage = _StorageClass.defaultInstance
}
#if swift(>=4.2)
@ -336,34 +333,70 @@ extension WebSocketProtos_WebSocketMessage: SwiftProtobuf.Message, SwiftProtobuf
3: .same(proto: "response"),
]
fileprivate class _StorageClass {
var _type: WebSocketProtos_WebSocketMessage.TypeEnum? = nil
var _request: WebSocketProtos_WebSocketRequestMessage? = nil
var _response: WebSocketProtos_WebSocketResponseMessage? = nil
static let defaultInstance = _StorageClass()
private init() {}
init(copying source: _StorageClass) {
_type = source._type
_request = source._request
_response = source._response
}
}
fileprivate mutating func _uniqueStorage() -> _StorageClass {
if !isKnownUniquelyReferenced(&_storage) {
_storage = _StorageClass(copying: _storage)
}
return _storage
}
mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
while let fieldNumber = try decoder.nextFieldNumber() {
switch fieldNumber {
case 1: try decoder.decodeSingularEnumField(value: &self._type)
case 2: try decoder.decodeSingularMessageField(value: &self._request)
case 3: try decoder.decodeSingularMessageField(value: &self._response)
default: break
_ = _uniqueStorage()
try withExtendedLifetime(_storage) { (_storage: _StorageClass) in
while let fieldNumber = try decoder.nextFieldNumber() {
switch fieldNumber {
case 1: try decoder.decodeSingularEnumField(value: &_storage._type)
case 2: try decoder.decodeSingularMessageField(value: &_storage._request)
case 3: try decoder.decodeSingularMessageField(value: &_storage._response)
default: break
}
}
}
}
func traverse<V: SwiftProtobuf.Visitor>(visitor: inout V) throws {
if let v = self._type {
try visitor.visitSingularEnumField(value: v, fieldNumber: 1)
}
if let v = self._request {
try visitor.visitSingularMessageField(value: v, fieldNumber: 2)
}
if let v = self._response {
try visitor.visitSingularMessageField(value: v, fieldNumber: 3)
try withExtendedLifetime(_storage) { (_storage: _StorageClass) in
if let v = _storage._type {
try visitor.visitSingularEnumField(value: v, fieldNumber: 1)
}
if let v = _storage._request {
try visitor.visitSingularMessageField(value: v, fieldNumber: 2)
}
if let v = _storage._response {
try visitor.visitSingularMessageField(value: v, fieldNumber: 3)
}
}
try unknownFields.traverse(visitor: &visitor)
}
static func ==(lhs: WebSocketProtos_WebSocketMessage, rhs: WebSocketProtos_WebSocketMessage) -> Bool {
if lhs._type != rhs._type {return false}
if lhs._request != rhs._request {return false}
if lhs._response != rhs._response {return false}
if lhs._storage !== rhs._storage {
let storagesAreEqual: Bool = withExtendedLifetime((lhs._storage, rhs._storage)) { (_args: (_StorageClass, _StorageClass)) in
let _storage = _args.0
let rhs_storage = _args.1
if _storage._type != rhs_storage._type {return false}
if _storage._request != rhs_storage._request {return false}
if _storage._response != rhs_storage._response {return false}
return true
}
if !storagesAreEqual {return false}
}
if lhs.unknownFields != rhs.unknownFields {return false}
return true
}

@ -7,8 +7,8 @@ package SessionProtos;
message Envelope {
enum Type {
UNIDENTIFIED_SENDER = 6;
CLOSED_GROUP_CIPHERTEXT = 7;
SESSION_MESSAGE = 6;
CLOSED_GROUP_MESSAGE = 7;
}
// @required
@ -16,7 +16,7 @@ message Envelope {
optional string source = 2;
optional uint32 sourceDevice = 7;
// @required
optional uint64 timestamp = 5;
required uint64 timestamp = 5;
optional bytes content = 8;
optional uint64 serverTimestamp = 10;
}
@ -29,23 +29,17 @@ message TypingMessage {
}
// @required
optional uint64 timestamp = 1;
required uint64 timestamp = 1;
// @required
optional Action action = 2;
required Action action = 2;
}
message Content {
optional DataMessage dataMessage = 1;
optional ReceiptMessage receiptMessage = 5;
optional TypingMessage typingMessage = 6;
optional ConfigurationMessage configurationMessage = 7;
}
message ClosedGroupCiphertextMessageWrapper {
// @required
optional bytes ciphertext = 1;
// @required
optional bytes ephemeralPublicKey = 2;
optional DataMessage dataMessage = 1;
optional ReceiptMessage receiptMessage = 5;
optional TypingMessage typingMessage = 6;
optional ConfigurationMessage configurationMessage = 7;
optional DataExtractionNotification dataExtractionNotification = 82;
}
message KeyPair {
@ -55,6 +49,18 @@ message KeyPair {
required bytes privateKey = 2;
}
message DataExtractionNotification {
enum Type {
SCREENSHOT = 1;
MEDIA_SAVED = 2; // timestamp
}
// @required
required Type type = 1;
optional uint64 timestamp = 2;
}
message DataMessage {
enum Flags {
@ -76,87 +82,16 @@ message DataMessage {
}
// @required
optional uint64 id = 1;
required uint64 id = 1;
// @required
optional string author = 2;
required string author = 2;
optional string text = 3;
repeated QuotedAttachment attachments = 4;
}
message Contact {
message Name {
optional string givenName = 1;
optional string familyName = 2;
optional string prefix = 3;
optional string suffix = 4;
optional string middleName = 5;
optional string displayName = 6;
}
message Phone {
enum Type {
HOME = 1;
MOBILE = 2;
WORK = 3;
CUSTOM = 4;
}
optional string value = 1;
optional Type type = 2;
optional string label = 3;
}
message Email {
enum Type {
HOME = 1;
MOBILE = 2;
WORK = 3;
CUSTOM = 4;
}
optional string value = 1;
optional Type type = 2;
optional string label = 3;
}
message PostalAddress {
enum Type {
HOME = 1;
WORK = 2;
CUSTOM = 3;
}
optional Type type = 1;
optional string label = 2;
optional string street = 3;
optional string pobox = 4;
optional string neighborhood = 5;
optional string city = 6;
optional string region = 7;
optional string postcode = 8;
optional string country = 9;
}
message Avatar {
optional AttachmentPointer avatar = 1;
optional bool isProfile = 2;
}
optional Name name = 1;
repeated Phone number = 3;
repeated Email email = 4;
repeated PostalAddress address = 5;
optional Avatar avatar = 6;
optional string organization = 7;
}
message Preview {
// @required
optional string url = 1;
required string url = 1;
optional string title = 2;
optional AttachmentPointer image = 3;
}
@ -170,7 +105,6 @@ message DataMessage {
enum Type {
NEW = 1; // publicKey, name, encryptionKeyPair, members, admins
UPDATE = 2; // name, members
ENCRYPTION_KEY_PAIR = 3; // publicKey, wrappers
NAME_CHANGE = 4; // name
MEMBERS_ADDED = 5; // members
@ -204,7 +138,6 @@ message DataMessage {
optional bytes profileKey = 6;
optional uint64 timestamp = 7;
optional Quote quote = 8;
repeated Contact contact = 9;
repeated Preview preview = 10;
optional LokiProfile profile = 101;
optional ClosedGroupControlMessage closedGroupControlMessage = 104;
@ -246,7 +179,7 @@ message ReceiptMessage {
}
// @required
optional Type type = 1;
required Type type = 1;
repeated uint64 timestamp = 2;
}
@ -257,7 +190,7 @@ message AttachmentPointer {
}
// @required
optional fixed64 id = 1;
required fixed64 id = 1;
optional string contentType = 2;
optional bytes key = 3;
optional uint32 size = 4;
@ -271,26 +204,6 @@ message AttachmentPointer {
optional string url = 101;
}
message ContactDetails {
message Avatar {
optional string contentType = 1;
optional uint32 length = 2;
}
// @required
optional string number = 1;
optional string name = 2;
optional Avatar avatar = 3;
optional string color = 4;
optional bytes profileKey = 6;
optional bool blocked = 7;
optional uint32 expireTimer = 8;
optional string nickname = 101;
}
// -------- Deprecated --------
message GroupContext {
enum Type {

@ -0,0 +1,29 @@
@objc(SNDataExtractionNotificationInfoMessage)
final class DataExtractionNotificationInfoMessage : TSInfoMessage {
init(type: TSInfoMessageType, sentTimestamp: UInt64, thread: TSThread, referencedAttachmentTimestamp: UInt64?) {
super.init(timestamp: sentTimestamp, in: thread, messageType: type)
}
required init(coder: NSCoder) {
super.init(coder: coder)
}
required init(dictionary dictionaryValue: [String:Any]!) throws {
try super.init(dictionary: dictionaryValue)
}
override func previewText(with transaction: YapDatabaseReadTransaction) -> String {
guard let thread = thread as? TSContactThread else { return "" } // Should never occur
let sessionID = thread.contactIdentifier()
let displayName = Storage.shared.getContact(with: sessionID)?.displayName(for: .regular) ?? sessionID
switch messageType {
case .screenshotNotification: return "\(displayName) took a screenshot."
case .mediaSavedNotification:
// TODO: Use referencedAttachmentTimestamp to tell the user * which * media was saved
return "Media saved by \(displayName)."
default: preconditionFailure()
}
}
}

@ -11,6 +11,7 @@ extension MessageReceiver {
case let message as ReadReceipt: handleReadReceipt(message, using: transaction)
case let message as TypingIndicator: handleTypingIndicator(message, using: transaction)
case let message as ClosedGroupControlMessage: handleClosedGroupControlMessage(message, using: transaction)
case let message as DataExtractionNotification: handleDataExtractionNotification(message, using: transaction)
case let message as ExpirationTimerUpdate: handleExpirationTimerUpdate(message, using: transaction)
case let message as ConfigurationMessage: handleConfigurationMessage(message, using: transaction)
case let message as VisibleMessage: try handleVisibleMessage(message, associatedWithProto: proto, openGroupID: openGroupID, isBackgroundPoll: isBackgroundPoll, using: transaction)
@ -104,6 +105,23 @@ extension MessageReceiver {
// MARK: - Data Extraction Notification
private static func handleDataExtractionNotification(_ message: DataExtractionNotification, using transaction: Any) {
let transaction = transaction as! YapDatabaseReadWriteTransaction
guard message.groupPublicKey == nil,
let thread = TSContactThread.getWithContactId(message.sender!, transaction: transaction), case .screenshot = message.kind else { return }
let type: TSInfoMessageType
switch message.kind! {
case .screenshot: type = .screenshotNotification
case .mediaSaved: type = .mediaSavedNotification
}
let message = DataExtractionNotificationInfoMessage(type: type, sentTimestamp: message.sentTimestamp!, thread: thread, referencedAttachmentTimestamp: nil)
message.save(with: transaction)
}
// MARK: - Expiration Timers
private static func handleExpirationTimerUpdate(_ message: ExpirationTimerUpdate, using transaction: Any) {
@ -311,7 +329,6 @@ extension MessageReceiver {
private static func handleClosedGroupControlMessage(_ message: ClosedGroupControlMessage, using transaction: Any) {
switch message.kind! {
case .new: handleNewClosedGroup(message, using: transaction)
case .update: handleClosedGroupUpdated(message, using: transaction) // Deprecated
case .encryptionKeyPair: handleClosedGroupEncryptionKeyPair(message, using: transaction)
case .nameChange: handleClosedGroupNameChanged(message, using: transaction)
case .membersAdded: handleClosedGroupMembersAdded(message, using: transaction)
@ -344,7 +361,7 @@ extension MessageReceiver {
thread = TSGroupThread.getOrCreateThread(with: group, transaction: transaction)
thread.save(with: transaction)
// Notify the user
let infoMessage = TSInfoMessage(timestamp: messageSentTimestamp, in: thread, messageType: .typeGroupUpdate)
let infoMessage = TSInfoMessage(timestamp: messageSentTimestamp, in: thread, messageType: .groupUpdate)
infoMessage.save(with: transaction)
}
// Add the group to the user's set of public keys to poll for
@ -414,7 +431,7 @@ extension MessageReceiver {
// Notify the user if needed
guard name != group.groupName else { return }
let updateInfo = group.getInfoStringAboutUpdate(to: newGroupModel)
let infoMessage = TSInfoMessage(timestamp: message.sentTimestamp!, in: thread, messageType: .typeGroupUpdate, customMessage: updateInfo)
let infoMessage = TSInfoMessage(timestamp: message.sentTimestamp!, in: thread, messageType: .groupUpdate, customMessage: updateInfo)
infoMessage.save(with: transaction)
}
}
@ -437,7 +454,7 @@ extension MessageReceiver {
// Notify the user if needed
guard members != Set(group.groupMemberIds) else { return }
let updateInfo = group.getInfoStringAboutUpdate(to: newGroupModel)
let infoMessage = TSInfoMessage(timestamp: message.sentTimestamp!, in: thread, messageType: .typeGroupUpdate, customMessage: updateInfo)
let infoMessage = TSInfoMessage(timestamp: message.sentTimestamp!, in: thread, messageType: .groupUpdate, customMessage: updateInfo)
infoMessage.save(with: transaction)
}
}
@ -478,7 +495,7 @@ extension MessageReceiver {
thread.setGroupModel(newGroupModel, with: transaction)
// Notify the user if needed
guard members != Set(group.groupMemberIds) else { return }
let infoMessageType: TSInfoMessageType = wasCurrentUserRemoved ? .typeGroupQuit : .typeGroupUpdate
let infoMessageType: TSInfoMessageType = wasCurrentUserRemoved ? .groupQuit : .groupUpdate
let updateInfo = group.getInfoStringAboutUpdate(to: newGroupModel)
let infoMessage = TSInfoMessage(timestamp: message.sentTimestamp!, in: thread, messageType: infoMessageType, customMessage: updateInfo)
infoMessage.save(with: transaction)
@ -518,7 +535,7 @@ extension MessageReceiver {
// Notify the user if needed
guard members != Set(group.groupMemberIds) else { return }
let updateInfo = group.getInfoStringAboutUpdate(to: newGroupModel)
let infoMessage = TSInfoMessage(timestamp: message.sentTimestamp!, in: thread, messageType: .typeGroupUpdate, customMessage: updateInfo)
let infoMessage = TSInfoMessage(timestamp: message.sentTimestamp!, in: thread, messageType: .groupUpdate, customMessage: updateInfo)
infoMessage.save(with: transaction)
}
}
@ -561,68 +578,4 @@ extension MessageReceiver {
// Perform the update
update(groupID, thread, group)
}
// MARK: - Deprecated
/// - Note: Deprecated.
private static func handleClosedGroupUpdated(_ message: ClosedGroupControlMessage, using transaction: Any) {
// Prepare
guard case let .update(name, membersAsData) = message.kind else { return }
let transaction = transaction as! YapDatabaseReadWriteTransaction
// Unwrap the message
guard let groupPublicKey = message.groupPublicKey else { return }
let members = membersAsData.map { $0.toHexString() }
// Get the group
let groupID = LKGroupUtilities.getEncodedClosedGroupIDAsData(groupPublicKey)
let threadID = TSGroupThread.threadId(fromGroupId: groupID)
guard let thread = TSGroupThread.fetch(uniqueId: threadID, transaction: transaction) else {
return SNLog("Ignoring closed group update message for nonexistent group.")
}
let group = thread.groupModel
let oldMembers = group.groupMemberIds
// Check that the message isn't from before the group was created
guard Double(message.sentTimestamp!) > thread.creationDate.timeIntervalSince1970 * 1000 else {
return SNLog("Ignoring closed group update from before thread was created.")
}
// Check that the sender is a member of the group (before the update)
guard Set(group.groupMemberIds).contains(message.sender!) else {
return SNLog("Ignoring closed group update message from non-member.")
}
// Check that the admin wasn't removed unless the group was destroyed entirely
if !members.contains(group.groupAdminIds.first!) && !members.isEmpty {
return SNLog("Ignoring invalid closed group update message.")
}
// Remove the group from the user's set of public keys to poll for if the current user was removed
let userPublicKey = getUserHexEncodedPublicKey()
let wasCurrentUserRemoved = !members.contains(userPublicKey)
if wasCurrentUserRemoved {
Storage.shared.removeClosedGroupPublicKey(groupPublicKey, using: transaction)
// Remove the key pairs
Storage.shared.removeAllClosedGroupEncryptionKeyPairs(for: groupPublicKey, using: transaction)
// Notify the PN server
let _ = PushNotificationAPI.performOperation(.unsubscribe, for: groupPublicKey, publicKey: userPublicKey)
}
// Generate and distribute a new encryption key pair if needed
let wasAnyUserRemoved = (Set(members).intersection(oldMembers) != Set(oldMembers))
let isCurrentUserAdmin = group.groupAdminIds.contains(getUserHexEncodedPublicKey())
if wasAnyUserRemoved && isCurrentUserAdmin {
do {
try MessageSender.generateAndSendNewEncryptionKeyPair(for: groupPublicKey, to: Set(members), using: transaction)
} catch {
SNLog("Couldn't distribute new encryption key pair.")
}
}
// Update the group
let newGroupModel = TSGroupModel(title: name, memberIds: members, image: nil, groupId: groupID, groupType: .closedGroup, adminIds: group.groupAdminIds)
thread.setGroupModel(newGroupModel, with: transaction)
// Notify the user if needed
if Set(members) != Set(oldMembers) || name != group.groupName {
let infoMessageType: TSInfoMessageType = wasCurrentUserRemoved ? .typeGroupQuit : .typeGroupUpdate
let updateInfo = group.getInfoStringAboutUpdate(to: newGroupModel)
let infoMessage = TSInfoMessage(timestamp: NSDate.ows_millisecondTimeStamp(), in: thread, messageType: infoMessageType, customMessage: updateInfo)
infoMessage.save(with: transaction)
}
}
}

@ -68,10 +68,10 @@ public enum MessageReceiver {
(plaintext, sender) = (envelope.content!, envelope.source!)
} else {
switch envelope.type {
case .unidentifiedSender:
case .sessionMessage:
guard let userX25519KeyPair = SNMessagingKitConfiguration.shared.storage.getUserKeyPair() else { throw Error.noUserX25519KeyPair }
(plaintext, sender) = try decryptWithSessionProtocol(ciphertext: ciphertext, using: userX25519KeyPair)
case .closedGroupCiphertext:
case .closedGroupMessage:
guard let hexEncodedGroupPublicKey = envelope.source, SNMessagingKitConfiguration.shared.storage.isClosedGroup(hexEncodedGroupPublicKey) else { throw Error.invalidGroupPublicKey }
var encryptionKeyPairs = Storage.shared.getClosedGroupEncryptionKeyPairs(for: hexEncodedGroupPublicKey)
guard !encryptionKeyPairs.isEmpty else { throw Error.noGroupKeyPair }
@ -126,6 +126,7 @@ public enum MessageReceiver {
if let readReceipt = ReadReceipt.fromProto(proto) { return readReceipt }
if let typingIndicator = TypingIndicator.fromProto(proto) { return typingIndicator }
if let closedGroupControlMessage = ClosedGroupControlMessage.fromProto(proto) { return closedGroupControlMessage }
if let dataExtractionNotification = DataExtractionNotification.fromProto(proto) { return dataExtractionNotification }
if let expirationTimerUpdate = ExpirationTimerUpdate.fromProto(proto) { return expirationTimerUpdate }
if let configurationMessage = ConfigurationMessage.fromProto(proto) { return configurationMessage }
if let visibleMessage = VisibleMessage.fromProto(proto) { return visibleMessage }

@ -39,7 +39,7 @@ extension MessageSender {
// Notify the PN server
promises.append(PushNotificationAPI.performOperation(.subscribe, for: groupPublicKey, publicKey: userPublicKey))
// Notify the user
let infoMessage = TSInfoMessage(timestamp: NSDate.ows_millisecondTimeStamp(), in: thread, messageType: .typeGroupUpdate)
let infoMessage = TSInfoMessage(timestamp: NSDate.ows_millisecondTimeStamp(), in: thread, messageType: .groupUpdate)
infoMessage.save(with: transaction)
// Return
return when(fulfilled: promises).map2 { thread }
@ -125,7 +125,7 @@ extension MessageSender {
thread.setGroupModel(newGroupModel, with: transaction)
// Notify the user
let updateInfo = group.getInfoStringAboutUpdate(to: newGroupModel)
let infoMessage = TSInfoMessage(timestamp: NSDate.ows_millisecondTimeStamp(), in: thread, messageType: .typeGroupUpdate, customMessage: updateInfo)
let infoMessage = TSInfoMessage(timestamp: NSDate.ows_millisecondTimeStamp(), in: thread, messageType: .groupUpdate, customMessage: updateInfo)
infoMessage.save(with: transaction)
}
@ -166,7 +166,7 @@ extension MessageSender {
thread.setGroupModel(newGroupModel, with: transaction)
// Notify the user
let updateInfo = group.getInfoStringAboutUpdate(to: newGroupModel)
let infoMessage = TSInfoMessage(timestamp: NSDate.ows_millisecondTimeStamp(), in: thread, messageType: .typeGroupUpdate, customMessage: updateInfo)
let infoMessage = TSInfoMessage(timestamp: NSDate.ows_millisecondTimeStamp(), in: thread, messageType: .groupUpdate, customMessage: updateInfo)
infoMessage.save(with: transaction)
}
@ -204,7 +204,7 @@ extension MessageSender {
thread.setGroupModel(newGroupModel, with: transaction)
// Notify the user
let updateInfo = group.getInfoStringAboutUpdate(to: newGroupModel)
let infoMessage = TSInfoMessage(timestamp: NSDate.ows_millisecondTimeStamp(), in: thread, messageType: .typeGroupUpdate, customMessage: updateInfo)
let infoMessage = TSInfoMessage(timestamp: NSDate.ows_millisecondTimeStamp(), in: thread, messageType: .groupUpdate, customMessage: updateInfo)
infoMessage.save(with: transaction)
}
@ -237,7 +237,7 @@ extension MessageSender {
thread.setGroupModel(newGroupModel, with: transaction)
// Notify the user
let updateInfo = group.getInfoStringAboutUpdate(to: newGroupModel)
let infoMessage = TSInfoMessage(timestamp: NSDate.ows_millisecondTimeStamp(), in: thread, messageType: .typeGroupUpdate, customMessage: updateInfo)
let infoMessage = TSInfoMessage(timestamp: NSDate.ows_millisecondTimeStamp(), in: thread, messageType: .groupUpdate, customMessage: updateInfo)
infoMessage.save(with: transaction)
}

@ -184,10 +184,10 @@ public final class MessageSender : NSObject {
let senderPublicKey: String
switch destination {
case .contact(_):
kind = .unidentifiedSender
kind = .sessionMessage
senderPublicKey = ""
case .closedGroup(let groupPublicKey):
kind = .closedGroupCiphertext
kind = .closedGroupMessage
senderPublicKey = groupPublicKey
case .openGroup(_, _): preconditionFailure()
}

@ -147,7 +147,7 @@ public final class OpenGroupPoller : NSObject {
let content = SNProtoContent.builder()
content.setDataMessage(try! dataMessageProto.build())
// Envelope
let envelope = SNProtoEnvelope.builder(type: .unidentifiedSender, timestamp: message.timestamp)
let envelope = SNProtoEnvelope.builder(type: .sessionMessage, timestamp: message.timestamp)
envelope.setSource(senderPublicKey)
envelope.setSourceDevice(1)
envelope.setContent(try! content.build().serializedData())

Loading…
Cancel
Save