Merge pull request #1321 from Bilb/use-created-at-server-timestamp

Fixes #1249
pull/1330/head
Audric Ackermann 5 years ago committed by GitHub
commit c34edee180
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -110,7 +110,6 @@ module.exports = {
getPublicConversationsByServer, getPublicConversationsByServer,
getPubkeysInPublicConversation, getPubkeysInPublicConversation,
getAllConversationIds, getAllConversationIds,
getAllPrivateConversations,
getAllGroupsInvolvingId, getAllGroupsInvolvingId,
removeAllConversations, removeAllConversations,
removeAllPrivateConversations, removeAllPrivateConversations,
@ -130,6 +129,7 @@ module.exports = {
removeMessage, removeMessage,
getUnreadByConversation, getUnreadByConversation,
getMessageBySender, getMessageBySender,
getMessagesBySender,
getMessageIdsFromServerIds, getMessageIdsFromServerIds,
getMessageById, getMessageById,
getAllMessages, getAllMessages,
@ -811,6 +811,7 @@ const LOKI_SCHEMA_VERSIONS = [
updateToLokiSchemaVersion5, updateToLokiSchemaVersion5,
updateToLokiSchemaVersion6, updateToLokiSchemaVersion6,
updateToLokiSchemaVersion7, updateToLokiSchemaVersion7,
updateToLokiSchemaVersion8,
]; ];
async function updateToLokiSchemaVersion1(currentVersion, instance) { async function updateToLokiSchemaVersion1(currentVersion, instance) {
@ -824,7 +825,6 @@ async function updateToLokiSchemaVersion1(currentVersion, instance) {
`ALTER TABLE messages `ALTER TABLE messages
ADD COLUMN serverId INTEGER;` ADD COLUMN serverId INTEGER;`
); );
await instance.run( await instance.run(
`CREATE TABLE servers( `CREATE TABLE servers(
serverUrl STRING PRIMARY KEY ASC, serverUrl STRING PRIMARY KEY ASC,
@ -1052,6 +1052,29 @@ async function updateToLokiSchemaVersion7(currentVersion, instance) {
console.log('updateToLokiSchemaVersion7: success!'); console.log('updateToLokiSchemaVersion7: success!');
} }
async function updateToLokiSchemaVersion8(currentVersion, instance) {
if (currentVersion >= 8) {
return;
}
console.log('updateToLokiSchemaVersion8: starting...');
await instance.run('BEGIN TRANSACTION;');
await instance.run(
`ALTER TABLE messages
ADD COLUMN serverTimestamp INTEGER;`
);
await instance.run(
`INSERT INTO loki_schema (
version
) values (
8
);`
);
await instance.run('COMMIT TRANSACTION;');
console.log('updateToLokiSchemaVersion8: success!');
}
async function updateLokiSchema(instance) { async function updateLokiSchema(instance) {
const result = await instance.get( const result = await instance.get(
"SELECT name FROM sqlite_master WHERE type = 'table' AND name='loki_schema';" "SELECT name FROM sqlite_master WHERE type = 'table' AND name='loki_schema';"
@ -1948,16 +1971,6 @@ async function getAllConversationIds() {
return map(rows, row => row.id); return map(rows, row => row.id);
} }
async function getAllPrivateConversations() {
const rows = await db.all(
`SELECT json FROM ${CONVERSATIONS_TABLE} WHERE
type = 'private'
ORDER BY id ASC;`
);
return map(rows, row => jsonToObject(row.json));
}
async function getAllPublicConversations() { async function getAllPublicConversations() {
const rows = await db.all( const rows = await db.all(
`SELECT json FROM conversations WHERE `SELECT json FROM conversations WHERE
@ -2103,6 +2116,7 @@ async function saveMessage(data, { forceSave } = {}) {
hasVisualMediaAttachments, hasVisualMediaAttachments,
id, id,
serverId, serverId,
serverTimestamp,
// eslint-disable-next-line camelcase // eslint-disable-next-line camelcase
received_at, received_at,
schemaVersion, schemaVersion,
@ -2122,6 +2136,7 @@ async function saveMessage(data, { forceSave } = {}) {
$json: objectToJSON(data), $json: objectToJSON(data),
$serverId: serverId, $serverId: serverId,
$serverTimestamp: serverTimestamp,
$body: body, $body: body,
$conversationId: conversationId, $conversationId: conversationId,
$expirationStartTimestamp: expirationStartTimestamp, $expirationStartTimestamp: expirationStartTimestamp,
@ -2145,6 +2160,7 @@ async function saveMessage(data, { forceSave } = {}) {
`UPDATE messages SET `UPDATE messages SET
json = $json, json = $json,
serverId = $serverId, serverId = $serverId,
serverTimestamp = $serverTimestamp,
body = $body, body = $body,
conversationId = $conversationId, conversationId = $conversationId,
expirationStartTimestamp = $expirationStartTimestamp, expirationStartTimestamp = $expirationStartTimestamp,
@ -2180,6 +2196,7 @@ async function saveMessage(data, { forceSave } = {}) {
json, json,
serverId, serverId,
serverTimestamp,
body, body,
conversationId, conversationId,
expirationStartTimestamp, expirationStartTimestamp,
@ -2201,6 +2218,7 @@ async function saveMessage(data, { forceSave } = {}) {
$json, $json,
$serverId, $serverId,
$serverTimestamp,
$body, $body,
$conversationId, $conversationId,
$expirationStartTimestamp, $expirationStartTimestamp,
@ -2394,6 +2412,20 @@ async function getMessageBySender({ source, sourceDevice, sent_at }) {
return map(rows, row => jsonToObject(row.json)); return map(rows, row => jsonToObject(row.json));
} }
async function getMessagesBySender({ source, sourceDevice }) {
const rows = await db.all(
`SELECT json FROM messages WHERE
source = $source AND
sourceDevice = $sourceDevice`,
{
$source: source,
$sourceDevice: sourceDevice,
}
);
return map(rows, row => jsonToObject(row.json));
}
async function getAllUnsentMessages() { async function getAllUnsentMessages() {
const rows = await db.all(` const rows = await db.all(`
SELECT json FROM messages WHERE SELECT json FROM messages WHERE
@ -2430,7 +2462,7 @@ async function getMessagesByConversation(
conversationId = $conversationId AND conversationId = $conversationId AND
received_at < $received_at AND received_at < $received_at AND
type LIKE $type type LIKE $type
ORDER BY sent_at DESC ORDER BY serverTimestamp DESC, serverId DESC, sent_at DESC
LIMIT $limit; LIMIT $limit;
`, `,
{ {

@ -359,7 +359,6 @@
<script type='text/javascript' src='js/views/device_pairing_dialog_view.js'></script> <script type='text/javascript' src='js/views/device_pairing_dialog_view.js'></script>
<script type='text/javascript' src='js/views/device_pairing_words_dialog_view.js'></script> <script type='text/javascript' src='js/views/device_pairing_words_dialog_view.js'></script>
<script type='text/javascript' src='js/views/create_group_dialog_view.js'></script> <script type='text/javascript' src='js/views/create_group_dialog_view.js'></script>
<script type='text/javascript' src='js/views/confirm_session_reset_view.js'></script>
<script type='text/javascript' src='js/views/edit_profile_dialog_view.js'></script> <script type='text/javascript' src='js/views/edit_profile_dialog_view.js'></script>
<script type='text/javascript' src='js/views/invite_contacts_dialog_view.js'></script> <script type='text/javascript' src='js/views/invite_contacts_dialog_view.js'></script>
<script type='text/javascript' src='js/views/moderators_add_dialog_view.js'></script> <script type='text/javascript' src='js/views/moderators_add_dialog_view.js'></script>

@ -362,7 +362,6 @@
<script type='text/javascript' src='js/views/device_pairing_dialog_view.js'></script> <script type='text/javascript' src='js/views/device_pairing_dialog_view.js'></script>
<script type='text/javascript' src='js/views/device_pairing_words_dialog_view.js'></script> <script type='text/javascript' src='js/views/device_pairing_words_dialog_view.js'></script>
<script type='text/javascript' src='js/views/create_group_dialog_view.js'></script> <script type='text/javascript' src='js/views/create_group_dialog_view.js'></script>
<script type='text/javascript' src='js/views/confirm_session_reset_view.js'></script>
<script type='text/javascript' src='js/views/edit_profile_dialog_view.js'></script> <script type='text/javascript' src='js/views/edit_profile_dialog_view.js'></script>
<script type='text/javascript' src='js/views/invite_contacts_dialog_view.js'></script> <script type='text/javascript' src='js/views/invite_contacts_dialog_view.js'></script>
<script type='text/javascript' src='js/views/moderators_add_dialog_view.js'></script> <script type='text/javascript' src='js/views/moderators_add_dialog_view.js'></script>

@ -1121,10 +1121,15 @@
Whisper.events.on( Whisper.events.on(
'publicMessageSent', 'publicMessageSent',
({ pubKey, timestamp, serverId }) => { ({ pubKey, timestamp, serverId, serverTimestamp }) => {
try { try {
const conversation = ConversationController.get(pubKey); const conversation = ConversationController.get(pubKey);
conversation.onPublicMessageSent(pubKey, timestamp, serverId); conversation.onPublicMessageSent(
pubKey,
timestamp,
serverId,
serverTimestamp
);
} catch (e) { } catch (e) {
window.log.error('Error setting public on message'); window.log.error('Error setting public on message');
} }
@ -1326,7 +1331,7 @@
messageReceiver = new textsecure.MessageReceiver(mySignalingKey, options); messageReceiver = new textsecure.MessageReceiver(mySignalingKey, options);
messageReceiver.addEventListener( messageReceiver.addEventListener(
'message', 'message',
window.NewReceiver.handleMessageEvent window.DataMessageReceiver.handleMessageEvent
); );
window.textsecure.messaging = new textsecure.MessageSender(); window.textsecure.messaging = new textsecure.MessageSender();
return; return;
@ -1337,11 +1342,11 @@
messageReceiver = new textsecure.MessageReceiver(mySignalingKey, options); messageReceiver = new textsecure.MessageReceiver(mySignalingKey, options);
messageReceiver.addEventListener( messageReceiver.addEventListener(
'message', 'message',
window.NewReceiver.handleMessageEvent window.DataMessageReceiver.handleMessageEvent
); );
messageReceiver.addEventListener( messageReceiver.addEventListener(
'sent', 'sent',
window.NewReceiver.handleMessageEvent window.DataMessageReceiver.handleMessageEvent
); );
messageReceiver.addEventListener('empty', onEmpty); messageReceiver.addEventListener('empty', onEmpty);
messageReceiver.addEventListener('reconnect', onReconnect); messageReceiver.addEventListener('reconnect', onReconnect);

@ -540,14 +540,13 @@
await Promise.all(messages.map(m => m.setCalculatingPoW())); await Promise.all(messages.map(m => m.setCalculatingPoW()));
}, },
async onPublicMessageSent(pubKey, timestamp, serverId) { async onPublicMessageSent(pubKey, timestamp, serverId, serverTimestamp) {
const messages = this._getMessagesWithTimestamp(pubKey, timestamp); const messages = this._getMessagesWithTimestamp(pubKey, timestamp);
await Promise.all( if (messages && messages.length === 1) {
messages.map(message => [ await messages[0].setIsPublic(true);
message.setIsPublic(true), await messages[0].setServerId(serverId);
message.setServerId(serverId), await messages[0].setServerTimestamp(serverTimestamp);
]) }
);
}, },
async onNewMessage(message) { async onNewMessage(message) {
@ -1295,6 +1294,9 @@
if (this.isPrivate()) { if (this.isPrivate()) {
message.set({ destination }); message.set({ destination });
} }
if (this.isPublic()) {
message.setServerTimestamp(new Date().getTime());
}
const id = await window.Signal.Data.saveMessage(message.attributes, { const id = await window.Signal.Data.saveMessage(message.attributes, {
Message: Whisper.Message, Message: Whisper.Message,

@ -596,6 +596,7 @@
id: this.id, id: this.id,
direction: this.isIncoming() ? 'incoming' : 'outgoing', direction: this.isIncoming() ? 'incoming' : 'outgoing',
timestamp: this.get('sent_at'), timestamp: this.get('sent_at'),
serverTimestamp: this.get('serverTimestamp'),
status: this.getMessagePropStatus(), status: this.getMessagePropStatus(),
contact: this.getPropsForEmbeddedContact(), contact: this.getPropsForEmbeddedContact(),
authorColor, authorColor,
@ -1440,6 +1441,19 @@
Message: Whisper.Message, Message: Whisper.Message,
}); });
}, },
async setServerTimestamp(serverTimestamp) {
if (_.isEqual(this.get('serverTimestamp'), serverTimestamp)) {
return;
}
this.set({
serverTimestamp,
});
await window.Signal.Data.saveMessage(this.attributes, {
Message: Whisper.Message,
});
},
async setIsPublic(isPublic) { async setIsPublic(isPublic) {
if (_.isEqual(this.get('isPublic'), isPublic)) { if (_.isEqual(this.get('isPublic'), isPublic)) {
return; return;
@ -1669,13 +1683,6 @@
Whisper.MessageCollection = Backbone.Collection.extend({ Whisper.MessageCollection = Backbone.Collection.extend({
model: Whisper.Message, model: Whisper.Message,
comparator(left, right) {
if (left.get('sent_at') === right.get('sent_at')) {
return (left.get('received_at') || 0) - (right.get('received_at') || 0);
}
return (left.get('sent_at') || 0) - (right.get('sent_at') || 0);
},
initialize(models, options) { initialize(models, options) {
if (options) { if (options) {
this.conversation = options.conversation; this.conversation = options.conversation;
@ -1726,7 +1733,7 @@
); );
} }
this.add(models); this.add(models.reverse());
if (unreadCount <= 0) { if (unreadCount <= 0) {
return; return;

@ -197,7 +197,6 @@ export function getAllConversations({
}): Promise<Array<ConversationCollection>>; }): Promise<Array<ConversationCollection>>;
export function getAllConversationIds(): Promise<Array<string>>; export function getAllConversationIds(): Promise<Array<string>>;
export function getAllPrivateConversations(): Promise<Array<string>>;
export function getAllPublicConversations(): Promise<Array<string>>; export function getAllPublicConversations(): Promise<Array<string>>;
export function getPublicConversationsByServer( export function getPublicConversationsByServer(
server: string, server: string,
@ -224,7 +223,6 @@ export function searchMessagesInConversation(
conversationId: string, conversationId: string,
{ limit }?: { limit: any } { limit }?: { limit: any }
): Promise<any>; ): Promise<any>;
export function getMessageCount(): Promise<number>;
export function saveMessage( export function saveMessage(
data: Mesasge, data: Mesasge,
{ forceSave, Message }?: { forceSave?: any; Message?: any } { forceSave, Message }?: { forceSave?: any; Message?: any }
@ -267,6 +265,10 @@ export function getMessageBySender(
}: { source: any; sourceDevice: any; sent_at: any }, }: { source: any; sourceDevice: any; sent_at: any },
{ Message }: { Message: any } { Message }: { Message: any }
): Promise<any>; ): Promise<any>;
export function getMessagesBySender(
{ source, sourceDevice }: { source: any; sourceDevice: any },
{ Message }: { Message: any }
): Promise<Whisper.MessageCollection>;
export function getMessageIdsFromServerIds( export function getMessageIdsFromServerIds(
serverIds: any, serverIds: any,
conversationId: any conversationId: any
@ -322,6 +324,7 @@ export function getMessagesByConversation(
type?: string; type?: string;
} }
): Promise<any>; ): Promise<any>;
export function getSeenMessagesByHashList(hashes: any): Promise<any>; export function getSeenMessagesByHashList(hashes: any): Promise<any>;
export function getLastHashBySnode(convoId: any, snode: any): Promise<any>; export function getLastHashBySnode(convoId: any, snode: any): Promise<any>;

@ -123,7 +123,6 @@ module.exports = {
getAllConversations, getAllConversations,
getAllConversationIds, getAllConversationIds,
getAllPrivateConversations,
getAllPublicConversations, getAllPublicConversations,
getPublicConversationsByServer, getPublicConversationsByServer,
getPubkeysInPublicConversation, getPubkeysInPublicConversation,
@ -135,7 +134,6 @@ module.exports = {
searchMessages, searchMessages,
searchMessagesInConversation, searchMessagesInConversation,
getMessageCount,
saveMessage, saveMessage,
cleanSeenMessages, cleanSeenMessages,
cleanLastHashes, cleanLastHashes,
@ -151,6 +149,7 @@ module.exports = {
removeAllMessagesInConversation, removeAllMessagesInConversation,
getMessageBySender, getMessageBySender,
getMessagesBySender,
getMessageIdsFromServerIds, getMessageIdsFromServerIds,
getMessageById, getMessageById,
getAllMessages, getAllMessages,
@ -818,14 +817,6 @@ async function getAllPublicConversations({ ConversationCollection }) {
return collection; return collection;
} }
async function getAllPrivateConversations({ ConversationCollection }) {
const conversations = await channels.getAllPrivateConversations();
const collection = new ConversationCollection();
collection.add(conversations);
return collection;
}
async function getPubkeysInPublicConversation(id) { async function getPubkeysInPublicConversation(id) {
return channels.getPubkeysInPublicConversation(id); return channels.getPubkeysInPublicConversation(id);
} }
@ -882,9 +873,6 @@ async function searchMessagesInConversation(
} }
// Message // Message
async function getMessageCount() {
return channels.getMessageCount();
}
async function cleanSeenMessages() { async function cleanSeenMessages() {
await channels.cleanSeenMessages(); await channels.cleanSeenMessages();
@ -1015,6 +1003,22 @@ async function getMessageBySender(
return new Message(messages[0]); return new Message(messages[0]);
} }
async function getMessagesBySender(
// eslint-disable-next-line camelcase
{ source, sourceDevice },
{ Message }
) {
const messages = await channels.getMessagesBySender({
source,
sourceDevice,
});
if (!messages || !messages.length) {
return null;
}
return messages.map(m => new Message(m));
}
async function getUnreadByConversation(conversationId, { MessageCollection }) { async function getUnreadByConversation(conversationId, { MessageCollection }) {
const messages = await channels.getUnreadByConversation(conversationId); const messages = await channels.getUnreadByConversation(conversationId);
return new MessageCollection(messages); return new MessageCollection(messages);

@ -30,7 +30,7 @@ export interface LokiPublicChannelAPI {
body?: string; body?: string;
}, },
timestamp: number timestamp: number
): Promise<number>; ): Promise<{ serverId; serverTimestamp }>;
} }
declare class LokiAppDotNetServerAPI implements LokiAppDotNetServerInterface { declare class LokiAppDotNetServerAPI implements LokiAppDotNetServerInterface {

@ -6,13 +6,13 @@ const { URL, URLSearchParams } = require('url');
const FormData = require('form-data'); const FormData = require('form-data');
const https = require('https'); const https = require('https');
const path = require('path'); const path = require('path');
const dataMessage = require('../../ts/receiver/dataMessage');
// Can't be less than 1200 if we have unauth'd requests // Can't be less than 1200 if we have unauth'd requests
const PUBLICCHAT_MSG_POLL_EVERY = 1.5 * 1000; // 1.5s const PUBLICCHAT_MSG_POLL_EVERY = 1.5 * 1000; // 1.5s
const PUBLICCHAT_CHAN_POLL_EVERY = 20 * 1000; // 20s const PUBLICCHAT_CHAN_POLL_EVERY = 20 * 1000; // 20s
const PUBLICCHAT_DELETION_POLL_EVERY = 5 * 1000; // 5s const PUBLICCHAT_DELETION_POLL_EVERY = 5 * 1000; // 5s
const PUBLICCHAT_MOD_POLL_EVERY = 30 * 1000; // 30s const PUBLICCHAT_MOD_POLL_EVERY = 30 * 1000; // 30s
const PUBLICCHAT_MIN_TIME_BETWEEN_DUPLICATE_MESSAGES = 10 * 1000; // 10s
// FIXME: replace with something on urlPubkeyMap... // FIXME: replace with something on urlPubkeyMap...
const FILESERVER_HOSTS = [ const FILESERVER_HOSTS = [
@ -1800,6 +1800,8 @@ class LokiPublicChannelAPI {
return { return {
timestamp, timestamp,
serverTimestamp:
new Date(`${adnMessage.created_at}`).getTime() || timestamp,
attachments, attachments,
preview, preview,
quote, quote,
@ -1913,9 +1915,13 @@ class LokiPublicChannelAPI {
if (messengerData === false) { if (messengerData === false) {
return false; return false;
} }
// eslint-disable-next-line no-param-reassign
adnMessage.timestamp = messengerData.timestamp;
// eslint-disable-next-line no-param-reassign
adnMessage.body = messengerData.text;
const { const {
timestamp, timestamp,
serverTimestamp,
quote, quote,
attachments, attachments,
preview, preview,
@ -1927,20 +1933,15 @@ class LokiPublicChannelAPI {
} }
// Duplicate check // Duplicate check
const isDuplicate = message => { const isDuplicate = (message, testedMessage) =>
// The username in this case is the users pubKey dataMessage.isDuplicate(
const sameUsername = message.username === pubKey; message,
const sameText = message.text === adnMessage.text; testedMessage,
// Don't filter out messages that are too far apart from each other testedMessage.user.username
const timestampsSimilar = );
Math.abs(message.timestamp - timestamp) <=
PUBLICCHAT_MIN_TIME_BETWEEN_DUPLICATE_MESSAGES;
return sameUsername && sameText && timestampsSimilar;
};
// Filter out any messages that we got previously // Filter out any messages that we got previously
if (this.lastMessagesCache.some(isDuplicate)) { if (this.lastMessagesCache.some(m => isDuplicate(m, adnMessage))) {
return false; // Duplicate message return false; // Duplicate message
} }
@ -1949,9 +1950,11 @@ class LokiPublicChannelAPI {
this.lastMessagesCache = [ this.lastMessagesCache = [
...this.lastMessagesCache, ...this.lastMessagesCache,
{ {
username: pubKey, attributes: {
text: adnMessage.text, source: pubKey,
timestamp, body: adnMessage.text,
sent_at: timestamp,
},
}, },
].splice(-5); ].splice(-5);
@ -1990,9 +1993,9 @@ class LokiPublicChannelAPI {
isSessionRequest: false, isSessionRequest: false,
source: pubKey, source: pubKey,
sourceDevice: 1, sourceDevice: 1,
timestamp, timestamp, // sender timestamp
serverTimestamp: timestamp, serverTimestamp, // server created_at, used to order messages
receivedAt, receivedAt,
isPublic: true, isPublic: true,
message: { message: {
@ -2008,7 +2011,7 @@ class LokiPublicChannelAPI {
profileKey, profileKey,
timestamp, timestamp,
received_at: receivedAt, received_at: receivedAt,
sent_at: timestamp, sent_at: timestamp, // sender timestamp inner
quote, quote,
contact: [], contact: [],
preview, preview,
@ -2342,7 +2345,10 @@ class LokiPublicChannelAPI {
objBody: payload, objBody: payload,
}); });
if (!res.err && res.response) { if (!res.err && res.response) {
return res.response.data.id; return {
serverId: res.response.data.id,
serverTimestamp: new Date(`${res.response.data.created_at}`).getTime(),
};
} }
if (res.err) { if (res.err) {
log.error(`POST ${this.baseChannelUrl}/messages failed`); log.error(`POST ${this.baseChannelUrl}/messages failed`);
@ -2354,9 +2360,8 @@ class LokiPublicChannelAPI {
} else { } else {
log.warn(res.response); log.warn(res.response);
} }
// there's no retry on desktop
// this is supposed to be after retries return { serverId: -1, serverTimestamp: -1 };
return -1;
} }
} }

@ -60,7 +60,8 @@ class LokiMessageAPI {
'Failed to send public chat message' 'Failed to send public chat message'
); );
} }
messageEventData.serverId = res; messageEventData.serverId = res.serverId;
messageEventData.serverTimestamp = res.serverTimestamp;
window.Whisper.events.trigger('publicMessageSent', messageEventData); window.Whisper.events.trigger('publicMessageSent', messageEventData);
return; return;
} }

@ -420,6 +420,7 @@ window.addEventListener('contextmenu', e => {
}); });
window.NewReceiver = require('./ts/receiver/receiver'); window.NewReceiver = require('./ts/receiver/receiver');
window.DataMessageReceiver = require('./ts/receiver/dataMessage');
window.NewSnodeAPI = require('./ts/session/snode_api/serviceNodeAPI'); window.NewSnodeAPI = require('./ts/session/snode_api/serviceNodeAPI');
window.SnodePool = require('./ts/session/snode_api/snodePool'); window.SnodePool = require('./ts/session/snode_api/snodePool');

@ -1200,7 +1200,9 @@
} }
.module-message-detail__unix-timestamp { .module-message-detail__unix-timestamp {
color: $color-light-10; @include themify($themes) {
color: subtle(themed('textColor'));
}
} }
.module-message-detail__delete-button-container { .module-message-detail__delete-button-container {

@ -370,7 +370,10 @@ $session-element-border-green: 4px solid $session-color-green;
font-size: $session-font-md; font-size: $session-font-md;
&-text { &-text {
@include session-color-subtle($session-color-white); @include themify($themes) {
@include session-color-subtle(themed('textColor'));
}
font-family: $session-font-default; font-family: $session-font-default;
font-weight: 300; font-weight: 300;
font-size: $session-font-xs; font-size: $session-font-xs;

@ -584,10 +584,6 @@
// Module: Message Detail // Module: Message Detail
.module-message-detail__unix-timestamp {
color: $color-dark-55;
}
.module-message-detail__delete-button { .module-message-detail__delete-button {
background-color: $session-color-danger; background-color: $session-color-danger;
color: $color-white; color: $color-white;

@ -370,7 +370,6 @@
<script type="text/javascript" src="../js/views/device_pairing_dialog_view.js"></script> <script type="text/javascript" src="../js/views/device_pairing_dialog_view.js"></script>
<script type="text/javascript" src="../js/views/device_pairing_words_dialog_view.js"></script> <script type="text/javascript" src="../js/views/device_pairing_words_dialog_view.js"></script>
<script type="text/javascript" src="../js/views/create_group_dialog_view.js"></script> <script type="text/javascript" src="../js/views/create_group_dialog_view.js"></script>
<script type="text/javascript" src="../js/views/confirm_session_reset_view.js"></script>
<script type="text/javascript" src="../js/views/edit_profile_dialog_view.js"></script> <script type="text/javascript" src="../js/views/edit_profile_dialog_view.js"></script>
<script type="text/javascript" src="../js/views/invite_contacts_dialog_view.js"></script> <script type="text/javascript" src="../js/views/invite_contacts_dialog_view.js"></script>
<script type="text/javascript" src="../js/views/moderators_add_dialog_view.js"></script> <script type="text/javascript" src="../js/views/moderators_add_dialog_view.js"></script>

@ -65,6 +65,7 @@ export interface Props {
collapseMetadata?: boolean; collapseMetadata?: boolean;
direction: 'incoming' | 'outgoing'; direction: 'incoming' | 'outgoing';
timestamp: number; timestamp: number;
serverTimestamp?: number;
status?: 'sending' | 'sent' | 'delivered' | 'read' | 'error'; status?: 'sending' | 'sent' | 'delivered' | 'read' | 'error';
// What if changed this over to a single contact like quote, and put the events on it? // What if changed this over to a single contact like quote, and put the events on it?
contact?: Contact & { contact?: Contact & {
@ -257,6 +258,7 @@ export class Message extends React.PureComponent<Props, State> {
text, text,
textPending, textPending,
timestamp, timestamp,
serverTimestamp,
} = this.props; } = this.props;
if (collapseMetadata) { if (collapseMetadata) {
@ -299,7 +301,7 @@ export class Message extends React.PureComponent<Props, State> {
) : ( ) : (
<Timestamp <Timestamp
i18n={i18n} i18n={i18n}
timestamp={timestamp} timestamp={serverTimestamp || timestamp}
extended={true} extended={true}
direction={direction} direction={direction}
withImageNoCaption={withImageNoCaption} withImageNoCaption={withImageNoCaption}

@ -356,30 +356,53 @@ interface MessageId {
source: any; source: any;
sourceDevice: any; sourceDevice: any;
timestamp: any; timestamp: any;
message: any;
} }
const PUBLICCHAT_MIN_TIME_BETWEEN_DUPLICATE_MESSAGES = 10 * 1000; // 10s
async function isMessageDuplicate({ async function isMessageDuplicate({
source, source,
sourceDevice, sourceDevice,
timestamp, timestamp,
message,
}: MessageId) { }: MessageId) {
const { Errors } = window.Signal.Types; const { Errors } = window.Signal.Types;
try { try {
const result = await window.Signal.Data.getMessageBySender( const result = await window.Signal.Data.getMessagesBySender(
{ source, sourceDevice, sent_at: timestamp }, { source, sourceDevice },
{ {
Message: window.Whisper.Message, Message: window.Whisper.Message,
} }
); );
if (!result) {
return Boolean(result); return false;
}
const filteredResult = result.filter(
(m: any) => m.attributes.body === message.body
);
const isSimilar = filteredResult.some((m: any) =>
isDuplicate(m, message, source)
);
return isSimilar;
} catch (error) { } catch (error) {
window.log.error('isMessageDuplicate error:', Errors.toLogFormat(error)); window.log.error('isMessageDuplicate error:', Errors.toLogFormat(error));
return false; return false;
} }
} }
export const isDuplicate = (m: any, testedMessage: any, source: string) => {
// The username in this case is the users pubKey
const sameUsername = m.attributes.source === source;
const sameText = m.attributes.body === testedMessage.body;
// Don't filter out messages that are too far apart from each other
const timestampsSimilar =
Math.abs(m.attributes.sent_at - testedMessage.timestamp) <=
PUBLICCHAT_MIN_TIME_BETWEEN_DUPLICATE_MESSAGES;
return sameUsername && sameText && timestampsSimilar;
};
async function handleProfileUpdate( async function handleProfileUpdate(
profileKeyBuffer: Uint8Array, profileKeyBuffer: Uint8Array,
convoId: string, convoId: string,
@ -427,6 +450,7 @@ interface MessageCreationData {
source: boolean; source: boolean;
serverId: string; serverId: string;
message: any; message: any;
serverTimestamp: any;
// Needed for synced outgoing messages // Needed for synced outgoing messages
unidentifiedStatus: any; // ??? unidentifiedStatus: any; // ???
@ -445,6 +469,7 @@ export function initIncomingMessage(data: MessageCreationData): MessageModel {
source, source,
serverId, serverId,
message, message,
serverTimestamp,
} = data; } = data;
const type = 'incoming'; const type = 'incoming';
@ -457,6 +482,7 @@ export function initIncomingMessage(data: MessageCreationData): MessageModel {
sourceDevice, sourceDevice,
serverId, // + (not present below in `createSentMessage`) serverId, // + (not present below in `createSentMessage`)
sent_at: timestamp, sent_at: timestamp,
serverTimestamp,
received_at: receivedAt || Date.now(), received_at: receivedAt || Date.now(),
conversationId: groupId ?? source, conversationId: groupId ?? source,
unidentifiedDeliveryReceived, // + unidentifiedDeliveryReceived, // +
@ -476,6 +502,7 @@ function createSentMessage(data: MessageCreationData): MessageModel {
const { const {
timestamp, timestamp,
serverTimestamp,
isPublic, isPublic,
receivedAt, receivedAt,
sourceDevice, sourceDevice,
@ -508,6 +535,7 @@ function createSentMessage(data: MessageCreationData): MessageModel {
const messageData: any = { const messageData: any = {
source: window.textsecure.storage.user.getNumber(), source: window.textsecure.storage.user.getNumber(),
sourceDevice, sourceDevice,
serverTimestamp,
sent_at: timestamp, sent_at: timestamp,
received_at: isPublic ? receivedAt : now, received_at: isPublic ? receivedAt : now,
conversationId: destination, // conversation ID will might change later (if it is a group) conversationId: destination, // conversation ID will might change later (if it is a group)
@ -588,9 +616,7 @@ export async function handleMessageEvent(event: MessageEvent): Promise<void> {
// if the message is `sent` (from secondary device) we have to set the sender manually... (at least for now) // if the message is `sent` (from secondary device) we have to set the sender manually... (at least for now)
source = source || msg.get('source'); source = source || msg.get('source');
const isDuplicate = await isMessageDuplicate(data); if (await isMessageDuplicate(data)) {
if (isDuplicate) {
// RSS expects duplicates, so squelch log // RSS expects duplicates, so squelch log
if (!source.match(/^rss:/)) { if (!source.match(/^rss:/)) {
window.log.warn('Received duplicate message', msg.idForLogging()); window.log.warn('Received duplicate message', msg.idForLogging());

@ -4,7 +4,7 @@ import { EnvelopePlus } from './types';
import * as Data from '../../js/modules/data'; import * as Data from '../../js/modules/data';
import { SignalService } from '../protobuf'; import { SignalService } from '../protobuf';
import { updateProfile } from './receiver'; import { updateProfile } from './dataMessage';
import { onVerified } from './syncMessages'; import { onVerified } from './syncMessages';
import { StringUtils } from '../session/utils'; import { StringUtils } from '../session/utils';

@ -19,27 +19,14 @@ import _ from 'lodash';
export { processMessage, onDeliveryReceipt }; export { processMessage, onDeliveryReceipt };
import { import { handleMessageEvent, updateProfile } from './dataMessage';
handleDataMessage,
handleMessageEvent,
updateProfile,
} from './dataMessage';
import { getEnvelopeId } from './common'; import { getEnvelopeId } from './common';
import { StringUtils } from '../session/utils'; import { StringUtils } from '../session/utils';
import { SignalService } from '../protobuf'; import { SignalService } from '../protobuf';
import { BlockedNumberController } from '../util/blockedNumberController';
import { MultiDeviceProtocol } from '../session/protocols'; import { MultiDeviceProtocol } from '../session/protocols';
// TODO: check if some of these exports no longer needed // TODO: check if some of these exports no longer needed
export {
handleEndSession,
handleMediumGroupUpdate,
downloadAttachment,
handleDataMessage,
updateProfile,
handleMessageEvent,
};
interface ReqOptions { interface ReqOptions {
conversationId: string; conversationId: string;

@ -11,14 +11,12 @@ import {
handleMessageEvent, handleMessageEvent,
isMessageEmpty, isMessageEmpty,
processDecrypted, processDecrypted,
updateProfile,
} from './dataMessage'; } from './dataMessage';
import { updateProfile } from './receiver';
import { handleContacts } from './multidevice'; import { handleContacts } from './multidevice';
import { updateOrCreateGroupFromSync } from '../session/medium_group'; import { updateOrCreateGroupFromSync } from '../session/medium_group';
import { MultiDeviceProtocol } from '../session/protocols'; import { MultiDeviceProtocol } from '../session/protocols';
import { DataMessage } from '../session/messages/outgoing';
import { BlockedNumberController } from '../util'; import { BlockedNumberController } from '../util';
import { StringUtils } from '../session/utils';
export async function handleSyncMessage( export async function handleSyncMessage(
envelope: EnvelopePlus, envelope: EnvelopePlus,

@ -62,13 +62,14 @@ export class MessageQueue implements MessageQueueInterface {
try { try {
const result = await MessageSender.sendToOpenGroup(message); const result = await MessageSender.sendToOpenGroup(message);
// sendToOpenGroup returns -1 if failed or an id if succeeded // sendToOpenGroup returns -1 if failed or an id if succeeded
if (result < 0) { if (result.serverId < 0) {
this.events.emit('fail', message, error); this.events.emit('fail', message, error);
} else { } else {
const messageEventData = { const messageEventData = {
pubKey: message.group.groupId, pubKey: message.group.groupId,
timestamp: message.timestamp, timestamp: message.timestamp,
serverId: result, serverId: result.serverId,
serverTimestamp: result.serverTimestamp,
}; };
this.events.emit('success', message); this.events.emit('success', message);

@ -98,7 +98,7 @@ function wrapEnvelope(envelope: SignalService.Envelope): Uint8Array {
*/ */
export async function sendToOpenGroup( export async function sendToOpenGroup(
message: OpenGroupMessage message: OpenGroupMessage
): Promise<number> { ): Promise<{ serverId: number; serverTimestamp: number }> {
/* /*
Note: Retrying wasn't added to this but it can be added in the future if needed. Note: Retrying wasn't added to this but it can be added in the future if needed.
The only problem is that `channelAPI.sendMessage` returns true/false and doesn't throw any error so we can never be sure why sending failed. The only problem is that `channelAPI.sendMessage` returns true/false and doesn't throw any error so we can never be sure why sending failed.
@ -112,7 +112,7 @@ export async function sendToOpenGroup(
); );
if (!channelAPI) { if (!channelAPI) {
return -1; return { serverId: -1, serverTimestamp: -1 };
} }
// Returns -1 on fail or an id > 0 on success // Returns -1 on fail or an id > 0 on success

@ -27,6 +27,7 @@ chai.use(chaiAsPromised);
const { expect } = chai; const { expect } = chai;
// tslint:disable-next-line: max-func-body-length
describe('MessageQueue', () => { describe('MessageQueue', () => {
// Initialize new stubbed cache // Initialize new stubbed cache
const sandbox = sinon.createSandbox(); const sandbox = sinon.createSandbox();
@ -321,12 +322,12 @@ describe('MessageQueue', () => {
describe('open groups', async () => { describe('open groups', async () => {
let sendToOpenGroupStub: sinon.SinonStub< let sendToOpenGroupStub: sinon.SinonStub<
[OpenGroupMessage], [OpenGroupMessage],
Promise<number> Promise<{ serverId: number; serverTimestamp: number }>
>; >;
beforeEach(() => { beforeEach(() => {
sendToOpenGroupStub = sandbox sendToOpenGroupStub = sandbox
.stub(MessageSender, 'sendToOpenGroup') .stub(MessageSender, 'sendToOpenGroup')
.resolves(-1); .resolves({ serverId: -1, serverTimestamp: -1 });
}); });
it('can send to open group', async () => { it('can send to open group', async () => {
@ -336,7 +337,7 @@ describe('MessageQueue', () => {
}); });
it('should emit a success event when send was successful', async () => { it('should emit a success event when send was successful', async () => {
sendToOpenGroupStub.resolves(123456); sendToOpenGroupStub.resolves({ serverId: 5125, serverTimestamp: 5125 });
const message = TestUtils.generateOpenGroupMessage(); const message = TestUtils.generateOpenGroupMessage();
const eventPromise = PromiseUtils.waitForTask(complete => { const eventPromise = PromiseUtils.waitForTask(complete => {
@ -348,7 +349,7 @@ describe('MessageQueue', () => {
}); });
it('should emit a fail event if something went wrong', async () => { it('should emit a fail event if something went wrong', async () => {
sendToOpenGroupStub.resolves(-1); sendToOpenGroupStub.resolves({ serverId: -1, serverTimestamp: -1 });
const message = TestUtils.generateOpenGroupMessage(); const message = TestUtils.generateOpenGroupMessage();
const eventPromise = PromiseUtils.waitForTask(complete => { const eventPromise = PromiseUtils.waitForTask(complete => {
messageQueueStub.events.once('fail', complete); messageQueueStub.events.once('fail', complete);

Loading…
Cancel
Save