Remove groups table, conversation is single source of truth
This commit is contained in:
parent
b69eea543c
commit
5b54c9554e
16 changed files with 214 additions and 912 deletions
49
app/sql.js
49
app/sql.js
|
@ -16,14 +16,6 @@ module.exports = {
|
||||||
removeDB,
|
removeDB,
|
||||||
removeIndexedDBFiles,
|
removeIndexedDBFiles,
|
||||||
|
|
||||||
createOrUpdateGroup,
|
|
||||||
getGroupById,
|
|
||||||
getAllGroupIds,
|
|
||||||
getAllGroups,
|
|
||||||
bulkAddGroups,
|
|
||||||
removeGroupById,
|
|
||||||
removeAllGroups,
|
|
||||||
|
|
||||||
createOrUpdateIdentityKey,
|
createOrUpdateIdentityKey,
|
||||||
getIdentityKeyById,
|
getIdentityKeyById,
|
||||||
bulkAddIdentityKeys,
|
bulkAddIdentityKeys,
|
||||||
|
@ -625,6 +617,20 @@ async function updateToSchemaVersion10(currentVersion, instance) {
|
||||||
console.log('updateToSchemaVersion10: success!');
|
console.log('updateToSchemaVersion10: success!');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function updateToSchemaVersion11(currentVersion, instance) {
|
||||||
|
if (currentVersion >= 11) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
console.log('updateToSchemaVersion11: starting...');
|
||||||
|
await instance.run('BEGIN TRANSACTION;');
|
||||||
|
|
||||||
|
await instance.run('DROP TABLE groups;');
|
||||||
|
|
||||||
|
await instance.run('PRAGMA schema_version = 11;');
|
||||||
|
await instance.run('COMMIT TRANSACTION;');
|
||||||
|
console.log('updateToSchemaVersion11: success!');
|
||||||
|
}
|
||||||
|
|
||||||
const SCHEMA_VERSIONS = [
|
const SCHEMA_VERSIONS = [
|
||||||
updateToSchemaVersion1,
|
updateToSchemaVersion1,
|
||||||
updateToSchemaVersion2,
|
updateToSchemaVersion2,
|
||||||
|
@ -636,6 +642,7 @@ const SCHEMA_VERSIONS = [
|
||||||
updateToSchemaVersion8,
|
updateToSchemaVersion8,
|
||||||
updateToSchemaVersion9,
|
updateToSchemaVersion9,
|
||||||
updateToSchemaVersion10,
|
updateToSchemaVersion10,
|
||||||
|
updateToSchemaVersion11,
|
||||||
];
|
];
|
||||||
|
|
||||||
async function updateSchema(instance) {
|
async function updateSchema(instance) {
|
||||||
|
@ -726,31 +733,6 @@ async function removeIndexedDBFiles() {
|
||||||
indexedDBPath = null;
|
indexedDBPath = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const GROUPS_TABLE = 'groups';
|
|
||||||
async function createOrUpdateGroup(data) {
|
|
||||||
return createOrUpdate(GROUPS_TABLE, data);
|
|
||||||
}
|
|
||||||
async function getGroupById(id) {
|
|
||||||
return getById(GROUPS_TABLE, id);
|
|
||||||
}
|
|
||||||
async function getAllGroupIds() {
|
|
||||||
const rows = await db.all('SELECT id FROM groups ORDER BY id ASC;');
|
|
||||||
return map(rows, row => row.id);
|
|
||||||
}
|
|
||||||
async function getAllGroups() {
|
|
||||||
const rows = await db.all('SELECT json FROM groups ORDER BY id ASC;');
|
|
||||||
return map(rows, row => jsonToObject(row.json));
|
|
||||||
}
|
|
||||||
async function bulkAddGroups(array) {
|
|
||||||
return bulkAdd(GROUPS_TABLE, array);
|
|
||||||
}
|
|
||||||
async function removeGroupById(id) {
|
|
||||||
return removeById(GROUPS_TABLE, id);
|
|
||||||
}
|
|
||||||
async function removeAllGroups() {
|
|
||||||
return removeAllFromTable(GROUPS_TABLE);
|
|
||||||
}
|
|
||||||
|
|
||||||
const IDENTITY_KEYS_TABLE = 'identityKeys';
|
const IDENTITY_KEYS_TABLE = 'identityKeys';
|
||||||
async function createOrUpdateIdentityKey(data) {
|
async function createOrUpdateIdentityKey(data) {
|
||||||
return createOrUpdate(IDENTITY_KEYS_TABLE, data);
|
return createOrUpdate(IDENTITY_KEYS_TABLE, data);
|
||||||
|
@ -1701,7 +1683,6 @@ async function removeAll() {
|
||||||
promise = Promise.all([
|
promise = Promise.all([
|
||||||
db.run('BEGIN TRANSACTION;'),
|
db.run('BEGIN TRANSACTION;'),
|
||||||
db.run('DELETE FROM conversations;'),
|
db.run('DELETE FROM conversations;'),
|
||||||
db.run('DELETE FROM groups;'),
|
|
||||||
db.run('DELETE FROM identityKeys;'),
|
db.run('DELETE FROM identityKeys;'),
|
||||||
db.run('DELETE FROM items;'),
|
db.run('DELETE FROM items;'),
|
||||||
db.run('DELETE FROM messages;'),
|
db.run('DELETE FROM messages;'),
|
||||||
|
|
|
@ -615,7 +615,6 @@
|
||||||
<script type='text/javascript' src='js/views/conversation_list_item_view.js'></script>
|
<script type='text/javascript' src='js/views/conversation_list_item_view.js'></script>
|
||||||
<script type='text/javascript' src='js/views/conversation_list_view.js'></script>
|
<script type='text/javascript' src='js/views/conversation_list_view.js'></script>
|
||||||
<script type='text/javascript' src='js/views/contact_list_view.js'></script>
|
<script type='text/javascript' src='js/views/contact_list_view.js'></script>
|
||||||
<script type='text/javascript' src='js/views/new_group_update_view.js'></script>
|
|
||||||
<script type='text/javascript' src='js/views/attachment_view.js'></script>
|
<script type='text/javascript' src='js/views/attachment_view.js'></script>
|
||||||
<script type='text/javascript' src='js/views/timestamp_view.js'></script>
|
<script type='text/javascript' src='js/views/timestamp_view.js'></script>
|
||||||
<script type='text/javascript' src='js/views/message_view.js'></script>
|
<script type='text/javascript' src='js/views/message_view.js'></script>
|
||||||
|
|
|
@ -210,14 +210,16 @@
|
||||||
sendTypingMessage(isTyping) {
|
sendTypingMessage(isTyping) {
|
||||||
const groupId = !this.isPrivate() ? this.id : null;
|
const groupId = !this.isPrivate() ? this.id : null;
|
||||||
const recipientId = this.isPrivate() ? this.id : null;
|
const recipientId = this.isPrivate() ? this.id : null;
|
||||||
|
const groupNumbers = this.getRecipients();
|
||||||
|
|
||||||
const sendOptions = this.getSendOptions();
|
const sendOptions = this.getSendOptions();
|
||||||
this.wrapSend(
|
this.wrapSend(
|
||||||
textsecure.messaging.sendTypingMessage(
|
textsecure.messaging.sendTypingMessage(
|
||||||
{
|
{
|
||||||
groupId,
|
|
||||||
isTyping,
|
isTyping,
|
||||||
recipientId,
|
recipientId,
|
||||||
|
groupId,
|
||||||
|
groupNumbers,
|
||||||
},
|
},
|
||||||
sendOptions
|
sendOptions
|
||||||
)
|
)
|
||||||
|
@ -929,12 +931,36 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
const conversationType = this.get('type');
|
const conversationType = this.get('type');
|
||||||
const sendFunction = (() => {
|
const options = this.getSendOptions();
|
||||||
|
const groupNumbers = this.getRecipients();
|
||||||
|
|
||||||
|
const promise = (() => {
|
||||||
switch (conversationType) {
|
switch (conversationType) {
|
||||||
case Message.PRIVATE:
|
case Message.PRIVATE:
|
||||||
return textsecure.messaging.sendMessageToNumber;
|
return textsecure.messaging.sendMessageToNumber(
|
||||||
|
destination,
|
||||||
|
body,
|
||||||
|
attachmentsWithData,
|
||||||
|
quote,
|
||||||
|
preview,
|
||||||
|
now,
|
||||||
|
expireTimer,
|
||||||
|
profileKey,
|
||||||
|
options
|
||||||
|
);
|
||||||
case Message.GROUP:
|
case Message.GROUP:
|
||||||
return textsecure.messaging.sendMessageToGroup;
|
return textsecure.messaging.sendMessageToGroup(
|
||||||
|
destination,
|
||||||
|
groupNumbers,
|
||||||
|
body,
|
||||||
|
attachmentsWithData,
|
||||||
|
quote,
|
||||||
|
preview,
|
||||||
|
now,
|
||||||
|
expireTimer,
|
||||||
|
profileKey,
|
||||||
|
options
|
||||||
|
);
|
||||||
default:
|
default:
|
||||||
throw new TypeError(
|
throw new TypeError(
|
||||||
`Invalid conversation type: '${conversationType}'`
|
`Invalid conversation type: '${conversationType}'`
|
||||||
|
@ -942,22 +968,7 @@
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
|
|
||||||
const options = this.getSendOptions();
|
return message.send(this.wrapSend(promise));
|
||||||
return message.send(
|
|
||||||
this.wrapSend(
|
|
||||||
sendFunction(
|
|
||||||
destination,
|
|
||||||
body,
|
|
||||||
attachmentsWithData,
|
|
||||||
quote,
|
|
||||||
preview,
|
|
||||||
now,
|
|
||||||
expireTimer,
|
|
||||||
profileKey,
|
|
||||||
options
|
|
||||||
)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -1239,25 +1250,31 @@
|
||||||
return message;
|
return message;
|
||||||
}
|
}
|
||||||
|
|
||||||
let sendFunc;
|
|
||||||
if (this.get('type') === 'private') {
|
|
||||||
sendFunc = textsecure.messaging.sendExpirationTimerUpdateToNumber;
|
|
||||||
} else {
|
|
||||||
sendFunc = textsecure.messaging.sendExpirationTimerUpdateToGroup;
|
|
||||||
}
|
|
||||||
let profileKey;
|
let profileKey;
|
||||||
if (this.get('profileSharing')) {
|
if (this.get('profileSharing')) {
|
||||||
profileKey = storage.get('profileKey');
|
profileKey = storage.get('profileKey');
|
||||||
}
|
}
|
||||||
|
|
||||||
const sendOptions = this.getSendOptions();
|
const sendOptions = this.getSendOptions();
|
||||||
const promise = sendFunc(
|
let promise;
|
||||||
this.get('id'),
|
|
||||||
this.get('expireTimer'),
|
if (this.get('type') === 'private') {
|
||||||
message.get('sent_at'),
|
promise = textsecure.messaging.sendExpirationTimerUpdateToNumber(
|
||||||
profileKey,
|
this.get('id'),
|
||||||
sendOptions
|
this.get('expireTimer'),
|
||||||
);
|
message.get('sent_at'),
|
||||||
|
profileKey,
|
||||||
|
sendOptions
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
promise = textsecure.messaging.sendExpirationTimerUpdateToGroup(
|
||||||
|
this.get('id'),
|
||||||
|
this.getRecipients(),
|
||||||
|
this.get('expireTimer'),
|
||||||
|
message.get('sent_at'),
|
||||||
|
profileKey,
|
||||||
|
sendOptions
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
await message.send(this.wrapSend(promise));
|
await message.send(this.wrapSend(promise));
|
||||||
|
|
||||||
|
@ -1335,6 +1352,7 @@
|
||||||
async leaveGroup() {
|
async leaveGroup() {
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
if (this.get('type') === 'group') {
|
if (this.get('type') === 'group') {
|
||||||
|
const groupNumbers = this.getRecipients();
|
||||||
this.set({ left: true });
|
this.set({ left: true });
|
||||||
await window.Signal.Data.updateConversation(this.id, this.attributes, {
|
await window.Signal.Data.updateConversation(this.id, this.attributes, {
|
||||||
Conversation: Whisper.Conversation,
|
Conversation: Whisper.Conversation,
|
||||||
|
@ -1355,7 +1373,9 @@
|
||||||
|
|
||||||
const options = this.getSendOptions();
|
const options = this.getSendOptions();
|
||||||
message.send(
|
message.send(
|
||||||
this.wrapSend(textsecure.messaging.leaveGroup(this.id, options))
|
this.wrapSend(
|
||||||
|
textsecure.messaging.leaveGroup(this.id, groupNumbers, options)
|
||||||
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
@ -706,21 +706,35 @@
|
||||||
this.isReplayableError.bind(this)
|
this.isReplayableError.bind(this)
|
||||||
);
|
);
|
||||||
|
|
||||||
// Remove the errors that aren't replayable
|
// Put the errors back which aren't replayable
|
||||||
this.set({ errors });
|
this.set({ errors });
|
||||||
|
|
||||||
const profileKey = null;
|
const conversation = this.getConversation();
|
||||||
let numbers = retries
|
const intendedRecipients = this.get('recipients') || [];
|
||||||
|
const currentRecipients = conversation.getRecipients();
|
||||||
|
|
||||||
|
const profileKey = conversation.get('profileSharing')
|
||||||
|
? storage.get('profileKey')
|
||||||
|
: null;
|
||||||
|
|
||||||
|
const errorNumbers = retries
|
||||||
.map(retry => retry.number)
|
.map(retry => retry.number)
|
||||||
.filter(item => Boolean(item));
|
.filter(item => Boolean(item));
|
||||||
|
let numbers = _.intersection(
|
||||||
|
errorNumbers,
|
||||||
|
intendedRecipients,
|
||||||
|
currentRecipients
|
||||||
|
);
|
||||||
|
|
||||||
if (!numbers.length) {
|
if (!numbers.length) {
|
||||||
window.log.warn(
|
window.log.warn(
|
||||||
'retrySend: No numbers in error set, using all recipients'
|
'retrySend: No numbers in error set, using all recipients'
|
||||||
);
|
);
|
||||||
const conversation = this.getConversation();
|
|
||||||
if (conversation) {
|
if (conversation) {
|
||||||
numbers = conversation.getRecipients();
|
numbers = _.intersection(currentRecipients, intendedRecipients);
|
||||||
|
// We clear all errors here to start with a fresh slate, since we are starting
|
||||||
|
// from scratch on this message with a fresh set of recipients
|
||||||
this.set({ errors: null });
|
this.set({ errors: null });
|
||||||
} else {
|
} else {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
|
@ -752,7 +766,6 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
let promise;
|
let promise;
|
||||||
const conversation = this.getConversation();
|
|
||||||
const options = conversation.getSendOptions();
|
const options = conversation.getSendOptions();
|
||||||
|
|
||||||
if (conversation.isPrivate()) {
|
if (conversation.isPrivate()) {
|
||||||
|
|
|
@ -109,9 +109,9 @@ function createOutputStream(writer) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async function exportContactAndGroupsToFile(parent) {
|
async function exportConversationListToFile(parent) {
|
||||||
const writer = await createFileAndWriter(parent, 'db.json');
|
const writer = await createFileAndWriter(parent, 'db.json');
|
||||||
return exportContactsAndGroups(writer);
|
return exportConversationList(writer);
|
||||||
}
|
}
|
||||||
|
|
||||||
function writeArray(stream, array) {
|
function writeArray(stream, array) {
|
||||||
|
@ -137,7 +137,7 @@ function getPlainJS(collection) {
|
||||||
return collection.map(model => model.attributes);
|
return collection.map(model => model.attributes);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function exportContactsAndGroups(fileWriter) {
|
async function exportConversationList(fileWriter) {
|
||||||
const stream = createOutputStream(fileWriter);
|
const stream = createOutputStream(fileWriter);
|
||||||
|
|
||||||
stream.write('{');
|
stream.write('{');
|
||||||
|
@ -149,13 +149,6 @@ async function exportContactsAndGroups(fileWriter) {
|
||||||
window.log.info(`Exporting ${conversations.length} conversations`);
|
window.log.info(`Exporting ${conversations.length} conversations`);
|
||||||
writeArray(stream, getPlainJS(conversations));
|
writeArray(stream, getPlainJS(conversations));
|
||||||
|
|
||||||
stream.write(',');
|
|
||||||
|
|
||||||
stream.write('"groups": ');
|
|
||||||
const groups = await window.Signal.Data.getAllGroups();
|
|
||||||
window.log.info(`Exporting ${groups.length} groups`);
|
|
||||||
writeArray(stream, groups);
|
|
||||||
|
|
||||||
stream.write('}');
|
stream.write('}');
|
||||||
await stream.close();
|
await stream.close();
|
||||||
}
|
}
|
||||||
|
@ -167,7 +160,7 @@ async function importNonMessages(parent, options) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function eliminateClientConfigInBackup(data, targetPath) {
|
function eliminateClientConfigInBackup(data, targetPath) {
|
||||||
const cleaned = _.pick(data, 'conversations', 'groups');
|
const cleaned = _.pick(data, 'conversations');
|
||||||
window.log.info('Writing configuration-free backup file back to disk');
|
window.log.info('Writing configuration-free backup file back to disk');
|
||||||
try {
|
try {
|
||||||
fs.writeFileSync(targetPath, JSON.stringify(cleaned));
|
fs.writeFileSync(targetPath, JSON.stringify(cleaned));
|
||||||
|
@ -223,10 +216,8 @@ async function importFromJsonString(jsonString, targetPath, options) {
|
||||||
_.defaults(options, {
|
_.defaults(options, {
|
||||||
forceLightImport: false,
|
forceLightImport: false,
|
||||||
conversationLookup: {},
|
conversationLookup: {},
|
||||||
groupLookup: {},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const { groupLookup } = options;
|
|
||||||
const result = {
|
const result = {
|
||||||
fullImport: true,
|
fullImport: true,
|
||||||
};
|
};
|
||||||
|
@ -251,7 +242,7 @@ async function importFromJsonString(jsonString, targetPath, options) {
|
||||||
|
|
||||||
// We mutate the on-disk backup to prevent the user from importing client
|
// We mutate the on-disk backup to prevent the user from importing client
|
||||||
// configuration more than once - that causes lots of encryption errors.
|
// configuration more than once - that causes lots of encryption errors.
|
||||||
// This of course preserves the true data: conversations and groups.
|
// This of course preserves the true data: conversations.
|
||||||
eliminateClientConfigInBackup(importObject, targetPath);
|
eliminateClientConfigInBackup(importObject, targetPath);
|
||||||
|
|
||||||
const storeNames = _.keys(importObject);
|
const storeNames = _.keys(importObject);
|
||||||
|
@ -262,12 +253,12 @@ async function importFromJsonString(jsonString, targetPath, options) {
|
||||||
const remainingStoreNames = _.without(
|
const remainingStoreNames = _.without(
|
||||||
storeNames,
|
storeNames,
|
||||||
'conversations',
|
'conversations',
|
||||||
'unprocessed'
|
'unprocessed',
|
||||||
|
'groups' // in old data sets, but no longer included in database schema
|
||||||
);
|
);
|
||||||
await importConversationsFromJSON(conversations, options);
|
await importConversationsFromJSON(conversations, options);
|
||||||
|
|
||||||
const SAVE_FUNCTIONS = {
|
const SAVE_FUNCTIONS = {
|
||||||
groups: window.Signal.Data.createOrUpdateGroup,
|
|
||||||
identityKeys: window.Signal.Data.createOrUpdateIdentityKey,
|
identityKeys: window.Signal.Data.createOrUpdateIdentityKey,
|
||||||
items: window.Signal.Data.createOrUpdateItem,
|
items: window.Signal.Data.createOrUpdateItem,
|
||||||
preKeys: window.Signal.Data.createOrUpdatePreKey,
|
preKeys: window.Signal.Data.createOrUpdatePreKey,
|
||||||
|
@ -292,29 +283,17 @@ async function importFromJsonString(jsonString, targetPath, options) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let skipCount = 0;
|
|
||||||
|
|
||||||
for (let i = 0, max = toImport.length; i < max; i += 1) {
|
for (let i = 0, max = toImport.length; i < max; i += 1) {
|
||||||
const toAdd = unstringify(toImport[i]);
|
const toAdd = unstringify(toImport[i]);
|
||||||
|
// eslint-disable-next-line no-await-in-loop
|
||||||
const haveGroupAlready =
|
await save(toAdd);
|
||||||
storeName === 'groups' && groupLookup[getGroupKey(toAdd)];
|
|
||||||
|
|
||||||
if (haveGroupAlready) {
|
|
||||||
skipCount += 1;
|
|
||||||
} else {
|
|
||||||
// eslint-disable-next-line no-await-in-loop
|
|
||||||
await save(toAdd);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
window.log.info(
|
window.log.info(
|
||||||
'Done importing to store',
|
'Done importing to store',
|
||||||
storeName,
|
storeName,
|
||||||
'Total count:',
|
'Total count:',
|
||||||
toImport.length,
|
toImport.length
|
||||||
'Skipped:',
|
|
||||||
skipCount
|
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
@ -1160,14 +1139,6 @@ async function loadConversationLookup() {
|
||||||
return fromPairs(map(array, item => [getConversationKey(item), true]));
|
return fromPairs(map(array, item => [getConversationKey(item), true]));
|
||||||
}
|
}
|
||||||
|
|
||||||
function getGroupKey(group) {
|
|
||||||
return group.id;
|
|
||||||
}
|
|
||||||
async function loadGroupsLookup() {
|
|
||||||
const array = await window.Signal.Data.getAllGroupIds();
|
|
||||||
return fromPairs(map(array, item => [getGroupKey(item), true]));
|
|
||||||
}
|
|
||||||
|
|
||||||
function getDirectoryForExport() {
|
function getDirectoryForExport() {
|
||||||
return getDirectory();
|
return getDirectory();
|
||||||
}
|
}
|
||||||
|
@ -1254,7 +1225,7 @@ async function exportToDirectory(directory, options) {
|
||||||
|
|
||||||
const attachmentsDir = await createDirectory(directory, 'attachments');
|
const attachmentsDir = await createDirectory(directory, 'attachments');
|
||||||
|
|
||||||
await exportContactAndGroupsToFile(stagingDir);
|
await exportConversationListToFile(stagingDir);
|
||||||
await exportConversations(
|
await exportConversations(
|
||||||
Object.assign({}, options, {
|
Object.assign({}, options, {
|
||||||
messagesDir: stagingDir,
|
messagesDir: stagingDir,
|
||||||
|
@ -1298,13 +1269,11 @@ async function importFromDirectory(directory, options) {
|
||||||
const lookups = await Promise.all([
|
const lookups = await Promise.all([
|
||||||
loadMessagesLookup(),
|
loadMessagesLookup(),
|
||||||
loadConversationLookup(),
|
loadConversationLookup(),
|
||||||
loadGroupsLookup(),
|
|
||||||
]);
|
]);
|
||||||
const [messageLookup, conversationLookup, groupLookup] = lookups;
|
const [messageLookup, conversationLookup] = lookups;
|
||||||
options = Object.assign({}, options, {
|
options = Object.assign({}, options, {
|
||||||
messageLookup,
|
messageLookup,
|
||||||
conversationLookup,
|
conversationLookup,
|
||||||
groupLookup,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const archivePath = path.join(directory, ARCHIVE_NAME);
|
const archivePath = path.join(directory, ARCHIVE_NAME);
|
||||||
|
|
|
@ -47,14 +47,6 @@ module.exports = {
|
||||||
removeDB,
|
removeDB,
|
||||||
removeIndexedDBFiles,
|
removeIndexedDBFiles,
|
||||||
|
|
||||||
createOrUpdateGroup,
|
|
||||||
getGroupById,
|
|
||||||
getAllGroupIds,
|
|
||||||
getAllGroups,
|
|
||||||
bulkAddGroups,
|
|
||||||
removeGroupById,
|
|
||||||
removeAllGroups,
|
|
||||||
|
|
||||||
createOrUpdateIdentityKey,
|
createOrUpdateIdentityKey,
|
||||||
getIdentityKeyById,
|
getIdentityKeyById,
|
||||||
bulkAddIdentityKeys,
|
bulkAddIdentityKeys,
|
||||||
|
@ -395,33 +387,6 @@ async function removeIndexedDBFiles() {
|
||||||
await channels.removeIndexedDBFiles();
|
await channels.removeIndexedDBFiles();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Groups
|
|
||||||
|
|
||||||
async function createOrUpdateGroup(data) {
|
|
||||||
await channels.createOrUpdateGroup(data);
|
|
||||||
}
|
|
||||||
async function getGroupById(id) {
|
|
||||||
const group = await channels.getGroupById(id);
|
|
||||||
return group;
|
|
||||||
}
|
|
||||||
async function getAllGroupIds() {
|
|
||||||
const ids = await channels.getAllGroupIds();
|
|
||||||
return ids;
|
|
||||||
}
|
|
||||||
async function getAllGroups() {
|
|
||||||
const groups = await channels.getAllGroups();
|
|
||||||
return groups;
|
|
||||||
}
|
|
||||||
async function bulkAddGroups(array) {
|
|
||||||
await channels.bulkAddGroups(array);
|
|
||||||
}
|
|
||||||
async function removeGroupById(id) {
|
|
||||||
await channels.removeGroupById(id);
|
|
||||||
}
|
|
||||||
async function removeAllGroups() {
|
|
||||||
await channels.removeAllGroups();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Identity Keys
|
// Identity Keys
|
||||||
|
|
||||||
const IDENTITY_KEY_KEYS = ['publicKey'];
|
const IDENTITY_KEY_KEYS = ['publicKey'];
|
||||||
|
|
|
@ -2,14 +2,12 @@
|
||||||
|
|
||||||
const { includes, isFunction, isString, last, map } = require('lodash');
|
const { includes, isFunction, isString, last, map } = require('lodash');
|
||||||
const {
|
const {
|
||||||
bulkAddGroups,
|
|
||||||
bulkAddSessions,
|
bulkAddSessions,
|
||||||
bulkAddIdentityKeys,
|
bulkAddIdentityKeys,
|
||||||
bulkAddPreKeys,
|
bulkAddPreKeys,
|
||||||
bulkAddSignedPreKeys,
|
bulkAddSignedPreKeys,
|
||||||
bulkAddItems,
|
bulkAddItems,
|
||||||
|
|
||||||
removeGroupById,
|
|
||||||
removeSessionById,
|
removeSessionById,
|
||||||
removeIdentityKeyById,
|
removeIdentityKeyById,
|
||||||
removePreKeyById,
|
removePreKeyById,
|
||||||
|
@ -184,31 +182,6 @@ async function migrateToSQL({
|
||||||
complete = false;
|
complete = false;
|
||||||
lastIndex = null;
|
lastIndex = null;
|
||||||
|
|
||||||
while (!complete) {
|
|
||||||
// eslint-disable-next-line no-await-in-loop
|
|
||||||
const status = await migrateStoreToSQLite({
|
|
||||||
db,
|
|
||||||
// eslint-disable-next-line no-loop-func
|
|
||||||
save: bulkAddGroups,
|
|
||||||
remove: removeGroupById,
|
|
||||||
storeName: 'groups',
|
|
||||||
handleDOMException,
|
|
||||||
lastIndex,
|
|
||||||
batchSize: 10,
|
|
||||||
});
|
|
||||||
|
|
||||||
({ complete, lastIndex } = status);
|
|
||||||
}
|
|
||||||
window.log.info('migrateToSQL: migrate of groups complete');
|
|
||||||
try {
|
|
||||||
await clearStores(['groups']);
|
|
||||||
} catch (error) {
|
|
||||||
window.log.warn('Failed to clear groups store');
|
|
||||||
}
|
|
||||||
|
|
||||||
complete = false;
|
|
||||||
lastIndex = null;
|
|
||||||
|
|
||||||
while (!complete) {
|
while (!complete) {
|
||||||
// eslint-disable-next-line no-await-in-loop
|
// eslint-disable-next-line no-await-in-loop
|
||||||
const status = await migrateStoreToSQLite({
|
const status = await migrateStoreToSQLite({
|
||||||
|
|
|
@ -828,41 +828,6 @@
|
||||||
await textsecure.storage.protocol.removeAllSessions(number);
|
await textsecure.storage.protocol.removeAllSessions(number);
|
||||||
},
|
},
|
||||||
|
|
||||||
// Groups
|
|
||||||
|
|
||||||
async getGroup(groupId) {
|
|
||||||
if (groupId === null || groupId === undefined) {
|
|
||||||
throw new Error('Tried to get group for undefined/null id');
|
|
||||||
}
|
|
||||||
|
|
||||||
const group = await window.Signal.Data.getGroupById(groupId);
|
|
||||||
if (group) {
|
|
||||||
return group.data;
|
|
||||||
}
|
|
||||||
|
|
||||||
return undefined;
|
|
||||||
},
|
|
||||||
async putGroup(groupId, group) {
|
|
||||||
if (groupId === null || groupId === undefined) {
|
|
||||||
throw new Error('Tried to put group key for undefined/null id');
|
|
||||||
}
|
|
||||||
if (group === null || group === undefined) {
|
|
||||||
throw new Error('Tried to put undefined/null group object');
|
|
||||||
}
|
|
||||||
const data = {
|
|
||||||
id: groupId,
|
|
||||||
data: group,
|
|
||||||
};
|
|
||||||
await window.Signal.Data.createOrUpdateGroup(data);
|
|
||||||
},
|
|
||||||
async removeGroup(groupId) {
|
|
||||||
if (groupId === null || groupId === undefined) {
|
|
||||||
throw new Error('Tried to remove group key for undefined/null id');
|
|
||||||
}
|
|
||||||
|
|
||||||
await window.Signal.Data.removeGroupById(groupId);
|
|
||||||
},
|
|
||||||
|
|
||||||
// Not yet processed messages - for resiliency
|
// Not yet processed messages - for resiliency
|
||||||
getUnprocessedCount() {
|
getUnprocessedCount() {
|
||||||
return window.Signal.Data.getUnprocessedCount();
|
return window.Signal.Data.getUnprocessedCount();
|
||||||
|
|
|
@ -1135,16 +1135,7 @@
|
||||||
async showMembers(e, providedMembers, options = {}) {
|
async showMembers(e, providedMembers, options = {}) {
|
||||||
_.defaults(options, { needVerify: false });
|
_.defaults(options, { needVerify: false });
|
||||||
|
|
||||||
const fromConversation = this.model.isPrivate()
|
const model = providedMembers || this.model.contactCollection;
|
||||||
? [this.model.id]
|
|
||||||
: await textsecure.storage.groups.getNumbers(this.model.id);
|
|
||||||
const members =
|
|
||||||
providedMembers ||
|
|
||||||
fromConversation.map(id => ConversationController.get(id));
|
|
||||||
|
|
||||||
const model = this.model.getContactCollection();
|
|
||||||
model.reset(members);
|
|
||||||
|
|
||||||
const view = new Whisper.GroupMemberList({
|
const view = new Whisper.GroupMemberList({
|
||||||
model,
|
model,
|
||||||
// we pass this in to allow nested panels
|
// we pass this in to allow nested panels
|
||||||
|
|
|
@ -1,99 +0,0 @@
|
||||||
/* global Whisper, _ */
|
|
||||||
|
|
||||||
/* eslint-disable more/no-then */
|
|
||||||
|
|
||||||
// eslint-disable-next-line func-names
|
|
||||||
(function() {
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
window.Whisper = window.Whisper || {};
|
|
||||||
|
|
||||||
Whisper.NewGroupUpdateView = Whisper.View.extend({
|
|
||||||
tagName: 'div',
|
|
||||||
className: 'new-group-update',
|
|
||||||
templateName: 'new-group-update',
|
|
||||||
initialize(options) {
|
|
||||||
this.render();
|
|
||||||
this.avatarInput = new Whisper.FileInputView({
|
|
||||||
el: this.$('.group-avatar'),
|
|
||||||
window: options.window,
|
|
||||||
});
|
|
||||||
|
|
||||||
this.recipients_view = new Whisper.RecipientsInputView();
|
|
||||||
this.listenTo(this.recipients_view.typeahead, 'sync', () =>
|
|
||||||
this.model.contactCollection.models.forEach(model => {
|
|
||||||
if (this.recipients_view.typeahead.get(model)) {
|
|
||||||
this.recipients_view.typeahead.remove(model);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
);
|
|
||||||
this.recipients_view.$el.insertBefore(this.$('.container'));
|
|
||||||
|
|
||||||
this.member_list_view = new Whisper.ContactListView({
|
|
||||||
collection: this.model.contactCollection,
|
|
||||||
className: 'members',
|
|
||||||
});
|
|
||||||
this.member_list_view.render();
|
|
||||||
this.$('.scrollable').append(this.member_list_view.el);
|
|
||||||
},
|
|
||||||
events: {
|
|
||||||
'click .back': 'goBack',
|
|
||||||
'click .send': 'send',
|
|
||||||
'focusin input.search': 'showResults',
|
|
||||||
'focusout input.search': 'hideResults',
|
|
||||||
},
|
|
||||||
hideResults() {
|
|
||||||
this.$('.results').hide();
|
|
||||||
},
|
|
||||||
showResults() {
|
|
||||||
this.$('.results').show();
|
|
||||||
},
|
|
||||||
goBack() {
|
|
||||||
this.trigger('back');
|
|
||||||
},
|
|
||||||
render_attributes() {
|
|
||||||
return {
|
|
||||||
name: this.model.getTitle(),
|
|
||||||
avatar: this.model.getAvatar(),
|
|
||||||
};
|
|
||||||
},
|
|
||||||
async send() {
|
|
||||||
// When we turn this view on again, need to handle avatars in the new way
|
|
||||||
|
|
||||||
// const avatarFile = await this.avatarInput.getThumbnail();
|
|
||||||
const now = Date.now();
|
|
||||||
const attrs = {
|
|
||||||
timestamp: now,
|
|
||||||
active_at: now,
|
|
||||||
name: this.$('.name').val(),
|
|
||||||
members: _.union(
|
|
||||||
this.model.get('members'),
|
|
||||||
this.recipients_view.recipients.pluck('id')
|
|
||||||
),
|
|
||||||
};
|
|
||||||
|
|
||||||
// if (avatarFile) {
|
|
||||||
// attrs.avatar = avatarFile;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// Because we're no longer using Backbone-integrated saves, we need to manually
|
|
||||||
// clear the changed fields here so model.changed is accurate.
|
|
||||||
this.model.changed = {};
|
|
||||||
this.model.set(attrs);
|
|
||||||
const groupUpdate = this.model.changed;
|
|
||||||
|
|
||||||
await window.Signal.Data.updateConversation(
|
|
||||||
this.model.id,
|
|
||||||
this.model.attributes,
|
|
||||||
{ Conversation: Whisper.Conversation }
|
|
||||||
);
|
|
||||||
|
|
||||||
if (groupUpdate.avatar) {
|
|
||||||
this.model.trigger('change:avatar');
|
|
||||||
}
|
|
||||||
|
|
||||||
this.model.updateGroup(groupUpdate);
|
|
||||||
this.goBack();
|
|
||||||
},
|
|
||||||
});
|
|
||||||
})();
|
|
|
@ -1,183 +0,0 @@
|
||||||
/* global Whisper, Backbone, ConversationController */
|
|
||||||
|
|
||||||
// eslint-disable-next-line func-names
|
|
||||||
(function() {
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
window.Whisper = window.Whisper || {};
|
|
||||||
|
|
||||||
const ContactsTypeahead = Backbone.TypeaheadCollection.extend({
|
|
||||||
typeaheadAttributes: [
|
|
||||||
'name',
|
|
||||||
'e164_number',
|
|
||||||
'national_number',
|
|
||||||
'international_number',
|
|
||||||
],
|
|
||||||
model: Whisper.Conversation,
|
|
||||||
async fetchContacts() {
|
|
||||||
const models = window.Signal.Data.getAllPrivateConversations({
|
|
||||||
ConversationCollection: Whisper.ConversationCollection,
|
|
||||||
});
|
|
||||||
|
|
||||||
this.reset(models);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
Whisper.ContactPillView = Whisper.View.extend({
|
|
||||||
tagName: 'span',
|
|
||||||
className: 'recipient',
|
|
||||||
events: {
|
|
||||||
'click .remove': 'removeModel',
|
|
||||||
},
|
|
||||||
templateName: 'contact_pill',
|
|
||||||
initialize() {
|
|
||||||
const error = this.model.validate(this.model.attributes);
|
|
||||||
if (error) {
|
|
||||||
this.$el.addClass('error');
|
|
||||||
}
|
|
||||||
},
|
|
||||||
removeModel() {
|
|
||||||
this.$el.trigger('remove', { modelId: this.model.id });
|
|
||||||
this.remove();
|
|
||||||
},
|
|
||||||
render_attributes() {
|
|
||||||
return { name: this.model.getTitle() };
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
Whisper.RecipientListView = Whisper.ListView.extend({
|
|
||||||
itemView: Whisper.ContactPillView,
|
|
||||||
});
|
|
||||||
|
|
||||||
Whisper.SuggestionView = Whisper.ConversationListItemView.extend({
|
|
||||||
className: 'contact-details contact',
|
|
||||||
templateName: 'contact_name_and_number',
|
|
||||||
});
|
|
||||||
|
|
||||||
Whisper.SuggestionListView = Whisper.ConversationListView.extend({
|
|
||||||
itemView: Whisper.SuggestionView,
|
|
||||||
});
|
|
||||||
|
|
||||||
Whisper.RecipientsInputView = Whisper.View.extend({
|
|
||||||
className: 'recipients-input',
|
|
||||||
templateName: 'recipients-input',
|
|
||||||
initialize(options) {
|
|
||||||
if (options) {
|
|
||||||
this.placeholder = options.placeholder;
|
|
||||||
}
|
|
||||||
this.render();
|
|
||||||
this.$input = this.$('input.search');
|
|
||||||
this.$new_contact = this.$('.new-contact');
|
|
||||||
|
|
||||||
// Collection of recipients selected for the new message
|
|
||||||
this.recipients = new Whisper.ConversationCollection([], {
|
|
||||||
comparator: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
// View to display the selected recipients
|
|
||||||
this.recipients_view = new Whisper.RecipientListView({
|
|
||||||
collection: this.recipients,
|
|
||||||
el: this.$('.recipients'),
|
|
||||||
});
|
|
||||||
|
|
||||||
// Collection of contacts to match user input against
|
|
||||||
this.typeahead = new ContactsTypeahead();
|
|
||||||
this.typeahead.fetchContacts();
|
|
||||||
|
|
||||||
// View to display the matched contacts from typeahead
|
|
||||||
this.typeahead_view = new Whisper.SuggestionListView({
|
|
||||||
collection: new Whisper.ConversationCollection([], {
|
|
||||||
comparator(m) {
|
|
||||||
return m.getTitle().toLowerCase();
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
this.$('.contacts').append(this.typeahead_view.el);
|
|
||||||
this.initNewContact();
|
|
||||||
this.listenTo(this.typeahead, 'reset', this.filterContacts);
|
|
||||||
},
|
|
||||||
|
|
||||||
render_attributes() {
|
|
||||||
return { placeholder: this.placeholder || 'name or phone number' };
|
|
||||||
},
|
|
||||||
|
|
||||||
events: {
|
|
||||||
'input input.search': 'filterContacts',
|
|
||||||
'select .new-contact': 'addNewRecipient',
|
|
||||||
'select .contacts': 'addRecipient',
|
|
||||||
'remove .recipient': 'removeRecipient',
|
|
||||||
},
|
|
||||||
|
|
||||||
filterContacts() {
|
|
||||||
const query = this.$input.val();
|
|
||||||
if (query.length) {
|
|
||||||
if (this.maybeNumber(query)) {
|
|
||||||
this.new_contact_view.model.set('id', query);
|
|
||||||
this.new_contact_view.render().$el.show();
|
|
||||||
} else {
|
|
||||||
this.new_contact_view.$el.hide();
|
|
||||||
}
|
|
||||||
this.typeahead_view.collection.reset(this.typeahead.typeahead(query));
|
|
||||||
} else {
|
|
||||||
this.resetTypeahead();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
initNewContact() {
|
|
||||||
if (this.new_contact_view) {
|
|
||||||
this.new_contact_view.undelegateEvents();
|
|
||||||
this.new_contact_view.$el.hide();
|
|
||||||
}
|
|
||||||
// Creates a view to display a new contact
|
|
||||||
this.new_contact_view = new Whisper.ConversationListItemView({
|
|
||||||
el: this.$new_contact,
|
|
||||||
model: ConversationController.create({
|
|
||||||
type: 'private',
|
|
||||||
newContact: true,
|
|
||||||
}),
|
|
||||||
}).render();
|
|
||||||
},
|
|
||||||
|
|
||||||
addNewRecipient() {
|
|
||||||
this.recipients.add(this.new_contact_view.model);
|
|
||||||
this.initNewContact();
|
|
||||||
this.resetTypeahead();
|
|
||||||
},
|
|
||||||
|
|
||||||
addRecipient(e, conversation) {
|
|
||||||
this.recipients.add(this.typeahead.remove(conversation.id));
|
|
||||||
this.resetTypeahead();
|
|
||||||
},
|
|
||||||
|
|
||||||
removeRecipient(e, data) {
|
|
||||||
const model = this.recipients.remove(data.modelId);
|
|
||||||
if (!model.get('newContact')) {
|
|
||||||
this.typeahead.add(model);
|
|
||||||
}
|
|
||||||
this.filterContacts();
|
|
||||||
},
|
|
||||||
|
|
||||||
reset() {
|
|
||||||
this.delegateEvents();
|
|
||||||
this.typeahead_view.delegateEvents();
|
|
||||||
this.recipients_view.delegateEvents();
|
|
||||||
this.new_contact_view.delegateEvents();
|
|
||||||
this.typeahead.add(
|
|
||||||
this.recipients.filter(model => !model.get('newContact'))
|
|
||||||
);
|
|
||||||
this.recipients.reset([]);
|
|
||||||
this.resetTypeahead();
|
|
||||||
this.typeahead.fetchContacts();
|
|
||||||
},
|
|
||||||
|
|
||||||
resetTypeahead() {
|
|
||||||
this.new_contact_view.$el.hide();
|
|
||||||
this.$input.val('').focus();
|
|
||||||
this.typeahead_view.collection.reset([]);
|
|
||||||
},
|
|
||||||
|
|
||||||
maybeNumber(number) {
|
|
||||||
return number.match(/^\+?[0-9]*$/);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
})();
|
|
|
@ -229,6 +229,8 @@ MessageReceiver.prototype.extend({
|
||||||
const job = () => appJobPromise;
|
const job = () => appJobPromise;
|
||||||
|
|
||||||
this.appPromise = promise.then(job, job);
|
this.appPromise = promise.then(job, job);
|
||||||
|
|
||||||
|
return Promise.resolve();
|
||||||
},
|
},
|
||||||
onclose(ev) {
|
onclose(ev) {
|
||||||
window.log.info(
|
window.log.info(
|
||||||
|
@ -868,7 +870,7 @@ MessageReceiver.prototype.extend({
|
||||||
p = this.handleEndSession(destination);
|
p = this.handleEndSession(destination);
|
||||||
}
|
}
|
||||||
return p.then(() =>
|
return p.then(() =>
|
||||||
this.processDecrypted(envelope, msg, this.number).then(message => {
|
this.processDecrypted(envelope, msg).then(message => {
|
||||||
const groupId = message.group && message.group.id;
|
const groupId = message.group && message.group.id;
|
||||||
const isBlocked = this.isGroupBlocked(groupId);
|
const isBlocked = this.isGroupBlocked(groupId);
|
||||||
const isMe = envelope.source === textsecure.storage.user.getNumber();
|
const isMe = envelope.source === textsecure.storage.user.getNumber();
|
||||||
|
@ -910,7 +912,7 @@ MessageReceiver.prototype.extend({
|
||||||
p = this.handleEndSession(envelope.source);
|
p = this.handleEndSession(envelope.source);
|
||||||
}
|
}
|
||||||
return p.then(() =>
|
return p.then(() =>
|
||||||
this.processDecrypted(envelope, msg, envelope.source).then(message => {
|
this.processDecrypted(envelope, msg).then(message => {
|
||||||
const groupId = message.group && message.group.id;
|
const groupId = message.group && message.group.id;
|
||||||
const isBlocked = this.isGroupBlocked(groupId);
|
const isBlocked = this.isGroupBlocked(groupId);
|
||||||
const isMe = envelope.source === textsecure.storage.user.getNumber();
|
const isMe = envelope.source === textsecure.storage.user.getNumber();
|
||||||
|
@ -1168,39 +1170,13 @@ MessageReceiver.prototype.extend({
|
||||||
let groupDetails = groupBuffer.next();
|
let groupDetails = groupBuffer.next();
|
||||||
const promises = [];
|
const promises = [];
|
||||||
while (groupDetails !== undefined) {
|
while (groupDetails !== undefined) {
|
||||||
const getGroupDetails = details => {
|
groupDetails.id = groupDetails.id.toBinary();
|
||||||
// eslint-disable-next-line no-param-reassign
|
const ev = new Event('group');
|
||||||
details.id = details.id.toBinary();
|
ev.confirm = this.removeFromCache.bind(this, envelope);
|
||||||
if (details.active) {
|
ev.groupDetails = groupDetails;
|
||||||
return textsecure.storage.groups
|
const promise = this.dispatchAndWait(ev).catch(e => {
|
||||||
.getGroup(details.id)
|
window.log.error('error processing group', e);
|
||||||
.then(existingGroup => {
|
});
|
||||||
if (existingGroup === undefined) {
|
|
||||||
return textsecure.storage.groups.createNewGroup(
|
|
||||||
details.members,
|
|
||||||
details.id
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return textsecure.storage.groups.updateNumbers(
|
|
||||||
details.id,
|
|
||||||
details.members
|
|
||||||
);
|
|
||||||
})
|
|
||||||
.then(() => details);
|
|
||||||
}
|
|
||||||
return Promise.resolve(details);
|
|
||||||
};
|
|
||||||
|
|
||||||
const promise = getGroupDetails(groupDetails)
|
|
||||||
.then(details => {
|
|
||||||
const ev = new Event('group');
|
|
||||||
ev.confirm = this.removeFromCache.bind(this, envelope);
|
|
||||||
ev.groupDetails = details;
|
|
||||||
return this.dispatchAndWait(ev);
|
|
||||||
})
|
|
||||||
.catch(e => {
|
|
||||||
window.log.error('error processing group', e);
|
|
||||||
});
|
|
||||||
groupDetails = groupBuffer.next();
|
groupDetails = groupBuffer.next();
|
||||||
promises.push(promise);
|
promises.push(promise);
|
||||||
}
|
}
|
||||||
|
@ -1275,7 +1251,7 @@ MessageReceiver.prototype.extend({
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
processDecrypted(envelope, decrypted, source) {
|
processDecrypted(envelope, decrypted) {
|
||||||
/* eslint-disable no-bitwise, no-param-reassign */
|
/* eslint-disable no-bitwise, no-param-reassign */
|
||||||
const FLAGS = textsecure.protobuf.DataMessage.Flags;
|
const FLAGS = textsecure.protobuf.DataMessage.Flags;
|
||||||
|
|
||||||
|
@ -1311,63 +1287,24 @@ MessageReceiver.prototype.extend({
|
||||||
if (decrypted.group !== null) {
|
if (decrypted.group !== null) {
|
||||||
decrypted.group.id = decrypted.group.id.toBinary();
|
decrypted.group.id = decrypted.group.id.toBinary();
|
||||||
|
|
||||||
const storageGroups = textsecure.storage.groups;
|
switch (decrypted.group.type) {
|
||||||
|
case textsecure.protobuf.GroupContext.Type.UPDATE:
|
||||||
promises.push(
|
decrypted.body = null;
|
||||||
storageGroups.getNumbers(decrypted.group.id).then(existingGroup => {
|
decrypted.attachments = [];
|
||||||
if (existingGroup === undefined) {
|
break;
|
||||||
if (
|
case textsecure.protobuf.GroupContext.Type.QUIT:
|
||||||
decrypted.group.type !==
|
decrypted.body = null;
|
||||||
textsecure.protobuf.GroupContext.Type.UPDATE
|
decrypted.attachments = [];
|
||||||
) {
|
break;
|
||||||
decrypted.group.members = [source];
|
case textsecure.protobuf.GroupContext.Type.DELIVER:
|
||||||
window.log.warn('Got message for unknown group');
|
decrypted.group.name = null;
|
||||||
}
|
decrypted.group.members = [];
|
||||||
return textsecure.storage.groups.createNewGroup(
|
decrypted.group.avatar = null;
|
||||||
decrypted.group.members,
|
break;
|
||||||
decrypted.group.id
|
default:
|
||||||
);
|
this.removeFromCache(envelope);
|
||||||
}
|
throw new Error('Unknown group message type');
|
||||||
const fromIndex = existingGroup.indexOf(source);
|
}
|
||||||
|
|
||||||
if (fromIndex < 0) {
|
|
||||||
// TODO: This could be indication of a race...
|
|
||||||
window.log.warn(
|
|
||||||
'Sender was not a member of the group they were sending from'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (decrypted.group.type) {
|
|
||||||
case textsecure.protobuf.GroupContext.Type.UPDATE:
|
|
||||||
decrypted.body = null;
|
|
||||||
decrypted.attachments = [];
|
|
||||||
return textsecure.storage.groups.updateNumbers(
|
|
||||||
decrypted.group.id,
|
|
||||||
decrypted.group.members
|
|
||||||
);
|
|
||||||
case textsecure.protobuf.GroupContext.Type.QUIT:
|
|
||||||
decrypted.body = null;
|
|
||||||
decrypted.attachments = [];
|
|
||||||
if (source === this.number) {
|
|
||||||
return textsecure.storage.groups.deleteGroup(
|
|
||||||
decrypted.group.id
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return textsecure.storage.groups.removeNumber(
|
|
||||||
decrypted.group.id,
|
|
||||||
source
|
|
||||||
);
|
|
||||||
case textsecure.protobuf.GroupContext.Type.DELIVER:
|
|
||||||
decrypted.group.name = null;
|
|
||||||
decrypted.group.members = [];
|
|
||||||
decrypted.group.avatar = null;
|
|
||||||
return Promise.resolve();
|
|
||||||
default:
|
|
||||||
this.removeFromCache(envelope);
|
|
||||||
throw new Error('Unknown group message type');
|
|
||||||
}
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const attachmentCount = decrypted.attachments.length;
|
const attachmentCount = decrypted.attachments.length;
|
||||||
|
|
|
@ -560,7 +560,7 @@ MessageSender.prototype = {
|
||||||
|
|
||||||
async sendTypingMessage(options = {}, sendOptions = {}) {
|
async sendTypingMessage(options = {}, sendOptions = {}) {
|
||||||
const ACTION_ENUM = textsecure.protobuf.TypingMessage.Action;
|
const ACTION_ENUM = textsecure.protobuf.TypingMessage.Action;
|
||||||
const { recipientId, groupId, isTyping, timestamp } = options;
|
const { recipientId, groupId, groupNumbers, isTyping, timestamp } = options;
|
||||||
|
|
||||||
// We don't want to send typing messages to our other devices, but we will
|
// We don't want to send typing messages to our other devices, but we will
|
||||||
// in the group case.
|
// in the group case.
|
||||||
|
@ -574,7 +574,7 @@ MessageSender.prototype = {
|
||||||
}
|
}
|
||||||
|
|
||||||
const recipients = groupId
|
const recipients = groupId
|
||||||
? _.without(await textsecure.storage.groups.getNumbers(groupId), myNumber)
|
? _.without(groupNumbers, myNumber)
|
||||||
: [recipientId];
|
: [recipientId];
|
||||||
const groupIdBuffer = groupId
|
const groupIdBuffer = groupId
|
||||||
? window.Signal.Crypto.fromEncodedBinaryToArrayBuffer(groupId)
|
? window.Signal.Crypto.fromEncodedBinaryToArrayBuffer(groupId)
|
||||||
|
@ -882,6 +882,7 @@ MessageSender.prototype = {
|
||||||
|
|
||||||
sendMessageToGroup(
|
sendMessageToGroup(
|
||||||
groupId,
|
groupId,
|
||||||
|
groupNumbers,
|
||||||
messageText,
|
messageText,
|
||||||
attachments,
|
attachments,
|
||||||
quote,
|
quote,
|
||||||
|
@ -891,59 +892,50 @@ MessageSender.prototype = {
|
||||||
profileKey,
|
profileKey,
|
||||||
options
|
options
|
||||||
) {
|
) {
|
||||||
return textsecure.storage.groups.getNumbers(groupId).then(targetNumbers => {
|
const me = textsecure.storage.user.getNumber();
|
||||||
if (targetNumbers === undefined) {
|
const numbers = groupNumbers.filter(number => number !== me);
|
||||||
return Promise.reject(new Error('Unknown Group'));
|
if (numbers.length === 0) {
|
||||||
}
|
return Promise.reject(new Error('No other members in the group'));
|
||||||
|
}
|
||||||
|
|
||||||
const me = textsecure.storage.user.getNumber();
|
return this.sendMessage(
|
||||||
const numbers = targetNumbers.filter(number => number !== me);
|
{
|
||||||
if (numbers.length === 0) {
|
recipients: numbers,
|
||||||
return Promise.reject(new Error('No other members in the group'));
|
body: messageText,
|
||||||
}
|
timestamp,
|
||||||
|
attachments,
|
||||||
return this.sendMessage(
|
quote,
|
||||||
{
|
preview,
|
||||||
recipients: numbers,
|
needsSync: true,
|
||||||
body: messageText,
|
expireTimer,
|
||||||
timestamp,
|
profileKey,
|
||||||
attachments,
|
group: {
|
||||||
quote,
|
id: groupId,
|
||||||
preview,
|
type: textsecure.protobuf.GroupContext.Type.DELIVER,
|
||||||
needsSync: true,
|
|
||||||
expireTimer,
|
|
||||||
profileKey,
|
|
||||||
group: {
|
|
||||||
id: groupId,
|
|
||||||
type: textsecure.protobuf.GroupContext.Type.DELIVER,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
options
|
},
|
||||||
);
|
options
|
||||||
});
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
createGroup(targetNumbers, name, avatar, options) {
|
createGroup(targetNumbers, id, name, avatar, options) {
|
||||||
const proto = new textsecure.protobuf.DataMessage();
|
const proto = new textsecure.protobuf.DataMessage();
|
||||||
proto.group = new textsecure.protobuf.GroupContext();
|
proto.group = new textsecure.protobuf.GroupContext();
|
||||||
|
proto.group.id = stringToArrayBuffer(id);
|
||||||
|
|
||||||
return textsecure.storage.groups
|
proto.group.type = textsecure.protobuf.GroupContext.Type.UPDATE;
|
||||||
.createNewGroup(targetNumbers)
|
proto.group.members = targetNumbers;
|
||||||
.then(group => {
|
proto.group.name = name;
|
||||||
proto.group.id = stringToArrayBuffer(group.id);
|
|
||||||
const { numbers } = group;
|
|
||||||
|
|
||||||
proto.group.type = textsecure.protobuf.GroupContext.Type.UPDATE;
|
return this.makeAttachmentPointer(avatar).then(attachment => {
|
||||||
proto.group.members = numbers;
|
proto.group.avatar = attachment;
|
||||||
proto.group.name = name;
|
return this.sendGroupProto(
|
||||||
|
targetNumbers,
|
||||||
return this.makeAttachmentPointer(avatar).then(attachment => {
|
proto,
|
||||||
proto.group.avatar = attachment;
|
Date.now(),
|
||||||
return this.sendGroupProto(numbers, proto, Date.now(), options).then(
|
options
|
||||||
() => proto.group.id
|
).then(() => proto.group.id);
|
||||||
);
|
});
|
||||||
});
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
|
|
||||||
updateGroup(groupId, name, avatar, targetNumbers, options) {
|
updateGroup(groupId, name, avatar, targetNumbers, options) {
|
||||||
|
@ -953,121 +945,87 @@ MessageSender.prototype = {
|
||||||
proto.group.id = stringToArrayBuffer(groupId);
|
proto.group.id = stringToArrayBuffer(groupId);
|
||||||
proto.group.type = textsecure.protobuf.GroupContext.Type.UPDATE;
|
proto.group.type = textsecure.protobuf.GroupContext.Type.UPDATE;
|
||||||
proto.group.name = name;
|
proto.group.name = name;
|
||||||
|
proto.group.members = targetNumbers;
|
||||||
|
|
||||||
return textsecure.storage.groups
|
return this.makeAttachmentPointer(avatar).then(attachment => {
|
||||||
.addNumbers(groupId, targetNumbers)
|
proto.group.avatar = attachment;
|
||||||
.then(numbers => {
|
return this.sendGroupProto(
|
||||||
if (numbers === undefined) {
|
targetNumbers,
|
||||||
return Promise.reject(new Error('Unknown Group'));
|
proto,
|
||||||
}
|
Date.now(),
|
||||||
proto.group.members = numbers;
|
options
|
||||||
|
).then(() => proto.group.id);
|
||||||
return this.makeAttachmentPointer(avatar).then(attachment => {
|
});
|
||||||
proto.group.avatar = attachment;
|
|
||||||
return this.sendGroupProto(numbers, proto, Date.now(), options).then(
|
|
||||||
() => proto.group.id
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
|
|
||||||
addNumberToGroup(groupId, number, options) {
|
addNumberToGroup(groupId, newNumbers, options) {
|
||||||
const proto = new textsecure.protobuf.DataMessage();
|
const proto = new textsecure.protobuf.DataMessage();
|
||||||
proto.group = new textsecure.protobuf.GroupContext();
|
proto.group = new textsecure.protobuf.GroupContext();
|
||||||
proto.group.id = stringToArrayBuffer(groupId);
|
proto.group.id = stringToArrayBuffer(groupId);
|
||||||
proto.group.type = textsecure.protobuf.GroupContext.Type.UPDATE;
|
proto.group.type = textsecure.protobuf.GroupContext.Type.UPDATE;
|
||||||
|
proto.group.members = newNumbers;
|
||||||
return textsecure.storage.groups
|
return this.sendGroupProto(newNumbers, proto, Date.now(), options);
|
||||||
.addNumbers(groupId, [number])
|
|
||||||
.then(numbers => {
|
|
||||||
if (numbers === undefined)
|
|
||||||
return Promise.reject(new Error('Unknown Group'));
|
|
||||||
proto.group.members = numbers;
|
|
||||||
|
|
||||||
return this.sendGroupProto(numbers, proto, Date.now(), options);
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
|
|
||||||
setGroupName(groupId, name, options) {
|
setGroupName(groupId, name, groupNumbers, options) {
|
||||||
const proto = new textsecure.protobuf.DataMessage();
|
const proto = new textsecure.protobuf.DataMessage();
|
||||||
proto.group = new textsecure.protobuf.GroupContext();
|
proto.group = new textsecure.protobuf.GroupContext();
|
||||||
proto.group.id = stringToArrayBuffer(groupId);
|
proto.group.id = stringToArrayBuffer(groupId);
|
||||||
proto.group.type = textsecure.protobuf.GroupContext.Type.UPDATE;
|
proto.group.type = textsecure.protobuf.GroupContext.Type.UPDATE;
|
||||||
proto.group.name = name;
|
proto.group.name = name;
|
||||||
|
proto.group.members = groupNumbers;
|
||||||
|
|
||||||
return textsecure.storage.groups.getNumbers(groupId).then(numbers => {
|
return this.sendGroupProto(groupNumbers, proto, Date.now(), options);
|
||||||
if (numbers === undefined)
|
|
||||||
return Promise.reject(new Error('Unknown Group'));
|
|
||||||
proto.group.members = numbers;
|
|
||||||
|
|
||||||
return this.sendGroupProto(numbers, proto, Date.now(), options);
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
|
|
||||||
setGroupAvatar(groupId, avatar, options) {
|
setGroupAvatar(groupId, avatar, groupNumbers, options) {
|
||||||
const proto = new textsecure.protobuf.DataMessage();
|
const proto = new textsecure.protobuf.DataMessage();
|
||||||
proto.group = new textsecure.protobuf.GroupContext();
|
proto.group = new textsecure.protobuf.GroupContext();
|
||||||
proto.group.id = stringToArrayBuffer(groupId);
|
proto.group.id = stringToArrayBuffer(groupId);
|
||||||
proto.group.type = textsecure.protobuf.GroupContext.Type.UPDATE;
|
proto.group.type = textsecure.protobuf.GroupContext.Type.UPDATE;
|
||||||
|
proto.group.members = groupNumbers;
|
||||||
|
|
||||||
return textsecure.storage.groups.getNumbers(groupId).then(numbers => {
|
return this.makeAttachmentPointer(avatar).then(attachment => {
|
||||||
if (numbers === undefined)
|
proto.group.avatar = attachment;
|
||||||
return Promise.reject(new Error('Unknown Group'));
|
return this.sendGroupProto(groupNumbers, proto, Date.now(), options);
|
||||||
proto.group.members = numbers;
|
|
||||||
|
|
||||||
return this.makeAttachmentPointer(avatar).then(attachment => {
|
|
||||||
proto.group.avatar = attachment;
|
|
||||||
return this.sendGroupProto(numbers, proto, Date.now(), options);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
leaveGroup(groupId, options) {
|
leaveGroup(groupId, groupNumbers, options) {
|
||||||
const proto = new textsecure.protobuf.DataMessage();
|
const proto = new textsecure.protobuf.DataMessage();
|
||||||
proto.group = new textsecure.protobuf.GroupContext();
|
proto.group = new textsecure.protobuf.GroupContext();
|
||||||
proto.group.id = stringToArrayBuffer(groupId);
|
proto.group.id = stringToArrayBuffer(groupId);
|
||||||
proto.group.type = textsecure.protobuf.GroupContext.Type.QUIT;
|
proto.group.type = textsecure.protobuf.GroupContext.Type.QUIT;
|
||||||
|
return this.sendGroupProto(groupNumbers, proto, Date.now(), options);
|
||||||
return textsecure.storage.groups.getNumbers(groupId).then(numbers => {
|
|
||||||
if (numbers === undefined)
|
|
||||||
return Promise.reject(new Error('Unknown Group'));
|
|
||||||
return textsecure.storage.groups
|
|
||||||
.deleteGroup(groupId)
|
|
||||||
.then(() => this.sendGroupProto(numbers, proto, Date.now(), options));
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
sendExpirationTimerUpdateToGroup(
|
sendExpirationTimerUpdateToGroup(
|
||||||
groupId,
|
groupId,
|
||||||
|
groupNumbers,
|
||||||
expireTimer,
|
expireTimer,
|
||||||
timestamp,
|
timestamp,
|
||||||
profileKey,
|
profileKey,
|
||||||
options
|
options
|
||||||
) {
|
) {
|
||||||
return textsecure.storage.groups.getNumbers(groupId).then(targetNumbers => {
|
const me = textsecure.storage.user.getNumber();
|
||||||
if (targetNumbers === undefined)
|
const numbers = groupNumbers.filter(number => number !== me);
|
||||||
return Promise.reject(new Error('Unknown Group'));
|
if (numbers.length === 0) {
|
||||||
|
return Promise.reject(new Error('No other members in the group'));
|
||||||
const me = textsecure.storage.user.getNumber();
|
}
|
||||||
const numbers = targetNumbers.filter(number => number !== me);
|
return this.sendMessage(
|
||||||
if (numbers.length === 0) {
|
{
|
||||||
return Promise.reject(new Error('No other members in the group'));
|
recipients: numbers,
|
||||||
}
|
timestamp,
|
||||||
return this.sendMessage(
|
needsSync: true,
|
||||||
{
|
expireTimer,
|
||||||
recipients: numbers,
|
profileKey,
|
||||||
timestamp,
|
flags: textsecure.protobuf.DataMessage.Flags.EXPIRATION_TIMER_UPDATE,
|
||||||
needsSync: true,
|
group: {
|
||||||
expireTimer,
|
id: groupId,
|
||||||
profileKey,
|
type: textsecure.protobuf.GroupContext.Type.DELIVER,
|
||||||
flags: textsecure.protobuf.DataMessage.Flags.EXPIRATION_TIMER_UPDATE,
|
|
||||||
group: {
|
|
||||||
id: groupId,
|
|
||||||
type: textsecure.protobuf.GroupContext.Type.DELIVER,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
options
|
},
|
||||||
);
|
options
|
||||||
});
|
);
|
||||||
},
|
},
|
||||||
sendExpirationTimerUpdateToNumber(
|
sendExpirationTimerUpdateToNumber(
|
||||||
number,
|
number,
|
||||||
|
|
|
@ -1,160 +0,0 @@
|
||||||
/* global window, getString, libsignal, textsecure */
|
|
||||||
|
|
||||||
/* eslint-disable more/no-then */
|
|
||||||
|
|
||||||
// eslint-disable-next-line func-names
|
|
||||||
(function() {
|
|
||||||
/** *******************
|
|
||||||
*** Group Storage ***
|
|
||||||
******************** */
|
|
||||||
window.textsecure = window.textsecure || {};
|
|
||||||
window.textsecure.storage = window.textsecure.storage || {};
|
|
||||||
|
|
||||||
// create a random group id that we haven't seen before.
|
|
||||||
function generateNewGroupId() {
|
|
||||||
const groupId = getString(libsignal.crypto.getRandomBytes(16));
|
|
||||||
return textsecure.storage.protocol.getGroup(groupId).then(group => {
|
|
||||||
if (group === undefined) {
|
|
||||||
return groupId;
|
|
||||||
}
|
|
||||||
window.log.warn('group id collision'); // probably a bad sign.
|
|
||||||
return generateNewGroupId();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
window.textsecure.storage.groups = {
|
|
||||||
createNewGroup(numbers, groupId) {
|
|
||||||
return new Promise(resolve => {
|
|
||||||
if (groupId !== undefined) {
|
|
||||||
resolve(
|
|
||||||
textsecure.storage.protocol.getGroup(groupId).then(group => {
|
|
||||||
if (group !== undefined) {
|
|
||||||
throw new Error('Tried to recreate group');
|
|
||||||
}
|
|
||||||
})
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
resolve(
|
|
||||||
generateNewGroupId().then(newGroupId => {
|
|
||||||
// eslint-disable-next-line no-param-reassign
|
|
||||||
groupId = newGroupId;
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}).then(() => {
|
|
||||||
const me = textsecure.storage.user.getNumber();
|
|
||||||
let haveMe = false;
|
|
||||||
const finalNumbers = [];
|
|
||||||
// eslint-disable-next-line no-restricted-syntax, guard-for-in
|
|
||||||
for (const i in numbers) {
|
|
||||||
const number = numbers[i];
|
|
||||||
if (!textsecure.utils.isNumberSane(number))
|
|
||||||
throw new Error('Invalid number in group');
|
|
||||||
if (number === me) haveMe = true;
|
|
||||||
if (finalNumbers.indexOf(number) < 0) finalNumbers.push(number);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!haveMe) finalNumbers.push(me);
|
|
||||||
|
|
||||||
const groupObject = {
|
|
||||||
numbers: finalNumbers,
|
|
||||||
numberRegistrationIds: {},
|
|
||||||
};
|
|
||||||
// eslint-disable-next-line no-restricted-syntax, guard-for-in
|
|
||||||
for (const i in finalNumbers) {
|
|
||||||
groupObject.numberRegistrationIds[finalNumbers[i]] = {};
|
|
||||||
}
|
|
||||||
|
|
||||||
return textsecure.storage.protocol
|
|
||||||
.putGroup(groupId, groupObject)
|
|
||||||
.then(() => ({ id: groupId, numbers: finalNumbers }));
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
getNumbers(groupId) {
|
|
||||||
return textsecure.storage.protocol.getGroup(groupId).then(group => {
|
|
||||||
if (!group) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
return group.numbers;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
removeNumber(groupId, number) {
|
|
||||||
return textsecure.storage.protocol.getGroup(groupId).then(group => {
|
|
||||||
if (group === undefined) return undefined;
|
|
||||||
|
|
||||||
const me = textsecure.storage.user.getNumber();
|
|
||||||
if (number === me)
|
|
||||||
throw new Error(
|
|
||||||
'Cannot remove ourselves from a group, leave the group instead'
|
|
||||||
);
|
|
||||||
|
|
||||||
const i = group.numbers.indexOf(number);
|
|
||||||
if (i > -1) {
|
|
||||||
group.numbers.splice(i, 1);
|
|
||||||
// eslint-disable-next-line no-param-reassign
|
|
||||||
delete group.numberRegistrationIds[number];
|
|
||||||
return textsecure.storage.protocol
|
|
||||||
.putGroup(groupId, group)
|
|
||||||
.then(() => group.numbers);
|
|
||||||
}
|
|
||||||
|
|
||||||
return group.numbers;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
addNumbers(groupId, numbers) {
|
|
||||||
return textsecure.storage.protocol.getGroup(groupId).then(group => {
|
|
||||||
if (group === undefined) return undefined;
|
|
||||||
|
|
||||||
// eslint-disable-next-line no-restricted-syntax, guard-for-in
|
|
||||||
for (const i in numbers) {
|
|
||||||
const number = numbers[i];
|
|
||||||
if (!textsecure.utils.isNumberSane(number))
|
|
||||||
throw new Error('Invalid number in set to add to group');
|
|
||||||
if (group.numbers.indexOf(number) < 0) {
|
|
||||||
group.numbers.push(number);
|
|
||||||
// eslint-disable-next-line no-param-reassign
|
|
||||||
group.numberRegistrationIds[number] = {};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return textsecure.storage.protocol
|
|
||||||
.putGroup(groupId, group)
|
|
||||||
.then(() => group.numbers);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
deleteGroup(groupId) {
|
|
||||||
return textsecure.storage.protocol.removeGroup(groupId);
|
|
||||||
},
|
|
||||||
|
|
||||||
getGroup(groupId) {
|
|
||||||
return textsecure.storage.protocol.getGroup(groupId).then(group => {
|
|
||||||
if (group === undefined) return undefined;
|
|
||||||
|
|
||||||
return { id: groupId, numbers: group.numbers };
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
updateNumbers(groupId, numbers) {
|
|
||||||
return textsecure.storage.protocol.getGroup(groupId).then(group => {
|
|
||||||
if (group === undefined)
|
|
||||||
throw new Error('Tried to update numbers for unknown group');
|
|
||||||
|
|
||||||
if (
|
|
||||||
numbers.filter(textsecure.utils.isNumberSane).length < numbers.length
|
|
||||||
)
|
|
||||||
throw new Error('Invalid number in new group members');
|
|
||||||
|
|
||||||
const added = numbers.filter(
|
|
||||||
number => group.numbers.indexOf(number) < 0
|
|
||||||
);
|
|
||||||
|
|
||||||
return textsecure.storage.groups.addNumbers(groupId, added);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
};
|
|
||||||
})();
|
|
|
@ -362,8 +362,6 @@
|
||||||
<script type='text/javascript' src='../js/views/conversation_list_item_view.js' data-cover></script>
|
<script type='text/javascript' src='../js/views/conversation_list_item_view.js' data-cover></script>
|
||||||
<script type='text/javascript' src='../js/views/conversation_list_view.js' data-cover></script>
|
<script type='text/javascript' src='../js/views/conversation_list_view.js' data-cover></script>
|
||||||
<script type='text/javascript' src='../js/views/contact_list_view.js' data-cover></script>
|
<script type='text/javascript' src='../js/views/contact_list_view.js' data-cover></script>
|
||||||
<script type='text/javascript' src='../js/views/new_group_update_view.js' data-cover></script>
|
|
||||||
<script type="text/javascript" src="../js/views/group_update_view.js"></script>
|
|
||||||
<script type='text/javascript' src='../js/views/attachment_view.js' data-cover></script>
|
<script type='text/javascript' src='../js/views/attachment_view.js' data-cover></script>
|
||||||
<script type='text/javascript' src='../js/views/timestamp_view.js' data-cover></script>
|
<script type='text/javascript' src='../js/views/timestamp_view.js' data-cover></script>
|
||||||
<script type='text/javascript' src='../js/views/message_view.js' data-cover></script>
|
<script type='text/javascript' src='../js/views/message_view.js' data-cover></script>
|
||||||
|
@ -386,7 +384,6 @@
|
||||||
<script type="text/javascript" src="metadata/SecretSessionCipher_test.js"></script>
|
<script type="text/javascript" src="metadata/SecretSessionCipher_test.js"></script>
|
||||||
|
|
||||||
<script type="text/javascript" src="views/whisper_view_test.js"></script>
|
<script type="text/javascript" src="views/whisper_view_test.js"></script>
|
||||||
<script type="text/javascript" src="views/group_update_view_test.js"></script>
|
|
||||||
<script type="text/javascript" src="views/attachment_view_test.js"></script>
|
<script type="text/javascript" src="views/attachment_view_test.js"></script>
|
||||||
<script type="text/javascript" src="views/timestamp_view_test.js"></script>
|
<script type="text/javascript" src="views/timestamp_view_test.js"></script>
|
||||||
<script type="text/javascript" src="views/list_view_test.js"></script>
|
<script type="text/javascript" src="views/list_view_test.js"></script>
|
||||||
|
|
|
@ -1,24 +0,0 @@
|
||||||
/* global Whisper */
|
|
||||||
|
|
||||||
describe('GroupUpdateView', () => {
|
|
||||||
it('should show new group members', () => {
|
|
||||||
const view = new Whisper.GroupUpdateView({
|
|
||||||
model: { joined: ['Alice', 'Bob'] },
|
|
||||||
}).render();
|
|
||||||
assert.match(view.$el.text(), /Alice.*Bob.*joined the group/);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should note updates to the title', () => {
|
|
||||||
const view = new Whisper.GroupUpdateView({
|
|
||||||
model: { name: 'New name' },
|
|
||||||
}).render();
|
|
||||||
assert.match(view.$el.text(), /Title is now 'New name'/);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should say "Updated the group"', () => {
|
|
||||||
const view = new Whisper.GroupUpdateView({
|
|
||||||
model: { avatar: 'New avatar' },
|
|
||||||
}).render();
|
|
||||||
assert.match(view.$el.text(), /Updated the group/);
|
|
||||||
});
|
|
||||||
});
|
|
Loading…
Add table
Reference in a new issue