Migrate message ids to UUIDv7

This commit is contained in:
Fedor Indutny 2024-10-07 20:17:03 -07:00 committed by GitHub
parent c1b5811c39
commit 60d7cbff3e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
24 changed files with 203 additions and 147 deletions

View file

@ -3352,25 +3352,13 @@ Signal Desktop makes use of the following open source projects.
The MIT License (MIT) The MIT License (MIT)
Copyright (c) 2010-2016 Robert Kieffer and other contributors Copyright (c) 2010-2020 Robert Kieffer and other contributors
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
## uuid-browser ## uuid-browser

28
package-lock.json generated
View file

@ -105,7 +105,7 @@
"split2": "4.0.0", "split2": "4.0.0",
"type-fest": "4.23.0", "type-fest": "4.23.0",
"urlpattern-polyfill": "9.0.0", "urlpattern-polyfill": "9.0.0",
"uuid": "3.3.2", "uuid": "10.0.0",
"uuid-browser": "3.1.0", "uuid-browser": "3.1.0",
"websocket": "1.0.34", "websocket": "1.0.34",
"write-file-atomic": "5.0.1", "write-file-atomic": "5.0.1",
@ -184,7 +184,7 @@
"@types/split2": "3.2.1", "@types/split2": "3.2.1",
"@types/terser-webpack-plugin": "5.0.3", "@types/terser-webpack-plugin": "5.0.3",
"@types/unzipper": "0.10.9", "@types/unzipper": "0.10.9",
"@types/uuid": "3.4.4", "@types/uuid": "10.0.0",
"@types/websocket": "1.0.0", "@types/websocket": "1.0.0",
"@types/write-file-atomic": "4.0.3", "@types/write-file-atomic": "4.0.3",
"@types/yargs": "17.0.7", "@types/yargs": "17.0.7",
@ -11446,13 +11446,11 @@
} }
}, },
"node_modules/@types/uuid": { "node_modules/@types/uuid": {
"version": "3.4.4", "version": "10.0.0",
"resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-3.4.4.tgz", "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz",
"integrity": "sha512-tPIgT0GUmdJQNSHxp0X2jnpQfBSTfGxUMc/2CXBU2mnyTFVYVa2ojpoQ74w0U2yn2vw3jnC640+77lkFFpdVDw==", "integrity": "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==",
"dev": true, "dev": true,
"dependencies": { "license": "MIT"
"@types/node": "*"
}
}, },
"node_modules/@types/wait-on": { "node_modules/@types/wait-on": {
"version": "5.3.4", "version": "5.3.4",
@ -35778,12 +35776,16 @@
} }
}, },
"node_modules/uuid": { "node_modules/uuid": {
"version": "3.3.2", "version": "10.0.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz",
"integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==", "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==",
"deprecated": "Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details.", "funding": [
"https://github.com/sponsors/broofa",
"https://github.com/sponsors/ctavan"
],
"license": "MIT",
"bin": { "bin": {
"uuid": "bin/uuid" "uuid": "dist/bin/uuid"
} }
}, },
"node_modules/uuid-browser": { "node_modules/uuid-browser": {

View file

@ -189,7 +189,7 @@
"split2": "4.0.0", "split2": "4.0.0",
"type-fest": "4.23.0", "type-fest": "4.23.0",
"urlpattern-polyfill": "9.0.0", "urlpattern-polyfill": "9.0.0",
"uuid": "3.3.2", "uuid": "10.0.0",
"uuid-browser": "3.1.0", "uuid-browser": "3.1.0",
"websocket": "1.0.34", "websocket": "1.0.34",
"write-file-atomic": "5.0.1", "write-file-atomic": "5.0.1",
@ -268,7 +268,7 @@
"@types/split2": "3.2.1", "@types/split2": "3.2.1",
"@types/terser-webpack-plugin": "5.0.3", "@types/terser-webpack-plugin": "5.0.3",
"@types/unzipper": "0.10.9", "@types/unzipper": "0.10.9",
"@types/uuid": "3.4.4", "@types/uuid": "10.0.0",
"@types/websocket": "1.0.0", "@types/websocket": "1.0.0",
"@types/write-file-atomic": "4.0.3", "@types/write-file-atomic": "4.0.3",
"@types/yargs": "17.0.7", "@types/yargs": "17.0.7",

View file

@ -6,7 +6,7 @@ import { render } from 'react-dom';
import { batch as batchDispatch } from 'react-redux'; import { batch as batchDispatch } from 'react-redux';
import PQueue from 'p-queue'; import PQueue from 'p-queue';
import pMap from 'p-map'; import pMap from 'p-map';
import { v4 as generateUuid } from 'uuid'; import { v7 as generateUuid } from 'uuid';
import * as Registration from './util/registration'; import * as Registration from './util/registration';
import MessageReceiver from './textsecure/MessageReceiver'; import MessageReceiver from './textsecure/MessageReceiver';
@ -169,6 +169,7 @@ import {
incrementMessageCounter, incrementMessageCounter,
initializeMessageCounter, initializeMessageCounter,
} from './util/incrementMessageCounter'; } from './util/incrementMessageCounter';
import { generateMessageId } from './util/generateMessageId';
import { RetryPlaceholders } from './util/retryPlaceholders'; import { RetryPlaceholders } from './util/retryPlaceholders';
import { setBatchingStrategy } from './util/messageBatcher'; import { setBatchingStrategy } from './util/messageBatcher';
import { parseRemoteClientExpiration } from './util/parseRemoteClientExpiration'; import { parseRemoteClientExpiration } from './util/parseRemoteClientExpiration';
@ -2694,7 +2695,8 @@ export async function startApp(): Promise<void> {
} }
const partialMessage: MessageAttributesType = { const partialMessage: MessageAttributesType = {
id: generateUuid(), ...generateMessageId(data.receivedAtCounter),
canReplyToStory: data.message.isStory canReplyToStory: data.message.isStory
? data.message.canReplyToStory ? data.message.canReplyToStory
: undefined, : undefined,
@ -2705,7 +2707,6 @@ export async function startApp(): Promise<void> {
), ),
readStatus: ReadStatus.Read, readStatus: ReadStatus.Read,
received_at_ms: data.receivedAtDate, received_at_ms: data.receivedAtDate,
received_at: data.receivedAtCounter,
seenStatus: SeenStatus.NotApplicable, seenStatus: SeenStatus.NotApplicable,
sendStateByConversationId, sendStateByConversationId,
sent_at: timestamp, sent_at: timestamp,
@ -2957,13 +2958,13 @@ export async function startApp(): Promise<void> {
`Did not receive receivedAtCounter for message: ${data.timestamp}` `Did not receive receivedAtCounter for message: ${data.timestamp}`
); );
const partialMessage: MessageAttributesType = { const partialMessage: MessageAttributesType = {
id: generateUuid(), ...generateMessageId(data.receivedAtCounter),
canReplyToStory: data.message.isStory canReplyToStory: data.message.isStory
? data.message.canReplyToStory ? data.message.canReplyToStory
: undefined, : undefined,
conversationId: descriptor.id, conversationId: descriptor.id,
readStatus: ReadStatus.Unread, readStatus: ReadStatus.Unread,
received_at: data.receivedAtCounter,
received_at_ms: data.receivedAtDate, received_at_ms: data.receivedAtDate,
seenStatus: SeenStatus.Unseen, seenStatus: SeenStatus.Unseen,
sent_at: data.timestamp, sent_at: data.timestamp,

View file

@ -2,7 +2,7 @@
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
import type { FormEvent } from 'react'; import type { FormEvent } from 'react';
import React, { useCallback, useMemo, useState } from 'react'; import React, { useCallback, useMemo, useState } from 'react';
import uuid from 'uuid'; import { v4 as uuid } from 'uuid';
import { z } from 'zod'; import { z } from 'zod';
import { Modal } from './Modal'; import { Modal } from './Modal';
import type { LocalizerType } from '../types/I18N'; import type { LocalizerType } from '../types/I18N';

View file

@ -3,7 +3,7 @@
import classNames from 'classnames'; import classNames from 'classnames';
import React, { useMemo } from 'react'; import React, { useMemo } from 'react';
import uuid from 'uuid'; import { v4 as uuid } from 'uuid';
import { getClassNamesFor } from '../util/getClassNamesFor'; import { getClassNamesFor } from '../util/getClassNamesFor';
import { CircleCheckbox } from './CircleCheckbox'; import { CircleCheckbox } from './CircleCheckbox';

View file

@ -12,7 +12,7 @@ import React, {
} from 'react'; } from 'react';
import { noop, partition } from 'lodash'; import { noop, partition } from 'lodash';
import classNames from 'classnames'; import classNames from 'classnames';
import uuid from 'uuid'; import { v4 as uuid } from 'uuid';
import * as LocaleMatcher from '@formatjs/intl-localematcher'; import * as LocaleMatcher from '@formatjs/intl-localematcher';
import type { MediaDeviceSettings } from '../types/Calling'; import type { MediaDeviceSettings } from '../types/Calling';

View file

@ -2,7 +2,7 @@
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
import type { UIEvent } from 'react'; import type { UIEvent } from 'react';
import React, { useEffect, useMemo, useRef, useState } from 'react'; import React, { useEffect, useMemo, useRef, useState } from 'react';
import uuid from 'uuid'; import { v4 as uuid } from 'uuid';
import type { LocalizerType } from '../types/I18N'; import type { LocalizerType } from '../types/I18N';
import { Modal } from './Modal'; import { Modal } from './Modal';
import { Button, ButtonVariant } from './Button'; import { Button, ButtonVariant } from './Button';

View file

@ -2,7 +2,7 @@
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
import React, { useMemo, useState } from 'react'; import React, { useMemo, useState } from 'react';
import uuid from 'uuid'; import { v4 as uuid } from 'uuid';
import type { LocalizerType } from '../../../types/Util'; import type { LocalizerType } from '../../../types/Util';
import { getMuteOptions } from '../../../util/getMuteOptions'; import { getMuteOptions } from '../../../util/getMuteOptions';

View file

@ -12,7 +12,6 @@ import {
} from 'lodash'; } from 'lodash';
import Long from 'long'; import Long from 'long';
import type { ClientZkGroupCipher } from '@signalapp/libsignal-client/zkgroup'; import type { ClientZkGroupCipher } from '@signalapp/libsignal-client/zkgroup';
import { v4 as getGuid } from 'uuid';
import LRU from 'lru-cache'; import LRU from 'lru-cache';
import * as log from './logging/log'; import * as log from './logging/log';
import { import {
@ -101,6 +100,7 @@ import {
decodeGroupSendEndorsementResponse, decodeGroupSendEndorsementResponse,
isValidGroupSendEndorsementsExpiration, isValidGroupSendEndorsementsExpiration,
} from './util/groupSendEndorsements'; } from './util/groupSendEndorsements';
import { generateMessageId } from './util/generateMessageId';
type AccessRequiredEnum = Proto.AccessControl.AccessRequired; type AccessRequiredEnum = Proto.AccessControl.AccessRequired;
@ -293,7 +293,7 @@ type UploadedAvatarType = {
type BasicMessageType = Pick< type BasicMessageType = Pick<
MessageAttributesType, MessageAttributesType,
'id' | 'schemaVersion' | 'readStatus' | 'seenStatus' 'readStatus' | 'seenStatus'
>; >;
type GroupV2ChangeMessageType = { type GroupV2ChangeMessageType = {
@ -335,14 +335,6 @@ const SUPPORTED_CHANGE_EPOCH = 5;
export const LINK_VERSION_ERROR = 'LINK_VERSION_ERROR'; export const LINK_VERSION_ERROR = 'LINK_VERSION_ERROR';
const GROUP_INVITE_LINK_PASSWORD_LENGTH = 16; const GROUP_INVITE_LINK_PASSWORD_LENGTH = 16;
function generateBasicMessage(): BasicMessageType {
return {
id: getGuid(),
schemaVersion: MAX_MESSAGE_SCHEMA,
// this is missing most properties to fulfill this type
};
}
// Group Links // Group Links
export function generateGroupInviteLinkPassword(): Uint8Array { export function generateGroupInviteLinkPassword(): Uint8Array {
@ -2024,12 +2016,13 @@ export async function createGroupV2(
}); });
const createdTheGroupMessage: MessageAttributesType = { const createdTheGroupMessage: MessageAttributesType = {
...generateBasicMessage(), ...generateMessageId(incrementMessageCounter()),
schemaVersion: MAX_MESSAGE_SCHEMA,
type: 'group-v2-change', type: 'group-v2-change',
sourceServiceId: ourAci, sourceServiceId: ourAci,
conversationId: conversation.id, conversationId: conversation.id,
readStatus: ReadStatus.Read, readStatus: ReadStatus.Read,
received_at: incrementMessageCounter(),
received_at_ms: timestamp, received_at_ms: timestamp,
timestamp, timestamp,
seenStatus: SeenStatus.Seen, seenStatus: SeenStatus.Seen,
@ -2468,7 +2461,6 @@ export async function initiateMigrationToGroupV2(
const groupChangeMessages: Array<GroupChangeMessageType> = []; const groupChangeMessages: Array<GroupChangeMessageType> = [];
groupChangeMessages.push({ groupChangeMessages.push({
...generateBasicMessage(),
type: 'group-v1-migration', type: 'group-v1-migration',
groupMigration: { groupMigration: {
areWeInvited: false, areWeInvited: false,
@ -2609,7 +2601,6 @@ export function buildMigrationBubble(
); );
return { return {
...generateBasicMessage(),
type: 'group-v1-migration', type: 'group-v1-migration',
groupMigration: { groupMigration: {
areWeInvited, areWeInvited,
@ -2623,7 +2614,6 @@ export function buildMigrationBubble(
export function getBasicMigrationBubble(): GroupChangeMessageType { export function getBasicMigrationBubble(): GroupChangeMessageType {
return { return {
...generateBasicMessage(),
type: 'group-v1-migration', type: 'group-v1-migration',
groupMigration: { groupMigration: {
areWeInvited: false, areWeInvited: false,
@ -2697,7 +2687,6 @@ export async function joinGroupV2ViaLinkAndMigrate({
}; };
const groupChangeMessages: Array<GroupChangeMessageType> = [ const groupChangeMessages: Array<GroupChangeMessageType> = [
{ {
...generateBasicMessage(),
type: 'group-v1-migration', type: 'group-v1-migration',
groupMigration: { groupMigration: {
areWeInvited: false, areWeInvited: false,
@ -2865,7 +2854,6 @@ export async function respondToGroupV2Migration({
seenStatus: SeenStatus.Seen, seenStatus: SeenStatus.Seen,
}, },
{ {
...generateBasicMessage(),
type: 'group-v2-change', type: 'group-v2-change',
groupV2Change: { groupV2Change: {
details: [ details: [
@ -2946,7 +2934,6 @@ export async function respondToGroupV2Migration({
if (!areWeInvited && !areWeMember) { if (!areWeInvited && !areWeMember) {
// Add a message to the timeline saying the user was removed. This shouldn't happen. // Add a message to the timeline saying the user was removed. This shouldn't happen.
groupChangeMessages.push({ groupChangeMessages.push({
...generateBasicMessage(),
type: 'group-v2-change', type: 'group-v2-change',
groupV2Change: { groupV2Change: {
details: [ details: [
@ -3184,8 +3171,9 @@ async function updateGroup(
return { return {
...changeMessage, ...changeMessage,
...generateMessageId(finalReceivedAt),
schemaVersion: MAX_MESSAGE_SCHEMA,
conversationId: conversation.id, conversationId: conversation.id,
received_at: finalReceivedAt,
received_at_ms: syntheticSentAt, received_at_ms: syntheticSentAt,
sent_at: syntheticSentAt, sent_at: syntheticSentAt,
timestamp, timestamp,
@ -3895,7 +3883,6 @@ async function updateGroupViaSingleChange({
catchupMessages.length > 0 catchupMessages.length > 0
) { ) {
groupChangeMessages.push({ groupChangeMessages.push({
...generateBasicMessage(),
type: 'group-v2-change', type: 'group-v2-change',
groupV2Change: { groupV2Change: {
details: [ details: [
@ -4901,7 +4888,6 @@ function extractDiffs({
if (firstUpdate && serviceIdKindInvitedToGroup !== undefined) { if (firstUpdate && serviceIdKindInvitedToGroup !== undefined) {
// Note, we will add 'you were invited' to group even if dropInitialJoinMessage = true // Note, we will add 'you were invited' to group even if dropInitialJoinMessage = true
message = { message = {
...generateBasicMessage(),
type: 'group-v2-change', type: 'group-v2-change',
groupV2Change: { groupV2Change: {
from: whoInvitedUsUserId || from, from: whoInvitedUsUserId || from,
@ -4919,7 +4905,6 @@ function extractDiffs({
}; };
} else if (firstUpdate && areWePendingApproval) { } else if (firstUpdate && areWePendingApproval) {
message = { message = {
...generateBasicMessage(),
type: 'group-v2-change', type: 'group-v2-change',
groupV2Change: { groupV2Change: {
from: ourAci, from: ourAci,
@ -4940,7 +4925,6 @@ function extractDiffs({
sourceServiceId === ourAci sourceServiceId === ourAci
) { ) {
message = { message = {
...generateBasicMessage(),
type: 'group-v2-change', type: 'group-v2-change',
groupV2Change: { groupV2Change: {
from, from,
@ -4962,7 +4946,6 @@ function extractDiffs({
); );
message = { message = {
...generateBasicMessage(),
type: 'group-v2-change', type: 'group-v2-change',
groupV2Change: { groupV2Change: {
from, from,
@ -4973,7 +4956,6 @@ function extractDiffs({
}; };
} else if (firstUpdate && current.revision === 0) { } else if (firstUpdate && current.revision === 0) {
message = { message = {
...generateBasicMessage(),
type: 'group-v2-change', type: 'group-v2-change',
groupV2Change: { groupV2Change: {
from, from,
@ -5002,7 +4984,6 @@ function extractDiffs({
} }
message = { message = {
...generateBasicMessage(),
type: 'group-v2-change', type: 'group-v2-change',
sourceServiceId, sourceServiceId,
groupV2Change: { groupV2Change: {
@ -5014,7 +4995,6 @@ function extractDiffs({
}; };
} else if (details.length > 0) { } else if (details.length > 0) {
message = { message = {
...generateBasicMessage(),
type: 'group-v2-change', type: 'group-v2-change',
sourceServiceId, sourceServiceId,
groupV2Change: { groupV2Change: {
@ -5041,7 +5021,6 @@ function extractDiffs({
`extractDiffs/${logId}: generating change notification for new ${expireTimer} timer` `extractDiffs/${logId}: generating change notification for new ${expireTimer} timer`
); );
timerNotification = { timerNotification = {
...generateBasicMessage(),
type: 'timer-notification', type: 'timer-notification',
sourceServiceId: isReJoin ? undefined : sourceServiceId, sourceServiceId: isReJoin ? undefined : sourceServiceId,
flags: Proto.DataMessage.Flags.EXPIRATION_TIMER_UPDATE, flags: Proto.DataMessage.Flags.EXPIRATION_TIMER_UPDATE,

View file

@ -2,7 +2,7 @@
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
import PQueue from 'p-queue'; import PQueue from 'p-queue';
import { v4 as uuid } from 'uuid'; import { v7 as uuid } from 'uuid';
import { noop } from 'lodash'; import { noop } from 'lodash';
import { Job } from './Job'; import { Job } from './Job';

View file

@ -2,7 +2,6 @@
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
import { isNumber } from 'lodash'; import { isNumber } from 'lodash';
import { v4 as generateUuid } from 'uuid';
import * as Errors from '../../types/errors'; import * as Errors from '../../types/errors';
import { strictAssert } from '../../util/assert'; import { strictAssert } from '../../util/assert';
@ -32,6 +31,7 @@ import type { AciString, ServiceIdString } from '../../types/ServiceId';
import { isAciString } from '../../util/isAciString'; import { isAciString } from '../../util/isAciString';
import { handleMultipleSendErrors } from './handleMultipleSendErrors'; import { handleMultipleSendErrors } from './handleMultipleSendErrors';
import { incrementMessageCounter } from '../../util/incrementMessageCounter'; import { incrementMessageCounter } from '../../util/incrementMessageCounter';
import { generateMessageId } from '../../util/generateMessageId';
import type { import type {
ConversationQueueJobBundle, ConversationQueueJobBundle,
@ -159,11 +159,10 @@ export async function sendReaction(
remove: !emoji, remove: !emoji,
}; };
const ephemeralMessageForReactionSend = new window.Whisper.Message({ const ephemeralMessageForReactionSend = new window.Whisper.Message({
id: generateUuid(), ...generateMessageId(incrementMessageCounter()),
type: 'outgoing', type: 'outgoing',
conversationId: conversation.get('id'), conversationId: conversation.get('id'),
sent_at: pendingReaction.timestamp, sent_at: pendingReaction.timestamp,
received_at: incrementMessageCounter(),
received_at_ms: pendingReaction.timestamp, received_at_ms: pendingReaction.timestamp,
timestamp: pendingReaction.timestamp, timestamp: pendingReaction.timestamp,
sendStateByConversationId: zipObject( sendStateByConversationId: zipObject(

View file

@ -174,6 +174,7 @@ import { ReceiptType } from '../types/Receipt';
import { getQuoteAttachment } from '../util/makeQuote'; import { getQuoteAttachment } from '../util/makeQuote';
import { deriveProfileKeyVersion } from '../util/zkgroup'; import { deriveProfileKeyVersion } from '../util/zkgroup';
import { incrementMessageCounter } from '../util/incrementMessageCounter'; import { incrementMessageCounter } from '../util/incrementMessageCounter';
import { generateMessageId } from '../util/generateMessageId';
import { getMessageAuthorText } from '../util/getMessageAuthorText'; import { getMessageAuthorText } from '../util/getMessageAuthorText';
import { downscaleOutgoingAttachment } from '../util/attachments'; import { downscaleOutgoingAttachment } from '../util/attachments';
import { MessageRequestResponseEvent } from '../types/MessageRequestResponseEvent'; import { MessageRequestResponseEvent } from '../types/MessageRequestResponseEvent';
@ -2328,11 +2329,10 @@ export class ConversationModel extends window.Backbone
: lastMessageTimestamp; : lastMessageTimestamp;
const message: MessageAttributesType = { const message: MessageAttributesType = {
id: generateGuid(), ...generateMessageId(incrementMessageCounter()),
conversationId: this.id, conversationId: this.id,
type: 'message-request-response-event', type: 'message-request-response-event',
sent_at: maybeLastMessageTimestamp, sent_at: maybeLastMessageTimestamp,
received_at: incrementMessageCounter(),
received_at_ms: maybeLastMessageTimestamp, received_at_ms: maybeLastMessageTimestamp,
readStatus: ReadStatus.Read, readStatus: ReadStatus.Read,
seenStatus: SeenStatus.NotApplicable, seenStatus: SeenStatus.NotApplicable,
@ -3085,12 +3085,11 @@ export class ConversationModel extends window.Backbone
}); });
const message: MessageAttributesType = { const message: MessageAttributesType = {
id: generateGuid(), ...generateMessageId(receivedAtCounter),
conversationId: this.id, conversationId: this.id,
type: 'chat-session-refreshed', type: 'chat-session-refreshed',
timestamp: receivedAt, timestamp: receivedAt,
sent_at: receivedAt, sent_at: receivedAt,
received_at: receivedAtCounter,
received_at_ms: receivedAt, received_at_ms: receivedAt,
readStatus: ReadStatus.Unread, readStatus: ReadStatus.Unread,
seenStatus: SeenStatus.Unseen, seenStatus: SeenStatus.Unseen,
@ -3133,12 +3132,11 @@ export class ConversationModel extends window.Backbone
} }
const message: MessageAttributesType = { const message: MessageAttributesType = {
id: generateGuid(), ...generateMessageId(receivedAtCounter),
conversationId: this.id, conversationId: this.id,
type: 'delivery-issue', type: 'delivery-issue',
sourceServiceId: senderAci, sourceServiceId: senderAci,
sent_at: receivedAt, sent_at: receivedAt,
received_at: receivedAtCounter,
received_at_ms: receivedAt, received_at_ms: receivedAt,
timestamp: receivedAt, timestamp: receivedAt,
readStatus: ReadStatus.Unread, readStatus: ReadStatus.Unread,
@ -3183,12 +3181,11 @@ export class ConversationModel extends window.Backbone
const timestamp = Date.now(); const timestamp = Date.now();
const message: MessageAttributesType = { const message: MessageAttributesType = {
id: generateGuid(), ...generateMessageId(incrementMessageCounter()),
conversationId: this.id, conversationId: this.id,
type: 'keychange', type: 'keychange',
sent_at: timestamp, sent_at: timestamp,
timestamp, timestamp,
received_at: incrementMessageCounter(),
received_at_ms: timestamp, received_at_ms: timestamp,
key_changed: keyChangedId, key_changed: keyChangedId,
readStatus: ReadStatus.Read, readStatus: ReadStatus.Read,
@ -3245,12 +3242,11 @@ export class ConversationModel extends window.Backbone
const timestamp = Date.now(); const timestamp = Date.now();
const message: MessageAttributesType = { const message: MessageAttributesType = {
id: generateGuid(), ...generateMessageId(incrementMessageCounter()),
conversationId: this.id, conversationId: this.id,
type: 'conversation-merge', type: 'conversation-merge',
sent_at: timestamp, sent_at: timestamp,
timestamp, timestamp,
received_at: incrementMessageCounter(),
received_at_ms: timestamp, received_at_ms: timestamp,
conversationMerge: { conversationMerge: {
renderInfo, renderInfo,
@ -3294,12 +3290,11 @@ export class ConversationModel extends window.Backbone
log.info(`${logId}: adding notification`); log.info(`${logId}: adding notification`);
const timestamp = Date.now(); const timestamp = Date.now();
const message: MessageAttributesType = { const message: MessageAttributesType = {
id: generateGuid(), ...generateMessageId(incrementMessageCounter()),
conversationId: this.id, conversationId: this.id,
type: 'phone-number-discovery', type: 'phone-number-discovery',
sent_at: timestamp, sent_at: timestamp,
timestamp, timestamp,
received_at: incrementMessageCounter(),
received_at_ms: timestamp, received_at_ms: timestamp,
phoneNumberDiscovery: { phoneNumberDiscovery: {
e164, e164,
@ -3343,12 +3338,11 @@ export class ConversationModel extends window.Backbone
const timestamp = Date.now(); const timestamp = Date.now();
const message: MessageAttributesType = { const message: MessageAttributesType = {
id: generateGuid(), ...generateMessageId(incrementMessageCounter()),
conversationId: this.id, conversationId: this.id,
local: Boolean(options.local), local: Boolean(options.local),
readStatus: ReadStatus.Read, readStatus: ReadStatus.Read,
received_at_ms: timestamp, received_at_ms: timestamp,
received_at: incrementMessageCounter(),
seenStatus: options.local ? SeenStatus.Seen : SeenStatus.Unseen, seenStatus: options.local ? SeenStatus.Seen : SeenStatus.Unseen,
sent_at: lastMessage, sent_at: lastMessage,
timestamp, timestamp,
@ -3388,11 +3382,10 @@ export class ConversationModel extends window.Backbone
): Promise<void> { ): Promise<void> {
const now = Date.now(); const now = Date.now();
const message: MessageAttributesType = { const message: MessageAttributesType = {
id: generateGuid(), ...generateMessageId(incrementMessageCounter()),
conversationId: this.id, conversationId: this.id,
type: 'profile-change', type: 'profile-change',
sent_at: now, sent_at: now,
received_at: incrementMessageCounter(),
received_at_ms: now, received_at_ms: now,
readStatus: ReadStatus.Read, readStatus: ReadStatus.Read,
seenStatus: SeenStatus.NotApplicable, seenStatus: SeenStatus.NotApplicable,
@ -3432,11 +3425,10 @@ export class ConversationModel extends window.Backbone
): Promise<string> { ): Promise<string> {
const now = Date.now(); const now = Date.now();
const message: MessageAttributesType = { const message: MessageAttributesType = {
id: generateGuid(), ...generateMessageId(incrementMessageCounter()),
conversationId: this.id, conversationId: this.id,
type, type,
sent_at: now, sent_at: now,
received_at: incrementMessageCounter(),
received_at_ms: now, received_at_ms: now,
timestamp: now, timestamp: now,
@ -4102,7 +4094,7 @@ export class ConversationModel extends window.Backbone
// Here we move attachments to disk // Here we move attachments to disk
const attributes = await upgradeMessageSchema({ const attributes = await upgradeMessageSchema({
id: generateGuid(), ...generateMessageId(incrementMessageCounter()),
timestamp: now, timestamp: now,
type: 'outgoing', type: 'outgoing',
body, body,
@ -4112,7 +4104,6 @@ export class ConversationModel extends window.Backbone
preview, preview,
attachments: attachmentsToSend, attachments: attachmentsToSend,
sent_at: now, sent_at: now,
received_at: incrementMessageCounter(),
received_at_ms: now, received_at_ms: now,
expirationStartTimestamp, expirationStartTimestamp,
expireTimer, expireTimer,
@ -4734,9 +4725,9 @@ export class ConversationModel extends window.Backbone
const shouldBeRead = const shouldBeRead =
(isInitialSync && isFromSyncOperation) || isFromMe || isNoteToSelf; (isInitialSync && isFromSyncOperation) || isFromMe || isNoteToSelf;
const id = generateGuid(); const counter = receivedAt ?? incrementMessageCounter();
const attributes = { const attributes = {
id, ...generateMessageId(counter),
conversationId: this.id, conversationId: this.id,
expirationTimerUpdate: { expirationTimerUpdate: {
expireTimer, expireTimer,
@ -4747,7 +4738,6 @@ export class ConversationModel extends window.Backbone
flags: Proto.DataMessage.Flags.EXPIRATION_TIMER_UPDATE, flags: Proto.DataMessage.Flags.EXPIRATION_TIMER_UPDATE,
readStatus: shouldBeRead ? ReadStatus.Read : ReadStatus.Unread, readStatus: shouldBeRead ? ReadStatus.Read : ReadStatus.Unread,
received_at_ms: receivedAtMS, received_at_ms: receivedAtMS,
received_at: receivedAt ?? incrementMessageCounter(),
seenStatus: shouldBeRead ? SeenStatus.Seen : SeenStatus.Unseen, seenStatus: shouldBeRead ? SeenStatus.Seen : SeenStatus.Unseen,
sent_at: sentAt, sent_at: sentAt,
timestamp: sentAt, timestamp: sentAt,
@ -4760,7 +4750,7 @@ export class ConversationModel extends window.Backbone
}); });
window.MessageCache.__DEPRECATED$register( window.MessageCache.__DEPRECATED$register(
id, attributes.id,
attributes, attributes,
'updateExpirationTimer' 'updateExpirationTimer'
); );

View file

@ -11,7 +11,6 @@ import {
pick, pick,
union, union,
} from 'lodash'; } from 'lodash';
import { v4 as generateUuid } from 'uuid';
import type { import type {
CustomError, CustomError,
@ -26,6 +25,7 @@ import { isNotNil } from '../util/isNotNil';
import { isNormalNumber } from '../util/isNormalNumber'; import { isNormalNumber } from '../util/isNormalNumber';
import { strictAssert } from '../util/assert'; import { strictAssert } from '../util/assert';
import { hydrateStoryContext } from '../util/hydrateStoryContext'; import { hydrateStoryContext } from '../util/hydrateStoryContext';
import { generateMessageId } from '../util/generateMessageId';
import { drop } from '../util/drop'; import { drop } from '../util/drop';
import type { ConversationModel } from './conversations'; import type { ConversationModel } from './conversations';
import type { import type {
@ -1642,7 +1642,8 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
} }
} }
const messageId = message.get('id') || generateUuid(); const messageId =
message.get('id') || generateMessageId(this.get('received_at')).id;
// Send delivery receipts, but only for non-story sealed sender messages // Send delivery receipts, but only for non-story sealed sender messages
// and not for messages from unaccepted conversations // and not for messages from unaccepted conversations

View file

@ -2,9 +2,10 @@
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
import noop from 'lodash/noop'; import noop from 'lodash/noop';
import { v4 as generateUuid } from 'uuid'; import { v7 as generateUuid } from 'uuid';
import { DataWriter } from '../sql/Client'; import { DataWriter } from '../sql/Client';
import type { MessageModel } from '../models/messages';
import type { ReactionAttributesType } from '../messageModifiers/Reactions'; import type { ReactionAttributesType } from '../messageModifiers/Reactions';
import { ReactionSource } from './ReactionSource'; import { ReactionSource } from './ReactionSource';
import { __DEPRECATED$getMessageById } from '../messages/getMessageById'; import { __DEPRECATED$getMessageById } from '../messages/getMessageById';
@ -12,6 +13,7 @@ import { getSourceServiceId, isStory } from '../messages/helpers';
import { strictAssert } from '../util/assert'; import { strictAssert } from '../util/assert';
import { isDirectConversation } from '../util/whatTypeOfConversation'; import { isDirectConversation } from '../util/whatTypeOfConversation';
import { incrementMessageCounter } from '../util/incrementMessageCounter'; import { incrementMessageCounter } from '../util/incrementMessageCounter';
import { generateMessageId } from '../util/generateMessageId';
import { repeat, zipObject } from '../util/iterables'; import { repeat, zipObject } from '../util/iterables';
import { getMessageSentTimestamp } from '../util/getMessageSentTimestamp'; import { getMessageSentTimestamp } from '../util/getMessageSentTimestamp';
import { isAciString } from '../util/isAciString'; import { isAciString } from '../util/isAciString';
@ -87,31 +89,31 @@ export async function enqueueReactionForSend({
: undefined; : undefined;
// Only used in story scenarios, where we use a whole message to represent the reaction // Only used in story scenarios, where we use a whole message to represent the reaction
const storyReactionMessage = storyMessage let storyReactionMessage: MessageModel | undefined;
? new window.Whisper.Message({ if (storyMessage) {
id: generateUuid(), storyReactionMessage = new window.Whisper.Message({
type: 'outgoing', ...generateMessageId(incrementMessageCounter()),
conversationId: targetConversation.id, type: 'outgoing',
sent_at: timestamp, conversationId: targetConversation.id,
received_at: incrementMessageCounter(), sent_at: timestamp,
received_at_ms: timestamp, received_at_ms: timestamp,
timestamp, timestamp,
expireTimer, expireTimer,
sendStateByConversationId: zipObject( sendStateByConversationId: zipObject(
targetConversation.getMemberConversationIds(), targetConversation.getMemberConversationIds(),
repeat({ repeat({
status: SendStatus.Pending, status: SendStatus.Pending,
updatedAt: Date.now(), updatedAt: Date.now(),
}) })
), ),
storyId: message.id, storyId: message.id,
storyReaction: { storyReaction: {
emoji, emoji,
targetAuthorAci, targetAuthorAci,
targetTimestamp, targetTimestamp,
}, },
}) });
: undefined; }
const reaction: ReactionAttributesType = { const reaction: ReactionAttributesType = {
envelopeId: generateUuid(), envelopeId: generateUuid(),

View file

@ -3,7 +3,7 @@
import { Aci, Pni, ServiceId } from '@signalapp/libsignal-client'; import { Aci, Pni, ServiceId } from '@signalapp/libsignal-client';
import { ReceiptCredentialPresentation } from '@signalapp/libsignal-client/zkgroup'; import { ReceiptCredentialPresentation } from '@signalapp/libsignal-client/zkgroup';
import { v4 as generateUuid } from 'uuid'; import { v7 as generateUuid } from 'uuid';
import pMap from 'p-map'; import pMap from 'p-map';
import { Writable } from 'stream'; import { Writable } from 'stream';
import { isNumber } from 'lodash'; import { isNumber } from 'lodash';
@ -63,6 +63,7 @@ import {
deriveGroupPublicParams, deriveGroupPublicParams,
} from '../../util/zkgroup'; } from '../../util/zkgroup';
import { incrementMessageCounter } from '../../util/incrementMessageCounter'; import { incrementMessageCounter } from '../../util/incrementMessageCounter';
import { generateMessageId } from '../../util/generateMessageId';
import { isAciString } from '../../util/isAciString'; import { isAciString } from '../../util/isAciString';
import { PhoneNumberDiscoverability } from '../../util/phoneNumberDiscoverability'; import { PhoneNumberDiscoverability } from '../../util/phoneNumberDiscoverability';
import { PhoneNumberSharingMode } from '../../util/phoneNumberSharingMode'; import { PhoneNumberSharingMode } from '../../util/phoneNumberSharingMode';
@ -487,6 +488,26 @@ export class BackupImportStream extends Writable {
const batch = Array.from(this.saveMessageBatch); const batch = Array.from(this.saveMessageBatch);
this.saveMessageBatch.clear(); this.saveMessageBatch.clear();
// There are a few indexes that start with message id, and many more that
// start with conversationId. Sort messages by both to make sure that we
// are not doing random insertions into the database file.
// This improves bulk insert performance >2x.
batch.sort((a, b) => {
if (a.conversationId > b.conversationId) {
return -1;
}
if (a.conversationId < b.conversationId) {
return 1;
}
if (a.id < b.id) {
return -1;
}
if (a.id > b.id) {
return 1;
}
return 0;
});
await DataWriter.saveMessages(batch, { await DataWriter.saveMessages(batch, {
forceSave: true, forceSave: true,
ourAci, ourAci,
@ -1235,9 +1256,8 @@ export class BackupImportStream extends Writable {
} }
let attributes: MessageAttributesType = { let attributes: MessageAttributesType = {
id: generateUuid(), ...generateMessageId(incrementMessageCounter()),
conversationId: chatConvo.id, conversationId: chatConvo.id,
received_at: incrementMessageCounter(),
sent_at: timestamp, sent_at: timestamp,
source: authorConvo?.e164, source: authorConvo?.e164,
sourceServiceId: authorConvo?.serviceId, sourceServiceId: authorConvo?.serviceId,

View file

@ -18,7 +18,7 @@ import { cleanDataForIpc } from './cleanDataForIpc';
import type { AciString, ServiceIdString } from '../types/ServiceId'; import type { AciString, ServiceIdString } from '../types/ServiceId';
import createTaskWithTimeout from '../textsecure/TaskWithTimeout'; import createTaskWithTimeout from '../textsecure/TaskWithTimeout';
import * as log from '../logging/log'; import * as log from '../logging/log';
import { isValidUuid } from '../util/isValidUuid'; import { isValidUuidV7 } from '../util/isValidUuid';
import * as Errors from '../types/errors'; import * as Errors from '../types/errors';
import type { StoredJob } from '../jobs/types'; import type { StoredJob } from '../jobs/types';
@ -603,7 +603,7 @@ async function saveMessage(
jobToInsert: options.jobToInsert && formatJobForInsert(options.jobToInsert), jobToInsert: options.jobToInsert && formatJobForInsert(options.jobToInsert),
}); });
softAssert(isValidUuid(id), 'saveMessage: messageId is not a UUID'); softAssert(isValidUuidV7(id), 'saveMessage: messageId is not a UUID');
void updateExpiringMessagesService(); void updateExpiringMessagesService();
void tapToViewMessagesDeletionService.update(); void tapToViewMessagesDeletionService.update();

View file

@ -9,7 +9,6 @@ import rimraf from 'rimraf';
import { randomBytes } from 'crypto'; import { randomBytes } from 'crypto';
import type { Database, Statement } from '@signalapp/better-sqlite3'; import type { Database, Statement } from '@signalapp/better-sqlite3';
import SQL from '@signalapp/better-sqlite3'; import SQL from '@signalapp/better-sqlite3';
import { v4 as generateUuid } from 'uuid';
import { z } from 'zod'; import { z } from 'zod';
import type { ReadonlyDeep } from 'type-fest'; import type { ReadonlyDeep } from 'type-fest';
@ -49,6 +48,7 @@ import { isNormalNumber } from '../util/isNormalNumber';
import { isNotNil } from '../util/isNotNil'; import { isNotNil } from '../util/isNotNil';
import { parseIntOrThrow } from '../util/parseIntOrThrow'; import { parseIntOrThrow } from '../util/parseIntOrThrow';
import * as durations from '../util/durations'; import * as durations from '../util/durations';
import { generateMessageId } from '../util/generateMessageId';
import { formatCountForLogging } from '../logging/formatCountForLogging'; import { formatCountForLogging } from '../logging/formatCountForLogging';
import type { ConversationColorType, CustomColorType } from '../types/Colors'; import type { ConversationColorType, CustomColorType } from '../types/Colors';
import type { BadgeType, BadgeImageType } from '../badges/types'; import type { BadgeType, BadgeImageType } from '../badges/types';
@ -2328,7 +2328,7 @@ export function saveMessage(
const toCreate = { const toCreate = {
...data, ...data,
id: id || generateUuid(), id: id || generateMessageId(data.received_at).id,
}; };
prepare( prepare(
@ -6813,23 +6813,30 @@ function pageMessages(
writable.exec( writable.exec(
` `
CREATE TEMP TABLE tmp_${runId}_updated_messages CREATE TEMP TABLE tmp_${runId}_updated_messages
(rowid INTEGER PRIMARY KEY ASC); (rowid INTEGER PRIMARY KEY, received_at INTEGER, sent_at INTEGER);
INSERT INTO tmp_${runId}_updated_messages (rowid) CREATE INDEX tmp_${runId}_updated_messages_received_at
SELECT rowid FROM messages; ON tmp_${runId}_updated_messages (received_at ASC, sent_at ASC);
INSERT INTO tmp_${runId}_updated_messages
(rowid, received_at, sent_at)
SELECT rowid, received_at, sent_at FROM messages
ORDER BY received_at ASC, sent_at ASC;
CREATE TEMP TRIGGER tmp_${runId}_message_updates CREATE TEMP TRIGGER tmp_${runId}_message_updates
UPDATE OF json ON messages UPDATE OF json ON messages
BEGIN BEGIN
INSERT OR IGNORE INTO tmp_${runId}_updated_messages (rowid) INSERT OR IGNORE INTO tmp_${runId}_updated_messages
VALUES (NEW.rowid); (rowid, received_at, sent_at)
VALUES (NEW.rowid, NEW.received_at, NEW.sent_at);
END; END;
CREATE TEMP TRIGGER tmp_${runId}_message_inserts CREATE TEMP TRIGGER tmp_${runId}_message_inserts
AFTER INSERT ON messages AFTER INSERT ON messages
BEGIN BEGIN
INSERT OR IGNORE INTO tmp_${runId}_updated_messages (rowid) INSERT OR IGNORE INTO tmp_${runId}_updated_messages
VALUES (NEW.rowid); (rowid, received_at, sent_at)
VALUES (NEW.rowid, NEW.received_at, NEW.sent_at);
END; END;
` `
); );
@ -6840,10 +6847,11 @@ function pageMessages(
const rowids: Array<number> = writable const rowids: Array<number> = writable
.prepare<Query>( .prepare<Query>(
` `
DELETE FROM tmp_${runId}_updated_messages DELETE FROM tmp_${runId}_updated_messages
RETURNING rowid RETURNING rowid
LIMIT $chunkSize; ORDER BY received_at ASC, sent_at ASC
` LIMIT $chunkSize;
`
) )
.pluck() .pluck()
.all({ chunkSize }); .all({ chunkSize });

View file

@ -1,7 +1,7 @@
// Copyright 2023 Signal Messenger, LLC // Copyright 2023 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
import uuid from 'uuid'; import { v4 as uuid } from 'uuid';
import { assert } from 'chai'; import { assert } from 'chai';
import { type AciString, generateAci } from '../types/ServiceId'; import { type AciString, generateAci } from '../types/ServiceId';

View file

@ -3,7 +3,7 @@
import { assert } from 'chai'; import { assert } from 'chai';
import type { PeekInfo } from '@signalapp/ringrtc'; import type { PeekInfo } from '@signalapp/ringrtc';
import uuid from 'uuid'; import { v4 as uuid } from 'uuid';
import { import {
getPeerIdFromConversation, getPeerIdFromConversation,
getCallIdFromEra, getCallIdFromEra,

View file

@ -0,0 +1,47 @@
// Copyright 2024 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { stringify } from 'uuid';
import { getRandomBytes } from '../Crypto';
export type GeneratedMessageIdType = Readonly<{
id: string;
received_at: number;
}>;
// See https://datatracker.ietf.org/doc/html/draft-ietf-uuidrev-rfc4122bis-00#section-5.7
export function generateMessageId(counter: number): GeneratedMessageIdType {
const uuid = getRandomBytes(16);
/* eslint-disable no-bitwise */
// We compose uuid out of 48 bits (6 bytes of) timestamp-like counter:
// `incrementMessageCounter`. Note big-endian encoding (which ensures proper
// lexicographical order), and floating point divisions (because `&` operator
// coerces to 32bit integers)
uuid[0] = (counter / 0x10000000000) & 0xff;
uuid[1] = (counter / 0x00100000000) & 0xff;
uuid[2] = (counter / 0x00001000000) & 0xff;
uuid[3] = (counter / 0x00000010000) & 0xff;
uuid[4] = (counter / 0x00000000100) & 0xff;
uuid[5] = (counter / 0x00000000001) & 0xff;
// Mask out 4 bits of version number
uuid[6] &= 0x0f;
// And set the version to 7
uuid[6] |= 0x70;
// Mask out 2 bits of variant
uuid[8] &= 0x3f;
// And set it to "2"
uuid[8] |= 0x80;
/* eslint-enable no-bitwise */
return {
id: stringify(uuid),
received_at: counter,
};
}

View file

@ -4,6 +4,7 @@
import { debounce, isNumber } from 'lodash'; import { debounce, isNumber } from 'lodash';
import { strictAssert } from './assert'; import { strictAssert } from './assert';
import { safeParseInteger } from './numbers';
import { DataReader } from '../sql/Client'; import { DataReader } from '../sql/Client';
import * as log from '../logging/log'; import * as log from '../logging/log';
@ -15,7 +16,9 @@ export async function initializeMessageCounter(): Promise<void> {
'incrementMessageCounter: already initialized' 'incrementMessageCounter: already initialized'
); );
const storedCounter = Number(localStorage.getItem('lastReceivedAtCounter')); const storedCounter = safeParseInteger(
localStorage.getItem('lastReceivedAtCounter') ?? ''
);
const dbCounter = await DataReader.getMaxMessageCounter(); const dbCounter = await DataReader.getMaxMessageCounter();
if (isNumber(dbCounter) && isNumber(storedCounter)) { if (isNumber(dbCounter) && isNumber(storedCounter)) {

View file

@ -16,3 +16,19 @@ export const isValidUuid = (value: unknown): value is string => {
return UUID_REGEXP.test(value); return UUID_REGEXP.test(value);
}; };
const UUID_V7_REGEXP =
/^[0-9A-F]{8}-[0-9A-F]{4}-7[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$/i;
export const isValidUuidV7 = (value: unknown): value is string => {
if (typeof value !== 'string') {
return false;
}
// Zero UUID is a valid uuid.
if (value === '00000000-0000-0000-0000-000000000000') {
return true;
}
return UUID_V7_REGEXP.test(value);
};

View file

@ -5,7 +5,7 @@ import { ipcRenderer } from 'electron';
import { isString, isTypedArray } from 'lodash'; import { isString, isTypedArray } from 'lodash';
import { join, normalize, basename } from 'path'; import { join, normalize, basename } from 'path';
import fse from 'fs-extra'; import fse from 'fs-extra';
import getGuid from 'uuid/v4'; import { v4 as getGuid } from 'uuid';
import { isPathInside } from '../util/isPathInside'; import { isPathInside } from '../util/isPathInside';
import { writeWindowsZoneIdentifier } from '../util/windowsZoneIdentifier'; import { writeWindowsZoneIdentifier } from '../util/windowsZoneIdentifier';