248 lines
10 KiB
JavaScript
248 lines
10 KiB
JavaScript
/* vim: ts=4:sw=4:expandtab
|
|
*
|
|
* This program is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU Lesser General Public License as published by
|
|
* the Free Software Foundation, either version 3 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU Lesser General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU Lesser General Public License
|
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
*/
|
|
(function () {
|
|
'use strict';
|
|
window.Whisper = window.Whisper || {};
|
|
|
|
var Message = Backbone.Model.extend({
|
|
database : Whisper.Database,
|
|
storeName : 'messages',
|
|
defaults : function() {
|
|
return {
|
|
timestamp: new Date().getTime(),
|
|
attachments: []
|
|
};
|
|
},
|
|
validate: function(attributes, options) {
|
|
var required = ['conversationId', 'received_at', 'sent_at'];
|
|
var missing = _.filter(required, function(attr) { return !attributes[attr]; });
|
|
if (missing.length) {
|
|
console.log("Message missing attributes: " + missing);
|
|
}
|
|
},
|
|
isEndSession: function() {
|
|
var flag = textsecure.protobuf.PushMessageContent.Flags.END_SESSION;
|
|
return !!(this.get('flags') & flag);
|
|
},
|
|
isGroupUpdate: function() {
|
|
return !!(this.get('group_update'));
|
|
},
|
|
isIncoming: function() {
|
|
return this.get('type') === 'incoming';
|
|
},
|
|
getDescription: function() {
|
|
if (this.isGroupUpdate()) {
|
|
return 'Updated the group';
|
|
}
|
|
if (this.isEndSession()) {
|
|
return 'Secure session ended';
|
|
}
|
|
if (this.isIncoming() && this.hasKeyConflicts()) {
|
|
return 'Received message with unknown identity key.';
|
|
}
|
|
|
|
return this.get('body');
|
|
},
|
|
getContact: function() {
|
|
if (this.collection) {
|
|
return this.collection.conversation.contactCollection.get(this.get('source'));
|
|
}
|
|
},
|
|
isOutgoing: function() {
|
|
return this.get('type') === 'outgoing';
|
|
},
|
|
hasKeyConflicts: function() {
|
|
return _.any(this.get('errors'), function(e) {
|
|
return (e.name === 'IncomingIdentityKeyError' ||
|
|
e.name === 'OutgoingIdentityKeyError');
|
|
});
|
|
},
|
|
hasKeyConflict: function(number) {
|
|
return _.any(this.get('errors'), function(e) {
|
|
return (e.name === 'IncomingIdentityKeyError' ||
|
|
e.name === 'OutgoingIdentityKeyError') &&
|
|
e.number === number;
|
|
});
|
|
},
|
|
getKeyConflict: function(number) {
|
|
return _.find(this.get('errors'), function(e) {
|
|
return (e.name === 'IncomingIdentityKeyError' ||
|
|
e.name === 'OutgoingIdentityKeyError') &&
|
|
e.number === number;
|
|
});
|
|
},
|
|
resolveConflict: function(number) {
|
|
var error = this.getKeyConflict(number);
|
|
if (error) {
|
|
var promise = new textsecure.ReplayableError(error).replay();
|
|
if (this.isIncoming()) {
|
|
promise.then(function(pushMessageContent) {
|
|
this.handlePushMessageContent(pushMessageContent);
|
|
this.save('errors', []);
|
|
}.bind(this)).catch(function(e) {
|
|
//this.save('errors', [_.pick(e, ['name', 'message'])]);
|
|
var errors = this.get('errors').concat(
|
|
_.pick(e, ['name', 'message'])
|
|
);
|
|
this.save('errors', errors);
|
|
}.bind(this));
|
|
} else {
|
|
promise.then(function() {
|
|
this.save('errors', _.reject(this.get('errors'), function(e) {
|
|
return e.name === 'OutgoingIdentityKeyError' &&
|
|
e.number === number;
|
|
}));
|
|
}.bind(this));
|
|
}
|
|
}
|
|
},
|
|
handlePushMessageContent: function(pushMessageContent) {
|
|
// This function can be called from the background script on an
|
|
// incoming message or from the frontend after the user accepts an
|
|
// identity key change.
|
|
var message = this;
|
|
var source = message.get('source');
|
|
var timestamp = message.get('sent_at');
|
|
return textsecure.processDecrypted(pushMessageContent, source).then(function(pushMessageContent) {
|
|
var type = 'incoming';
|
|
if (pushMessageContent.sync) {
|
|
type = 'outgoing';
|
|
timestamp = pushMessageContent.sync.timestamp;
|
|
}
|
|
var now = new Date().getTime();
|
|
|
|
var conversationId = source;
|
|
if (pushMessageContent.sync) {
|
|
conversationId = pushMessageContent.sync.destination;
|
|
} else if (pushMessageContent.group) {
|
|
conversationId = pushMessageContent.group.id;
|
|
}
|
|
var conversation = new Whisper.Conversation({id: conversationId});
|
|
var attributes = {};
|
|
conversation.fetch().always(function() {
|
|
if (pushMessageContent.group) {
|
|
var group_update = {};
|
|
attributes = {
|
|
type: 'group',
|
|
groupId: pushMessageContent.group.id,
|
|
};
|
|
if (pushMessageContent.group.type === textsecure.protobuf.PushMessageContent.GroupContext.Type.UPDATE) {
|
|
attributes = {
|
|
type : 'group',
|
|
groupId : pushMessageContent.group.id,
|
|
name : pushMessageContent.group.name,
|
|
avatar : pushMessageContent.group.avatar,
|
|
members : pushMessageContent.group.members,
|
|
};
|
|
group_update = conversation.changedAttributes(_.pick(pushMessageContent.group, 'name', 'avatar'));
|
|
var difference = _.difference(pushMessageContent.group.members, conversation.get('members'));
|
|
if (difference.length > 0) {
|
|
group_update.joined = difference;
|
|
}
|
|
}
|
|
else if (pushMessageContent.group.type === textsecure.protobuf.PushMessageContent.GroupContext.Type.QUIT) {
|
|
group_update = { left: source };
|
|
attributes.members = _.without(conversation.get('members'), source);
|
|
}
|
|
|
|
if (_.keys(group_update).length > 0) {
|
|
message.set({group_update: group_update});
|
|
}
|
|
}
|
|
attributes.active_at = now;
|
|
if (type === 'incoming') {
|
|
attributes.unreadCount = conversation.get('unreadCount') + 1;
|
|
}
|
|
conversation.set(attributes);
|
|
|
|
message.set({
|
|
body : pushMessageContent.body,
|
|
conversationId : conversation.id,
|
|
attachments : pushMessageContent.attachments,
|
|
decrypted_at : now,
|
|
type : type,
|
|
sent_at : timestamp,
|
|
flags : pushMessageContent.flags,
|
|
errors : []
|
|
});
|
|
|
|
if (message.get('sent_at') > conversation.get('timestamp')) {
|
|
conversation.set({
|
|
timestamp: message.get('sent_at'),
|
|
lastMessage: message.get('body')
|
|
});
|
|
}
|
|
|
|
conversation.save().then(function() {
|
|
message.save().then(function() {
|
|
extension.trigger('message', message); // notify frontend listeners
|
|
notifyConversation(message);
|
|
});
|
|
});
|
|
});
|
|
});
|
|
}
|
|
|
|
});
|
|
|
|
Whisper.MessageCollection = Backbone.Collection.extend({
|
|
model : Message,
|
|
database : Whisper.Database,
|
|
storeName : 'messages',
|
|
comparator : 'received_at',
|
|
initialize : function(models, options) {
|
|
if (options) {
|
|
this.conversation = options.conversation;
|
|
}
|
|
},
|
|
destroyAll : function () {
|
|
return Promise.all(this.models.map(function(m) {
|
|
return new Promise(function(resolve, reject) {
|
|
m.destroy().then(resolve).fail(reject);
|
|
});
|
|
}));
|
|
},
|
|
|
|
fetchSentAt: function(timestamp) {
|
|
return this.fetch({
|
|
index: {
|
|
// 'receipt' index on sent_at
|
|
name: 'receipt',
|
|
only: timestamp
|
|
}
|
|
});
|
|
},
|
|
|
|
fetchConversation: function(conversationId, options) {
|
|
options = options || {};
|
|
options.index = {
|
|
// 'conversation' index on [conversationId, received_at]
|
|
name : 'conversation',
|
|
lower : [conversationId],
|
|
upper : [conversationId, Number.MAX_VALUE]
|
|
// SELECT messages WHERE conversationId = this.id ORDER
|
|
// received_at DESC
|
|
};
|
|
// TODO pagination/infinite scroll
|
|
// limit: 10, offset: page*10,
|
|
return this.fetch(options);
|
|
},
|
|
|
|
hasKeyConflicts: function() {
|
|
return this.any(function(m) { return m.hasKeyConflicts(); });
|
|
}
|
|
});
|
|
})();
|