Use protopiler for protocol buffers

Co-authored-by: Jamie Kyle <jamie@signal.org>
This commit is contained in:
Fedor Indutny
2026-03-10 15:31:29 -07:00
committed by GitHub
parent b0e19f334e
commit c4ee32e9ee
97 changed files with 6197 additions and 6362 deletions

View File

@@ -3,7 +3,6 @@
import lodash from 'lodash';
import type { SignalService as Proto } from '../protobuf/index.std.js';
import {
BodyRange,
type RawBodyRange,
@@ -26,7 +25,7 @@ const MAX_PER_TYPE = 250;
// We drop unknown bodyRanges and remove extra stuff so they serialize properly
export function filterAndClean(
ranges: ReadonlyArray<Proto.IBodyRange | RawBodyRange> | undefined | null
ranges: ReadonlyArray<RawBodyRange> | undefined | null
): ReadonlyArray<RawBodyRange> | undefined {
if (!ranges) {
return undefined;
@@ -108,7 +107,7 @@ export function filterAndClean(
}
export function hydrateRanges(
ranges: ReadonlyArray<BodyRange<object>> | undefined,
ranges: ReadonlyArray<RawBodyRange> | undefined,
conversationSelector: (id: string) => { id: string; title: string }
): Array<HydratedBodyRangeType> | undefined {
if (!ranges) {

View File

@@ -78,6 +78,12 @@ export function fromAciUuidBytesOrString(
context: string
): AciString;
export function fromAciUuidBytesOrString(
bytes: Uint8Array | undefined | null,
fallback: string,
context: string
): AciString;
export function fromAciUuidBytesOrString(
bytes: Uint8Array | undefined | null,
fallback: string | undefined | null,

View File

@@ -6,7 +6,7 @@ import * as Bytes from '../Bytes.std.js';
import { SignalService as Proto } from '../protobuf/index.std.js';
import { fromServiceIdBinaryOrString } from './ServiceId.node.js';
import PinnedConversation = Proto.AccountRecord.IPinnedConversation;
import PinnedConversation = Proto.AccountRecord.PinnedConversation.Params;
export function arePinnedConversationsEqual(
localValue: Array<PinnedConversation>,
@@ -17,10 +17,17 @@ export function arePinnedConversationsEqual(
}
return localValue.every(
(localPinnedConversation: PinnedConversation, index: number) => {
const remotePinnedConversation = remoteValue[index];
const remotePinnedConversation = remoteValue[index].identifier;
if (!remotePinnedConversation) {
return false;
}
if (!localPinnedConversation.identifier) {
return false;
}
const { contact, groupMasterKey, legacyGroupId } =
localPinnedConversation;
localPinnedConversation.identifier;
if (contact) {
const { contact: remoteContact } = remotePinnedConversation;

View File

@@ -15,6 +15,8 @@ import { canBeTranscoded } from './Attachment.std.js';
import * as Errors from '../types/errors.std.js';
import * as Bytes from '../Bytes.std.js';
import { toNumber } from './toNumber.std.js';
const log = createLogger('attachments');
// All outgoing images go through handleImageAttachment before being sent and thus have
@@ -100,8 +102,8 @@ export function copyCdnFields(
return {};
}
return {
cdnId: dropNull(uploaded.cdnId)?.toString(),
cdnKey: uploaded.cdnKey,
cdnId: undefined,
cdnKey: uploaded.attachmentIdentifier.cdnKey,
cdnNumber: dropNull(uploaded.cdnNumber),
digest: Bytes.toBase64(uploaded.digest),
incrementalMac: uploaded.incrementalMac
@@ -110,6 +112,6 @@ export function copyCdnFields(
chunkSize: dropNull(uploaded.chunkSize),
key: Bytes.toBase64(uploaded.key),
plaintextHash: uploaded.plaintextHash,
uploadTimestamp: uploaded.uploadTimestamp?.toNumber(),
uploadTimestamp: toNumber(uploaded.uploadTimestamp) ?? undefined,
};
}

View File

@@ -1,7 +1,6 @@
// Copyright 2024 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import Long from 'long';
import type { Backups, SignalService } from '../protobuf/index.std.js';
import * as Bytes from '../Bytes.std.js';
import { backupsService } from '../services/backups/index.preload.js';
@@ -17,8 +16,8 @@ const log = createLogger('BackupSubscriptionData');
// we'll need separate logic for each
export async function saveBackupsSubscriberData(
backupsSubscriberData:
| Backups.AccountData.IIAPSubscriberData
| SignalService.AccountRecord.IIAPSubscriberData
| Backups.AccountData.IAPSubscriberData.Params
| SignalService.AccountRecord.IAPSubscriberData.Params
| null
| undefined
): Promise<void> {
@@ -35,8 +34,7 @@ export async function saveBackupsSubscriberData(
return;
}
const { subscriberId, purchaseToken, originalTransactionId } =
backupsSubscriberData;
const { subscriberId, iapSubscriptionId } = backupsSubscriberData;
if (Bytes.isNotEmpty(subscriberId)) {
await itemStorage.put('backupsSubscriberId', subscriberId);
@@ -44,16 +42,19 @@ export async function saveBackupsSubscriberData(
await itemStorage.remove('backupsSubscriberId');
}
if (purchaseToken) {
await itemStorage.put('backupsSubscriberPurchaseToken', purchaseToken);
if (iapSubscriptionId?.purchaseToken != null) {
await itemStorage.put(
'backupsSubscriberPurchaseToken',
iapSubscriptionId.purchaseToken
);
} else {
await itemStorage.remove('backupsSubscriberPurchaseToken');
}
if (originalTransactionId) {
if (iapSubscriptionId?.originalTransactionId != null) {
await itemStorage.put(
'backupsSubscriberOriginalTransactionId',
originalTransactionId.toString()
iapSubscriptionId.originalTransactionId.toString()
);
} else {
await itemStorage.remove('backupsSubscriberOriginalTransactionId');
@@ -72,27 +73,38 @@ export async function saveBackupTier(
}
}
export function generateBackupsSubscriberData(): Backups.AccountData.IIAPSubscriberData | null {
const backupsSubscriberId = itemStorage.get('backupsSubscriberId');
export function generateBackupsSubscriberData(): Backups.AccountData.IAPSubscriberData.Params | null {
const subscriberId = itemStorage.get('backupsSubscriberId') ?? null;
if (Bytes.isEmpty(backupsSubscriberId)) {
if (Bytes.isEmpty(subscriberId)) {
return null;
}
const backupsSubscriberData: Backups.AccountData.IIAPSubscriberData = {
subscriberId: backupsSubscriberId,
};
let backupsSubscriberData: Backups.AccountData.IAPSubscriberData.Params;
const purchaseToken = itemStorage.get('backupsSubscriberPurchaseToken');
if (purchaseToken) {
backupsSubscriberData.purchaseToken = purchaseToken;
backupsSubscriberData = {
subscriberId,
iapSubscriptionId: {
purchaseToken,
},
};
} else {
const originalTransactionId = itemStorage.get(
'backupsSubscriberOriginalTransactionId'
);
if (originalTransactionId) {
backupsSubscriberData.originalTransactionId = Long.fromString(
originalTransactionId
);
if (originalTransactionId != null) {
backupsSubscriberData = {
subscriberId,
iapSubscriptionId: {
originalTransactionId: BigInt(originalTransactionId),
},
};
} else {
backupsSubscriberData = {
subscriberId,
iapSubscriptionId: null,
};
}
}

View File

@@ -1,7 +1,6 @@
// Copyright 2023 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import Long from 'long';
import type { Call, PeekInfo, LocalDeviceState } from '@signalapp/ringrtc';
import {
CallState,
@@ -67,7 +66,7 @@ import { drop } from './drop.std.js';
import { sendCallLinkUpdateSync } from './sendCallLinkUpdateSync.preload.js';
import { storageServiceUploadJob } from '../services/storage.preload.js';
import { CallLinkFinalizeDeleteManager } from '../jobs/CallLinkFinalizeDeleteManager.preload.js';
import { parsePartial, parseStrict } from './schemas.std.js';
import { parseLoose, parseStrict } from './schemas.std.js';
import { calling } from '../services/calling.preload.js';
import { cleanupMessages } from './cleanup.preload.js';
import { MessageModel } from '../models/messages.preload.js';
@@ -141,11 +140,11 @@ export function formatLocalDeviceState(
}
export function getCallIdFromRing(ringId: bigint): string {
return Long.fromValue(callIdFromRingId(ringId)).toString();
return BigInt(callIdFromRingId(ringId)).toString();
}
export function getCallIdFromEra(eraId: string): string {
return Long.fromValue(callIdFromEra(eraId)).toString();
return BigInt(callIdFromEra(eraId)).toString();
}
export function getCreatorAci(creator: Uint8Array): AciString {
@@ -205,11 +204,11 @@ export function convertJoinState(joinState: JoinState): GroupCallJoinState {
// ----------------------
export function getCallEventForProto(
callEventProto: Proto.SyncMessage.ICallEvent,
callEventProto: Proto.SyncMessage.CallEvent.Params,
eventSource: string
): CallEventDetails {
const callEvent = parsePartial(callEventNormalizeSchema, callEventProto);
const { callId, peerId, timestamp } = callEvent;
const callEvent = parseLoose(callEventNormalizeSchema, callEventProto);
const { callId, conversationId: peerId, timestamp } = callEvent;
let type: CallType;
if (callEvent.type === Proto.SyncMessage.CallEvent.Type.GROUP_CALL) {
@@ -234,10 +233,12 @@ export function getCallEventForProto(
}
let direction: CallDirection;
if (callEvent.direction === Proto.SyncMessage.CallEvent.Direction.INCOMING) {
if (
callEventProto.direction === Proto.SyncMessage.CallEvent.Direction.INCOMING
) {
direction = CallDirection.Incoming;
} else if (
callEvent.direction === Proto.SyncMessage.CallEvent.Direction.OUTGOING
callEventProto.direction === Proto.SyncMessage.CallEvent.Direction.OUTGOING
) {
direction = CallDirection.Outgoing;
} else {
@@ -285,12 +286,13 @@ const callLogEventFromProto: Partial<
};
export function getCallLogEventForProto(
callLogEventProto: Proto.SyncMessage.ICallLogEvent
callLogEventProto: Proto.SyncMessage.CallLogEvent.Params
): CallLogEventDetails {
// CallLogEvent peerId is ambiguous whether it's a conversationId (direct, or groupId)
// or roomId so handle both cases
const { peerId: peerIdBytes } = callLogEventProto;
const callLogEvent = parsePartial(callLogEventNormalizeSchema, {
const { conversationId: peerIdBytes } = callLogEventProto;
const callLogEvent = parseLoose(callLogEventNormalizeSchema, {
...callLogEventProto,
peerIdAsConversationId: peerIdBytes,
peerIdAsRoomId: peerIdBytes,
@@ -365,24 +367,24 @@ export function getBytesForPeerId(callHistory: CallHistoryDetails): Uint8Array {
export function getCallIdForProto(
callHistory: CallHistoryDetails
): Long | undefined {
): bigint | null {
try {
return Long.fromString(callHistory.callId);
} catch (error) {
return BigInt(callHistory.callId);
} catch {
// When CallHistory is a placeholder record for call links, then the history item's
// callId is invalid. We will ignore it and only send the timestamp.
if (callHistory.mode === CallMode.Adhoc) {
return undefined;
return null;
}
// For other calls, we expect a valid callId.
throw error;
throw new Error(`Invalid callId: ${callHistory.callId}`);
}
}
export function getProtoForCallHistory(
callHistory: CallHistoryDetails
): Proto.SyncMessage.ICallEvent | null {
): Proto.SyncMessage.CallEvent.Params {
const event = statusToProto[callHistory.status];
strictAssert(
@@ -392,14 +394,14 @@ export function getProtoForCallHistory(
)}`
);
return new Proto.SyncMessage.CallEvent({
peerId: getBytesForPeerId(callHistory),
return {
conversationId: getBytesForPeerId(callHistory),
callId: getCallIdForProto(callHistory),
type: typeToProto[callHistory.type],
direction: directionToProto[callHistory.direction],
event,
timestamp: Long.fromNumber(callHistory.timestamp),
});
timestamp: BigInt(callHistory.timestamp),
};
}
// Local Events
@@ -536,7 +538,7 @@ export function getCallDetailsFromDirectCall(
): CallDetails {
const ringerId = call.isIncoming ? call.remoteUserId : null;
return parseStrict(callDetailsSchema, {
callId: Long.fromValue(call.callId).toString(),
callId: (call.callId satisfies bigint).toString(),
peerId,
ringerId,
startedById: ringerId,
@@ -1304,19 +1306,24 @@ async function updateRemoteCallHistory(
try {
const ourAci = itemStorage.user.getCheckedAci();
const syncMessage = MessageSender.createSyncMessage();
syncMessage.callEvent = getProtoForCallHistory(callHistory);
const contentMessage = new Proto.Content();
contentMessage.syncMessage = syncMessage;
const syncMessage = MessageSender.padSyncMessage({
content: {
callEvent: getProtoForCallHistory(callHistory),
},
});
await singleProtoJobQueue.add({
contentHint: ContentHint.Resendable,
serviceId: ourAci,
isSyncMessage: true,
protoBase64: Bytes.toBase64(
Proto.Content.encode(contentMessage).finish()
Proto.Content.encode({
content: {
syncMessage,
},
senderKeyDistributionMessage: null,
pniSignatureMessage: null,
})
),
type: 'callEventSync',
urgent: false,
@@ -1457,28 +1464,34 @@ export async function markAllCallHistoryReadAndSync(
const ourAci = itemStorage.user.getCheckedAci();
const callLogEvent = new Proto.SyncMessage.CallLogEvent({
const callLogEvent: Proto.SyncMessage.CallLogEvent.Params = {
type: inConversation
? Proto.SyncMessage.CallLogEvent.Type.MARKED_AS_READ_IN_CONVERSATION
: Proto.SyncMessage.CallLogEvent.Type.MARKED_AS_READ,
timestamp: Long.fromNumber(latestCall.timestamp),
peerId: getBytesForPeerId(latestCall),
timestamp: BigInt(latestCall.timestamp),
conversationId: getBytesForPeerId(latestCall),
callId: getCallIdForProto(latestCall),
};
const syncMessage = MessageSender.padSyncMessage({
content: {
callLogEvent,
},
});
const syncMessage = MessageSender.createSyncMessage();
syncMessage.callLogEvent = callLogEvent;
const contentMessage = new Proto.Content();
contentMessage.syncMessage = syncMessage;
log.info('markAllCallHistoryReadAndSync: Queueing sync message');
await singleProtoJobQueue.add({
contentHint: ContentHint.Resendable,
serviceId: ourAci,
isSyncMessage: true,
protoBase64: Bytes.toBase64(
Proto.Content.encode(contentMessage).finish()
Proto.Content.encode({
content: {
syncMessage,
},
senderKeyDistributionMessage: null,
pniSignatureMessage: null,
})
),
type: 'callLogEventSync',
urgent: false,

View File

@@ -3,7 +3,6 @@
import type { CallingMessage } from '@signalapp/ringrtc';
import { CallMessageUrgency } from '@signalapp/ringrtc';
import Long from 'long';
import { SignalService as Proto } from '../protobuf/index.std.js';
import { createLogger } from '../logging/log.std.js';
import { toLogFormat } from '../types/errors.std.js';
@@ -22,17 +21,18 @@ export function callingMessageToProto(
opaque,
}: CallingMessage,
urgency?: CallMessageUrgency
): Proto.ICallMessage {
let opaqueField: undefined | Proto.CallMessage.IOpaque;
): Proto.CallMessage.Params {
let opaqueField: undefined | Proto.CallMessage.Opaque.Params;
if (opaque) {
opaqueField = {
...opaque,
data: opaque.data,
urgency: null,
data: opaque.data ?? null,
};
}
if (urgency !== undefined) {
opaqueField = {
...(opaqueField ?? {}),
...(opaqueField ?? { data: null, urgency: null }),
urgency: urgencyToProto(urgency),
};
}
@@ -41,42 +41,42 @@ export function callingMessageToProto(
offer: offer
? {
...offer,
id: Long.fromValue(offer.callId),
id: offer.callId,
type: offer.type as number,
opaque: offer.opaque,
}
: undefined,
: null,
answer: answer
? {
...answer,
id: Long.fromValue(answer.callId),
id: answer.callId,
opaque: answer.opaque,
}
: undefined,
: null,
iceUpdate: iceCandidates
? iceCandidates.map((candidate): Proto.CallMessage.IIceUpdate => {
? iceCandidates.map((candidate): Proto.CallMessage.IceUpdate.Params => {
return {
...candidate,
id: Long.fromValue(candidate.callId),
id: candidate.callId,
opaque: candidate.opaque,
};
})
: undefined,
: null,
busy: busy
? {
...busy,
id: Long.fromValue(busy.callId),
id: busy.callId,
}
: undefined,
: null,
hangup: hangup
? {
...hangup,
id: Long.fromValue(hangup.callId),
id: hangup.callId,
type: hangup.type as number,
}
: undefined,
destinationDeviceId,
opaque: opaqueField,
: null,
destinationDeviceId: destinationDeviceId ?? null,
opaque: opaqueField ?? null,
};
}

View File

@@ -12,6 +12,8 @@ import { isProduction } from './version.std.js';
import type { IncomingWebSocketRequest } from '../textsecure/WebsocketResources.preload.js';
import { toNumber } from './toNumber.std.js';
const { isNumber } = lodash;
const log = createLogger('checkFirstEnvelope');
@@ -33,7 +35,7 @@ export function checkFirstEnvelope(incoming: IncomingWebSocketRequest): void {
}
const decoded = Proto.Envelope.decode(plaintext);
const newEnvelopeTimestamp = decoded.clientTimestamp?.toNumber();
const newEnvelopeTimestamp = toNumber(decoded.clientTimestamp);
if (!isNumber(newEnvelopeTimestamp)) {
log.warn('timestamp is not a number!');
return;

View File

@@ -65,7 +65,8 @@ export function computeBlurHashUrl(
desiredWidth = 1,
desiredHeight = 1
): string {
const invAspect = Math.abs(desiredHeight) / (Math.abs(desiredWidth) + 1e-23);
const invAspect =
(Math.abs(desiredHeight) + 1e-23) / (Math.abs(desiredWidth) + 1e-23);
// Calculate width and height that roughly satisfy the desired PIXEL_COUNT
//

View File

@@ -207,7 +207,12 @@ export async function deleteStoryForEveryone(
{
destinationServiceId,
timestamp: story.timestamp,
storyMessageRecipients: newStoryMessageRecipients,
storyMessageRecipients: newStoryMessageRecipients.map(recipient => {
return {
...recipient,
destinationServiceId: recipient.destinationServiceId ?? undefined,
};
}),
},
noop
);

View File

@@ -15,7 +15,7 @@ const log = createLogger('denyPendingApprovalRequest');
export async function denyPendingApprovalRequest(
conversationAttributes: ConversationAttributesType,
aci: AciString
): Promise<Proto.GroupChange.Actions | undefined> {
): Promise<Proto.GroupChange.Actions.Params | undefined> {
const idLog = getConversationIdForLogging(conversationAttributes);
// This user's pending state may have changed in the time between the user's

View File

@@ -0,0 +1,36 @@
// Copyright 2026 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
export function encodeDelimited(buf: Uint8Array): [Uint8Array, Uint8Array] {
const len = buf.byteLength;
let prefix: Uint8Array;
/* eslint-disable no-bitwise */
if (len < 0x80) {
prefix = new Uint8Array(1);
prefix[0] = len;
} else if (len < 0x4000) {
prefix = new Uint8Array(2);
prefix[0] = 0x80 | (len & 0x7f);
prefix[1] = len >>> 7;
} else if (len < 0x200000) {
prefix = new Uint8Array(3);
prefix[0] = 0x80 | (len & 0x7f);
prefix[1] = 0x80 | ((len >>> 7) & 0x7f);
prefix[2] = len >>> 14;
} else if (len < 0x10000000) {
prefix = new Uint8Array(4);
prefix[0] = 0x80 | (len & 0x7f);
prefix[1] = 0x80 | ((len >>> 7) & 0x7f);
prefix[2] = 0x80 | ((len >>> 14) & 0x7f);
prefix[3] = len >>> 21;
} else {
prefix = new Uint8Array(5);
prefix[0] = 0x80 | (len & 0x7f);
prefix[1] = 0x80 | ((len >>> 7) & 0x7f);
prefix[2] = 0x80 | ((len >>> 14) & 0x7f);
prefix[3] = 0x80 | ((len >>> 21) & 0x7f);
prefix[4] = len >>> 28;
}
/* eslint-enable no-bitwise */
return [prefix, buf];
}

View File

@@ -224,13 +224,13 @@ export async function onRetryRequest(event: RetryRequestEvent): Promise<void> {
timestamp,
});
// eslint-disable-next-line prefer-destructuring
let contentProto: Proto.IContent | undefined =
let contentProto: Proto.Content.Params | undefined =
addSenderKeyResult.contentProto;
const { groupId } = addSenderKeyResult;
// Assert that the requesting UUID is still part of a story distribution list that
// the message was sent to, and add its sender key distribution message (SKDM).
if (contentProto.storyMessage && !groupId) {
if (contentProto.content?.storyMessage && !groupId) {
contentProto = await checkDistributionListAndAddSKDM({
confirm,
contentProto,
@@ -243,20 +243,19 @@ export async function onRetryRequest(event: RetryRequestEvent): Promise<void> {
return;
}
}
const story = Boolean(contentProto.storyMessage);
const story = Boolean(contentProto.content?.storyMessage);
const recipientConversation = window.ConversationController.getOrCreate(
requesterAci,
'private'
);
const protoToSend = new Proto.Content(contentProto);
await conversationJobQueue.add({
type: 'SavedProto',
conversationId: recipientConversation.id,
contentHint,
groupId,
protoBase64: Bytes.toBase64(Proto.Content.encode(protoToSend).finish()),
protoBase64: Bytes.toBase64(Proto.Content.encode(contentProto)),
story,
timestamp,
urgent,
@@ -481,13 +480,13 @@ async function checkDistributionListAndAddSKDM({
requesterAci,
messaging,
}: {
contentProto: Proto.IContent;
contentProto: Proto.Content.Params;
timestamp: number;
confirm: () => void;
requesterAci: AciString;
logId: string;
messaging: MessageSender;
}): Promise<Proto.IContent | undefined> {
}): Promise<Proto.Content.Params | undefined> {
let distributionList: StoryDistributionListDataType | undefined;
const { storyDistributionLists } = window.reduxStore.getState();
const membersByListId = new Map<string, Set<ServiceIdString>>();
@@ -562,14 +561,14 @@ async function maybeAddSenderKeyDistributionMessage({
requesterAci,
timestamp,
}: {
contentProto: Proto.IContent;
contentProto: Proto.Content.Params;
logId: string;
messageIds: Array<string>;
requestGroupId?: string;
requesterAci: AciString;
timestamp: number;
}): Promise<{
contentProto: Proto.IContent;
contentProto: Proto.Content.Params;
groupId?: string;
}> {
const conversation = await getRetryConversation({

View File

@@ -6,7 +6,7 @@ import protobufjs from 'protobufjs';
const { Reader } = protobufjs;
type MessageWithUnknownFields = {
$unknownFields?: ReadonlyArray<Uint8Array>;
$unknown?: ReadonlyArray<Uint8Array>;
};
/**
@@ -38,7 +38,7 @@ export function inspectUnknownFieldTags(
message: MessageWithUnknownFields
): Array<number> {
return (
message.$unknownFields?.map(field => {
message.$unknown?.map(field => {
// https://protobuf.dev/programming-guides/encoding/
// The first byte of a field is a varint encoding the tag bit-shifted << 3
// eslint-disable-next-line no-bitwise

View File

@@ -0,0 +1,9 @@
// Copyright 2026 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
export function isKnownProtoEnumMember<E extends number>(
enum_: Record<string | `${E}`, E | string>,
value: unknown
): value is E {
return typeof value === 'number' && Object.hasOwn(enum_, value);
}

4
ts/util/long.std.ts Normal file
View File

@@ -0,0 +1,4 @@
// Copyright 2026 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
export const MAX_VALUE = 0xffffffff_ffffffffn;

View File

@@ -12,7 +12,6 @@ import { isStory } from '../state/selectors/message.preload.js';
import { queueUpdateMessage } from './messageBatcher.preload.js';
import { isMe } from './whatTypeOfConversation.dom.js';
import { drop } from './drop.std.js';
import { fromServiceIdBinaryOrString } from './ServiceId.node.js';
import { applyDeleteForEveryone } from './deleteForEveryone.preload.js';
import { itemStorage } from '../textsecure/Storage.preload.js';
import { MessageModel } from '../models/messages.preload.js';
@@ -64,16 +63,7 @@ export async function onStoryRecipientUpdate(
Set<string>
>();
data.storyMessageRecipients.forEach(item => {
const {
destinationServiceId: rawDestinationServiceId,
destinationServiceIdBinary,
} = item;
const recipientServiceId = fromServiceIdBinaryOrString(
destinationServiceIdBinary,
rawDestinationServiceId,
`${logId}.recipientServiceId`
);
const { destinationServiceId: recipientServiceId } = item;
if (recipientServiceId == null) {
return;

View File

@@ -15,7 +15,7 @@ const log = createLogger('removePendingMember');
export async function removePendingMember(
conversationAttributes: ConversationAttributesType,
serviceIds: ReadonlyArray<ServiceIdString>
): Promise<Proto.GroupChange.Actions | undefined> {
): Promise<Proto.GroupChange.Actions.Params | undefined> {
const idLog = getConversationIdForLogging(conversationAttributes);
const pendingServiceIds = serviceIds

View File

@@ -43,26 +43,32 @@ async function _sendCallLinkUpdateSync(
try {
const ourAci = itemStorage.user.getCheckedAci();
const callLinkUpdate = new Proto.SyncMessage.CallLinkUpdate({
const callLinkUpdate: Proto.SyncMessage.CallLinkUpdate.Params = {
type: protoType,
rootKey: toRootKeyBytes(callLink.rootKey),
adminPasskey: callLink.adminKey
? toAdminKeyBytes(callLink.adminKey)
: null,
};
const syncMessage = MessageSender.padSyncMessage({
content: {
callLinkUpdate,
},
});
const syncMessage = MessageSender.createSyncMessage();
syncMessage.callLinkUpdate = callLinkUpdate;
const contentMessage = new Proto.Content();
contentMessage.syncMessage = syncMessage;
await singleProtoJobQueue.add({
contentHint: ContentHint.Resendable,
serviceId: ourAci,
isSyncMessage: true,
protoBase64: Bytes.toBase64(
Proto.Content.encode(contentMessage).finish()
Proto.Content.encode({
content: {
syncMessage,
},
senderKeyDistributionMessage: null,
pniSignatureMessage: null,
})
),
type: 'callLinkUpdateSync',
urgent: false,

View File

@@ -183,7 +183,7 @@ export async function sendToGroup({
type SendToGroupOptions = Readonly<{
contentHint: number;
contentMessage: Proto.Content;
contentMessage: Proto.Content.Params;
isPartialSend?: boolean;
messageId: string | undefined;
online?: boolean;
@@ -249,7 +249,7 @@ export async function sendContentMessageToGroup(
const sendLogCallback = messageSender.makeSendLogCallback({
contentHint,
messageId,
proto: Proto.Content.encode(contentMessage).finish(),
proto: Proto.Content.encode(contentMessage),
sendType,
timestamp,
urgent,
@@ -563,7 +563,7 @@ export async function sendToGroupViaSenderKey(
contentHint,
devices: devicesForSenderKey,
distributionId,
contentMessage: Proto.Content.encode(contentMessage).finish(),
contentMessage: Proto.Content.encode(contentMessage),
groupId,
});
@@ -617,7 +617,7 @@ export async function sendToGroupViaSenderKey(
sendLogId = await DataWriter.insertSentProto(
{
contentHint,
proto: Proto.Content.encode(contentMessage).finish(),
proto: Proto.Content.encode(contentMessage),
timestamp,
urgent,
hasPniSignatureMessage: false,
@@ -810,18 +810,18 @@ export async function sendToGroupViaSenderKey(
// 11. Return early if there are no normal send recipients
if (normalSendRecipients.length === 0) {
return {
dataMessage: contentMessage.dataMessage
? Proto.DataMessage.encode(contentMessage.dataMessage).finish()
dataMessage: contentMessage.content?.dataMessage
? Proto.DataMessage.encode(contentMessage.content.dataMessage)
: undefined,
editMessage: contentMessage.editMessage
? Proto.EditMessage.encode(contentMessage.editMessage).finish()
editMessage: contentMessage.content?.editMessage
? Proto.EditMessage.encode(contentMessage.content.editMessage)
: undefined,
successfulServiceIds: senderKeyRecipients,
unidentifiedDeliveries: senderKeyRecipients,
contentHint,
timestamp,
contentProto: Proto.Content.encode(contentMessage).finish(),
contentProto: Proto.Content.encode(contentMessage),
recipients: senderKeyRecipientsWithDevices,
urgent,
};

View File

@@ -9,9 +9,6 @@ import { deriveSecrets } from '../Crypto.node.js';
const { get, isFinite, isInteger, isString } = lodash;
const { RecordStructure, SessionStructure } = signal.proto.storage;
const { Chain } = SessionStructure;
type KeyPairType = {
privKey?: string;
pubKey?: string;
@@ -78,19 +75,15 @@ export type LocalUserDataType = {
};
export function sessionStructureToBytes(
recordStructure: signal.proto.storage.RecordStructure
recordStructure: signal.proto.storage.RecordStructure.Params
): Uint8Array {
return signal.proto.storage.RecordStructure.encode(recordStructure).finish();
return signal.proto.storage.RecordStructure.encode(recordStructure);
}
export function sessionRecordToProtobuf(
record: SessionRecordType,
ourData: LocalUserDataType
): signal.proto.storage.RecordStructure {
const proto = new RecordStructure();
proto.previousSessions = [];
): signal.proto.storage.RecordStructure.Params {
const sessionGroup = record.sessions || {};
const sessions = Object.values(sessionGroup);
@@ -98,8 +91,11 @@ export function sessionRecordToProtobuf(
return session?.indexInfo?.closed === -1;
});
let currentSession: signal.proto.storage.SessionStructure.Params | null;
if (first) {
proto.currentSession = toProtobufSession(first, ourData);
currentSession = toProtobufSession(first, ourData);
} else {
currentSession = null;
}
sessions.sort((left, right) => {
@@ -114,69 +110,26 @@ export function sessionRecordToProtobuf(
throw new Error('toProtobuf: More than one open session!');
}
proto.previousSessions = [];
const previousSessions =
new Array<signal.proto.storage.SessionStructure.Params>();
onlyClosed.forEach(session => {
proto.previousSessions.push(toProtobufSession(session, ourData));
previousSessions.push(toProtobufSession(session, ourData));
});
if (!proto.currentSession && proto.previousSessions.length === 0) {
if (!currentSession && previousSessions.length === 0) {
throw new Error('toProtobuf: Record had no sessions!');
}
return proto;
return {
currentSession,
previousSessions,
};
}
function toProtobufSession(
session: SessionType,
ourData: LocalUserDataType
): signal.proto.storage.SessionStructure {
const proto = new SessionStructure();
// Core Fields
proto.aliceBaseKey = binaryToUint8Array(session, 'indexInfo.baseKey', 33);
proto.localIdentityPublic = ourData.identityKeyPublic;
proto.localRegistrationId = ourData.registrationId;
proto.previousCounter =
getInteger(session, 'currentRatchet.previousCounter') + 1;
proto.remoteIdentityPublic = binaryToUint8Array(
session,
'indexInfo.remoteIdentityKey',
33
);
proto.remoteRegistrationId = getInteger(session, 'registrationId');
proto.rootKey = binaryToUint8Array(session, 'currentRatchet.rootKey', 32);
proto.sessionVersion = 3;
// Note: currently unused
// proto.needsRefresh = null;
// Pending PreKey
if (session.pendingPreKey) {
proto.pendingPreKey =
new signal.proto.storage.SessionStructure.PendingPreKey();
proto.pendingPreKey.baseKey = binaryToUint8Array(
session,
'pendingPreKey.baseKey',
33
);
proto.pendingPreKey.signedPreKeyId = getInteger(
session,
'pendingPreKey.signedKeyId'
);
if (session.pendingPreKey.preKeyId !== undefined) {
proto.pendingPreKey.preKeyId = getInteger(
session,
'pendingPreKey.preKeyId'
);
}
}
// Sender Chain
): signal.proto.storage.SessionStructure.Params {
const senderBaseKey = session.currentRatchet?.ephemeralKeyPair?.pubKey;
if (!senderBaseKey) {
throw new Error('toProtobufSession: No sender base key!');
@@ -208,12 +161,8 @@ function toProtobufSession(
32
);
proto.senderChain = protoSenderChain;
// First Receiver Chain
proto.receiverChains = [];
const firstReceiverChainBaseKey =
session.currentRatchet?.lastRemoteEphemeralKey;
if (!firstReceiverChainBaseKey) {
@@ -225,6 +174,9 @@ function toProtobufSession(
| ChainType
| undefined;
const receiverChains =
new Array<signal.proto.storage.SessionStructure.Chain.Params>();
// If the session was just initialized, then there will be no receiver chain
if (firstReceiverChain) {
const protoFirstReceiverChain = toProtobufChain(firstReceiverChain);
@@ -241,7 +193,7 @@ function toProtobufSession(
33
);
proto.receiverChains.push(protoFirstReceiverChain);
receiverChains.push(protoFirstReceiverChain);
}
// Old Receiver Chains
@@ -277,40 +229,79 @@ function toProtobufSession(
33
);
proto.receiverChains.push(protoChain);
receiverChains.push(protoChain);
});
return proto;
return {
// Core Fields
aliceBaseKey: binaryToUint8Array(session, 'indexInfo.baseKey', 33),
localIdentityPublic: ourData.identityKeyPublic,
localRegistrationId: ourData.registrationId,
previousCounter: getInteger(session, 'currentRatchet.previousCounter') + 1,
remoteIdentityPublic: binaryToUint8Array(
session,
'indexInfo.remoteIdentityKey',
33
),
remoteRegistrationId: getInteger(session, 'registrationId'),
rootKey: binaryToUint8Array(session, 'currentRatchet.rootKey', 32),
sessionVersion: 3,
// Note: currently unused
needsRefresh: null,
// Pending PreKey
pendingPreKey: session.pendingPreKey
? {
baseKey: binaryToUint8Array(session, 'pendingPreKey.baseKey', 33),
signedPreKeyId: getInteger(session, 'pendingPreKey.signedKeyId'),
preKeyId:
session.pendingPreKey.preKeyId !== undefined
? getInteger(session, 'pendingPreKey.preKeyId')
: null,
}
: null,
// Sender Chain
senderChain: protoSenderChain,
receiverChains,
};
}
function toProtobufChain(
chain: ChainType
): signal.proto.storage.SessionStructure.Chain {
const proto = new Chain();
const protoChainKey = new Chain.ChainKey();
protoChainKey.index = getInteger(chain, 'chainKey.counter') + 1;
if (chain.chainKey?.key !== undefined) {
protoChainKey.key = binaryToUint8Array(chain, 'chainKey.key', 32);
}
proto.chainKey = protoChainKey;
): signal.proto.storage.SessionStructure.Chain.Params {
const messageKeys = Object.entries(chain.messageKeys || {});
proto.messageKeys = messageKeys.map(entry => {
const protoMessageKey = new SessionStructure.Chain.MessageKey();
protoMessageKey.index = getInteger(entry, '0') + 1;
const key = binaryToUint8Array(entry, '1', 32);
const { cipherKey, macKey, iv } = translateMessageKey(key);
return {
chainKey: {
index: getInteger(chain, 'chainKey.counter') + 1,
key:
chain.chainKey?.key !== undefined
? binaryToUint8Array(chain, 'chainKey.key', 32)
: null,
},
messageKeys: messageKeys.map(
(
entry
): signal.proto.storage.SessionStructure.Chain.MessageKey.Params => {
const key = binaryToUint8Array(entry, '1', 32);
protoMessageKey.cipherKey = cipherKey;
protoMessageKey.macKey = macKey;
protoMessageKey.iv = iv;
return protoMessageKey;
});
return proto;
const { cipherKey, macKey, iv } = translateMessageKey(key);
return {
index: getInteger(entry, '0') + 1,
cipherKey,
macKey,
iv,
};
}
),
senderRatchetKey: null,
senderRatchetKeyPrivate: null,
};
}
// Utility functions

View File

@@ -1,33 +1,32 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import Long from 'long';
import { MAX_SAFE_DATE } from './timestamp.std.js';
import { toNumber } from './toNumber.std.js';
export function getSafeLongFromTimestamp(
timestamp = 0,
maxValue: Long | number = MAX_SAFE_DATE
): Long {
maxValue: bigint | number = MAX_SAFE_DATE
): bigint {
if (timestamp >= MAX_SAFE_DATE) {
if (typeof maxValue === 'number') {
return Long.fromNumber(maxValue);
return BigInt(maxValue);
}
return maxValue;
}
return Long.fromNumber(timestamp);
return BigInt(timestamp);
}
export function getTimestampFromLong(
value?: Long | null,
value?: bigint | null,
maxValue = MAX_SAFE_DATE
): number {
if (!value || value.isNegative()) {
if (!value || value < 0n) {
return 0;
}
const num = value.toNumber();
const num = toNumber(value);
if (num > MAX_SAFE_DATE) {
return maxValue;
@@ -42,12 +41,12 @@ export class InvalidTimestampError extends Error {
}
}
export function getCheckedTimestampFromLong(value?: Long | null): number {
export function getCheckedTimestampFromLong(value?: bigint | null): number {
if (value == null) {
throw new InvalidTimestampError('No number');
}
const num = value.toNumber();
const num = toNumber(value);
if (num < 0) {
throw new InvalidTimestampError('Underflow');
@@ -61,9 +60,9 @@ export function getCheckedTimestampFromLong(value?: Long | null): number {
}
export function getTimestampOrUndefinedFromLong(
value?: Long | null
value?: bigint | null
): number | undefined {
if (!value || value.isZero()) {
if (!value || value === 0n) {
return undefined;
}
@@ -71,9 +70,9 @@ export function getTimestampOrUndefinedFromLong(
}
export function getCheckedTimestampOrUndefinedFromLong(
value?: Long | null
value?: bigint | null
): number | undefined {
if (!value || value.isZero()) {
if (!value || value === 0n) {
return undefined;
}

View File

@@ -1,6 +1,5 @@
// Copyright 2023 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import Long from 'long';
import { createReadStream } from 'node:fs';
import type {
AttachmentType,
@@ -100,24 +99,28 @@ export async function uploadAttachment(
const fileName = shouldStripFilename ? undefined : attachment.fileName;
return {
cdnKey,
attachmentIdentifier: {
cdnKey,
},
cdnNumber,
clientUuid: clientUuid ? uuidToBytes(clientUuid) : undefined,
clientUuid: clientUuid ? uuidToBytes(clientUuid) : null,
key: keys,
size: attachment.data.byteLength,
digest,
plaintextHash,
incrementalMac,
chunkSize,
uploadTimestamp: Long.fromNumber(uploadTimestamp),
incrementalMac: incrementalMac ?? null,
chunkSize: chunkSize ?? null,
uploadTimestamp: BigInt(uploadTimestamp),
contentType: MIMETypeToString(attachment.contentType),
fileName,
flags,
width,
height,
caption,
blurHash,
fileName: fileName ?? null,
flags: flags ?? null,
width: width ?? null,
height: height ?? null,
caption: caption ?? null,
blurHash: blurHash ?? null,
thumbnail: null,
};
}