Merge pull request #12 from loki-project/friend-request-backend

Friend request + Send message fix
This commit is contained in:
Niels Andriesse 2019-05-10 09:06:42 +10:00 committed by GitHub
commit f391c96747
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
28 changed files with 303 additions and 77 deletions

2
Pods

@ -1 +1 @@
Subproject commit de364c557a6e2f70e187b1c4a7852b4ea4b9b68a
Subproject commit 51a2b3e8610c1083db88502aaa76da1f352757da

View file

@ -92,6 +92,7 @@
#import <SignalServiceKit/OWSEndSessionMessage.h>
#import <SignalServiceKit/OWSError.h>
#import <SignalServiceKit/OWSFileSystem.h>
#import <SignalServiceKit/OWSFriendRequestMessage.h>
#import <SignalServiceKit/OWSIdentityManager.h>
#import <SignalServiceKit/OWSMediaGalleryFinder.h>
#import <SignalServiceKit/OWSMessageManager.h>

View file

@ -2570,3 +2570,5 @@
"No search results" = "No search results";
"Calculating proof of work" = "Calculating proof of work";
"Failed to calculate proof of work." = "Failed to calculate proof of work.";
"Failed to wrap data in an Envelope" = "Failed to wrap data in an Envelope.";
"Failed to wrap data in an WebSocket" = "Failed to wrap data in an WebSocket.";

View file

@ -85,6 +85,27 @@ typedef void (^BuildOutgoingMessageCompletionBlock)(TSOutgoingMessage *savedMess
#pragma mark - Durable Message Enqueue
// Loki: TODO We may change this?
+ (TSOutgoingMessage *)enqueueFriendRequestAcceptMessageInThread:(TSThread *)thread
{
TSOutgoingMessage *message =
[[TSOutgoingMessage alloc] initOutgoingMessageWithTimestamp:[NSDate ows_millisecondTimeStamp]
inThread:thread
messageBody:@""
attachmentIds:[NSMutableArray new]
expiresInSeconds:0
expireStartedAt:0
isVoiceMessage:NO
groupMetaMessage:TSGroupMetaMessageUnspecified
quotedMessage:nil
contactShare:nil
linkPreview:nil];
[self.dbConnection asyncReadWriteWithBlock:^(YapDatabaseReadWriteTransaction *_Nonnull transaction) {
[self.messageSenderJobQueue addMessage:message transaction:transaction];
}];
return message;
}
+ (TSOutgoingMessage *)enqueueMessageWithText:(NSString *)fullMessageText
inThread:(TSThread *)thread
quotedReplyModel:(nullable OWSQuotedReplyModel *)quotedReplyModel
@ -129,6 +150,7 @@ typedef void (^BuildOutgoingMessageCompletionBlock)(TSOutgoingMessage *savedMess
}];
}
// Loki: TODO Disable attachment and link preview for now
+ (TSOutgoingMessage *)buildOutgoingMessageWithText:(nullable NSString *)fullMessageText
mediaAttachments:(NSArray<SignalAttachment *> *)mediaAttachments
thread:(TSThread *)thread
@ -172,19 +194,24 @@ typedef void (^BuildOutgoingMessageCompletionBlock)(TSOutgoingMessage *savedMess
}
BOOL isVoiceMessage = (attachments.count == 1 && attachments.lastObject.isVoiceMessage);
// Loki: If we're not friends then always set the message to a friend request message
// If we're friends then the assumption is that we have the other users pre-key bundle
NSString *messageClassString = [thread isFriend] ? @"TSOutgoingMessage" : @"OWSFriendRequestMessage";
Class messageClass = NSClassFromString(messageClassString);
TSOutgoingMessage *message =
[[TSOutgoingMessage alloc] initOutgoingMessageWithTimestamp:[NSDate ows_millisecondTimeStamp]
inThread:thread
messageBody:truncatedText
attachmentIds:[NSMutableArray new]
expiresInSeconds:expiresInSeconds
expireStartedAt:0
isVoiceMessage:isVoiceMessage
groupMetaMessage:TSGroupMetaMessageUnspecified
quotedMessage:[quotedReplyModel buildQuotedMessageForSending]
contactShare:nil
linkPreview:nil];
[[messageClass alloc] initOutgoingMessageWithTimestamp:[NSDate ows_millisecondTimeStamp]
inThread:thread
messageBody:truncatedText
attachmentIds:[NSMutableArray new]
expiresInSeconds:expiresInSeconds
expireStartedAt:0
isVoiceMessage:isVoiceMessage
groupMetaMessage:TSGroupMetaMessageUnspecified
quotedMessage:[quotedReplyModel buildQuotedMessageForSending]
contactShare:nil
linkPreview:nil];
[BenchManager
benchAsyncWithTitle:@"Saving outgoing message"

View file

@ -29,6 +29,22 @@ extern ConversationColorName const ConversationColorNameSteel;
extern ConversationColorName const kConversationColorName_Default;
// Loki: Friend request state
typedef NS_ENUM(NSInteger, TSThreadFriendRequestState) {
// New conversation, no messages sent or received
TSThreadFriendRequestStateNone,
// This state is used to lock the input early while sending
TSThreadFriendRequestStatePendingSend,
// Friend request send, awaiting response
TSThreadFriendRequestStateRequestSent,
// Friend request received, awaiting user input
TSThreadFriendRequestStateRequestReceived,
// We are friends with the user of this thread
TSThreadFriendRequestStateFriends,
// Friend request sent but it timed out (user didn't accept within x time)
TSThreadFriendRequestStateRequestExpired,
};
/**
* TSThread is the superclass of TSContactThread and TSGroupThread
*/
@ -38,6 +54,9 @@ extern ConversationColorName const kConversationColorName_Default;
@property (nonatomic, readonly) NSDate *creationDate;
@property (nonatomic, readonly) BOOL isArchivedByLegacyTimestampForSorting;
// Loki: The current friend request state with this thread
@property (atomic, readonly) TSThreadFriendRequestState friendRequestState;
/**
* Whether the object is a group thread or not.
*
@ -170,6 +189,21 @@ extern ConversationColorName const kConversationColorName_Default;
- (void)updateWithMutedUntilDate:(NSDate *)mutedUntilDate transaction:(YapDatabaseReadWriteTransaction *)transaction;
#pragma mark - Loki Friend Request
/// Check if this thread is a friend
- (BOOL)isFriend;
/// Check if a friend request is pending
- (BOOL)isPendingFriendRequest;
/// Check if a friend request has been sent to this thread
- (BOOL)hasSentFriendRequest;
/// Check if a friend request has been received from this thread
- (BOOL)hasReceivedFriendRequest;
@end
NS_ASSUME_NONNULL_END

View file

@ -50,6 +50,8 @@ ConversationColorName const kConversationColorName_Default = ConversationColorNa
@property (nonatomic, copy, nullable) NSString *messageDraft;
@property (atomic, nullable) NSDate *mutedUntilDate;
@property (atomic) TSThreadFriendRequestState friendRequestState;
// DEPRECATED - not used since migrating to sortId
// but keeping these properties around to ease any pain in the back-forth
// migration while testing. Eventually we can safely delete these as they aren't used anywhere.
@ -84,6 +86,9 @@ ConversationColorName const kConversationColorName_Default = ConversationColorNa
if (self) {
_creationDate = [NSDate date];
_messageDraft = nil;
// We are initially not friends
_friendRequestState = TSThreadFriendRequestStateNone;
NSString *_Nullable contactId = self.contactIdentifier;
if (contactId.length > 0) {
@ -694,6 +699,35 @@ ConversationColorName const kConversationColorName_Default = ConversationColorNa
}];
}
# pragma mark - Loki Friend Request
- (BOOL)isFriend
{
return _friendRequestState == TSThreadFriendRequestStateFriends;
}
- (BOOL)isPendingFriendRequest
{
return (
_friendRequestState == TSThreadFriendRequestStatePendingSend ||
_friendRequestState == TSThreadFriendRequestStateRequestSent ||
_friendRequestState == TSThreadFriendRequestStateRequestReceived
);
}
- (BOOL)hasSentFriendRequest
{
return (
_friendRequestState == TSThreadFriendRequestStateRequestSent ||
_friendRequestState == TSThreadFriendRequestStateRequestExpired
);
}
- (BOOL)hasReceivedFriendRequest
{
return _friendRequestState == TSThreadFriendRequestStateRequestReceived;
}
@end
NS_ASSUME_NONNULL_END

View file

@ -21,10 +21,14 @@ import PromiseKit
public enum Error : LocalizedError {
case proofOfWorkCalculationFailed
case failedToWrapInEnvelope
case failedToWrapInWebSocket
public var errorDescription: String? {
switch self {
case .proofOfWorkCalculationFailed: return NSLocalizedString("Failed to calculate proof of work.", comment: "")
case .failedToWrapInEnvelope: return NSLocalizedString("Failed to wrap data in an Envelope", comment: "")
case .failedToWrapInWebSocket: return NSLocalizedString("Failed to wrap data in an WebSocket", comment: "")
}
}
}
@ -73,8 +77,8 @@ import PromiseKit
return anyPromise
}
@objc public static func objc_sendSignalMessage(_ signalMessage: SignalMessage, to destination: String, requiringPoW isPoWRequired: Bool) -> AnyPromise {
let promise = LokiMessage.fromSignalMessage(signalMessage, requiringPoW: isPoWRequired)
@objc public static func objc_sendSignalMessage(_ signalMessage: SignalMessage, to destination: String, timestamp: UInt64, requiringPoW isPoWRequired: Bool) -> AnyPromise {
let promise = LokiMessage.from(signalMessage: signalMessage, timestamp: timestamp, requiringPoW: isPoWRequired)
.then(sendMessage)
.recoverNetworkError(on: DispatchQueue.global())
let anyPromise = AnyPromise(promise)

View file

@ -0,0 +1,134 @@
import PromiseKit
public struct LokiMessage {
/// The hex encoded public key of the receiver.
let destination: String
/// The content of the message.
let data: LosslessStringConvertible
/// The time to live for the message.
let ttl: UInt64
/// When the proof of work was calculated, if applicable.
///
/// - Note: Expressed as milliseconds since 00:00:00 UTC on 1 January 1970.
let timestamp: UInt64?
/// The base 64 encoded proof of work, if applicable.
let nonce: String?
public init(destination: String, data: LosslessStringConvertible, ttl: UInt64, timestamp: UInt64?, nonce: String?) {
self.destination = destination
self.data = data
self.ttl = ttl
self.timestamp = timestamp
self.nonce = nonce
}
/// Build a LokiMessage from a SignalMessage
///
/// - Parameters:
/// - signalMessage: the signal message
/// - timestamp: the original message timestamp (TSOutgoingMessage.timestamp)
/// - isPoWRequired: Should we calculate proof of work
/// - Returns: The loki message
public static func from(signalMessage: SignalMessage, timestamp: UInt64, requiringPoW isPoWRequired: Bool) -> Promise<LokiMessage> {
// To match the desktop application we have to take the data
// wrap it in an envelope, then
// wrap it in a websocket
return Promise<LokiMessage> { seal in
DispatchQueue.global(qos: .default).async {
guard let envelope = buildEnvelope(fromSignalMessage: signalMessage, timestamp: timestamp) else {
seal.reject(LokiAPI.Error.failedToWrapInEnvelope)
return
}
// Make the data
guard let websocket = wrapInWebsocket(envelope: envelope),
let serialized = try? websocket.serializedData() else {
seal.reject(LokiAPI.Error.failedToWrapInWebSocket)
return;
}
let data = serialized.base64EncodedString()
let destination = signalMessage["destination"] as! String
let ttl = LokiAPI.defaultMessageTTL
if isPoWRequired {
// timeIntervalSince1970 returns timestamp in seconds but the storage server only accepts timestamp in milliseconds
let now = UInt64(Date().timeIntervalSince1970 * 1000)
if let nonce = ProofOfWork.calculate(data: data, pubKey: destination, timestamp: now, ttl: ttl) {
let result = LokiMessage(destination: destination, data: data, ttl: ttl, timestamp: now, nonce: nonce)
seal.fulfill(result)
} else {
seal.reject(LokiAPI.Error.proofOfWorkCalculationFailed)
}
} else {
let result = LokiMessage(destination: destination, data: data, ttl: ttl, timestamp: nil, nonce: nil)
seal.fulfill(result)
}
}
}
}
/// Wrap EnvelopeProto in a WebSocketProto
/// This is needed because it is done automatically on the desktop
private static func wrapInWebsocket(envelope: SSKProtoEnvelope) -> WebSocketProtoWebSocketMessage? {
do {
// This request is just a copy of the one on desktop
let requestBuilder = WebSocketProtoWebSocketRequestMessage.builder(verb: "PUT", path: "/api/v1/message", requestID: UInt64.random(in: 1..<UInt64.max))
let envelopeData = try envelope.serializedData()
requestBuilder.setBody(envelopeData)
// Build the websocket message
let builder = WebSocketProtoWebSocketMessage.builder(type: .request)
let request = try requestBuilder.build()
builder.setRequest(request)
return try builder.build()
} catch {
owsFailDebug("Loki Message: error building websocket message: \(error)")
return nil
}
}
/// Build the EnvelopeProto from SignalMessage
private static func buildEnvelope(fromSignalMessage signalMessage: SignalMessage, timestamp: UInt64) -> SSKProtoEnvelope? {
guard let ourKeys = SSKEnvironment.shared.identityManager.identityKeyPair() else {
owsFailDebug("error building envelope: identityManager.identityKeyPair() is invalid")
return nil;
}
do {
let ourPubKey = ourKeys.hexEncodedPublicKey
let params = ParamParser(dictionary: signalMessage)
let typeInt: Int32 = try params.required(key: "type")
guard let type: SSKProtoEnvelope.SSKProtoEnvelopeType = SSKProtoEnvelope.SSKProtoEnvelopeType(rawValue: typeInt) else {
Logger.error("`type` was invalid: \(typeInt)")
throw ParamParser.ParseError.invalidFormat("type")
}
let builder = SSKProtoEnvelope.builder(type: type, timestamp: timestamp)
builder.setSource(ourPubKey)
builder.setSourceDevice(OWSDevicePrimaryDeviceId)
if let content = try params.optionalBase64EncodedData(key: "content") {
builder.setContent(content)
}
return try builder.build()
} catch {
owsFailDebug("Loki Message: error building envelope: \(error)")
return nil
}
}
public func toJSON() -> JSON {
var result = [ "pubKey" : destination, "data" : data.description, "ttl" : String(ttl) ]
if let timestamp = timestamp, let nonce = nonce {
result["timestamp"] = String(timestamp)
result["nonce"] = nonce
}
return result
}
}

View file

@ -1,56 +0,0 @@
import PromiseKit
public struct LokiMessage {
/// The hex encoded public key of the receiver.
let destination: String
/// The content of the message.
let data: LosslessStringConvertible
/// The time to live for the message.
let ttl: UInt64
/// When the proof of work was calculated, if applicable.
///
/// - Note: Expressed as milliseconds since 00:00:00 UTC on 1 January 1970.
let timestamp: UInt64?
/// The base 64 encoded proof of work, if applicable.
let nonce: String?
public init(destination: String, data: LosslessStringConvertible, ttl: UInt64, timestamp: UInt64?, nonce: String?) {
self.destination = destination
self.data = data
self.ttl = ttl
self.timestamp = timestamp
self.nonce = nonce
}
public static func fromSignalMessage(_ signalMessage: SignalMessage, requiringPoW isPoWRequired: Bool) -> Promise<LokiMessage> {
return Promise<LokiMessage> { seal in
DispatchQueue.global(qos: .default).async {
let destination = signalMessage["destination"] as! String
let data = signalMessage["content"] as! String
let ttl = LokiAPI.defaultMessageTTL
if isPoWRequired {
// timeIntervalSince1970 returns timestamp in seconds but the storage server only accepts timestamp in milliseconds
let timestamp = UInt64(Date().timeIntervalSince1970 * 1000)
if let nonce = ProofOfWork.calculate(data: data, pubKey: destination, timestamp: timestamp, ttl: ttl) {
let result = LokiMessage(destination: destination, data: data, ttl: ttl, timestamp: timestamp, nonce: nonce)
seal.fulfill(result)
} else {
seal.reject(LokiAPI.Error.proofOfWorkCalculationFailed)
}
} else {
let result = LokiMessage(destination: destination, data: data, ttl: ttl, timestamp: nil, nonce: nil)
seal.fulfill(result)
}
}
}
}
public func toJSON() -> JSON {
var result = [ "pubKey" : destination, "data" : data.description, "ttl" : String(ttl) ]
if let timestamp = timestamp, let nonce = nonce {
result["timestamp"] = String(timestamp)
result["nonce"] = nonce
}
return result
}
}

View file

@ -0,0 +1,10 @@
#import <SignalServiceKit/SignalServiceKit.h>
NS_ASSUME_NONNULL_BEGIN
NS_SWIFT_NAME(FriendRequestMessage)
@interface OWSFriendRequestMessage : TSOutgoingMessage
@end
NS_ASSUME_NONNULL_END

View file

@ -0,0 +1,13 @@
#import "OWSFriendRequestMessage.h"
@implementation OWSFriendRequestMessage
- (SSKProtoContentBuilder *)contentBuilder {
SSKProtoContentBuilder *builder = [super contentBuilder];
// TODO: Attach pre-key bundle here
return builder;
}
@end

View file

@ -46,6 +46,14 @@ NS_ASSUME_NONNULL_BEGIN
return builder;
}
- (SSKProtoContentBuilder *)contentBuilder {
SSKProtoContentBuilder *builder = [super contentBuilder];
// TODO Loki: Attach pre key bundle here
return builder;
}
@end
NS_ASSUME_NONNULL_END

View file

@ -155,6 +155,12 @@ typedef NS_ENUM(NSInteger, TSGroupMetaMessage) {
*/
- (nullable SSKProtoDataMessageBuilder *)dataMessageBuilder;
/**
* Intermediate protobuf representation
* Subclasses can augment if they want to manipulate the content message before building.
*/
- (SSKProtoContentBuilder *)contentBuilder;
/**
* Should this message be synced to the users other registered devices? This is
* generally always true, except in the case of the sync messages themseleves

View file

@ -1113,6 +1113,12 @@ NSString *NSStringForOutgoingMessageRecipientState(OWSOutgoingMessageRecipientSt
return dataProto;
}
- (SSKProtoContentBuilder *)contentBuilder
{
SSKProtoContentBuilder *contentBuilder = [SSKProtoContent builder];
return contentBuilder;
}
- (nullable NSData *)buildPlainTextData:(SignalRecipient *)recipient
{
NSError *error;
@ -1122,7 +1128,7 @@ NSString *NSStringForOutgoingMessageRecipientState(OWSOutgoingMessageRecipientSt
return nil;
}
SSKProtoContentBuilder *contentBuilder = [SSKProtoContent builder];
SSKProtoContentBuilder *contentBuilder = [self contentBuilder];
[contentBuilder setDataMessage:dataMessage];
NSData *_Nullable contentData = [contentBuilder buildSerializedDataAndReturnError:&error];
if (error || !contentData) {

View file

@ -42,6 +42,7 @@
#import "TSRequest.h"
#import "TSSocketManager.h"
#import "TSThread.h"
#import "OWSFriendRequestMessage.h"
#import <AxolotlKit/AxolotlExceptions.h>
#import <AxolotlKit/CipherMessage.h>
#import <AxolotlKit/PreKeyBundle.h>
@ -1111,7 +1112,7 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException";
// Convert the message to a Loki message and send it using the Loki messaging API
NSDictionary *signalMessage = deviceMessages.firstObject;
BOOL isPoWRequired = YES; // TODO: Base on message type
[[LokiAPI objc_sendSignalMessage:signalMessage to:recipient.recipientId requiringPoW:isPoWRequired]
[[LokiAPI objc_sendSignalMessage:signalMessage to:recipient.recipientId timestamp:message.timestamp requiringPoW:isPoWRequired]
.thenOn(OWSDispatch.sendingQueue, ^(id result) {
[self messageSendDidSucceed:messageSend
deviceMessages:deviceMessages
@ -1546,8 +1547,10 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException";
@try {
// This may involve blocking network requests, so we do it _before_
// we open a transaction.
// TODO: Replace this when we add in friend request stuff
Boolean isFriendRequest = true;
// Friend requests means we don't have a session with the person
// There's no point to check for it
Boolean isFriendRequest = [messageSend.message isKindOfClass:[OWSFriendRequestMessage class]];
if (!isFriendRequest) {
[self throws_ensureRecipientHasSessionForMessageSend:messageSend deviceId:deviceId];
}
@ -1782,8 +1785,8 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException";
NSString *recipientId = recipient.recipientId;
OWSAssertDebug(recipientId.length > 0);
// TODO: Change this when we have friend request support
Boolean isFriendRequest = true;
// Loki: Handle friend requests differently
Boolean isFriendRequest = [messageSend.message isKindOfClass:[OWSFriendRequestMessage class]];
if (isFriendRequest) {
return [self throws_encryptedFriendMessageForMessageSend:messageSend deviceId:deviceId plainText:plainText];
}