2014-06-03 16:39:29 +00:00
// sendMessage(numbers = [], message = PushMessageContentProto, callback(success/failure map))
window . textsecure . messaging = function ( ) {
2014-07-23 01:33:35 +00:00
'use strict' ;
2014-06-03 16:39:29 +00:00
var self = { } ;
function getKeysForNumber ( number , updateDevices ) {
2014-07-25 00:15:27 +00:00
var handleResult = function ( response ) {
2014-07-23 01:33:35 +00:00
for ( var i in response . devices ) {
2014-07-21 02:55:07 +00:00
if ( updateDevices === undefined || updateDevices . indexOf ( response . devices [ i ] . deviceId ) > - 1 )
2014-07-25 00:15:27 +00:00
textsecure . storage . devices . saveKeysToDeviceObject ( {
2014-07-21 02:55:07 +00:00
encodedNumber : number + "." + response . devices [ i ] . deviceId ,
identityKey : response . identityKey ,
preKey : response . devices [ i ] . preKey . publicKey ,
preKeyId : response . devices [ i ] . preKey . keyId ,
signedKey : response . devices [ i ] . signedPreKey . publicKey ,
signedKeyId : response . devices [ i ] . signedPreKey . keyId ,
registrationId : response . devices [ i ] . registrationId
2014-06-03 16:39:29 +00:00
} ) ;
}
2014-07-25 00:15:27 +00:00
} ;
var promises = [ ] ;
if ( updateDevices !== undefined )
for ( var i in updateDevices )
promises [ promises . length ] = textsecure . api . getKeysForNumber ( number , updateDevices [ i ] ) . then ( handleResult ) ;
else
return textsecure . api . getKeysForNumber ( number ) . then ( handleResult ) ;
return Promise . all ( promises ) ;
2014-06-03 16:39:29 +00:00
}
// success_callback(server success/failure map), error_callback(error_msg)
// message == PushMessageContentProto (NOT STRING)
function sendMessageToDevices ( number , deviceObjectList , message , success _callback , error _callback ) {
var jsonData = [ ] ;
var relay = undefined ;
var promises = [ ] ;
var addEncryptionFor = function ( i ) {
if ( deviceObjectList [ i ] . relay !== undefined ) {
if ( relay === undefined )
relay = deviceObjectList [ i ] . relay ;
else if ( relay != deviceObjectList [ i ] . relay )
return new Promise ( function ( ) { throw new Error ( "Mismatched relays for number " + number ) ; } ) ;
} else {
if ( relay === undefined )
relay = "" ;
else if ( relay != "" )
return new Promise ( function ( ) { throw new Error ( "Mismatched relays for number " + number ) ; } ) ;
}
return textsecure . crypto . encryptMessageFor ( deviceObjectList [ i ] , message ) . then ( function ( encryptedMsg ) {
jsonData [ i ] = {
type : encryptedMsg . type ,
destinationDeviceId : textsecure . utils . unencodeNumber ( deviceObjectList [ i ] . encodedNumber ) [ 1 ] ,
destinationRegistrationId : deviceObjectList [ i ] . registrationId ,
body : encryptedMsg . body ,
timestamp : new Date ( ) . getTime ( )
} ;
if ( deviceObjectList [ i ] . relay !== undefined )
jsonData [ i ] . relay = deviceObjectList [ i ] . relay ;
} ) ;
}
for ( var i = 0 ; i < deviceObjectList . length ; i ++ )
promises [ i ] = addEncryptionFor ( i ) ;
return Promise . all ( promises ) . then ( function ( ) {
return textsecure . api . sendMessages ( number , jsonData ) ;
} ) ;
}
2014-06-06 02:26:01 +00:00
var sendGroupProto ;
var makeAttachmentPointer ;
var refreshGroups = function ( number ) {
var groups = textsecure . storage . groups . getGroupListForNumber ( number ) ;
var promises = [ ] ;
2014-07-23 01:33:35 +00:00
for ( var i in groups ) {
2014-06-06 02:26:01 +00:00
var group = textsecure . storage . groups . getGroup ( groups [ i ] ) ;
var proto = new textsecure . protos . PushMessageContentProtobuf ( ) ;
proto . group = new textsecure . protos . PushMessageContentProtobuf . GroupContext ( ) ;
proto . group . id = group . id ;
proto . group . type = textsecure . protos . PushMessageContentProtobuf . GroupContext . UPDATE ;
proto . group . members = group . numbers ;
proto . group . name = group . name === undefined ? null : group . name ;
if ( group . avatar !== undefined ) {
return makeAttachmentPointer ( group . avatar ) . then ( function ( attachment ) {
proto . group . avatar = attachment ;
promises . push ( sendGroupProto ( [ number ] , proto ) ) ;
} ) ;
} else {
promises . push ( sendGroupProto ( [ number ] , proto ) ) ;
}
}
return Promise . all ( promises ) ;
}
2014-06-03 16:39:29 +00:00
var tryMessageAgain = function ( number , encodedMessage , callback ) {
//TODO: Wipe identity key!
2014-06-06 02:26:01 +00:00
refreshGroups ( number ) . then ( function ( ) {
var message = textsecure . protos . decodePushMessageContentProtobuf ( encodedMessage ) ;
textsecure . sendMessage ( [ number ] , message , callback ) ;
} ) ;
2014-06-03 16:39:29 +00:00
}
textsecure . replay . registerReplayFunction ( tryMessageAgain , textsecure . replay . SEND _MESSAGE ) ;
var sendMessageProto = function ( numbers , message , callback ) {
var numbersCompleted = 0 ;
var errors = [ ] ;
var successfulNumbers = [ ] ;
var numberCompleted = function ( ) {
numbersCompleted ++ ;
if ( numbersCompleted >= numbers . length )
callback ( { success : successfulNumbers , failure : errors } ) ;
}
var registerError = function ( number , message , error ) {
2014-07-21 02:55:07 +00:00
if ( error ) {
if ( error . humanError )
message = error . humanError ;
} else
error = new Error ( message ) ;
2014-06-03 16:39:29 +00:00
errors [ errors . length ] = { number : number , reason : message , error : error } ;
numberCompleted ( ) ;
}
var doSendMessage ;
var reloadDevicesAndSend = function ( number , recurse ) {
return function ( ) {
var devicesForNumber = textsecure . storage . devices . getDeviceObjectsForNumber ( number ) ;
if ( devicesForNumber . length == 0 )
2014-07-21 02:55:07 +00:00
return registerError ( number , "Got empty device list when loading device keys" , null ) ;
2014-06-06 02:26:01 +00:00
refreshGroups ( number ) . then ( function ( ) {
doSendMessage ( number , devicesForNumber , recurse ) ;
} ) ;
2014-06-03 16:39:29 +00:00
}
}
doSendMessage = function ( number , devicesForNumber , recurse ) {
return sendMessageToDevices ( number , devicesForNumber , message ) . then ( function ( result ) {
successfulNumbers [ successfulNumbers . length ] = number ;
numberCompleted ( ) ;
} ) . catch ( function ( error ) {
if ( error instanceof Error && error . name == "HTTPError" && ( error . message == 410 || error . message == 409 ) ) {
if ( ! recurse )
return registerError ( number , "Hit retry limit attempting to reload device list" , error ) ;
if ( error . message == 409 )
textsecure . storage . devices . removeDeviceIdsForNumber ( number , error . response . extraDevices ) ;
var resetDevices = ( ( error . message == 410 ) ? error . response . staleDevices : error . response . missingDevices ) ;
getKeysForNumber ( number , resetDevices )
. then ( reloadDevicesAndSend ( number , false ) )
. catch ( function ( error ) {
if ( error . message !== "Identity key changed" )
registerError ( number , "Failed to reload device keys" , error ) ;
else {
error = textsecure . replay . createReplayableError ( "The destination's identity key has changed" , "The identity of the destination has changed. This may be malicious, or the destination may have simply reinstalled TextSecure." ,
textsecure . replay . SEND _MESSAGE , [ number , getString ( message . encode ( ) ) ] ) ;
registerError ( number , "Identity key changed" , error ) ;
}
} ) ;
} else
registerError ( number , "Failed to create or send message" , error ) ;
} ) ;
}
for ( var i = 0 ; i < numbers . length ; i ++ ) {
var number = numbers [ i ] ;
var devicesForNumber = textsecure . storage . devices . getDeviceObjectsForNumber ( number ) ;
2014-07-25 00:15:27 +00:00
var promises = [ ] ;
for ( var i in devicesForNumber )
if ( devicesForNumber [ i ] . registrationId === undefined )
promises [ promises . length ] = getKeysForNumber ( number , [ parseInt ( textsecure . utils . unencodeNumber ( devicesForNumber [ i ] . encodedNumber ) [ 1 ] ) ] ) ;
Promise . all ( promises ) . then ( function ( ) {
devicesForNumber = textsecure . storage . devices . getDeviceObjectsForNumber ( number ) ;
if ( devicesForNumber . length == 0 ) {
getKeysForNumber ( number )
. then ( reloadDevicesAndSend ( number , true ) )
. catch ( function ( error ) {
registerError ( number , "Failed to retreive new device keys for number " + number , error ) ;
} ) ;
} else
doSendMessage ( number , devicesForNumber , true ) ;
} ) ;
2014-06-03 16:39:29 +00:00
}
}
2014-06-06 02:26:01 +00:00
makeAttachmentPointer = function ( attachment ) {
2014-06-03 16:39:29 +00:00
var proto = new textsecure . protos . PushMessageContentProtobuf . AttachmentPointer ( ) ;
proto . key = textsecure . crypto . getRandomBytes ( 64 ) ;
var iv = textsecure . crypto . getRandomBytes ( 16 ) ;
return textsecure . crypto . encryptAttachment ( attachment . data , proto . key , iv ) . then ( function ( encryptedBin ) {
return textsecure . api . putAttachment ( encryptedBin ) . then ( function ( id ) {
proto . id = id ;
proto . contentType = attachment . contentType ;
return proto ;
} ) ;
} ) ;
}
2014-06-04 01:09:04 +00:00
var sendIndividualProto = function ( number , proto ) {
2014-06-03 16:39:29 +00:00
return new Promise ( function ( resolve , reject ) {
2014-06-04 01:09:04 +00:00
sendMessageProto ( [ number ] , proto , function ( res ) {
if ( res . failure . length > 0 )
reject ( res . failure [ 0 ] . error ) ;
else
resolve ( ) ;
2014-06-03 16:39:29 +00:00
} ) ;
} ) ;
}
2014-06-06 02:26:01 +00:00
sendGroupProto = function ( numbers , proto ) {
2014-06-05 23:20:09 +00:00
var me = textsecure . utils . unencodeNumber ( textsecure . storage . getUnencrypted ( "number_id" ) ) [ 0 ] ;
numbers = numbers . filter ( function ( number ) { return number != me ; } ) ;
2014-06-03 16:39:29 +00:00
return new Promise ( function ( resolve , reject ) {
2014-06-04 01:09:04 +00:00
sendMessageProto ( numbers , proto , function ( res ) {
if ( res . failure . length > 0 )
reject ( res . failure ) ;
else
resolve ( ) ;
2014-06-03 16:39:29 +00:00
} ) ;
} ) ;
}
2014-06-04 01:09:04 +00:00
self . sendMessageToNumber = function ( number , messageText , attachments ) {
var proto = new textsecure . protos . PushMessageContentProtobuf ( ) ;
proto . body = messageText ;
var promises = [ ] ;
2014-07-23 01:33:35 +00:00
for ( var i in attachments )
2014-06-04 01:09:04 +00:00
promises . push ( makeAttachmentPointer ( attachments [ i ] ) ) ;
return Promise . all ( promises ) . then ( function ( attachmentsArray ) {
proto . attachments = attachmentsArray ;
return sendIndividualProto ( number , proto ) ;
} ) ;
}
2014-06-03 16:39:29 +00:00
self . closeSession = function ( number ) {
2014-06-04 01:09:04 +00:00
var proto = new textsecure . protos . PushMessageContentProtobuf ( ) ;
2014-07-25 00:15:27 +00:00
proto . body = "TERMINATE" ;
2014-06-04 01:09:04 +00:00
proto . flags = textsecure . protos . PushMessageContentProtobuf . Flags . END _SESSION ;
2014-07-25 00:15:27 +00:00
return sendIndividualProto ( number , proto ) . then ( function ( res ) {
var devices = textsecure . storage . devices . getDeviceObjectsForNumber ( number ) ;
for ( var i in devices )
textsecure . crypto . closeOpenSessionForDevice ( devices [ i ] . encodedNumber ) ;
return res ;
} ) ;
2014-06-04 01:09:04 +00:00
}
self . sendMessageToGroup = function ( groupId , messageText , attachments ) {
var proto = new textsecure . protos . PushMessageContentProtobuf ( ) ;
proto . body = messageText ;
proto . group = new textsecure . protos . PushMessageContentProtobuf . GroupContext ( ) ;
proto . group . id = groupId ;
proto . group . type = textsecure . protos . PushMessageContentProtobuf . GroupContext . DELIVER ;
var numbers = textsecure . storage . groups . getNumbers ( groupId ) ;
2014-06-04 02:18:14 +00:00
if ( numbers === undefined )
return new Promise ( function ( resolve , reject ) { reject ( new Error ( "Unknown Group" ) ) ; } ) ;
2014-06-04 01:09:04 +00:00
var promises = [ ] ;
2014-07-23 01:33:35 +00:00
for ( var i in attachments )
2014-06-04 01:09:04 +00:00
promises . push ( makeAttachmentPointer ( attachments [ i ] ) ) ;
return Promise . all ( promises ) . then ( function ( attachmentsArray ) {
proto . attachments = attachmentsArray ;
return sendGroupProto ( numbers , proto ) ;
} ) ;
}
self . createGroup = function ( numbers , name , avatar ) {
var proto = new textsecure . protos . PushMessageContentProtobuf ( ) ;
proto . group = new textsecure . protos . PushMessageContentProtobuf . GroupContext ( ) ;
2014-06-05 23:20:09 +00:00
var group = textsecure . storage . groups . createNewGroup ( numbers ) ;
proto . group . id = group . id ;
2014-06-06 02:26:01 +00:00
var numbers = group . numbers ;
2014-06-05 23:20:09 +00:00
2014-06-04 01:09:04 +00:00
proto . group . type = textsecure . protos . PushMessageContentProtobuf . GroupContext . UPDATE ;
proto . group . members = numbers ;
proto . group . name = name ;
2014-06-05 23:20:09 +00:00
if ( avatar !== undefined ) {
return makeAttachmentPointer ( avatar ) . then ( function ( attachment ) {
proto . group . avatar = attachment ;
return sendGroupProto ( numbers , proto ) . then ( function ( ) {
return proto . group . id ;
} ) ;
} ) ;
} else {
2014-06-04 01:09:04 +00:00
return sendGroupProto ( numbers , proto ) . then ( function ( ) {
return proto . group . id ;
2014-06-03 21:44:30 +00:00
} ) ;
2014-06-05 23:20:09 +00:00
}
2014-06-03 16:39:29 +00:00
}
2014-06-04 01:09:04 +00:00
self . addNumberToGroup = function ( groupId , number ) {
var proto = new textsecure . protos . PushMessageContentProtobuf ( ) ;
proto . group = new textsecure . protos . PushMessageContentProtobuf . GroupContext ( ) ;
proto . group . id = groupId ;
proto . group . type = textsecure . protos . PushMessageContentProtobuf . GroupContext . UPDATE ;
2014-06-06 02:26:01 +00:00
var numbers = textsecure . storage . groups . addNumbers ( groupId , [ number ] ) ;
2014-06-04 02:18:14 +00:00
if ( numbers === undefined )
return new Promise ( function ( resolve , reject ) { reject ( new Error ( "Unknown Group" ) ) ; } ) ;
2014-06-04 01:09:04 +00:00
proto . group . members = numbers ;
return sendGroupProto ( numbers , proto ) ;
}
self . setGroupName = function ( groupId , name ) {
var proto = new textsecure . protos . PushMessageContentProtobuf ( ) ;
proto . group = new textsecure . protos . PushMessageContentProtobuf . GroupContext ( ) ;
proto . group . id = groupId ;
proto . group . type = textsecure . protos . PushMessageContentProtobuf . GroupContext . UPDATE ;
proto . group . name = name ;
var numbers = textsecure . storage . groups . getNumbers ( groupId ) ;
2014-06-04 02:18:14 +00:00
if ( numbers === undefined )
return new Promise ( function ( resolve , reject ) { reject ( new Error ( "Unknown Group" ) ) ; } ) ;
2014-06-04 01:09:04 +00:00
proto . group . members = numbers ;
return sendGroupProto ( numbers , proto ) ;
}
self . setGroupAvatar = function ( groupId , avatar ) {
var proto = new textsecure . protos . PushMessageContentProtobuf ( ) ;
proto . group = new textsecure . protos . PushMessageContentProtobuf . GroupContext ( ) ;
proto . group . id = groupId ;
proto . group . type = textsecure . protos . PushMessageContentProtobuf . GroupContext . UPDATE ;
var numbers = textsecure . storage . groups . getNumbers ( groupId ) ;
2014-06-04 02:18:14 +00:00
if ( numbers === undefined )
return new Promise ( function ( resolve , reject ) { reject ( new Error ( "Unknown Group" ) ) ; } ) ;
2014-06-04 01:09:04 +00:00
proto . group . members = numbers ;
return makeAttachmentPointer ( avatar ) . then ( function ( attachment ) {
proto . group . avatar = attachment ;
return sendGroupProto ( numbers , proto ) ;
} ) ;
}
self . leaveGroup = function ( groupId ) {
var proto = new textsecure . protos . PushMessageContentProtobuf ( ) ;
proto . group = new textsecure . protos . PushMessageContentProtobuf . GroupContext ( ) ;
proto . group . id = groupId ;
proto . group . type = textsecure . protos . PushMessageContentProtobuf . GroupContext . QUIT ;
var numbers = textsecure . storage . groups . getNumbers ( groupId ) ;
2014-06-04 02:18:14 +00:00
if ( numbers === undefined )
return new Promise ( function ( resolve , reject ) { reject ( new Error ( "Unknown Group" ) ) ; } ) ;
2014-06-04 01:09:04 +00:00
textsecure . storage . groups . deleteGroup ( groupId ) ;
return sendGroupProto ( numbers , proto ) ;
}
2014-06-03 16:39:29 +00:00
return self ;
} ( ) ;