Merge pull request #57 from BeaudanBrown/friend-state

Refactor friend request status to use a state enum variable
pull/58/head
BeaudanBrown 6 years ago committed by GitHub
commit f51dc811d4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -394,6 +394,11 @@ async function updateToSchemaVersion6(currentVersion, instance) {
console.log('updateToSchemaVersion6: starting...');
await instance.run('BEGIN TRANSACTION;');
await instance.run(
`ALTER TABLE conversations
ADD COLUMN friendStatus INTEGER;`
);
await instance.run(
`CREATE TABLE seenMessages(
hash STRING PRIMARY KEY,
@ -959,7 +964,7 @@ async function getConversationCount() {
async function saveConversation(data) {
// eslint-disable-next-line camelcase
const { id, active_at, type, members, name, profileName } = data;
const { id, active_at, type, members, name, friendStatus, profileName } = data;
await db.run(
`INSERT INTO conversations (
@ -970,6 +975,7 @@ async function saveConversation(data) {
type,
members,
name,
friendStatus,
profileName
) values (
$id,
@ -979,6 +985,7 @@ async function saveConversation(data) {
$type,
$members,
$name,
$friendStatus,
$profileName
);`,
{
@ -989,6 +996,7 @@ async function saveConversation(data) {
$type: type,
$members: members ? members.join(' ') : null,
$name: name,
$friendStatus: friendStatus,
$profileName: profileName,
}
);
@ -1012,7 +1020,7 @@ async function saveConversations(arrayOfConversations) {
async function updateConversation(data) {
// eslint-disable-next-line camelcase
const { id, active_at, type, members, name, profileName } = data;
const { id, active_at, type, members, name, friendStatus, profileName } = data;
await db.run(
`UPDATE conversations SET
@ -1022,6 +1030,7 @@ async function updateConversation(data) {
type = $type,
members = $members,
name = $name,
friendStatus = $friendStatus,
profileName = $profileName
WHERE id = $id;`,
{
@ -1032,6 +1041,7 @@ async function updateConversation(data) {
$type: type,
$members: members ? members.join(' ') : null,
$name: name,
$friendStatus: friendStatus,
$profileName: profileName,
}
);

@ -568,14 +568,6 @@
}
});
// Gets called when a user accepts or declines a friend request
Whisper.events.on('friendRequestUpdated', friendRequest => {
const { pubKey, ...message } = friendRequest;
if (messageReceiver) {
messageReceiver.onFriendRequestUpdate(pubKey, message);
}
});
Whisper.events.on('calculatingPoW', ({ pubKey, timestamp }) => {
try {
const conversation = ConversationController.get(pubKey);

@ -38,6 +38,20 @@
deleteAttachmentData,
} = window.Signal.Migrations;
// Possible conversation friend states
const FriendStatusEnum = Object.freeze({
// New conversation, no messages sent or received
none: 0,
// Have received a friend request, waiting to accept/decline
pendingAction: 1,
// Have send a friend request, waiting for response
pendingResponse: 2,
// Have send and received prekeybundle, waiting for ciphertext confirming key exchange
pendingCipher: 3,
// We did it!
friends: 4,
});
const COLORS = [
'red',
'deep_orange',
@ -78,6 +92,7 @@
return {
unreadCount: 0,
verified: textsecure.storage.protocol.VerifiedStatus.DEFAULT,
friendStatus: FriendStatusEnum.none,
isFriend: false,
keyExchangeCompleted: false,
unlockTimestamp: null, // Timestamp used for expiring friend requests.
@ -473,13 +488,12 @@
return true;
}
return this.get('keyExchangeCompleted') || false;
return this.get('friendStatus') === FriendStatusEnum.friends;
},
async setKeyExchangeCompleted(value) {
// Only update the value if it's different
if (this.get('keyExchangeCompleted') === value) return;
async setKeyExchangeCompleted() {
if (this.get('friendStatus') !== FriendStatusEnum.pendingCipher) return;
this.set({ keyExchangeCompleted: value });
this.set({ friendStatus: FriendStatusEnum.friends });
await window.Signal.Data.updateConversation(this.id, this.attributes, {
Conversation: Whisper.Conversation,
});
@ -492,9 +506,22 @@
const successfulOutgoing = outgoing.filter(o => !o.hasErrors());
return (incoming.length > 0 || successfulOutgoing.length > 0);
},
},
getPreKeyBundleType() {
switch (this.get('friendStatus')) {
case FriendStatusEnum.none:
case FriendStatusEnum.pendingResponse:
return textsecure.protobuf.PreKeyBundleMessage.Type.FRIEND_REQUEST;
case FriendStatusEnum.pendingAction:
case FriendStatusEnum.pendingCipher:
return textsecure.protobuf.PreKeyBundleMessage.Type.FRIEND_REQUEST_ACCEPT;
default:
return textsecure.protobuf.PreKeyBundleMessage.Type.UNKNOWN;
}
},
isFriend() {
return this.get('isFriend');
return this.get('friendStatus') === FriendStatusEnum.pendingCipher ||
this.get('friendStatus') === FriendStatusEnum.friends;
},
// Update any pending friend requests for the current user
async updateFriendRequestUI() {
@ -525,15 +552,42 @@
if (pending.length > 0)
this.notifyFriendRequest(this.id, 'accepted')
},
// We have declined an incoming friend request
async onDeclineFriendRequest() {
// Should we change states for other states? (They should never happen)
if (this.get('friendStatus') === FriendStatusEnum.pendingAction) {
this.set({ friendStatus: FriendStatusEnum.none });
await window.Signal.Data.updateConversation(this.id, this.attributes, {
Conversation: Whisper.Conversation,
});
await this.updateFriendRequestUI();
await this.updatePendingFriendRequests();
await window.libloki.removePreKeyBundleForNumber(this.id);
}
},
// We have accepted an incoming friend request
async onAcceptFriendRequest() {
// Should we change states for other states? (They should never happen)
if (this.get('friendStatus') === FriendStatusEnum.pendingAction) {
this.set({ friendStatus: FriendStatusEnum.pendingCipher });
await window.Signal.Data.updateConversation(this.id, this.attributes, {
Conversation: Whisper.Conversation,
});
await this.updateFriendRequestUI();
await this.updatePendingFriendRequests();
window.libloki.sendFriendRequestAccepted(this.id);
}
},
// Our outgoing friend request has been accepted
async onFriendRequestAccepted() {
if (!this.isFriend()) {
this.set({ isFriend: true });
// TODO: Think about how we want to handle other states
if (this.get('friendStatus') === FriendStatusEnum.pendingResponse) {
this.set({ friendStatus: FriendStatusEnum.pendingCipher });
await window.Signal.Data.updateConversation(this.id, this.attributes, {
Conversation: Whisper.Conversation,
});
await this.updateFriendRequestUI();
}
await this.updateFriendRequestUI();
},
async onFriendRequestTimeout() {
// Unset the timer
@ -568,6 +622,33 @@
await this.updatePendingFriendRequests();
await this.updateFriendRequestUI();
},
async onFriendRequestReceived() {
switch (this.get('friendStatus')) {
case FriendStatusEnum.none:
this.set({ friendStatus: FriendStatusEnum.pendingAction });
await window.Signal.Data.updateConversation(this.id, this.attributes, {
Conversation: Whisper.Conversation,
});
await this.updateFriendRequestUI();
return;
case FriendStatusEnum.pendingResponse:
this.set({ friendStatus: FriendStatusEnum.pendingCipher });
await window.Signal.Data.updateConversation(this.id, this.attributes, {
Conversation: Whisper.Conversation,
});
await this.updateFriendRequestUI();
return;
case FriendStatusEnum.pendingAction:
case FriendStatusEnum.pendingCipher:
// No need to change state
return;
case FriendStatusEnum.friends:
// TODO: Handle this case (discuss with squad)
return;
default:
throw new TypeError(`Invalid friendStatus type: '${this.friendStatus}'`);
}
},
async onFriendRequestSent() {
// Check if we need to set the friend request expiry
const unlockTimestamp = this.get('unlockTimestamp');
@ -584,6 +665,12 @@
this.setFriendRequestExpiryTimeout();
}
if (this.get('friendStatus') === FriendStatusEnum.none) {
this.set({ friendStatus: FriendStatusEnum.pendingResponse });
await window.Signal.Data.updateConversation(this.id, this.attributes, {
Conversation: Whisper.Conversation,
});
}
this.updateFriendRequestUI();
},
setFriendRequestExpiryTimeout() {
@ -1209,10 +1296,12 @@
getSendOptions(options = {}) {
const senderCertificate = storage.get('senderCertificate');
const numberInfo = this.getNumberInfo(options);
const preKeyBundleType = this.getPreKeyBundleType();
return {
senderCertificate,
numberInfo,
preKeyBundleType,
};
},

@ -306,11 +306,7 @@
await window.Signal.Data.saveMessage(this.attributes, {
Message: Whisper.Message,
});
window.Whisper.events.trigger('friendRequestUpdated', {
pubKey: conversation.id,
...this.attributes,
});
conversation.onAcceptFriendRequest();
},
async declineFriendRequest() {
if (this.get('friendStatus') !== 'pending') return;
@ -320,11 +316,7 @@
await window.Signal.Data.saveMessage(this.attributes, {
Message: Whisper.Message,
});
window.Whisper.events.trigger('friendRequestUpdated', {
pubKey: conversation.id,
...this.attributes,
});
conversation.onDeclineFriendRequest();
},
getPropsForFriendRequest() {
const friendStatus = this.get('friendStatus') || 'pending';

@ -25,7 +25,7 @@
ivAndCiphertext.set(new Uint8Array(iv));
ivAndCiphertext.set(new Uint8Array(ciphertext), iv.byteLength);
return {
type: textsecure.protobuf.Envelope.Type.FRIEND_REQUEST,
type: textsecure.protobuf.Envelope.Type.FALLBACK_CIPHERTEXT,
body: ivAndCiphertext,
registrationId: null,
};

@ -632,9 +632,13 @@ MessageReceiver.prototype.extend({
return this.onDeliveryReceipt(envelope);
}
if (envelope.preKeyBundleMessage) {
return this.handlePreKeyBundleMessage(envelope);
}
if (envelope.content) {
return this.handleContentMessage(envelope);
} else if (envelope.legacyMessage) {
}
if (envelope.legacyMessage) {
return this.handleLegacyMessage(envelope);
}
this.removeFromCache(envelope);
@ -711,23 +715,6 @@ MessageReceiver.prototype.extend({
address
);
// Check if we have preKey bundles to decrypt
if (envelope.preKeyBundleMessage) {
const decryptedText = await fallBackSessionCipher.decrypt(envelope.preKeyBundleMessage.toArrayBuffer());
const unpadded = await this.unpad(decryptedText);
const decodedProto = textsecure.protobuf.PreKeyBundleMessage.decode(unpadded);
const decodedBundle = this.decodePreKeyBundleMessage(decodedProto);
// eslint-disable-next-line no-param-reassign
envelope.preKeyBundleMessage = decodedBundle;
// Save the preKeyBundle
await this.handlePreKeyBundleMessage(
envelope.source,
envelope.preKeyBundleMessage
);
}
const me = {
number: ourNumber,
deviceId: parseInt(textsecure.storage.user.getDeviceId(), 10),
@ -739,7 +726,7 @@ MessageReceiver.prototype.extend({
promise = sessionCipher.decryptWhisperMessage(ciphertext)
.then(this.unpad);
break;
case textsecure.protobuf.Envelope.Type.FRIEND_REQUEST: {
case textsecure.protobuf.Envelope.Type.FALLBACK_CIPHERTEXT: {
window.log.info('friend-request message from ', envelope.source);
promise = fallBackSessionCipher.decrypt(ciphertext.toArrayBuffer())
.then(this.unpad);
@ -993,37 +980,6 @@ MessageReceiver.prototype.extend({
return this.innerHandleContentMessage(envelope, plaintext);
});
},
// A handler function for when a friend request is accepted or declined
async onFriendRequestUpdate(pubKey, message) {
if (!message || !message.direction || !message.friendStatus) return;
// Update the conversation
const conversation = window.ConversationController.get(pubKey);
if (conversation) {
// Update the conversation friend request indicator
await conversation.updatePendingFriendRequests();
await conversation.updateTextInputState();
}
// Check if we changed the state of the incoming friend request
if (message.direction === 'incoming') {
// If we accepted an incoming friend request then update our state
if (message.friendStatus === 'accepted') {
// Accept the friend request
if (conversation) {
await conversation.onFriendRequestAccepted();
}
// Send a reply back
libloki.sendFriendRequestAccepted(pubKey);
} else if (message.friendStatus === 'declined') {
// Delete the preKeys
await libloki.removePreKeyBundleForNumber(pubKey);
}
}
window.log.info(`Friend request for ${pubKey} was ${message.friendStatus}`, message);
},
async innerHandleContentMessage(envelope, plaintext) {
const content = textsecure.protobuf.Content.decode(plaintext);
@ -1034,27 +990,22 @@ MessageReceiver.prototype.extend({
window.log.info('Error getting conversation: ', envelope.source);
}
// Check if the other user accepted our friend request
if (
envelope.preKeyBundleMessage &&
envelope.preKeyBundleMessage.type === textsecure.protobuf.PreKeyBundleMessage.Type.FRIEND_REQUEST_ACCEPT &&
conversation
envelope.preKeyBundleMessage.type ===
textsecure.protobuf.PreKeyBundleMessage.Type.FRIEND_REQUEST
) {
await conversation.onFriendRequestAccepted();
}
if (envelope.type === textsecure.protobuf.Envelope.Type.FRIEND_REQUEST) {
return this.handleFriendRequestMessage(envelope, content.dataMessage);
} else if (
envelope.type === textsecure.protobuf.Envelope.Type.CIPHERTEXT ||
// We also need to check for PREKEY_BUNDLE aswell if the session hasn't started.
// ref: libsignal-protocol.js:36120
envelope.type === textsecure.protobuf.Envelope.Type.PREKEY_BUNDLE
) {
envelope.type === textsecure.protobuf.Envelope.Type.CIPHERTEXT ||
// We also need to check for PREKEY_BUNDLE aswell if the session hasn't started.
// ref: libsignal-protocol.js:36120
envelope.type === textsecure.protobuf.Envelope.Type.PREKEY_BUNDLE
) {
// If we get a cipher and we're already friends
// then we set our key exchange to complete
if (conversation && conversation.isFriend()) {
await conversation.setKeyExchangeCompleted(true);
await conversation.setKeyExchangeCompleted();
}
}
@ -1289,7 +1240,44 @@ MessageReceiver.prototype.extend({
signature,
};
},
async handlePreKeyBundleMessage(pubKey, preKeyBundleMessage) {
async handlePreKeyBundleMessage(envelope) {
const preKeyBundleMessage = await this.decryptPreKeyBundleMessage(envelope);
// eslint-disable-next-line no-param-reassign
envelope.preKeyBundleMessage = preKeyBundleMessage;
await this.savePreKeyBundleMessage(
envelope.source,
preKeyBundleMessage
);
const conversation = await window.ConversationController.getOrCreateAndWait(
envelope.source,
'private'
);
if (preKeyBundleMessage.type === textsecure.protobuf.PreKeyBundleMessage.Type.FRIEND_REQUEST) {
conversation.onFriendRequestReceived();
} else if (preKeyBundleMessage.type === textsecure.protobuf.PreKeyBundleMessage.Type.FRIEND_REQUEST_ACCEPT) {
conversation.onFriendRequestAccepted();
} else {
window.log.warn('Unknown PreKeyBundleMessage Type')
}
if (envelope.content)
return this.handleContentMessage(envelope);
return null;
},
async decryptPreKeyBundleMessage(envelope) {
if (!envelope.preKeyBundleMessage) return null;
const address = new libsignal.SignalProtocolAddress(envelope.source, envelope.sourceDevice);
const fallBackSessionCipher = new libloki.FallBackSessionCipher(
address
);
const decryptedText =
await fallBackSessionCipher.decrypt(envelope.preKeyBundleMessage.toArrayBuffer());
const unpadded = await this.unpad(decryptedText);
const decodedProto = textsecure.protobuf.PreKeyBundleMessage.decode(unpadded);
const decodedBundle = this.decodePreKeyBundleMessage(decodedProto);
return decodedBundle;
},
async savePreKeyBundleMessage(pubKey, preKeyBundleMessage) {
if (!preKeyBundleMessage) return null;
const {
@ -1303,7 +1291,7 @@ MessageReceiver.prototype.extend({
if (pubKey !== StringView.arrayBufferToHex(identityKey)) {
throw new Error(
'Error in handlePreKeyBundleMessage: envelope pubkey does not match pubkey in prekey bundle'
'Error in savePreKeyBundleMessage: envelope pubkey does not match pubkey in prekey bundle'
);
}
@ -1551,8 +1539,7 @@ textsecure.MessageReceiver = function MessageReceiverWrapper(
);
this.getStatus = messageReceiver.getStatus.bind(messageReceiver);
this.close = messageReceiver.close.bind(messageReceiver);
this.onFriendRequestUpdate = messageReceiver.onFriendRequestUpdate.bind(messageReceiver);
this.handlePreKeyBundleMessage = messageReceiver.handlePreKeyBundleMessage.bind(messageReceiver);
this.savePreKeyBundleMessage = messageReceiver.savePreKeyBundleMessage.bind(messageReceiver);
messageReceiver.connect();
};

@ -46,7 +46,8 @@ function OutgoingMessage(
const { numberInfo, senderCertificate, preKeyBundleType } = options;
this.numberInfo = numberInfo;
this.senderCertificate = senderCertificate;
this.preKeyBundleType = preKeyBundleType || textsecure.protobuf.PreKeyBundleMessage.Type.UNKNOWN;
this.preKeyBundleType =
preKeyBundleType || textsecure.protobuf.PreKeyBundleMessage.Type.UNKNOWN;
}
OutgoingMessage.prototype = {
@ -293,10 +294,6 @@ OutgoingMessage.prototype = {
const preKeyBundleMessage = await libloki.getPreKeyBundleForNumber(number);
preKeyBundleMessage.type = this.preKeyBundleType;
// If we have to use fallback encryption then this must be a friend request
if (this.fallBackEncryption)
preKeyBundleMessage.type = textsecure.protobuf.PreKeyBundleMessage.Type.FRIEND_REQUEST;
const textBundle = this.convertMessageToText(preKeyBundleMessage);
const encryptedBundle = await fallBackEncryption.encrypt(textBundle);
preKeys = { preKeyBundleMessage: encryptedBundle.body };
@ -349,7 +346,7 @@ OutgoingMessage.prototype = {
// TODO: Allow user to set ttl manually
if (
outgoingObject.type ===
textsecure.protobuf.Envelope.Type.FRIEND_REQUEST
textsecure.protobuf.Envelope.Type.FALLBACK_CIPHERTEXT
) {
ttl = 4 * 24 * 60 * 60; // 4 days for friend request message
} else {

@ -12,7 +12,7 @@ message Envelope {
PREKEY_BUNDLE = 3; //Used By Signal. DO NOT TOUCH! we don't use this at all.
RECEIPT = 5;
UNIDENTIFIED_SENDER = 6;
FRIEND_REQUEST = 101; // contains prekeys + message and is using simple encryption
FALLBACK_CIPHERTEXT = 101; // contains prekeys + message and is using simple encryption
}
optional Type type = 1;

Loading…
Cancel
Save