Fetch PNI group credentials

This commit is contained in:
Fedor Indutny 2022-07-08 13:46:25 -07:00 committed by GitHub
parent b9ba732724
commit a450e13a99
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
61 changed files with 1911 additions and 875 deletions

View File

@ -207,3 +207,11 @@ jobs:
env:
NODE_ENV: production
DEBUG: mock:test:*
ARTIFACTS_DIR: artifacts/startup
- name: Upload mock server test logs on failure
if: failure()
uses: actions/upload-artifact@v2
with:
name: logs
path: artifacts

View File

@ -9,7 +9,7 @@
"directoryV2PublicKey": null,
"directoryV2CodeHashes": null,
"directoryV3Url": "https://cdsi.staging.signal.org",
"directoryV3MRENCLAVE": "51133fecb3fa18aaf0c8f64cb763656d3272d9faaacdb26ae7df082e414fb142",
"directoryV3MRENCLAVE": "e5eaa62da3514e8b37ccabddb87e52e7f319ccf5120a13f9e1b42b87ec9dd3dd",
"directoryV3Root": "-----BEGIN CERTIFICATE-----\nMIICjzCCAjSgAwIBAgIUImUM1lqdNInzg7SVUr9QGzknBqwwCgYIKoZIzj0EAwIw\naDEaMBgGA1UEAwwRSW50ZWwgU0dYIFJvb3QgQ0ExGjAYBgNVBAoMEUludGVsIENv\ncnBvcmF0aW9uMRQwEgYDVQQHDAtTYW50YSBDbGFyYTELMAkGA1UECAwCQ0ExCzAJ\nBgNVBAYTAlVTMB4XDTE4MDUyMTEwNDUxMFoXDTQ5MTIzMTIzNTk1OVowaDEaMBgG\nA1UEAwwRSW50ZWwgU0dYIFJvb3QgQ0ExGjAYBgNVBAoMEUludGVsIENvcnBvcmF0\naW9uMRQwEgYDVQQHDAtTYW50YSBDbGFyYTELMAkGA1UECAwCQ0ExCzAJBgNVBAYT\nAlVTMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEC6nEwMDIYZOj/iPWsCzaEKi7\n1OiOSLRFhWGjbnBVJfVnkY4u3IjkDYYL0MxO4mqsyYjlBalTVYxFP2sJBK5zlKOB\nuzCBuDAfBgNVHSMEGDAWgBQiZQzWWp00ifODtJVSv1AbOScGrDBSBgNVHR8ESzBJ\nMEegRaBDhkFodHRwczovL2NlcnRpZmljYXRlcy50cnVzdGVkc2VydmljZXMuaW50\nZWwuY29tL0ludGVsU0dYUm9vdENBLmRlcjAdBgNVHQ4EFgQUImUM1lqdNInzg7SV\nUr9QGzknBqwwDgYDVR0PAQH/BAQDAgEGMBIGA1UdEwEB/wQIMAYBAf8CAQEwCgYI\nKoZIzj0EAwIDSQAwRgIhAOW/5QkR+S9CiSDcNoowLuPRLsWGf/Yi7GSX94BgwTwg\nAiEA4J0lrHoMs+Xo5o/sX6O9QWxHRAvZUGOdRQ7cvqRXaqI=\n-----END CERTIFICATE-----\n",
"cdn": {
"0": "https://cdn-staging.signal.org",
@ -26,6 +26,6 @@
"buildCreation": 0,
"buildExpiration": 0,
"certificateAuthority": "-----BEGIN CERTIFICATE-----\nMIIF2zCCA8OgAwIBAgIUAMHz4g60cIDBpPr1gyZ/JDaaPpcwDQYJKoZIhvcNAQEL\nBQAwdTELMAkGA1UEBhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWExFjAUBgNVBAcT\nDU1vdW50YWluIFZpZXcxHjAcBgNVBAoTFVNpZ25hbCBNZXNzZW5nZXIsIExMQzEZ\nMBcGA1UEAxMQU2lnbmFsIE1lc3NlbmdlcjAeFw0yMjAxMjYwMDQ1NTFaFw0zMjAx\nMjQwMDQ1NTBaMHUxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYw\nFAYDVQQHEw1Nb3VudGFpbiBWaWV3MR4wHAYDVQQKExVTaWduYWwgTWVzc2VuZ2Vy\nLCBMTEMxGTAXBgNVBAMTEFNpZ25hbCBNZXNzZW5nZXIwggIiMA0GCSqGSIb3DQEB\nAQUAA4ICDwAwggIKAoICAQDEecifxMHHlDhxbERVdErOhGsLO08PUdNkATjZ1kT5\n1uPf5JPiRbus9F4J/GgBQ4ANSAjIDZuFY0WOvG/i0qvxthpW70ocp8IjkiWTNiA8\n1zQNQdCiWbGDU4B1sLi2o4JgJMweSkQFiyDynqWgHpw+KmvytCzRWnvrrptIfE4G\nPxNOsAtXFbVH++8JO42IaKRVlbfpe/lUHbjiYmIpQroZPGPY4Oql8KM3o39ObPnT\no1WoM4moyOOZpU3lV1awftvWBx1sbTBL02sQWfHRxgNVF+Pj0fdDMMFdFJobArrL\nVfK2Ua+dYN4pV5XIxzVarSRW73CXqQ+2qloPW/ynpa3gRtYeGWV4jl7eD0PmeHpK\nOY78idP4H1jfAv0TAVeKpuB5ZFZ2szcySxrQa8d7FIf0kNJe9gIRjbQ+XrvnN+ZZ\nvj6d+8uBJq8LfQaFhlVfI0/aIdggScapR7w8oLpvdflUWqcTLeXVNLVrg15cEDwd\nlV8PVscT/KT0bfNzKI80qBq8LyRmauAqP0CDjayYGb2UAabnhefgmRY6aBE5mXxd\nbyAEzzCS3vDxjeTD8v8nbDq+SD6lJi0i7jgwEfNDhe9XK50baK15Udc8Cr/ZlhGM\njNmWqBd0jIpaZm1rzWA0k4VwXtDwpBXSz8oBFshiXs3FD6jHY2IhOR3ppbyd4qRU\npwIDAQABo2MwYTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNV\nHQ4EFgQUtfNLxuXWS9DlgGuMUMNnW7yx83EwHwYDVR0jBBgwFoAUtfNLxuXWS9Dl\ngGuMUMNnW7yx83EwDQYJKoZIhvcNAQELBQADggIBABUeiryS0qjykBN75aoHO9bV\nPrrX+DSJIB9V2YzkFVyh/io65QJMG8naWVGOSpVRwUwhZVKh3JVp/miPgzTGAo7z\nhrDIoXc+ih7orAMb19qol/2Ha8OZLa75LojJNRbZoCR5C+gM8C+spMLjFf9k3JVx\ndajhtRUcR0zYhwsBS7qZ5Me0d6gRXD0ZiSbadMMxSw6KfKk3ePmPb9gX+MRTS63c\n8mLzVYB/3fe/bkpq4RUwzUHvoZf+SUD7NzSQRQQMfvAHlxk11TVNxScYPtxXDyiy\n3Cssl9gWrrWqQ/omuHipoH62J7h8KAYbr6oEIq+Czuenc3eCIBGBBfvCpuFOgckA\nXXE4MlBasEU0MO66GrTCgMt9bAmSw3TrRP12+ZUFxYNtqWluRU8JWQ4FCCPcz9pg\nMRBOgn4lTxDZG+I47OKNuSRjFEP94cdgxd3H/5BK7WHUz1tAGQ4BgepSXgmjzifF\nT5FVTDTl3ZnWUVBXiHYtbOBgLiSIkbqGMCLtrBtFIeQ7RRTb3L+IE9R0UB0cJB3A\nXbf1lVkOcmrdu2h8A32aCwtr5S1fBF1unlG7imPmqJfpOMWa8yIF/KWVm29JAPq8\nLrsybb0z5gg8w7ZblEuB9zOW9M3l60DXuJO6l7g+deV6P96rv2unHS8UlvWiVWDy\n9qfgAJizyy3kqM4lOwBH\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIID7zCCAtegAwIBAgIJAIm6LatK5PNiMA0GCSqGSIb3DQEBBQUAMIGNMQswCQYD\nVQQGEwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNU2FuIEZyYW5j\naXNjbzEdMBsGA1UECgwUT3BlbiBXaGlzcGVyIFN5c3RlbXMxHTAbBgNVBAsMFE9w\nZW4gV2hpc3BlciBTeXN0ZW1zMRMwEQYDVQQDDApUZXh0U2VjdXJlMB4XDTEzMDMy\nNTIyMTgzNVoXDTIzMDMyMzIyMTgzNVowgY0xCzAJBgNVBAYTAlVTMRMwEQYDVQQI\nDApDYWxpZm9ybmlhMRYwFAYDVQQHDA1TYW4gRnJhbmNpc2NvMR0wGwYDVQQKDBRP\ncGVuIFdoaXNwZXIgU3lzdGVtczEdMBsGA1UECwwUT3BlbiBXaGlzcGVyIFN5c3Rl\nbXMxEzARBgNVBAMMClRleHRTZWN1cmUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAw\nggEKAoIBAQDBSWBpOCBDF0i4q2d4jAXkSXUGpbeWugVPQCjaL6qD9QDOxeW1afvf\nPo863i6Crq1KDxHpB36EwzVcjwLkFTIMeo7t9s1FQolAt3mErV2U0vie6Ves+yj6\ngrSfxwIDAcdsKmI0a1SQCZlr3Q1tcHAkAKFRxYNawADyps5B+Zmqcgf653TXS5/0\nIPPQLocLn8GWLwOYNnYfBvILKDMItmZTtEbucdigxEA9mfIvvHADEbteLtVgwBm9\nR5vVvtwrD6CCxI3pgH7EH7kMP0Od93wLisvn1yhHY7FuYlrkYqdkMvWUrKoASVw4\njb69vaeJCUdU+HCoXOSP1PQcL6WenNCHAgMBAAGjUDBOMB0GA1UdDgQWBBQBixjx\nP/s5GURuhYa+lGUypzI8kDAfBgNVHSMEGDAWgBQBixjxP/s5GURuhYa+lGUypzI8\nkDAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBBQUAA4IBAQB+Hr4hC56m0LvJAu1R\nK6NuPDbTMEN7/jMojFHxH4P3XPFfupjR+bkDq0pPOU6JjIxnrD1XD/EVmTTaTVY5\niOheyv7UzJOefb2pLOc9qsuvI4fnaESh9bhzln+LXxtCrRPGhkxA1IMIo3J/s2WF\n/KVYZyciu6b4ubJ91XPAuBNZwImug7/srWvbpk0hq6A6z140WTVSKtJG7EP41kJe\n/oF4usY5J7LPkxK3LWzMJnb5EIJDmRvyH8pyRwWg6Qm6qiGFaI4nL8QU4La1x2en\n4DGXRaLMPRwjELNgQPodR38zoCMuA8gHZfZYYoZ7D7Q1wNUiVHcxuFrEeBaYJbLE\nrwLV\n-----END CERTIFICATE-----\n",
"serverPublicParams": "ABSY21VckQcbSXVNCGRYJcfWHiAMZmpTtTELcDmxgdFbtp/bWsSxZdMKzfCp8rvIs8ocCU3B37fT3r4Mi5qAemeGeR2X+/YmOGR5ofui7tD5mDQfstAI9i+4WpMtIe8KC3wU5w3Inq3uNWVmoGtpKndsNfwJrCg0Hd9zmObhypUnSkfYn2ooMOOnBpfdanRtrvetZUayDMSC5iSRcXKpdlukrpzzsCIvEwjwQlJYVPOQPj4V0F4UXXBdHSLK05uoPBCQG8G9rYIGedYsClJXnbrgGYG3eMTG5hnx4X4ntARBgELuMWWUEEfSK0mjXg+/2lPmWcTZWR9nkqgQQP0tbzuiPm74H2wMO4u1Wafe+UwyIlIT9L7KLS19Aw8r4sPrXQ==",
"serverPublicParams": "ABSY21VckQcbSXVNCGRYJcfWHiAMZmpTtTELcDmxgdFbtp/bWsSxZdMKzfCp8rvIs8ocCU3B37fT3r4Mi5qAemeGeR2X+/YmOGR5ofui7tD5mDQfstAI9i+4WpMtIe8KC3wU5w3Inq3uNWVmoGtpKndsNfwJrCg0Hd9zmObhypUnSkfYn2ooMOOnBpfdanRtrvetZUayDMSC5iSRcXKpdlukrpzzsCIvEwjwQlJYVPOQPj4V0F4UXXBdHSLK05uoPBCQG8G9rYIGedYsClJXnbrgGYG3eMTG5hnx4X4ntARBgELuMWWUEEfSK0mjXg+/2lPmWcTZWR9nkqgQQP0tbzuiPm74H2wMO4u1Wafe+UwyIlIT9L7KLS19Aw8r4sPrXZSSsOZ6s7M1+rTJN0bI5CKY2PX29y5Ok3jSWufIKcgKOnWoP67d5b2du2ZVJjpjfibNIHbT/cegy/sBLoFwtHogVYUewANUAXIaMPyCLRArsKhfJ5wBtTminG/PAvuBdJ70Z/bXVPf8TVsR292zQ65xwvWTejROW6AZX6aqucUj",
"serverTrustRoot": "BbqY1DzohE4NUZoVF+L18oUPrK3kILllLEJh2UnPSsEx"
}

View File

@ -9,7 +9,7 @@
"0": "https://cdn.signal.org",
"2": "https://cdn2.signal.org"
},
"serverPublicParams": "AMhf5ywVwITZMsff/eCyudZx9JDmkkkbV6PInzG4p8x3VqVJSFiMvnvlEKWuRob/1eaIetR31IYeAbm0NdOuHH8Qi+Rexi1wLlpzIo1gstHWBfZzy1+qHRV5A4TqPp15YzBPm0WSggW6PbSn+F4lf57VCnHF7p8SvzAA2ZZJPYJURt8X7bbg+H3i+PEjH9DXItNEqs2sNcug37xZQDLm7X36nOoGPs54XsEGzPdEV+itQNGUFEjY6X9Uv+Acuks7NpyGvCoKxGwgKgE5XyJ+nNKlyHHOLb6N1NuHyBrZrgtY/JYJHRooo5CEqYKBqdFnmbTVGEkCvJKxLnjwKWf+fEPoWeQFj5ObDjcKMZf2Jm2Ae69x+ikU5gBXsRmoF94GXQ==",
"serverPublicParams": "AMhf5ywVwITZMsff/eCyudZx9JDmkkkbV6PInzG4p8x3VqVJSFiMvnvlEKWuRob/1eaIetR31IYeAbm0NdOuHH8Qi+Rexi1wLlpzIo1gstHWBfZzy1+qHRV5A4TqPp15YzBPm0WSggW6PbSn+F4lf57VCnHF7p8SvzAA2ZZJPYJURt8X7bbg+H3i+PEjH9DXItNEqs2sNcug37xZQDLm7X36nOoGPs54XsEGzPdEV+itQNGUFEjY6X9Uv+Acuks7NpyGvCoKxGwgKgE5XyJ+nNKlyHHOLb6N1NuHyBrZrgtY/JYJHRooo5CEqYKBqdFnmbTVGEkCvJKxLnjwKWf+fEPoWeQFj5ObDjcKMZf2Jm2Ae69x+ikU5gBXsRmoF94GXTLfN0/vLt98KDPnxwAQL9j5V1jGOY8jQl6MLxEs56cwXN0dqCnImzVH3TZT1cJ8SW1BRX6qIVxEzjsSGx3yxF3suAilPMqGRp4ffyopjMD1JXiKR2RwLKzizUe5e8XyGOy9fplzhw3jVzTRyUZTRSZKkMLWcQ/gv0E4aONNqs4P",
"serverTrustRoot": "BXu6QIKVz5MA8gstzfOgRQGqyLqOwNKHL6INkv3IHWMF",
"updatesEnabled": true
}

View File

@ -80,7 +80,7 @@
"@indutny/frameless-titlebar": "2.3.4",
"@popperjs/core": "2.9.2",
"@react-spring/web": "9.4.5",
"@signalapp/libsignal-client": "0.17.0",
"@signalapp/libsignal-client": "0.18.1",
"@sindresorhus/is": "0.8.0",
"@types/fabric": "4.5.3",
"abort-controller": "3.0.0",
@ -190,7 +190,7 @@
"@babel/preset-typescript": "7.17.12",
"@electron/fuses": "1.5.0",
"@mixer/parallel-prettier": "2.0.1",
"@signalapp/mock-server": "1.5.1",
"@signalapp/mock-server": "2.0.1",
"@storybook/addon-a11y": "6.5.6",
"@storybook/addon-actions": "6.5.6",
"@storybook/addon-controls": "6.5.6",

View File

@ -101,6 +101,8 @@ message GroupChange {
message ModifyMemberProfileKeyAction {
bytes presentation = 1;
bytes user_id = 2;
bytes profile_key = 3;
}
message AddMemberPendingProfileKeyAction {
@ -113,6 +115,15 @@ message GroupChange {
message PromoteMemberPendingProfileKeyAction {
bytes presentation = 1;
bytes user_id = 2;
bytes profile_key = 3;
}
message PromoteMemberPendingPniAciProfileKeyAction {
bytes presentation = 1;
bytes user_id = 2;
bytes pni = 3;
bytes profile_key = 4;
}
message AddMemberPendingAdminApprovalAction {
@ -200,7 +211,8 @@ message GroupChange {
ModifyAnnouncementsOnlyAction modifyAnnouncementsOnly = 21; // change epoch = 3
repeated AddMemberBannedAction addMembersBanned = 22; // change epoch = 4
repeated DeleteMemberBannedAction deleteMembersBanned = 23; // change epoch = 4
// next: 24
repeated PromoteMemberPendingPniAciProfileKeyAction promoteMembersPendingPniAciProfileKey = 24; // change epoch = 5
// next: 25
}
bytes actions = 1; // The serialized actions

View File

@ -51,7 +51,7 @@ import {
import { senderCertificateService } from './services/senderCertificate';
import { GROUP_CREDENTIALS_KEY } from './services/groupCredentialFetcher';
import * as KeyboardLayout from './services/keyboardLayout';
import { routineProfileRefresh } from './routineProfileRefresh';
import { RoutineProfileRefresher } from './routineProfileRefresh';
import { isMoreRecentThan, isOlderThan, toDayMillis } from './util/timestamp';
import { isValidReactionEmoji } from './reactions/isValidReactionEmoji';
import type { ConversationModel } from './models/conversations';
@ -220,6 +220,7 @@ export async function startApp(): Promise<void> {
let server: WebAPIType | undefined;
let messageReceiver: MessageReceiver | undefined;
let challengeHandler: ChallengeHandler | undefined;
let routineProfileRefresher: RoutineProfileRefresher | undefined;
window.storage.onready(() => {
server = window.WebAPI.connect(
@ -812,6 +813,11 @@ export async function startApp(): Promise<void> {
await window.Signal.Data.clearAllErrorStickerPackAttempts();
}
if (window.isBeforeVersion(lastVersion, 'v5.50.0-alpha.1')) {
await window.storage.put('groupCredentials', []);
await window.Signal.Data.removeAllProfileKeyCredentials();
}
// This one should always be last - it could restart the app
if (window.isBeforeVersion(lastVersion, 'v5.30.0-alpha')) {
await deleteAllLogs();
@ -1172,7 +1178,12 @@ export async function startApp(): Promise<void> {
window.Whisper.events.on('userChanged', (reconnect = false) => {
const newDeviceId = window.textsecure.storage.user.getDeviceId();
const newNumber = window.textsecure.storage.user.getNumber();
const newUuid = window.textsecure.storage.user.getUuid()?.toString();
const newACI = window.textsecure.storage.user
.getUuid(UUIDKind.ACI)
?.toString();
const newPNI = window.textsecure.storage.user
.getUuid(UUIDKind.PNI)
?.toString();
const ourConversation =
window.ConversationController.getOurConversation();
@ -1184,7 +1195,8 @@ export async function startApp(): Promise<void> {
ourConversationId: ourConversation?.get('id'),
ourDeviceId: newDeviceId,
ourNumber: newNumber,
ourUuid: newUuid,
ourACI: newACI,
ourPNI: newPNI,
regionCode: window.storage.get('regionCode'),
});
@ -2492,14 +2504,15 @@ export async function startApp(): Promise<void> {
// Kick off a profile refresh if necessary, but don't wait for it, as failure is
// tolerable.
const ourConversationId =
window.ConversationController.getOurConversationId();
if (ourConversationId) {
routineProfileRefresh({
allConversations: window.ConversationController.getAll(),
ourConversationId,
if (!routineProfileRefresher) {
routineProfileRefresher = new RoutineProfileRefresher({
getAllConversations: () => window.ConversationController.getAll(),
getOurConversationId: () =>
window.ConversationController.getOurConversationId(),
storage,
});
routineProfileRefresher.start();
} else {
assert(
false,
@ -2625,10 +2638,14 @@ export async function startApp(): Promise<void> {
return;
}
const ourACI = window.textsecure.storage.user.getUuid(UUIDKind.ACI);
const ourPNI = window.textsecure.storage.user.getUuid(UUIDKind.PNI);
// We drop typing notifications in groups we're not a part of
if (
!isDirectConversation(conversation.attributes) &&
!conversation.hasMember(ourId)
!(ourACI && conversation.hasMember(ourACI)) &&
!(ourPNI && conversation.hasMember(ourPNI))
) {
log.warn(
`Received typing indicator for group ${conversation.idForLogging()}, which we're not a part of. Dropping.`

View File

@ -16,7 +16,8 @@ import type { FullJSXType } from '../Intl';
const i18n = setupI18n('en', enMessages);
const OUR_ID = UUID.generate().toString();
const OUR_ACI = UUID.generate().toString();
const OUR_PNI = UUID.generate().toString();
const CONTACT_A = UUID.generate().toString();
const CONTACT_B = UUID.generate().toString();
const CONTACT_C = UUID.generate().toString();
@ -59,7 +60,8 @@ const renderChange = (
groupMemberships={groupMemberships}
groupName={groupName}
i18n={i18n}
ourUuid={OUR_ID}
ourACI={OUR_ACI}
ourPNI={OUR_PNI}
renderContact={renderContact}
/>
);
@ -89,7 +91,11 @@ export const Multiple = (): JSX.Element => {
},
{
type: 'member-add',
uuid: OUR_ID,
uuid: OUR_ACI,
},
{
type: 'member-add',
uuid: OUR_PNI,
},
{
type: 'description',
@ -97,7 +103,7 @@ export const Multiple = (): JSX.Element => {
},
{
type: 'member-privilege',
uuid: OUR_ID,
uuid: OUR_ACI,
newPrivilege: RoleEnum.ADMINISTRATOR,
},
],
@ -110,7 +116,7 @@ export const Create = (): JSX.Element => {
return (
<>
{renderChange({
from: OUR_ID,
from: OUR_ACI,
details: [
{
type: 'create',
@ -140,7 +146,7 @@ export const Title = (): JSX.Element => {
return (
<>
{renderChange({
from: OUR_ID,
from: OUR_ACI,
details: [
{
type: 'title',
@ -166,7 +172,7 @@ export const Title = (): JSX.Element => {
],
})}
{renderChange({
from: OUR_ID,
from: OUR_ACI,
details: [
{
type: 'title',
@ -196,7 +202,7 @@ export const Avatar = (): JSX.Element => {
return (
<>
{renderChange({
from: OUR_ID,
from: OUR_ACI,
details: [
{
type: 'avatar',
@ -222,7 +228,7 @@ export const Avatar = (): JSX.Element => {
],
})}
{renderChange({
from: OUR_ID,
from: OUR_ACI,
details: [
{
type: 'avatar',
@ -255,7 +261,7 @@ export const AccessAttributes = (): JSX.Element => {
return (
<>
{renderChange({
from: OUR_ID,
from: OUR_ACI,
details: [
{
type: 'access-attributes',
@ -281,7 +287,7 @@ export const AccessAttributes = (): JSX.Element => {
],
})}
{renderChange({
from: OUR_ID,
from: OUR_ACI,
details: [
{
type: 'access-attributes',
@ -318,7 +324,7 @@ export const AccessMembers = (): JSX.Element => {
return (
<>
{renderChange({
from: OUR_ID,
from: OUR_ACI,
details: [
{
type: 'access-members',
@ -344,7 +350,7 @@ export const AccessMembers = (): JSX.Element => {
],
})}
{renderChange({
from: OUR_ID,
from: OUR_ACI,
details: [
{
type: 'access-members',
@ -381,7 +387,7 @@ export const AccessInviteLink = (): JSX.Element => {
return (
<>
{renderChange({
from: OUR_ID,
from: OUR_ACI,
details: [
{
type: 'access-invite-link',
@ -407,7 +413,7 @@ export const AccessInviteLink = (): JSX.Element => {
],
})}
{renderChange({
from: OUR_ID,
from: OUR_ACI,
details: [
{
type: 'access-invite-link',
@ -444,11 +450,11 @@ export const MemberAdd = (): JSX.Element => {
return (
<>
{renderChange({
from: OUR_ID,
from: OUR_ACI,
details: [
{
type: 'member-add',
uuid: OUR_ID,
uuid: OUR_ACI,
},
],
})}
@ -457,7 +463,7 @@ export const MemberAdd = (): JSX.Element => {
details: [
{
type: 'member-add',
uuid: OUR_ID,
uuid: OUR_ACI,
},
],
})}
@ -465,12 +471,12 @@ export const MemberAdd = (): JSX.Element => {
details: [
{
type: 'member-add',
uuid: OUR_ID,
uuid: OUR_ACI,
},
],
})}
{renderChange({
from: OUR_ID,
from: OUR_ACI,
details: [
{
type: 'member-add',
@ -508,7 +514,7 @@ export const MemberAddFromInvited = (): JSX.Element => {
details: [
{
type: 'member-add-from-invite',
uuid: OUR_ID,
uuid: OUR_ACI,
inviter: CONTACT_B,
},
],
@ -517,14 +523,14 @@ export const MemberAddFromInvited = (): JSX.Element => {
details: [
{
type: 'member-add-from-invite',
uuid: OUR_ID,
uuid: OUR_ACI,
inviter: CONTACT_A,
},
],
})}
{/* the rest of the 'someone added someone else' checks */}
{renderChange({
from: OUR_ID,
from: OUR_ACI,
details: [
{
type: 'member-add-from-invite',
@ -554,21 +560,21 @@ export const MemberAddFromInvited = (): JSX.Element => {
})}
{/* in all of these we know the user has accepted the invite */}
{renderChange({
from: OUR_ID,
from: OUR_ACI,
details: [
{
type: 'member-add-from-invite',
uuid: OUR_ID,
uuid: OUR_ACI,
inviter: CONTACT_A,
},
],
})}
{renderChange({
from: OUR_ID,
from: OUR_ACI,
details: [
{
type: 'member-add-from-invite',
uuid: OUR_ID,
uuid: OUR_ACI,
},
],
})}
@ -578,7 +584,7 @@ export const MemberAddFromInvited = (): JSX.Element => {
{
type: 'member-add-from-invite',
uuid: CONTACT_A,
inviter: OUR_ID,
inviter: OUR_ACI,
},
],
})}
@ -601,6 +607,17 @@ export const MemberAddFromInvited = (): JSX.Element => {
},
],
})}
ACI accepts PNI invite:
{renderChange({
from: OUR_PNI,
details: [
{
type: 'member-add-from-invite',
uuid: OUR_ACI,
inviter: CONTACT_B,
},
],
})}
</>
);
};
@ -613,11 +630,11 @@ export const MemberAddFromLink = (): JSX.Element => {
return (
<>
{renderChange({
from: OUR_ID,
from: OUR_ACI,
details: [
{
type: 'member-add-from-link',
uuid: OUR_ID,
uuid: OUR_ACI,
},
],
})}
@ -654,7 +671,7 @@ export const MemberAddFromAdminApproval = (): JSX.Element => {
details: [
{
type: 'member-add-from-admin-approval',
uuid: OUR_ID,
uuid: OUR_ACI,
},
],
})}
@ -662,12 +679,12 @@ export const MemberAddFromAdminApproval = (): JSX.Element => {
details: [
{
type: 'member-add-from-admin-approval',
uuid: OUR_ID,
uuid: OUR_ACI,
},
],
})}
{renderChange({
from: OUR_ID,
from: OUR_ACI,
details: [
{
type: 'member-add-from-admin-approval',
@ -704,11 +721,11 @@ export const MemberRemove = (): JSX.Element => {
return (
<>
{renderChange({
from: OUR_ID,
from: OUR_ACI,
details: [
{
type: 'member-remove',
uuid: OUR_ID,
uuid: OUR_ACI,
},
],
})}
@ -717,7 +734,7 @@ export const MemberRemove = (): JSX.Element => {
details: [
{
type: 'member-remove',
uuid: OUR_ID,
uuid: OUR_ACI,
},
],
})}
@ -725,12 +742,12 @@ export const MemberRemove = (): JSX.Element => {
details: [
{
type: 'member-remove',
uuid: OUR_ID,
uuid: OUR_ACI,
},
],
})}
{renderChange({
from: OUR_ID,
from: OUR_ACI,
details: [
{
type: 'member-remove',
@ -776,7 +793,7 @@ export const MemberPrivilege = (): JSX.Element => {
details: [
{
type: 'member-privilege',
uuid: OUR_ID,
uuid: OUR_ACI,
newPrivilege: RoleEnum.ADMINISTRATOR,
},
],
@ -785,13 +802,13 @@ export const MemberPrivilege = (): JSX.Element => {
details: [
{
type: 'member-privilege',
uuid: OUR_ID,
uuid: OUR_ACI,
newPrivilege: RoleEnum.ADMINISTRATOR,
},
],
})}
{renderChange({
from: OUR_ID,
from: OUR_ACI,
details: [
{
type: 'member-privilege',
@ -824,7 +841,7 @@ export const MemberPrivilege = (): JSX.Element => {
details: [
{
type: 'member-privilege',
uuid: OUR_ID,
uuid: OUR_ACI,
newPrivilege: RoleEnum.DEFAULT,
},
],
@ -833,13 +850,13 @@ export const MemberPrivilege = (): JSX.Element => {
details: [
{
type: 'member-privilege',
uuid: OUR_ID,
uuid: OUR_ACI,
newPrivilege: RoleEnum.DEFAULT,
},
],
})}
{renderChange({
from: OUR_ID,
from: OUR_ACI,
details: [
{
type: 'member-privilege',
@ -879,7 +896,7 @@ export const PendingAddOne = (): JSX.Element => {
details: [
{
type: 'pending-add-one',
uuid: OUR_ID,
uuid: OUR_ACI,
},
],
})}
@ -887,12 +904,12 @@ export const PendingAddOne = (): JSX.Element => {
details: [
{
type: 'pending-add-one',
uuid: OUR_ID,
uuid: OUR_ACI,
},
],
})}
{renderChange({
from: OUR_ID,
from: OUR_ACI,
details: [
{
type: 'pending-add-one',
@ -929,7 +946,7 @@ export const PendingAddMany = (): JSX.Element => {
return (
<>
{renderChange({
from: OUR_ID,
from: OUR_ACI,
details: [
{
type: 'pending-add-many',
@ -971,17 +988,17 @@ export const PendingRemoveOne = (): JSX.Element => {
{
type: 'pending-remove-one',
uuid: INVITEE_A,
inviter: OUR_ID,
inviter: OUR_ACI,
},
],
})}
{renderChange({
from: OUR_ID,
from: OUR_ACI,
details: [
{
type: 'pending-remove-one',
uuid: INVITEE_A,
inviter: OUR_ID,
inviter: OUR_ACI,
},
],
})}
@ -991,7 +1008,7 @@ export const PendingRemoveOne = (): JSX.Element => {
{
type: 'pending-remove-one',
uuid: INVITEE_A,
inviter: OUR_ID,
inviter: OUR_ACI,
},
],
})}
@ -1000,7 +1017,7 @@ export const PendingRemoveOne = (): JSX.Element => {
{
type: 'pending-remove-one',
uuid: INVITEE_A,
inviter: OUR_ID,
inviter: OUR_ACI,
},
],
})}
@ -1029,7 +1046,7 @@ export const PendingRemoveOne = (): JSX.Element => {
details: [
{
type: 'pending-remove-one',
uuid: OUR_ID,
uuid: OUR_ACI,
inviter: CONTACT_B,
},
],
@ -1056,7 +1073,7 @@ export const PendingRemoveOne = (): JSX.Element => {
],
})}
{renderChange({
from: OUR_ID,
from: OUR_ACI,
details: [
{
type: 'pending-remove-one',
@ -1076,7 +1093,7 @@ export const PendingRemoveOne = (): JSX.Element => {
})}
{renderChange({
from: OUR_ID,
from: OUR_ACI,
details: [
{
type: 'pending-remove-one',
@ -1113,12 +1130,12 @@ export const PendingRemoveMany = (): JSX.Element => {
return (
<>
{renderChange({
from: OUR_ID,
from: OUR_ACI,
details: [
{
type: 'pending-remove-many',
count: 5,
inviter: OUR_ID,
inviter: OUR_ACI,
},
],
})}
@ -1128,7 +1145,7 @@ export const PendingRemoveMany = (): JSX.Element => {
{
type: 'pending-remove-many',
count: 5,
inviter: OUR_ID,
inviter: OUR_ACI,
},
],
})}
@ -1137,12 +1154,12 @@ export const PendingRemoveMany = (): JSX.Element => {
{
type: 'pending-remove-many',
count: 5,
inviter: OUR_ID,
inviter: OUR_ACI,
},
],
})}
{renderChange({
from: OUR_ID,
from: OUR_ACI,
details: [
{
type: 'pending-remove-many',
@ -1171,7 +1188,7 @@ export const PendingRemoveMany = (): JSX.Element => {
],
})}
{renderChange({
from: OUR_ID,
from: OUR_ACI,
details: [
{
type: 'pending-remove-many',
@ -1212,7 +1229,7 @@ export const AdminApprovalAdd = (): JSX.Element => {
details: [
{
type: 'admin-approval-add-one',
uuid: OUR_ID,
uuid: OUR_ACI,
},
],
})}
@ -1236,11 +1253,11 @@ export const AdminApprovalRemove = (): JSX.Element => {
return (
<>
{renderChange({
from: OUR_ID,
from: OUR_ACI,
details: [
{
type: 'admin-approval-remove-one',
uuid: OUR_ID,
uuid: OUR_ACI,
},
],
})}
@ -1248,12 +1265,12 @@ export const AdminApprovalRemove = (): JSX.Element => {
details: [
{
type: 'admin-approval-remove-one',
uuid: OUR_ID,
uuid: OUR_ACI,
},
],
})}
{renderChange({
from: OUR_ID,
from: OUR_ACI,
details: [
{
type: 'admin-approval-remove-one',
@ -1354,7 +1371,7 @@ export const GroupLinkAdd = (): JSX.Element => {
return (
<>
{renderChange({
from: OUR_ID,
from: OUR_ACI,
details: [
{
type: 'group-link-add',
@ -1380,7 +1397,7 @@ export const GroupLinkAdd = (): JSX.Element => {
],
})}
{renderChange({
from: OUR_ID,
from: OUR_ACI,
details: [
{
type: 'group-link-add',
@ -1417,7 +1434,7 @@ export const GroupLinkReset = (): JSX.Element => {
return (
<>
{renderChange({
from: OUR_ID,
from: OUR_ACI,
details: [
{
type: 'group-link-reset',
@ -1451,7 +1468,7 @@ export const GroupLinkRemove = (): JSX.Element => {
return (
<>
{renderChange({
from: OUR_ID,
from: OUR_ACI,
details: [
{
type: 'group-link-remove',
@ -1485,7 +1502,7 @@ export const DescriptionRemove = (): JSX.Element => {
return (
<>
{renderChange({
from: OUR_ID,
from: OUR_ACI,
details: [
{
removed: true,
@ -1523,7 +1540,7 @@ export const DescriptionChange = (): JSX.Element => {
<>
{renderChange(
{
from: OUR_ID,
from: OUR_ACI,
details: [
{
type: 'description',
@ -1571,7 +1588,7 @@ export const AnnouncementGroupChange = (): JSX.Element => {
return (
<>
{renderChange({
from: OUR_ID,
from: OUR_ACI,
details: [
{
type: 'announcements-only',
@ -1597,7 +1614,7 @@ export const AnnouncementGroupChange = (): JSX.Element => {
],
})}
{renderChange({
from: OUR_ID,
from: OUR_ACI,
details: [
{
type: 'announcements-only',

View File

@ -30,7 +30,8 @@ export type PropsDataType = {
}>;
groupBannedMemberships?: Array<UUIDStringType>;
groupName?: string;
ourUuid?: UUIDStringType;
ourACI?: UUIDStringType;
ourPNI?: UUIDStringType;
change: GroupV2ChangeType;
};
@ -132,7 +133,8 @@ function GroupV2Detail({
groupBannedMemberships,
groupName,
i18n,
ourUuid,
ourACI,
ourPNI,
renderContact,
text,
}: {
@ -148,7 +150,8 @@ function GroupV2Detail({
groupName?: string;
i18n: LocalizerType;
fromId?: UUIDStringType;
ourUuid?: UUIDStringType;
ourACI?: UUIDStringType;
ourPNI?: UUIDStringType;
renderContact: SmartContactRendererType<FullJSXType>;
text: FullJSXType;
}): JSX.Element {
@ -241,7 +244,8 @@ function GroupV2Detail({
detail.type === 'admin-approval-bounce' &&
areWeAdmin &&
detail.uuid &&
detail.uuid !== ourUuid &&
detail.uuid !== ourACI &&
detail.uuid !== ourPNI &&
(!fromId || fromId === detail.uuid) &&
!groupMemberships?.some(item => item.uuid === detail.uuid) &&
!groupBannedMemberships?.some(uuid => uuid === detail.uuid)
@ -276,7 +280,8 @@ export function GroupV2Change(props: PropsType): ReactElement {
groupMemberships,
groupName,
i18n,
ourUuid,
ourACI,
ourPNI,
renderContact,
} = props;
@ -284,7 +289,8 @@ export function GroupV2Change(props: PropsType): ReactElement {
<>
{renderChange<FullJSXType>(change, {
i18n,
ourUuid,
ourACI,
ourPNI,
renderContact,
renderString: renderStringToIntl,
}).map(({ detail, isLastText, text }, index) => {
@ -302,7 +308,8 @@ export function GroupV2Change(props: PropsType): ReactElement {
// Difficult to find a unique key for this type
// eslint-disable-next-line react/no-array-index-key
key={index}
ourUuid={ourUuid}
ourACI={ourACI}
ourPNI={ourPNI}
renderContact={renderContact}
text={text}
/>

View File

@ -115,9 +115,9 @@ export const ChooseGroupMembersModal: FunctionComponent<PropsType> = ({
contact => contact.username === username
);
isUsernameVisible = candidateContacts.every(
contact => contact.username !== username
);
isUsernameVisible =
Boolean(username) &&
candidateContacts.every(contact => contact.username !== username);
}
const inputRef = useRef<null | HTMLInputElement>(null);

View File

@ -20,7 +20,8 @@ export type StringRendererType<T> = (
export type RenderOptionsType<T> = {
from?: UUIDStringType;
i18n: LocalizerType;
ourUuid?: UUIDStringType;
ourACI?: UUIDStringType;
ourPNI?: UUIDStringType;
renderContact: SmartContactRendererType<T>;
renderString: StringRendererType<T>;
};
@ -66,8 +67,15 @@ export function renderChangeDetail<T>(
detail: GroupV2ChangeDetailType,
options: RenderOptionsType<T>
): T | string | ReadonlyArray<T | string> {
const { from, i18n, ourUuid, renderContact, renderString } = options;
const fromYou = Boolean(from && ourUuid && from === ourUuid);
const { from, i18n, ourACI, ourPNI, renderContact, renderString } = options;
const isOurUuid = (uuid?: UUIDStringType): boolean => {
if (!uuid) {
return false;
}
return Boolean((ourACI && uuid === ourACI) || (ourPNI && uuid === ourPNI));
};
const fromYou = isOurUuid(from);
if (detail.type === 'create') {
if (fromYou) {
@ -229,7 +237,7 @@ export function renderChangeDetail<T>(
}
if (detail.type === 'member-add') {
const { uuid } = detail;
const weAreJoiner = Boolean(ourUuid && uuid === ourUuid);
const weAreJoiner = isOurUuid(uuid);
if (weAreJoiner) {
if (fromYou) {
@ -259,10 +267,11 @@ export function renderChangeDetail<T>(
}
if (detail.type === 'member-add-from-invite') {
const { uuid, inviter } = detail;
const weAreJoiner = Boolean(ourUuid && uuid === ourUuid);
const weAreInviter = Boolean(inviter && ourUuid && inviter === ourUuid);
const weAreJoiner = isOurUuid(uuid);
const weAreInviter = isOurUuid(inviter);
const pniPromotedToACI = weAreJoiner && from === ourPNI;
if (!from || from !== uuid) {
if (!from || (from !== uuid && !pniPromotedToACI)) {
if (weAreJoiner) {
// They can't be the same, no fromYou check here
if (from) {
@ -322,7 +331,7 @@ export function renderChangeDetail<T>(
if (detail.type === 'member-add-from-link') {
const { uuid } = detail;
if (fromYou && ourUuid && uuid === ourUuid) {
if (fromYou && isOurUuid(uuid)) {
return renderString('GroupV2--member-add-from-link--you--you', i18n);
}
if (from && uuid === from) {
@ -340,7 +349,7 @@ export function renderChangeDetail<T>(
}
if (detail.type === 'member-add-from-admin-approval') {
const { uuid } = detail;
const weAreJoiner = Boolean(ourUuid && uuid === ourUuid);
const weAreJoiner = isOurUuid(uuid);
if (weAreJoiner) {
if (from) {
@ -391,7 +400,7 @@ export function renderChangeDetail<T>(
}
if (detail.type === 'member-remove') {
const { uuid } = detail;
const weAreLeaver = Boolean(ourUuid && uuid === ourUuid);
const weAreLeaver = isOurUuid(uuid);
if (weAreLeaver) {
if (fromYou) {
@ -427,7 +436,7 @@ export function renderChangeDetail<T>(
}
if (detail.type === 'member-privilege') {
const { uuid, newPrivilege } = detail;
const weAreMember = Boolean(ourUuid && uuid === ourUuid);
const weAreMember = isOurUuid(uuid);
if (newPrivilege === RoleEnum.ADMINISTRATOR) {
if (weAreMember) {
@ -513,7 +522,7 @@ export function renderChangeDetail<T>(
}
if (detail.type === 'pending-add-one') {
const { uuid } = detail;
const weAreInvited = Boolean(ourUuid && uuid === ourUuid);
const weAreInvited = isOurUuid(uuid);
if (weAreInvited) {
if (from) {
return renderString('GroupV2--pending-add--one--you--other', i18n, [
@ -554,8 +563,8 @@ export function renderChangeDetail<T>(
}
if (detail.type === 'pending-remove-one') {
const { inviter, uuid } = detail;
const weAreInviter = Boolean(inviter && ourUuid && inviter === ourUuid);
const weAreInvited = Boolean(ourUuid && uuid === ourUuid);
const weAreInviter = isOurUuid(inviter);
const weAreInvited = isOurUuid(uuid);
const sentByInvited = Boolean(from && from === uuid);
const sentByInviter = Boolean(from && inviter && from === inviter);
@ -649,7 +658,7 @@ export function renderChangeDetail<T>(
}
if (detail.type === 'pending-remove-many') {
const { count, inviter } = detail;
const weAreInviter = Boolean(inviter && ourUuid && inviter === ourUuid);
const weAreInviter = isOurUuid(inviter);
if (weAreInviter) {
if (fromYou) {
@ -729,7 +738,7 @@ export function renderChangeDetail<T>(
}
if (detail.type === 'admin-approval-add-one') {
const { uuid } = detail;
const weAreJoiner = Boolean(ourUuid && uuid === ourUuid);
const weAreJoiner = isOurUuid(uuid);
if (weAreJoiner) {
return renderString('GroupV2--admin-approval-add-one--you', i18n);
@ -740,7 +749,7 @@ export function renderChangeDetail<T>(
}
if (detail.type === 'admin-approval-remove-one') {
const { uuid } = detail;
const weAreJoiner = Boolean(ourUuid && uuid === ourUuid);
const weAreJoiner = isOurUuid(uuid);
if (weAreJoiner) {
if (fromYou) {

File diff suppressed because it is too large Load Diff

View File

@ -13,6 +13,7 @@ import {
parseGroupLink,
} from '../groups';
import * as Errors from '../types/errors';
import { UUIDKind } from '../types/UUID';
import * as Bytes from '../Bytes';
import { longRunningTaskWrapper } from '../util/longRunningTaskWrapper';
import { isGroupV1 } from '../util/whatTypeOfConversation';
@ -64,13 +65,9 @@ export async function joinViaLink(hash: string): Promise<void> {
const existingConversation =
window.ConversationController.get(id) ||
window.ConversationController.getByDerivedGroupV2Id(id);
const ourConversationId =
window.ConversationController.getOurConversationIdOrThrow();
const ourUuid = window.textsecure.storage.user.getCheckedUuid(UUIDKind.ACI);
if (
existingConversation &&
existingConversation.hasMember(ourConversationId)
) {
if (existingConversation && existingConversation.hasMember(ourUuid)) {
log.warn(
`joinViaLink/${logId}: Already a member of group, opening conversation`
);
@ -152,7 +149,7 @@ export async function joinViaLink(hash: string): Promise<void> {
if (
approvalRequired &&
existingConversation &&
existingConversation.isMemberAwaitingApproval(ourConversationId)
existingConversation.isMemberAwaitingApproval(ourUuid)
) {
log.warn(
`joinViaLink/${logId}: Already awaiting approval, opening conversation`
@ -246,9 +243,9 @@ export async function joinViaLink(hash: string): Promise<void> {
// via some other process. If so, just open that conversation.
if (
targetConversation &&
(targetConversation.hasMember(ourConversationId) ||
(targetConversation.hasMember(ourUuid) ||
(approvalRequired &&
targetConversation.isMemberAwaitingApproval(ourConversationId)))
targetConversation.isMemberAwaitingApproval(ourUuid)))
) {
log.warn(
`joinViaLink/${logId}: User is part of group on second check, opening conversation`

4
ts/model-types.d.ts vendored
View File

@ -21,7 +21,7 @@ import { AttachmentDraftType, AttachmentType } from './types/Attachment';
import { EmbeddedContactType } from './types/EmbeddedContact';
import { SignalService as Proto } from './protobuf';
import { AvatarDataType } from './types/Avatar';
import { UUIDStringType } from './types/UUID';
import { UUIDStringType, UUIDKind } from './types/UUID';
import { ReactionSource } from './reactions/ReactionSource';
import AccessRequiredEnum = Proto.AccessControl.AccessRequired;
@ -282,6 +282,8 @@ export type ConversationAttributesType = {
path: string;
};
profileKeyCredential?: string | null;
profileKeyCredentialExpiration?: number | null;
pniCredential?: string | null;
lastProfile?: ConversationLastProfileType;
quotedMessageId?: string | null;
sealedSender?: unknown;

View File

@ -24,6 +24,7 @@ import {
parseNumber,
} from '../util/libphonenumberUtil';
import { clearTimeoutIfNecessary } from '../util/clearTimeoutIfNecessary';
import { toDayMillis } from '../util/timestamp';
import type { AttachmentType } from '../types/Attachment';
import { isGIF } from '../types/Attachment';
import type { CallHistoryDetailsType } from '../types/Calling';
@ -380,7 +381,7 @@ export class ConversationModel extends window.Backbone
return {
getGroupId: () => this.get('groupId'),
getMembers: () => this.getMembers(),
hasMember: (id: string) => this.hasMember(id),
hasMember: (uuid: UUIDStringType) => this.hasMember(new UUID(uuid)),
idForLogging: () => this.idForLogging(),
isGroupV2: () => isGroupV2(this.attributes),
isValid: () => isGroupV2(this.attributes),
@ -393,7 +394,7 @@ export class ConversationModel extends window.Backbone
};
}
isMemberRequestingToJoin(id: string): boolean {
private isMemberRequestingToJoin(uuid: UUID): boolean {
if (!isGroupV2(this.attributes)) {
return false;
}
@ -403,11 +404,10 @@ export class ConversationModel extends window.Backbone
return false;
}
const uuid = UUID.checkedLookup(id).toString();
return pendingAdminApprovalV2.some(item => item.uuid === uuid);
return pendingAdminApprovalV2.some(item => item.uuid === uuid.toString());
}
isMemberPending(id: string): boolean {
isMemberPending(uuid: UUID): boolean {
if (!isGroupV2(this.attributes)) {
return false;
}
@ -417,11 +417,10 @@ export class ConversationModel extends window.Backbone
return false;
}
const uuid = UUID.checkedLookup(id).toString();
return pendingMembersV2.some(item => item.uuid === uuid);
return pendingMembersV2.some(item => item.uuid === uuid.toString());
}
isMemberBanned(id: string): boolean {
private isMemberBanned(uuid: UUID): boolean {
if (!isGroupV2(this.attributes)) {
return false;
}
@ -431,11 +430,10 @@ export class ConversationModel extends window.Backbone
return false;
}
const uuid = UUID.checkedLookup(id).toString();
return bannedMembersV2.some(member => member.uuid === uuid);
return bannedMembersV2.some(member => member.uuid === uuid.toString());
}
isMemberAwaitingApproval(id: string): boolean {
isMemberAwaitingApproval(uuid: UUID): boolean {
if (!isGroupV2(this.attributes)) {
return false;
}
@ -445,24 +443,22 @@ export class ConversationModel extends window.Backbone
return false;
}
const uuid = UUID.checkedLookup(id).toString();
return window._.any(pendingAdminApprovalV2, item => item.uuid === uuid);
return pendingAdminApprovalV2.some(
member => member.uuid === uuid.toString()
);
}
isMember(id: string): boolean {
isMember(uuid: UUID): boolean {
if (!isGroupV2(this.attributes)) {
throw new Error(
`isMember: Called for non-GroupV2 conversation ${this.idForLogging()}`
);
return false;
}
const membersV2 = this.get('membersV2');
if (!membersV2 || !membersV2.length) {
return false;
}
const uuid = UUID.checkedLookup(id).toString();
return window._.any(membersV2, item => item.uuid === uuid);
return window._.any(membersV2, item => item.uuid === uuid.toString());
}
async updateExpirationTimerInGroupV2(
@ -485,117 +481,101 @@ export class ConversationModel extends window.Backbone
});
}
async promotePendingMember(
conversationId: string
private async promotePendingMember(
uuidKind: UUIDKind
): Promise<Proto.GroupChange.Actions | undefined> {
const idLog = this.idForLogging();
const us = window.ConversationController.getOurConversationOrThrow();
const uuid = window.storage.user.getCheckedUuid(uuidKind);
// This user's pending state may have changed in the time between the user's
// button press and when we get here. It's especially important to check here
// in conflict/retry cases.
if (!this.isMemberPending(conversationId)) {
if (!this.isMemberPending(uuid)) {
log.warn(
`promotePendingMember/${idLog}: ${conversationId} is not a pending member of group. Returning early.`
`promotePendingMember/${idLog}: we are not a pending member of group. Returning early.`
);
return undefined;
}
const pendingMember = window.ConversationController.get(conversationId);
if (!pendingMember) {
throw new Error(
`promotePendingMember/${idLog}: No conversation found for conversation ${conversationId}`
);
}
// We need the user's profileKeyCredential, which requires a roundtrip with the
// server, and most definitely their profileKey. A getProfiles() call will
// ensure that we have as much as we can get with the data we have.
let profileKeyCredentialBase64 = pendingMember.get('profileKeyCredential');
if (!profileKeyCredentialBase64) {
await pendingMember.getProfiles();
profileKeyCredentialBase64 = pendingMember.get('profileKeyCredential');
if (!profileKeyCredentialBase64) {
throw new Error(
`promotePendingMember/${idLog}: No profileKeyCredential for conversation ${pendingMember.idForLogging()}`
);
if (uuidKind === UUIDKind.ACI) {
if (!us.get('profileKeyCredential')) {
await us.getProfiles();
}
const profileKeyCredentialBase64 = us.get('profileKeyCredential');
strictAssert(
profileKeyCredentialBase64,
'Must have profileKeyCredential'
);
return window.Signal.Groups.buildPromoteMemberChange({
group: this.attributes,
profileKeyCredentialBase64,
serverPublicParamsBase64: window.getServerPublicParams(),
});
}
strictAssert(uuidKind === UUIDKind.PNI, 'Must be a PNI promotion');
// Similarly we need `pniCredential` even if this would require a server
// roundtrip.
if (!us.get('pniCredential')) {
await us.getProfiles();
}
const pniCredentialBase64 = us.get('pniCredential');
strictAssert(pniCredentialBase64, 'Must have pniCredential');
return window.Signal.Groups.buildPromoteMemberChange({
group: this.attributes,
profileKeyCredentialBase64,
pniCredentialBase64,
serverPublicParamsBase64: window.getServerPublicParams(),
});
}
async approvePendingApprovalRequest(
conversationId: string
private async approvePendingApprovalRequest(
uuid: UUID
): Promise<Proto.GroupChange.Actions | undefined> {
const idLog = this.idForLogging();
// This user's pending state may have changed in the time between the user's
// button press and when we get here. It's especially important to check here
// in conflict/retry cases.
if (!this.isMemberRequestingToJoin(conversationId)) {
if (!this.isMemberRequestingToJoin(uuid)) {
log.warn(
`approvePendingApprovalRequest/${idLog}: ${conversationId} is not requesting to join the group. Returning early.`
`approvePendingApprovalRequest/${idLog}: ${uuid} is not requesting ` +
'to join the group. Returning early.'
);
return undefined;
}
const pendingMember = window.ConversationController.get(conversationId);
if (!pendingMember) {
throw new Error(
`approvePendingApprovalRequest/${idLog}: No conversation found for conversation ${conversationId}`
);
}
const uuid = pendingMember.get('uuid');
if (!uuid) {
throw new Error(
`approvePendingApprovalRequest/${idLog}: Missing uuid for conversation ${conversationId}`
);
}
return window.Signal.Groups.buildPromotePendingAdminApprovalMemberChange({
group: this.attributes,
uuid,
});
}
async denyPendingApprovalRequest(
conversationId: string
private async denyPendingApprovalRequest(
uuid: UUID
): Promise<Proto.GroupChange.Actions | undefined> {
const idLog = this.idForLogging();
// This user's pending state may have changed in the time between the user's
// button press and when we get here. It's especially important to check here
// in conflict/retry cases.
if (!this.isMemberRequestingToJoin(conversationId)) {
if (!this.isMemberRequestingToJoin(uuid)) {
log.warn(
`denyPendingApprovalRequest/${idLog}: ${conversationId} is not requesting to join the group. Returning early.`
`denyPendingApprovalRequest/${idLog}: ${uuid} is not requesting ` +
'to join the group. Returning early.'
);
return undefined;
}
const pendingMember = window.ConversationController.get(conversationId);
if (!pendingMember) {
throw new Error(
`denyPendingApprovalRequest/${idLog}: No conversation found for conversation ${conversationId}`
);
}
const uuid = pendingMember.get('uuid');
if (!uuid) {
throw new Error(
`denyPendingApprovalRequest/${idLog}: Missing uuid for conversation ${pendingMember.idForLogging()}`
);
}
const ourUuid = window.textsecure.storage.user
.getCheckedUuid(UUIDKind.ACI)
.toString();
const ourUuid = window.textsecure.storage.user.getCheckedUuid(UUIDKind.ACI);
return window.Signal.Groups.buildDeletePendingAdminApprovalMemberChange({
group: this.attributes,
@ -620,6 +600,8 @@ export class ConversationModel extends window.Backbone
);
}
const uuid = toRequest.getCheckedUuid(`addPendingApprovalRequest/${idLog}`);
// We need the user's profileKeyCredential, which requires a roundtrip with the
// server, and most definitely their profileKey. A getProfiles() call will
// ensure that we have as much as we can get with the data we have.
@ -638,9 +620,10 @@ export class ConversationModel extends window.Backbone
// This user's pending state may have changed in the time between the user's
// button press and when we get here. It's especially important to check here
// in conflict/retry cases.
if (this.isMemberAwaitingApproval(conversationId)) {
if (this.isMemberAwaitingApproval(uuid)) {
log.warn(
`addPendingApprovalRequest/${idLog}: ${conversationId} already in pending approval.`
`addPendingApprovalRequest/${idLog}: ` +
`${toRequest.idForLogging()} already in pending approval.`
);
return undefined;
}
@ -652,23 +635,12 @@ export class ConversationModel extends window.Backbone
});
}
async addMember(
conversationId: string
): Promise<Proto.GroupChange.Actions | undefined> {
async addMember(uuid: UUID): Promise<Proto.GroupChange.Actions | undefined> {
const idLog = this.idForLogging();
const toRequest = window.ConversationController.get(conversationId);
const toRequest = window.ConversationController.get(uuid.toString());
if (!toRequest) {
throw new Error(
`addMember/${idLog}: No conversation found for conversation ${conversationId}`
);
}
const uuid = toRequest.get('uuid');
if (!uuid) {
throw new Error(
`addMember/${idLog}: ${toRequest.idForLogging()} is missing a uuid!`
);
throw new Error(`addMember/${idLog}: No conversation found for ${uuid}`);
}
// We need the user's profileKeyCredential, which requires a roundtrip with the
@ -689,8 +661,11 @@ export class ConversationModel extends window.Backbone
// This user's pending state may have changed in the time between the user's
// button press and when we get here. It's especially important to check here
// in conflict/retry cases.
if (this.isMember(conversationId)) {
log.warn(`addMember/${idLog}: ${conversationId} already a member.`);
if (this.isMember(uuid)) {
log.warn(
`addMember/${idLog}: ${toRequest.idForLogging()} ` +
'is already a member.'
);
return undefined;
}
@ -702,38 +677,23 @@ export class ConversationModel extends window.Backbone
});
}
async removePendingMember(
conversationIds: Array<string>
private async removePendingMember(
uuids: ReadonlyArray<UUID>
): Promise<Proto.GroupChange.Actions | undefined> {
const idLog = this.idForLogging();
const uuids = conversationIds
.map(conversationId => {
const pendingUuids = uuids
.map(uuid => {
// This user's pending state may have changed in the time between the user's
// button press and when we get here. It's especially important to check here
// in conflict/retry cases.
if (!this.isMemberPending(conversationId)) {
if (!this.isMemberPending(uuid)) {
log.warn(
`removePendingMember/${idLog}: ${conversationId} is not a pending member of group. Returning early.`
`removePendingMember/${idLog}: ${uuid} is not a pending member of group. Returning early.`
);
return undefined;
}
const pendingMember = window.ConversationController.get(conversationId);
if (!pendingMember) {
log.warn(
`removePendingMember/${idLog}: No conversation found for conversation ${conversationId}`
);
return undefined;
}
const uuid = pendingMember.get('uuid');
if (!uuid) {
log.warn(
`removePendingMember/${idLog}: Missing uuid for conversation ${pendingMember.idForLogging()}`
);
return undefined;
}
return uuid;
})
.filter(isNotNil);
@ -744,42 +704,26 @@ export class ConversationModel extends window.Backbone
return window.Signal.Groups.buildDeletePendingMemberChange({
group: this.attributes,
uuids,
uuids: pendingUuids,
});
}
async removeMember(
conversationId: string
private async removeMember(
uuid: UUID
): Promise<Proto.GroupChange.Actions | undefined> {
const idLog = this.idForLogging();
// This user's pending state may have changed in the time between the user's
// button press and when we get here. It's especially important to check here
// in conflict/retry cases.
if (!this.isMember(conversationId)) {
if (!this.isMember(uuid)) {
log.warn(
`removeMember/${idLog}: ${conversationId} is not a pending member of group. Returning early.`
`removeMember/${idLog}: ${uuid} is not a pending member of group. Returning early.`
);
return undefined;
}
const member = window.ConversationController.get(conversationId);
if (!member) {
throw new Error(
`removeMember/${idLog}: No conversation found for conversation ${conversationId}`
);
}
const uuid = member.get('uuid');
if (!uuid) {
throw new Error(
`removeMember/${idLog}: Missing uuid for conversation ${member.idForLogging()}`
);
}
const ourUuid = window.textsecure.storage.user
.getCheckedUuid(UUIDKind.ACI)
.toString();
const ourUuid = window.textsecure.storage.user.getCheckedUuid(UUIDKind.ACI);
return window.Signal.Groups.buildDeleteMemberChange({
group: this.attributes,
@ -788,8 +732,8 @@ export class ConversationModel extends window.Backbone
});
}
async toggleAdminChange(
conversationId: string
private async toggleAdminChange(
uuid: UUID
): Promise<Proto.GroupChange.Actions | undefined> {
if (!isGroupV2(this.attributes)) {
return undefined;
@ -797,30 +741,16 @@ export class ConversationModel extends window.Backbone
const idLog = this.idForLogging();
if (!this.isMember(conversationId)) {
if (!this.isMember(uuid)) {
log.warn(
`toggleAdminChange/${idLog}: ${conversationId} is not a pending member of group. Returning early.`
`toggleAdminChange/${idLog}: ${uuid} is not a pending member of group. Returning early.`
);
return undefined;
}
const conversation = window.ConversationController.get(conversationId);
if (!conversation) {
throw new Error(
`toggleAdminChange/${idLog}: No conversation found for conversation ${conversationId}`
);
}
const uuid = conversation.get('uuid');
if (!uuid) {
throw new Error(
`toggleAdminChange/${idLog}: Missing uuid for conversation ${conversationId}`
);
}
const MEMBER_ROLES = Proto.Member.Role;
const role = this.isAdmin(conversationId)
const role = this.isAdmin(uuid)
? MEMBER_ROLES.DEFAULT
: MEMBER_ROLES.ADMINISTRATOR;
@ -832,11 +762,13 @@ export class ConversationModel extends window.Backbone
}
async modifyGroupV2({
usingCredentialsFrom,
createGroupChange,
extraConversationsForSend,
inviteLinkPassword,
name,
}: {
usingCredentialsFrom: ReadonlyArray<ConversationModel>;
createGroupChange: () => Promise<Proto.GroupChange.Actions | undefined>;
extraConversationsForSend?: Array<string>;
inviteLinkPassword?: string;
@ -844,6 +776,7 @@ export class ConversationModel extends window.Backbone
}): Promise<void> {
await window.Signal.Groups.modifyGroupV2({
conversation: this,
usingCredentialsFrom,
createGroupChange,
extraConversationsForSend,
inviteLinkPassword,
@ -1828,6 +1761,9 @@ export class ConversationModel extends window.Backbone
const { customColor, customColorId } = this.getCustomColorData();
const ourACI = window.textsecure.storage.user.getCheckedUuid(UUIDKind.ACI);
const ourPNI = window.textsecure.storage.user.getUuid(UUIDKind.PNI);
// TODO: DESKTOP-720
return {
id: this.id,
@ -1844,11 +1780,13 @@ export class ConversationModel extends window.Backbone
acceptedMessageRequest: this.getAccepted(),
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
activeAt: this.get('active_at')!,
areWePending: Boolean(
ourConversationId && this.isMemberPending(ourConversationId)
),
areWePending:
this.isMemberPending(ourACI) ||
Boolean(
ourPNI && !this.isMember(ourACI) && this.isMemberPending(ourPNI)
),
areWePendingApproval: Boolean(
ourConversationId && this.isMemberAwaitingApproval(ourConversationId)
ourConversationId && this.isMemberAwaitingApproval(ourACI)
),
areWeAdmin: this.areWeAdmin(),
avatars: getAvatarData(this.attributes),
@ -2093,8 +2031,6 @@ export class ConversationModel extends window.Backbone
try {
const messageRequestEnum = Proto.SyncMessage.MessageRequestResponse.Type;
const isLocalAction = !fromSync && !viaStorageServiceSync;
const ourConversationId =
window.ConversationController.getOurConversationId();
const currentMessageRequestState = this.get('messageRequestResponseType');
const didResponseChange = response !== currentMessageRequestState;
@ -2116,26 +2052,36 @@ export class ConversationModel extends window.Backbone
}
if (isLocalAction) {
const ourACI = window.textsecure.storage.user.getCheckedUuid(
UUIDKind.ACI
);
const ourPNI = window.textsecure.storage.user.getUuid(UUIDKind.PNI);
if (
isGroupV1(this.attributes) ||
isDirectConversation(this.attributes)
) {
this.sendProfileKeyUpdate();
} else if (
ourConversationId &&
isGroupV2(this.attributes) &&
this.isMemberPending(ourConversationId)
this.isMemberPending(ourACI)
) {
await this.modifyGroupV2({
name: 'promotePendingMember',
createGroupChange: () =>
this.promotePendingMember(ourConversationId),
usingCredentialsFrom: [],
createGroupChange: () => this.promotePendingMember(UUIDKind.ACI),
});
} else if (
ourConversationId &&
ourPNI &&
isGroupV2(this.attributes) &&
this.isMember(ourConversationId)
this.isMemberPending(ourPNI)
) {
await this.modifyGroupV2({
name: 'promotePendingMember',
usingCredentialsFrom: [],
createGroupChange: () => this.promotePendingMember(UUIDKind.PNI),
});
} else if (isGroupV2(this.attributes) && this.isMember(ourACI)) {
log.info(
'applyMessageRequestResponse/accept: Already a member of v2 group'
);
@ -2223,21 +2169,21 @@ export class ConversationModel extends window.Backbone
inviteLinkPassword: string;
approvalRequired: boolean;
}): Promise<void> {
const ourConversationId =
window.ConversationController.getOurConversationIdOrThrow();
const ourUuid = window.textsecure.storage.user.getCheckedUuid().toString();
const ourACI = window.textsecure.storage.user.getCheckedUuid();
try {
if (approvalRequired) {
await this.modifyGroupV2({
name: 'requestToJoin',
usingCredentialsFrom: [],
inviteLinkPassword,
createGroupChange: () => this.addPendingApprovalRequest(),
});
} else {
await this.modifyGroupV2({
name: 'joinGroup',
usingCredentialsFrom: [],
inviteLinkPassword,
createGroupChange: () => this.addMember(ourConversationId),
createGroupChange: () => this.addMember(ourACI),
});
}
} catch (error) {
@ -2256,7 +2202,7 @@ export class ConversationModel extends window.Backbone
this.set({
pendingAdminApprovalV2: [
{
uuid: ourUuid,
uuid: ourACI.toString(),
timestamp: Date.now(),
},
],
@ -2277,8 +2223,7 @@ export class ConversationModel extends window.Backbone
}
async cancelJoinRequest(): Promise<void> {
const ourConversationId =
window.ConversationController.getOurConversationIdOrThrow();
const ourACI = window.storage.user.getCheckedUuid(UUIDKind.ACI);
const inviteLinkPassword = this.get('groupInviteLinkPassword');
if (!inviteLinkPassword) {
@ -2289,15 +2234,18 @@ export class ConversationModel extends window.Backbone
await this.modifyGroupV2({
name: 'cancelJoinRequest',
usingCredentialsFrom: [],
inviteLinkPassword,
createGroupChange: () =>
this.denyPendingApprovalRequest(ourConversationId),
createGroupChange: () => this.denyPendingApprovalRequest(ourACI),
});
}
async addMembersV2(conversationIds: ReadonlyArray<string>): Promise<void> {
await this.modifyGroupV2({
name: 'addMembersV2',
usingCredentialsFrom: conversationIds
.map(id => window.ConversationController.get(id))
.filter(isNotNil),
createGroupChange: () =>
window.Signal.Groups.buildAddMembersChange(
this.attributes,
@ -2315,6 +2263,7 @@ export class ConversationModel extends window.Backbone
): Promise<void> {
await this.modifyGroupV2({
name: 'updateGroupAttributesV2',
usingCredentialsFrom: [],
createGroupChange: () =>
window.Signal.Groups.buildUpdateAttributesChange(
{
@ -2329,36 +2278,43 @@ export class ConversationModel extends window.Backbone
}
async leaveGroupV2(): Promise<void> {
const ourConversationId =
window.ConversationController.getOurConversationId();
if (!isGroupV2(this.attributes)) {
return;
}
if (
ourConversationId &&
isGroupV2(this.attributes) &&
this.isMemberPending(ourConversationId)
) {
const ourACI = window.textsecure.storage.user.getCheckedUuid(UUIDKind.ACI);
const ourPNI = window.textsecure.storage.user.getUuid(UUIDKind.PNI);
if (this.isMemberPending(ourACI)) {
await this.modifyGroupV2({
name: 'delete',
createGroupChange: () => this.removePendingMember([ourConversationId]),
usingCredentialsFrom: [],
createGroupChange: () => this.removePendingMember([ourACI]),
});
} else if (
ourConversationId &&
isGroupV2(this.attributes) &&
this.isMember(ourConversationId)
) {
} else if (this.isMember(ourACI)) {
await this.modifyGroupV2({
name: 'delete',
createGroupChange: () => this.removeMember(ourConversationId),
usingCredentialsFrom: [],
createGroupChange: () => this.removeMember(ourACI),
});
// Keep PNI in pending if ACI was a member.
} else if (ourPNI && this.isMemberPending(ourPNI)) {
await this.modifyGroupV2({
name: 'delete',
usingCredentialsFrom: [],
createGroupChange: () => this.removePendingMember([ourPNI]),
});
} else {
const logId = this.idForLogging();
log.error(
'leaveGroupV2: We were neither a member nor a pending member of the group'
'leaveGroupV2: We were neither a member nor a pending member of ' +
`the group ${logId}`
);
}
}
async addBannedMember(
uuid: UUIDStringType
uuid: UUID
): Promise<Proto.GroupChange.Actions | undefined> {
if (this.isMember(uuid)) {
log.warn('addBannedMember: Member is a part of the group!');
@ -2387,7 +2343,8 @@ export class ConversationModel extends window.Backbone
async blockGroupLinkRequests(uuid: UUIDStringType): Promise<void> {
await this.modifyGroupV2({
name: 'addBannedMember',
createGroupChange: async () => this.addBannedMember(uuid),
usingCredentialsFrom: [],
createGroupChange: async () => this.addBannedMember(new UUID(uuid)),
});
}
@ -2396,7 +2353,17 @@ export class ConversationModel extends window.Backbone
return;
}
if (!this.isMember(conversationId)) {
const logId = this.idForLogging();
const member = window.ConversationController.get(conversationId);
if (!member) {
log.error(`toggleAdmin/${logId}: ${conversationId} does not exist`);
return;
}
const uuid = member.getCheckedUuid(`toggleAdmin/${logId}`);
if (!this.isMember(uuid)) {
log.error(
`toggleAdmin: Member ${conversationId} is not a member of the group`
);
@ -2405,21 +2372,32 @@ export class ConversationModel extends window.Backbone
await this.modifyGroupV2({
name: 'toggleAdmin',
createGroupChange: () => this.toggleAdminChange(conversationId),
usingCredentialsFrom: [member],
createGroupChange: () => this.toggleAdminChange(uuid),
});
}
async approvePendingMembershipFromGroupV2(
conversationId: string
): Promise<void> {
if (
isGroupV2(this.attributes) &&
this.isMemberRequestingToJoin(conversationId)
) {
const logId = this.idForLogging();
const pendingMember = window.ConversationController.get(conversationId);
if (!pendingMember) {
throw new Error(
`approvePendingMembershipFromGroupV2/${logId}: No conversation found for conversation ${conversationId}`
);
}
const uuid = pendingMember.getCheckedUuid(
`approvePendingMembershipFromGroupV2/${logId}`
);
if (isGroupV2(this.attributes) && this.isMemberRequestingToJoin(uuid)) {
await this.modifyGroupV2({
name: 'approvePendingApprovalRequest',
createGroupChange: () =>
this.approvePendingApprovalRequest(conversationId),
usingCredentialsFrom: [pendingMember],
createGroupChange: () => this.approvePendingApprovalRequest(uuid),
});
}
}
@ -2431,55 +2409,89 @@ export class ConversationModel extends window.Backbone
return;
}
const [conversationId] = conversationIds;
// Only pending memberships can be revoked for multiple members at once
if (conversationIds.length > 1) {
const uuids = conversationIds.map(id => {
const uuid = window.ConversationController.get(id)?.getUuid();
strictAssert(uuid, `UUID does not exist for ${id}`);
return uuid;
});
await this.modifyGroupV2({
name: 'removePendingMember',
createGroupChange: () => this.removePendingMember(conversationIds),
usingCredentialsFrom: conversationIds
.map(id => window.ConversationController.get(id))
.filter(isNotNil),
createGroupChange: () => this.removePendingMember(uuids),
extraConversationsForSend: conversationIds,
});
} else if (this.isMemberRequestingToJoin(conversationId)) {
return;
}
const [conversationId] = conversationIds;
const pendingMember = window.ConversationController.get(conversationId);
if (!pendingMember) {
const logId = this.idForLogging();
throw new Error(
`revokePendingMembershipsFromGroupV2/${logId}: No conversation found for conversation ${conversationId}`
);
}
const uuid = pendingMember.getCheckedUuid(
'revokePendingMembershipsFromGroupV2'
);
if (this.isMemberRequestingToJoin(uuid)) {
await this.modifyGroupV2({
name: 'denyPendingApprovalRequest',
createGroupChange: () =>
this.denyPendingApprovalRequest(conversationId),
usingCredentialsFrom: [pendingMember],
createGroupChange: () => this.denyPendingApprovalRequest(uuid),
extraConversationsForSend: [conversationId],
});
} else if (this.isMemberPending(conversationId)) {
} else if (this.isMemberPending(uuid)) {
await this.modifyGroupV2({
name: 'removePendingMember',
createGroupChange: () => this.removePendingMember([conversationId]),
usingCredentialsFrom: [pendingMember],
createGroupChange: () => this.removePendingMember([uuid]),
extraConversationsForSend: [conversationId],
});
}
}
async removeFromGroupV2(conversationId: string): Promise<void> {
if (
isGroupV2(this.attributes) &&
this.isMemberRequestingToJoin(conversationId)
) {
if (!isGroupV2(this.attributes)) {
return;
}
const logId = this.idForLogging();
const pendingMember = window.ConversationController.get(conversationId);
if (!pendingMember) {
throw new Error(
`removeFromGroupV2/${logId}: No conversation found for conversation ${conversationId}`
);
}
const uuid = pendingMember.getCheckedUuid(`removeFromGroupV2/${logId}`);
if (this.isMemberRequestingToJoin(uuid)) {
await this.modifyGroupV2({
name: 'denyPendingApprovalRequest',
createGroupChange: () =>
this.denyPendingApprovalRequest(conversationId),
usingCredentialsFrom: [pendingMember],
createGroupChange: () => this.denyPendingApprovalRequest(uuid),
extraConversationsForSend: [conversationId],
});
} else if (
isGroupV2(this.attributes) &&
this.isMemberPending(conversationId)
) {
} else if (this.isMemberPending(uuid)) {
await this.modifyGroupV2({
name: 'removePendingMember',
createGroupChange: () => this.removePendingMember([conversationId]),
usingCredentialsFrom: [pendingMember],
createGroupChange: () => this.removePendingMember([uuid]),
extraConversationsForSend: [conversationId],
});
} else if (isGroupV2(this.attributes) && this.isMember(conversationId)) {
} else if (this.isMember(uuid)) {
await this.modifyGroupV2({
name: 'removeFromGroup',
createGroupChange: () => this.removeMember(conversationId),
usingCredentialsFrom: [pendingMember],
createGroupChange: () => this.removeMember(uuid),
extraConversationsForSend: [conversationId],
});
} else {
@ -3478,14 +3490,13 @@ export class ConversationModel extends window.Backbone
});
}
isAdmin(id: string): boolean {
isAdmin(uuid: UUID): boolean {
if (!isGroupV2(this.attributes)) {
return false;
}
const uuid = UUID.checkedLookup(id).toString();
const members = this.get('membersV2') || [];
const member = members.find(x => x.uuid === uuid);
const member = members.find(x => x.uuid === uuid.toString());
if (!member) {
return false;
}
@ -4187,6 +4198,7 @@ export class ConversationModel extends window.Backbone
await this.modifyGroupV2({
name: 'updateInviteLinkPassword',
usingCredentialsFrom: [],
createGroupChange: async () =>
window.Signal.Groups.buildInviteLinkPasswordChange(
this.attributes,
@ -4218,6 +4230,7 @@ export class ConversationModel extends window.Backbone
if (shouldCreateNewGroupLink) {
await this.modifyGroupV2({
name: 'updateNewGroupLink',
usingCredentialsFrom: [],
createGroupChange: async () =>
window.Signal.Groups.buildNewGroupLinkChange(
this.attributes,
@ -4228,6 +4241,7 @@ export class ConversationModel extends window.Backbone
} else {
await this.modifyGroupV2({
name: 'updateAccessControlAddFromInviteLink',
usingCredentialsFrom: [],
createGroupChange: async () =>
window.Signal.Groups.buildAccessControlAddFromInviteLinkChange(
this.attributes,
@ -4262,6 +4276,7 @@ export class ConversationModel extends window.Backbone
await this.modifyGroupV2({
name: 'updateAccessControlAddFromInviteLink',
usingCredentialsFrom: [],
createGroupChange: async () =>
window.Signal.Groups.buildAccessControlAddFromInviteLinkChange(
this.attributes,
@ -4285,6 +4300,7 @@ export class ConversationModel extends window.Backbone
await this.modifyGroupV2({
name: 'updateAccessControlAttributes',
usingCredentialsFrom: [],
createGroupChange: async () =>
window.Signal.Groups.buildAccessControlAttributesChange(
this.attributes,
@ -4310,6 +4326,7 @@ export class ConversationModel extends window.Backbone
await this.modifyGroupV2({
name: 'updateAccessControlMembers',
usingCredentialsFrom: [],
createGroupChange: async () =>
window.Signal.Groups.buildAccessControlMembersChange(
this.attributes,
@ -4335,6 +4352,7 @@ export class ConversationModel extends window.Backbone
await this.modifyGroupV2({
name: 'updateAnnouncementsOnly',
usingCredentialsFrom: [],
createGroupChange: async () =>
window.Signal.Groups.buildAnnouncementsOnlyChange(
this.attributes,
@ -4377,6 +4395,7 @@ export class ConversationModel extends window.Backbone
}
await this.modifyGroupV2({
name: 'updateExpirationTimer',
usingCredentialsFrom: [],
createGroupChange: () =>
this.updateExpirationTimerInGroupV2(providedExpireTimer),
});
@ -4588,10 +4607,7 @@ export class ConversationModel extends window.Backbone
const ourGroups =
await window.ConversationController.getAllGroupsInvolvingUuid(ourUuid);
const sharedGroups = ourGroups
.filter(
c =>
c.hasMember(ourUuid.toString()) && c.hasMember(theirUuid.toString())
)
.filter(c => c.hasMember(ourUuid) && c.hasMember(theirUuid))
.sort(
(left, right) =>
(right.get('timestamp') || 0) - (left.get('timestamp') || 0)
@ -4733,6 +4749,8 @@ export class ConversationModel extends window.Backbone
);
this.set({
profileKeyCredential: null,
profileKeyCredentialExpiration: null,
pniCredential: null,
accessKey: null,
sealedSender: SEALED_SENDER.UNKNOWN,
});
@ -4759,6 +4777,27 @@ export class ConversationModel extends window.Backbone
return false;
}
hasProfileKeyCredentialExpired(): boolean {
const profileKeyCredential = this.get('profileKeyCredential');
const profileKeyCredentialExpiration = this.get(
'profileKeyCredentialExpiration'
);
if (!profileKeyCredential) {
return false;
}
if (!isNumber(profileKeyCredentialExpiration)) {
const logId = this.idForLogging();
log.warn(`hasProfileKeyCredentialExpired(${logId}): missing expiration`);
return true;
}
const today = toDayMillis(Date.now());
return profileKeyCredentialExpiration <= today;
}
deriveAccessKeyIfNeeded(): void {
const profileKey = this.get('profileKey');
if (!profileKey) {
@ -4860,11 +4899,10 @@ export class ConversationModel extends window.Backbone
await window.Signal.Data.updateConversation(this.attributes);
}
hasMember(identifier: string): boolean {
const id = window.ConversationController.getConversationId(identifier);
const memberIds = this.getMemberIds();
hasMember(uuid: UUID): boolean {
const members = this.getMembers();
return window._.contains(memberIds, id);
return members.some(member => member.get('uuid') === uuid.toString());
}
fetchContacts(): void {
@ -5234,9 +5272,19 @@ export class ConversationModel extends window.Backbone
return;
}
const sender = window.ConversationController.get(senderId);
if (!sender) {
return;
}
const senderUuid = sender.getUuid();
if (!senderUuid) {
return;
}
// Drop typing indicators for announcement only groups where the sender
// is not an admin
if (this.get('announcementsOnly') && !this.isAdmin(senderId)) {
if (this.get('announcementsOnly') && !this.isAdmin(senderUuid)) {
return;
}

View File

@ -459,7 +459,12 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
conversationSelector: findAndFormatContact,
ourConversationId,
ourNumber: window.textsecure.storage.user.getNumber(),
ourUuid: window.textsecure.storage.user.getCheckedUuid().toString(),
ourACI: window.textsecure.storage.user
.getCheckedUuid(UUIDKind.ACI)
.toString(),
ourPNI: window.textsecure.storage.user
.getCheckedUuid(UUIDKind.PNI)
.toString(),
regionCode: window.storage.get('regionCode', 'ZZ'),
accountSelector: (identifier?: string) => {
const state = window.reduxStore.getState();
@ -540,7 +545,12 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
const changes = GroupChange.renderChange<string>(change, {
i18n: window.i18n,
ourUuid: window.textsecure.storage.user.getCheckedUuid().toString(),
ourACI: window.textsecure.storage.user
.getCheckedUuid(UUIDKind.ACI)
.toString(),
ourPNI: window.textsecure.storage.user
.getCheckedUuid(UUIDKind.PNI)
.toString(),
renderContact: (conversationId: string) => {
const conversation =
window.ConversationController.get(conversationId);
@ -2213,12 +2223,14 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
publicParams: initialMessage.groupV2.publicParams,
});
// Standard GroupV2 modification codepath
const existingRevision = conversation.get('revision');
const isFirstUpdate = !_.isNumber(existingRevision);
// Standard GroupV2 modification codepath
const isV2GroupUpdate =
initialMessage.groupV2 &&
_.isNumber(initialMessage.groupV2.revision) &&
(!_.isNumber(existingRevision) ||
(isFirstUpdate ||
initialMessage.groupV2.revision > existingRevision);
if (isV2GroupUpdate && initialMessage.groupV2) {
@ -2247,9 +2259,9 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
}
}
const ourConversationId =
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
window.ConversationController.getOurConversationId()!;
const ourACI = window.textsecure.storage.user.getCheckedUuid(
UUIDKind.ACI
);
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const senderId = window.ConversationController.ensureContactIds({
e164: source,
@ -2273,15 +2285,17 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
return;
}
const areWeMember =
!conversation.get('left') && conversation.hasMember(ourACI);
// Drop an incoming GroupV2 message if we or the sender are not part of the group
// after applying the message's associated group changes.
if (
type === 'incoming' &&
!isDirectConversation(conversation.attributes) &&
hasGroupV2Prop &&
(conversation.get('left') ||
!conversation.hasMember(ourConversationId) ||
!conversation.hasMember(senderId))
(!areWeMember ||
(sourceUuid && !conversation.hasMember(new UUID(sourceUuid))))
) {
log.warn(
`Received message destined for group ${conversation.idForLogging()}, which we or the sender are not a part of. Dropping.`
@ -2301,7 +2315,7 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
!hasGroupV2Prop &&
!isV1GroupUpdate &&
conversation.get('members') &&
(conversation.get('left') || !conversation.hasMember(ourConversationId))
!areWeMember
) {
log.warn(
`Received message destined for group ${conversation.idForLogging()}, which we're not a part of. Dropping.`
@ -2323,7 +2337,7 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
// Drop incoming messages to announcement only groups where sender is not admin
if (
conversation.get('announcementsOnly') &&
!conversation.isAdmin(senderId)
!conversation.isAdmin(UUID.checkedLookup(senderId))
) {
confirm();
return;

View File

@ -6,20 +6,76 @@ import PQueue from 'p-queue';
import * as log from './logging/log';
import { assert } from './util/assert';
import { sleep } from './util/sleep';
import { missingCaseError } from './util/missingCaseError';
import { isNormalNumber } from './util/isNormalNumber';
import { take } from './util/iterables';
import { isOlderThan } from './util/timestamp';
import type { ConversationModel } from './models/conversations';
import type { StorageInterface } from './types/Storage.d';
import * as Errors from './types/errors';
import { getProfile } from './util/getProfile';
import { MINUTE } from './util/durations';
import { MINUTE, HOUR, DAY, MONTH } from './util/durations';
const STORAGE_KEY = 'lastAttemptedToRefreshProfilesAt';
const MAX_AGE_TO_BE_CONSIDERED_ACTIVE = 30 * 24 * 60 * 60 * 1000;
const MAX_AGE_TO_BE_CONSIDERED_RECENTLY_REFRESHED = 1 * 24 * 60 * 60 * 1000;
const MAX_AGE_TO_BE_CONSIDERED_ACTIVE = MONTH;
const MAX_AGE_TO_BE_CONSIDERED_RECENTLY_REFRESHED = DAY;
const MAX_CONVERSATIONS_TO_REFRESH = 50;
const MIN_ELAPSED_DURATION_TO_REFRESH_AGAIN = 12 * 3600 * 1000;
const MIN_ELAPSED_DURATION_TO_REFRESH_AGAIN = 12 * HOUR;
const MIN_REFRESH_DELAY = MINUTE;
export class RoutineProfileRefresher {
private interval: NodeJS.Timeout | undefined;
constructor(
private readonly options: {
getAllConversations: () => ReadonlyArray<ConversationModel>;
getOurConversationId: () => string | undefined;
storage: Pick<StorageInterface, 'get' | 'put'>;
}
) {}
public async start(): Promise<void> {
if (this.interval !== undefined) {
clearInterval(this.interval);
}
const { storage, getAllConversations, getOurConversationId } = this.options;
// eslint-disable-next-line no-constant-condition
while (true) {
const refreshInMs = timeUntilNextRefresh(storage);
log.info(`routineProfileRefresh: waiting for ${refreshInMs}ms`);
// eslint-disable-next-line no-await-in-loop
await sleep(refreshInMs);
const ourConversationId = getOurConversationId();
if (!ourConversationId) {
log.warn('routineProfileRefresh: missing our conversation id');
// eslint-disable-next-line no-await-in-loop
await sleep(MIN_REFRESH_DELAY);
continue;
}
try {
// eslint-disable-next-line no-await-in-loop
await routineProfileRefresh({
allConversations: getAllConversations(),
ourConversationId,
storage,
});
} catch (error) {
log.error('routineProfileRefresh: failure', Errors.toLogFormat(error));
// eslint-disable-next-line no-await-in-loop
await sleep(MIN_REFRESH_DELAY);
}
}
}
}
export async function routineProfileRefresh({
allConversations,
@ -29,14 +85,15 @@ export async function routineProfileRefresh({
// Only for tests
getProfileFn = getProfile,
}: {
allConversations: Array<ConversationModel>;
allConversations: ReadonlyArray<ConversationModel>;
ourConversationId: string;
storage: Pick<StorageInterface, 'get' | 'put'>;
getProfileFn?: typeof getProfile;
}): Promise<void> {
log.info('routineProfileRefresh: starting');
if (!hasEnoughTimeElapsedSinceLastRefresh(storage)) {
const refreshInMs = timeUntilNextRefresh(storage);
if (refreshInMs > 0) {
log.info('routineProfileRefresh: too soon to refresh. Doing nothing');
return;
}
@ -91,24 +148,24 @@ export async function routineProfileRefresh({
);
}
function hasEnoughTimeElapsedSinceLastRefresh(
storage: Pick<StorageInterface, 'get'>
): boolean {
function timeUntilNextRefresh(storage: Pick<StorageInterface, 'get'>): number {
const storedValue = storage.get(STORAGE_KEY);
if (isNil(storedValue)) {
return true;
return 0;
}
if (isNormalNumber(storedValue)) {
return isOlderThan(storedValue, MIN_ELAPSED_DURATION_TO_REFRESH_AGAIN);
const planned = storedValue + MIN_ELAPSED_DURATION_TO_REFRESH_AGAIN;
const now = Date.now();
return Math.max(0, planned - now);
}
assert(
false,
`An invalid value was stored in ${STORAGE_KEY}; treating it as nil`
);
return true;
return 0;
}
function getConversationsToRefresh(
@ -134,6 +191,20 @@ function* getFilteredConversations(
const type = conversation.get('type');
switch (type) {
case 'private':
if (
conversation.hasProfileKeyCredentialExpired() &&
(conversation.id === ourConversationId ||
!conversationIdsSeen.has(conversation.id))
) {
conversation.set({
profileKeyCredential: null,
profileKeyCredentialExpiration: null,
});
conversationIdsSeen.add(conversation.id);
yield conversation;
break;
}
if (
!conversationIdsSeen.has(conversation.id) &&
isConversationActive(conversation) &&

View File

@ -2,14 +2,16 @@
// SPDX-License-Identifier: AGPL-3.0-only
import { last, sortBy } from 'lodash';
import { AuthCredentialResponse } from '@signalapp/libsignal-client/zkgroup';
import { AuthCredentialWithPniResponse } from '@signalapp/libsignal-client/zkgroup';
import { getClientZkAuthOperations } from '../util/zkgroup';
import type { GroupCredentialType } from '../textsecure/WebAPI';
import { strictAssert } from '../util/assert';
import * as durations from '../util/durations';
import { BackOff } from '../util/BackOff';
import { sleep } from '../util/sleep';
import { toDayMillis } from '../util/timestamp';
import { UUIDKind } from '../types/UUID';
import * as log from '../logging/log';
@ -17,20 +19,25 @@ export const GROUP_CREDENTIALS_KEY = 'groupCredentials';
type CredentialsDataType = Array<GroupCredentialType>;
type RequestDatesType = {
startDay: number;
endDay: number;
startDayInMs: number;
endDayInMs: number;
};
type NextCredentialsType = {
today: GroupCredentialType;
tomorrow: GroupCredentialType;
};
function getTodayInEpoch() {
return Math.floor(Date.now() / durations.DAY);
}
let started = false;
function getCheckedCredentials(reason: string): CredentialsDataType {
const result = window.storage.get('groupCredentials');
strictAssert(
result !== undefined,
`getCheckedCredentials: no credentials found, ${reason}`
);
return result;
}
export async function initializeGroupCredentialFetcher(): Promise<void> {
if (started) {
return;
@ -89,16 +96,14 @@ export async function runWithRetry(
}
// In cases where we are at a day boundary, we might need to use tomorrow in a retry
export function getCredentialsForToday(
data: CredentialsDataType | undefined
export function getCheckedCredentialsForToday(
reason: string
): NextCredentialsType {
if (!data) {
throw new Error('getCredentialsForToday: No credentials fetched!');
}
const data = getCheckedCredentials(reason);
const todayInEpoch = getTodayInEpoch();
const today = toDayMillis(Date.now());
const todayIndex = data.findIndex(
(item: GroupCredentialType) => item.redemptionTime === todayInEpoch
(item: GroupCredentialType) => item.redemptionTime === today
);
if (todayIndex < 0) {
throw new Error(
@ -113,29 +118,37 @@ export function getCredentialsForToday(
}
export async function maybeFetchNewCredentials(): Promise<void> {
const uuid = window.textsecure.storage.user.getUuid()?.toString();
if (!uuid) {
log.info('maybeFetchCredentials: no UUID, returning early');
const logId = 'maybeFetchNewCredentials';
const aci = window.textsecure.storage.user.getUuid(UUIDKind.ACI)?.toString();
if (!aci) {
log.info(`${logId}: no ACI, returning early`);
return;
}
const previous: CredentialsDataType | undefined = window.storage.get(
GROUP_CREDENTIALS_KEY
);
const pni = window.textsecure.storage.user.getUuid(UUIDKind.PNI)?.toString();
if (!pni) {
log.info(`${logId}: no PNI, returning early`);
return;
}
const previous: CredentialsDataType | undefined =
window.storage.get('groupCredentials');
const requestDates = getDatesForRequest(previous);
if (!requestDates) {
log.info('maybeFetchCredentials: no new credentials needed');
log.info(`${logId}: no new credentials needed`);
return;
}
const accountManager = window.getAccountManager();
if (!accountManager) {
log.info('maybeFetchCredentials: unable to get AccountManager');
const { server } = window.textsecure;
if (!server) {
log.error(`${logId}: unable to get server`);
return;
}
const { startDay, endDay } = requestDates;
const { startDayInMs, endDayInMs } = requestDates;
log.info(
`maybeFetchCredentials: fetching credentials for ${startDay} through ${endDay}`
`${logId}: fetching credentials for ${startDayInMs} through ${endDayInMs}`
);
const serverPublicParamsBase64 = window.getServerPublicParams();
@ -143,46 +156,47 @@ export async function maybeFetchNewCredentials(): Promise<void> {
serverPublicParamsBase64
);
const newCredentials = sortCredentials(
await accountManager.getGroupCredentials(startDay, endDay, UUIDKind.ACI)
await server.getGroupCredentials({ startDayInMs, endDayInMs })
).map((item: GroupCredentialType) => {
const authCredential = clientZKAuthOperations.receiveAuthCredential(
uuid,
const authCredential = clientZKAuthOperations.receiveAuthCredentialWithPni(
aci,
pni,
item.redemptionTime,
new AuthCredentialResponse(Buffer.from(item.credential, 'base64'))
new AuthCredentialWithPniResponse(Buffer.from(item.credential, 'base64'))
);
const credential = authCredential.serialize().toString('base64');
return {
redemptionTime: item.redemptionTime,
redemptionTime: item.redemptionTime * durations.SECOND,
credential,
};
});
const todayInEpoch = getTodayInEpoch();
const today = toDayMillis(Date.now());
const previousCleaned = previous
? previous.filter(
(item: GroupCredentialType) => item.redemptionTime >= todayInEpoch
(item: GroupCredentialType) => item.redemptionTime >= today
)
: [];
const finalCredentials = [...previousCleaned, ...newCredentials];
log.info('maybeFetchCredentials: Saving new credentials...');
log.info(`${logId}: Saving new credentials...`);
// Note: we don't wait for this to finish
window.storage.put(GROUP_CREDENTIALS_KEY, finalCredentials);
log.info('maybeFetchCredentials: Save complete.');
window.storage.put('groupCredentials', finalCredentials);
log.info(`${logId}: Save complete.`);
}
export function getDatesForRequest(
data?: CredentialsDataType
): RequestDatesType | undefined {
const todayInEpoch = getTodayInEpoch();
const oneWeekOut = todayInEpoch + 7;
const today = toDayMillis(Date.now());
const oneWeekOut = today + durations.WEEK;
const lastCredential = last(data);
if (!lastCredential || lastCredential.redemptionTime < todayInEpoch) {
if (!lastCredential || lastCredential.redemptionTime < today) {
return {
startDay: todayInEpoch,
endDay: oneWeekOut,
startDayInMs: today,
endDayInMs: oneWeekOut,
};
}
@ -191,8 +205,8 @@ export function getDatesForRequest(
}
return {
startDay: lastCredential.redemptionTime + 1,
endDay: oneWeekOut,
startDayInMs: lastCredential.redemptionTime + durations.DAY,
endDayInMs: oneWeekOut,
};
}

View File

@ -205,6 +205,7 @@ const dataInterface: ClientInterface = {
updateConversations,
removeConversation,
updateAllConversationColors,
removeAllProfileKeyCredentials,
getAllConversations,
getAllConversationIds,
@ -1161,7 +1162,7 @@ async function getMessageBySender({
sent_at,
}: {
source: string;
sourceUuid: string;
sourceUuid: UUIDStringType;
sourceDevice: number;
sent_at: number;
}) {
@ -1271,7 +1272,7 @@ async function getOlderStories(options: {
limit?: number;
receivedAt?: number;
sentAt?: number;
sourceUuid?: string;
sourceUuid?: UUIDStringType;
}): Promise<Array<MessageType>> {
return channels.getOlderStories(options);
}
@ -1794,6 +1795,10 @@ async function updateAllConversationColors(
);
}
async function removeAllProfileKeyCredentials(): Promise<void> {
return channels.removeAllProfileKeyCredentials();
}
function getMaxMessageCounter(): Promise<number | undefined> {
return channels.getMaxMessageCounter();
}

View File

@ -202,7 +202,7 @@ export type UnprocessedType = {
messageAgeSec?: number;
source?: string;
sourceUuid?: string;
sourceUuid?: UUIDStringType;
sourceDevice?: number;
destinationUuid?: string;
serverGuid?: string;
@ -213,7 +213,7 @@ export type UnprocessedType = {
export type UnprocessedUpdateType = {
source?: string;
sourceUuid?: string;
sourceUuid?: UUIDStringType;
sourceDevice?: number;
serverGuid?: string;
serverTimestamp?: number;
@ -257,8 +257,8 @@ export type StoryDistributionWithMembersType = Readonly<
export type StoryReadType = Readonly<{
authorId: UUIDStringType;
conversationId: UUIDStringType;
storyId: UUIDStringType;
conversationId: string;
storyId: string;
storyReadDate: number;
}>;
@ -362,6 +362,7 @@ export type DataInterface = {
value: CustomColorType;
}
) => Promise<void>;
removeAllProfileKeyCredentials: () => Promise<void>;
getAllConversations: () => Promise<Array<ConversationType>>;
getAllConversationIds: () => Promise<Array<string>>;
@ -439,7 +440,7 @@ export type DataInterface = {
_removeAllReactions: () => Promise<void>;
getMessageBySender: (options: {
source: string;
sourceUuid: string;
sourceUuid: UUIDStringType;
sourceDevice: number;
sent_at: number;
}) => Promise<MessageType | undefined>;
@ -462,7 +463,7 @@ export type DataInterface = {
limit?: number;
receivedAt?: number;
sentAt?: number;
sourceUuid?: string;
sourceUuid?: UUIDStringType;
}) => Promise<Array<MessageType>>;
// getNewerMessagesByConversation is JSON on server, full message on Client
getMessageMetricsForConversation: (

View File

@ -200,6 +200,7 @@ const dataInterface: ServerInterface = {
updateConversations,
removeConversation,
updateAllConversationColors,
removeAllProfileKeyCredentials,
getAllConversations,
getAllConversationIds,
@ -2033,7 +2034,7 @@ async function getMessageBySender({
sent_at,
}: {
source: string;
sourceUuid: string;
sourceUuid: UUIDStringType;
sourceDevice: number;
sent_at: number;
}): Promise<MessageType | undefined> {
@ -2443,7 +2444,7 @@ async function getOlderStories({
limit?: number;
receivedAt?: number;
sentAt?: number;
sourceUuid?: string;
sourceUuid?: UUIDStringType;
}): Promise<Array<MessageType>> {
const db = getInstance();
const rows: JSONRows = db
@ -5067,3 +5068,15 @@ async function updateAllConversationColors(
}),
});
}
async function removeAllProfileKeyCredentials(): Promise<void> {
const db = getInstance();
db.exec(
`
UPDATE conversations
SET
json = json_remove(json, '$.profileKeyCredential')
`
);
}

View File

@ -852,7 +852,7 @@ function groupCallStateChange(
didSomeoneStartPresenting = false;
}
const { ourUuid } = getState().user;
const { ourACI: ourUuid } = getState().user;
strictAssert(ourUuid, 'groupCallStateChange failed to fetch our uuid');
dispatch({

View File

@ -20,7 +20,6 @@ import { DAY } from '../../util/durations';
import { ReadStatus } from '../../messages/MessageReadStatus';
import { StoryViewDirectionType, StoryViewModeType } from '../../types/Stories';
import { ToastReactionFailed } from '../../components/ToastReactionFailed';
import { UUID } from '../../types/UUID';
import { enqueueReactionForSend } from '../../reactions/enqueueReactionForSend';
import { getMessageById } from '../../messages/getMessageById';
import { markViewed } from '../../services/MessageUpdater';
@ -271,7 +270,7 @@ function markStoryRead(
await dataInterface.addNewStoryRead({
authorId: message.attributes.sourceUuid,
conversationId: message.attributes.conversationId,
storyId: UUID.fromString(messageId),
storyId: messageId,
storyReadDate,
});

View File

@ -18,7 +18,8 @@ export type UserStateType = {
tempPath: string;
ourConversationId: string | undefined;
ourDeviceId: number | undefined;
ourUuid: UUIDStringType | undefined;
ourACI: UUIDStringType | undefined;
ourPNI: UUIDStringType | undefined;
ourNumber: string | undefined;
platform: string;
regionCode: string | undefined;
@ -39,7 +40,8 @@ type UserChangedActionType = {
payload: {
ourConversationId?: string;
ourDeviceId?: number;
ourUuid?: UUIDStringType;
ourACI?: UUIDStringType;
ourPNI?: UUIDStringType;
ourNumber?: string;
regionCode?: string;
interactionMode?: 'mouse' | 'keyboard';
@ -64,7 +66,8 @@ function userChanged(attributes: {
ourConversationId?: string;
ourDeviceId?: number;
ourNumber?: string;
ourUuid?: UUIDStringType;
ourACI?: UUIDStringType;
ourPNI?: UUIDStringType;
regionCode?: string;
theme?: ThemeType;
isMainWindowMaximized?: boolean;
@ -95,7 +98,8 @@ export function getEmptyState(): UserStateType {
tempPath: 'missing',
ourConversationId: 'missing',
ourDeviceId: 0,
ourUuid: '00000000-0000-4000-8000-000000000000',
ourACI: undefined,
ourPNI: undefined,
ourNumber: 'missing',
regionCode: 'missing',
platform: 'missing',

View File

@ -28,6 +28,7 @@ import type { StoryDataType } from './ducks/stories';
import type { StoryDistributionListDataType } from './ducks/storyDistributionLists';
import { getInitialState as stickers } from '../types/Stickers';
import type { MenuOptionsType } from '../types/menu';
import { UUIDKind } from '../types/UUID';
import { getEmojiReducerState as emojis } from '../util/loadRecentEmojis';
import type { MainWindowStatsType } from '../windows/context';
@ -51,7 +52,12 @@ export function getInitialState({
conversation.format()
);
const ourNumber = window.textsecure.storage.user.getNumber();
const ourUuid = window.textsecure.storage.user.getUuid()?.toString();
const ourACI = window.textsecure.storage.user
.getUuid(UUIDKind.ACI)
?.toString();
const ourPNI = window.textsecure.storage.user
.getUuid(UUIDKind.PNI)
?.toString();
const ourConversationId =
window.ConversationController.getOurConversationId();
const ourDeviceId = window.textsecure.storage.user.getDeviceId();
@ -119,7 +125,8 @@ export function getInitialState({
ourConversationId,
ourDeviceId,
ourNumber,
ourUuid,
ourACI,
ourPNI,
platform: window.platform,
i18n: window.i18n,
localeMessages: window.SignalContext.localeMessages,

View File

@ -12,7 +12,7 @@ import type {
GroupCallStateType,
} from '../ducks/calling';
import { getIncomingCall as getIncomingCallHelper } from '../ducks/calling';
import { getUserUuid } from './user';
import { getUserACI } from './user';
import { getOwn } from '../../util/getOwn';
import { CallViewMode } from '../../types/Calling';
import type { UUIDStringType } from '../../types/UUID';
@ -61,7 +61,7 @@ export const isInCall = createSelector(
export const getIncomingCall = createSelector(
getCallsByConversation,
getUserUuid,
getUserACI,
(
callsByConversation: CallsByConversationType,
ourUuid: UUIDStringType | undefined

View File

@ -49,7 +49,8 @@ import {
getRegionCode,
getUserConversationId,
getUserNumber,
getUserUuid,
getUserACI,
getUserPNI,
} from './user';
import { getPinnedConversationIds } from './items';
import { getPropsForBubble } from './message';
@ -780,7 +781,8 @@ export const getMessageSelector = createSelector(
getConversationSelector,
getRegionCode,
getUserNumber,
getUserUuid,
getUserACI,
getUserPNI,
getUserConversationId,
getCallSelector,
getActiveCall,
@ -793,7 +795,8 @@ export const getMessageSelector = createSelector(
conversationSelector: GetConversationByIdType,
regionCode: string | undefined,
ourNumber: string | undefined,
ourUuid: UUIDStringType | undefined,
ourACI: UUIDStringType | undefined,
ourPNI: UUIDStringType | undefined,
ourConversationId: string | undefined,
callSelector: CallSelectorType,
activeCall: undefined | CallStateType,
@ -810,7 +813,8 @@ export const getMessageSelector = createSelector(
conversationSelector,
ourConversationId,
ourNumber,
ourUuid,
ourACI,
ourPNI,
regionCode,
selectedMessageId: selectedMessage?.id,
selectedMessageCounter: selectedMessage?.counter,

View File

@ -61,7 +61,8 @@ import {
getRegionCode,
getUserConversationId,
getUserNumber,
getUserUuid,
getUserACI,
getUserPNI,
} from './user';
import type {
@ -114,7 +115,8 @@ export type GetPropsForBubbleOptions = Readonly<{
conversationSelector: GetConversationByIdType;
ourConversationId?: string;
ourNumber?: string;
ourUuid?: UUIDStringType;
ourACI?: UUIDStringType;
ourPNI?: UUIDStringType;
selectedMessageId?: string;
selectedMessageCounter?: number;
regionCode?: string;
@ -182,7 +184,7 @@ export function getSourceDevice(
export function getSourceUuid(
message: MessageWithUIFieldsType,
ourUuid: string | undefined
ourACI: string | undefined
): string | undefined {
if (isIncoming(message)) {
return message.sourceUuid;
@ -193,12 +195,16 @@ export function getSourceUuid(
);
}
return ourUuid;
return ourACI;
}
export type GetContactOptions = Pick<
GetPropsForBubbleOptions,
'conversationSelector' | 'ourConversationId' | 'ourNumber' | 'ourUuid'
| 'conversationSelector'
| 'ourConversationId'
| 'ourNumber'
| 'ourACI'
| 'ourPNI'
>;
export function getContactId(
@ -207,11 +213,11 @@ export function getContactId(
conversationSelector,
ourConversationId,
ourNumber,
ourUuid,
ourACI,
}: GetContactOptions
): string | undefined {
const source = getSource(message, ourNumber);
const sourceUuid = getSourceUuid(message, ourUuid);
const sourceUuid = getSourceUuid(message, ourACI);
if (!source && !sourceUuid) {
return ourConversationId;
@ -228,11 +234,11 @@ export function getContact(
conversationSelector,
ourConversationId,
ourNumber,
ourUuid,
ourACI,
}: GetContactOptions
): ConversationType {
const source = getSource(message, ourNumber);
const sourceUuid = getSourceUuid(message, ourUuid);
const sourceUuid = getSourceUuid(message, ourACI);
if (!source && !sourceUuid) {
return conversationSelector(ourConversationId);
@ -563,7 +569,8 @@ export type GetPropsForMessageOptions = Pick<
GetPropsForBubbleOptions,
| 'conversationSelector'
| 'ourConversationId'
| 'ourUuid'
| 'ourACI'
| 'ourPNI'
| 'ourNumber'
| 'selectedMessageId'
| 'selectedMessageCounter'
@ -621,7 +628,7 @@ const getShallowPropsForMessage = createSelectorCreator(memoizeByRoot, isEqual)(
conversationSelector,
ourConversationId,
ourNumber,
ourUuid,
ourACI,
regionCode,
selectedMessageId,
selectedMessageCounter,
@ -652,7 +659,7 @@ const getShallowPropsForMessage = createSelectorCreator(memoizeByRoot, isEqual)(
conversationSelector,
ourConversationId,
ourNumber,
ourUuid,
ourACI,
});
const contactNameColor = contactNameColorSelector(conversationId, authorId);
@ -781,7 +788,8 @@ export const getPropsForMessage: (
export const getMessagePropsSelector = createSelector(
getConversationSelector,
getUserConversationId,
getUserUuid,
getUserACI,
getUserPNI,
getUserNumber,
getRegionCode,
getAccountSelector,
@ -790,7 +798,8 @@ export const getMessagePropsSelector = createSelector(
(
conversationSelector,
ourConversationId,
ourUuid,
ourACI,
ourPNI,
ourNumber,
regionCode,
accountSelector,
@ -804,7 +813,8 @@ export const getMessagePropsSelector = createSelector(
conversationSelector,
ourConversationId,
ourNumber,
ourUuid,
ourACI,
ourPNI,
regionCode,
selectedMessageCounter: selectedMessage?.counter,
selectedMessageId: selectedMessage?.id,
@ -977,7 +987,7 @@ export function isGroupV2Change(message: MessageWithUIFieldsType): boolean {
function getPropsForGroupV2Change(
message: MessageWithUIFieldsType,
{ conversationSelector, ourUuid }: GetPropsForBubbleOptions
{ conversationSelector, ourACI, ourPNI }: GetPropsForBubbleOptions
): GroupsV2Props {
const change = message.groupV2Change;
@ -992,7 +1002,8 @@ function getPropsForGroupV2Change(
groupName: conversation?.type === 'group' ? conversation?.name : undefined,
groupMemberships: conversation.memberships,
groupBannedMemberships: conversation.bannedMemberships,
ourUuid,
ourACI,
ourPNI,
change,
};
}

View File

@ -35,9 +35,14 @@ export const getUserConversationId = createSelector(
(state: UserStateType): string | undefined => state.ourConversationId
);
export const getUserUuid = createSelector(
export const getUserACI = createSelector(
getUser,
(state: UserStateType): UUIDStringType | undefined => state.ourUuid
(state: UserStateType): UUIDStringType | undefined => state.ourACI
);
export const getUserPNI = createSelector(
getUser,
(state: UserStateType): UUIDStringType | undefined => state.ourPNI
);
export const getIntl = createSelector(

View File

@ -17,7 +17,7 @@ import { CallMode } from '../../types/Calling';
import type { ConversationType } from '../ducks/conversations';
import { getConversationCallMode } from '../ducks/conversations';
import { getActiveCall, isAnybodyElseInGroupCall } from '../ducks/calling';
import { getUserUuid, getIntl, getTheme } from '../selectors/user';
import { getUserACI, getIntl, getTheme } from '../selectors/user';
import { getOwn } from '../../util/getOwn';
import { missingCaseError } from '../../util/missingCaseError';
import { isConversationSMSOnly } from '../../util/isConversationSMSOnly';
@ -47,8 +47,8 @@ const getOutgoingCallButtonStyle = (
state: StateType
): OutgoingCallButtonStyle => {
const { calling } = state;
const ourUuid = getUserUuid(state);
strictAssert(ourUuid, 'getOutgoingCallButtonStyle missing our uuid');
const ourACI = getUserACI(state);
strictAssert(ourACI, 'getOutgoingCallButtonStyle missing our uuid');
if (getActiveCall(calling)) {
return OutgoingCallButtonStyle.None;
@ -64,7 +64,7 @@ const getOutgoingCallButtonStyle = (
const call = getOwn(calling.callsByConversation, conversation.id);
if (
call?.callMode === CallMode.Group &&
isAnybodyElseInGroupCall(call.peekInfo, ourUuid)
isAnybodyElseInGroupCall(call.peekInfo, ourACI)
) {
return OutgoingCallButtonStyle.Join;
}

View File

@ -14,7 +14,6 @@ import {
getTheme,
getUserConversationId,
getUserNumber,
getUserUuid,
} from '../selectors/user';
import { getMe } from '../selectors/conversations';
import { getStoriesEnabled } from '../selectors/items';
@ -28,7 +27,6 @@ const mapStateToProps = (state: StateType) => {
regionCode: getRegionCode(state),
ourConversationId: getUserConversationId(state),
ourNumber: getUserNumber(state),
ourUuid: getUserUuid(state),
...me,
badge: getPreferredBadgeSelector(state)(me.badges),
theme: getTheme(state),

View File

@ -11,8 +11,8 @@ import { updateRemoteConfig } from '../helpers/RemoteConfigStub';
const HARD_LIMIT_KEY = 'global.groupsv2.groupSizeHardLimit';
describe('group add banned member', () => {
const uuid = UUID.generate().toString();
const ourUuid = UUID.generate().toString();
const uuid = UUID.generate();
const ourUuid = UUID.generate();
const existing = Array.from({ length: 10 }, (_, index) => ({
uuid: UUID.generate().toString(),
timestamp: index,
@ -49,7 +49,7 @@ describe('group add banned member', () => {
clientZkGroupCipher,
actions.addMembersBanned?.[0]?.added?.userId ?? new Uint8Array(0)
),
uuid
uuid.toString()
);
assert.strictEqual(actions.deleteMembersBanned, null);
});
@ -77,7 +77,7 @@ describe('group add banned member', () => {
clientZkGroupCipher,
actions.addMembersBanned?.[0]?.added?.userId ?? new Uint8Array(0)
),
uuid
uuid.toString()
);
assert.deepStrictEqual(
deleted,
@ -108,7 +108,7 @@ describe('group add banned member', () => {
uuid,
ourUuid,
group: {
bannedMembersV2: [{ uuid, timestamp: 1 }],
bannedMembersV2: [{ uuid: uuid.toString(), timestamp: 1 }],
},
});

View File

@ -6,6 +6,7 @@ import { times } from 'lodash';
import { ConversationModel } from '../models/conversations';
import type { ConversationAttributesType } from '../model-types.d';
import { UUID } from '../types/UUID';
import { DAY } from '../util/durations';
import { routineProfileRefresh } from '../routineProfileRefresh';
@ -44,6 +45,7 @@ describe('routineProfileRefresh', () => {
muteExpiresAt: 0,
profileAvatar: undefined,
profileKeyCredential: UUID.generate().toString(),
profileKeyCredentialExpiration: Date.now() + 2 * DAY,
profileSharing: true,
quotedMessageId: null,
sealedSender: 1,

View File

@ -157,7 +157,7 @@ describe('calling duck', () => {
},
};
const ourUuid = UUID.generate().toString();
const ourACI = UUID.generate().toString();
const getEmptyRootState = () => {
const rootState = rootReducer(undefined, noopAction());
@ -165,7 +165,7 @@ describe('calling duck', () => {
...rootState,
user: {
...rootState.user,
ourUuid,
ourACI,
},
};
};

View File

@ -4,6 +4,7 @@
import { assert } from 'chai';
import { reducer as rootReducer } from '../../../state/reducer';
import { noopAction } from '../../../state/ducks/noop';
import { actions as userActions } from '../../../state/ducks/user';
import {
CallMode,
CallState,
@ -25,7 +26,15 @@ import type {
import { getEmptyState } from '../../../state/ducks/calling';
describe('state/selectors/calling', () => {
const getEmptyRootState = () => rootReducer(undefined, noopAction());
const getEmptyRootState = () => {
const initial = rootReducer(undefined, noopAction());
return rootReducer(
initial,
userActions.userChanged({
ourACI: '00000000-0000-4000-8000-000000000000',
})
);
};
const getCallingState = (calling: CallingStateType) => ({
...getEmptyRootState(),

View File

@ -2,12 +2,14 @@
// SPDX-License-Identifier: AGPL-3.0-only
import { assert } from 'chai';
import * as sinon from 'sinon';
import {
_analyzeSenderKeyDevices,
_waitForAll,
_shouldFailSend,
} from '../../util/sendToGroup';
import { UUID } from '../../types/UUID';
import type { DeviceType } from '../../textsecure/Types.d';
import {
@ -23,21 +25,39 @@ import {
} from '../../textsecure/Errors';
describe('sendToGroup', () => {
const uuidOne = UUID.generate().toString();
const uuidTwo = UUID.generate().toString();
let sandbox: sinon.SinonSandbox;
beforeEach(() => {
sandbox = sinon.createSandbox();
const stub = sandbox.stub(UUID, 'lookup');
stub.withArgs(uuidOne).returns(new UUID(uuidOne));
stub.withArgs(uuidTwo).returns(new UUID(uuidTwo));
stub.returns(undefined);
});
afterEach(() => {
sandbox.restore();
});
describe('#_analyzeSenderKeyDevices', () => {
function getDefaultDeviceList(): Array<DeviceType> {
return [
{
identifier: 'ident-guid-one',
identifier: uuidOne,
id: 1,
registrationId: 11,
},
{
identifier: 'ident-guid-one',
identifier: uuidOne,
id: 2,
registrationId: 22,
},
{
identifier: 'ident-guid-two',
identifier: uuidTwo,
id: 2,
registrationId: 33,
},
@ -76,17 +96,17 @@ describe('sendToGroup', () => {
assert.deepEqual(newToMemberDevices, [
{
identifier: 'ident-guid-one',
identifier: uuidOne,
id: 2,
registrationId: 22,
},
{
identifier: 'ident-guid-two',
identifier: uuidTwo,
id: 2,
registrationId: 33,
},
]);
assert.deepEqual(newToMemberUuids, ['ident-guid-one', 'ident-guid-two']);
assert.deepEqual(newToMemberUuids, [uuidOne, uuidTwo]);
assert.isEmpty(removedFromMemberDevices);
assert.isEmpty(removedFromMemberUuids);
});
@ -108,20 +128,17 @@ describe('sendToGroup', () => {
assert.isEmpty(newToMemberUuids);
assert.deepEqual(removedFromMemberDevices, [
{
identifier: 'ident-guid-one',
identifier: uuidOne,
id: 2,
registrationId: 22,
},
{
identifier: 'ident-guid-two',
identifier: uuidTwo,
id: 2,
registrationId: 33,
},
]);
assert.deepEqual(removedFromMemberUuids, [
'ident-guid-one',
'ident-guid-two',
]);
assert.deepEqual(removedFromMemberUuids, [uuidOne, uuidTwo]);
});
it('returns empty removals if partial send', () => {
const memberDevices = getDefaultDeviceList();

View File

@ -2,16 +2,11 @@
// SPDX-License-Identifier: AGPL-3.0-only
/* eslint-disable no-await-in-loop, no-console */
import assert from 'assert';
import type { PrimaryDevice } from '@signalapp/mock-server';
import {
Bootstrap,
debug,
saveLogs,
stats,
RUN_COUNT,
DISCARD_COUNT,
} from './fixtures';
import type { App } from './fixtures';
import { Bootstrap, debug, stats, RUN_COUNT, DISCARD_COUNT } from './fixtures';
const CONVERSATION_SIZE = 1000; // messages
const DELAY = 50; // milliseconds
@ -22,9 +17,10 @@ const DELAY = 50; // milliseconds
});
await bootstrap.init();
const app = await bootstrap.link();
let app: App | undefined;
try {
app = await bootstrap.link();
const { server, contacts, phone, desktop } = bootstrap;
const [first, second] = contacts;
@ -65,6 +61,7 @@ const DELAY = 50; // milliseconds
};
const measure = async (): Promise<void> => {
assert(app);
const window = await app.getWindow();
const leftPane = window.locator('.left-pane-wrapper');
@ -102,10 +99,10 @@ const DELAY = 50; // milliseconds
await Promise.all([sendQueue(), measure()]);
} catch (error) {
await saveLogs(bootstrap);
await bootstrap.saveLogs();
throw error;
} finally {
await app.close();
await app?.close();
await bootstrap.teardown();
}
})();

View File

@ -3,8 +3,6 @@
/* eslint-disable no-console */
import createDebug from 'debug';
import fs from 'fs/promises';
import path from 'path';
import { Bootstrap } from '../bootstrap';
@ -63,19 +61,6 @@ export function stats(
return result;
}
export async function saveLogs(bootstrap: Bootstrap): Promise<void> {
const { ARTIFACTS_DIR } = process.env;
if (!ARTIFACTS_DIR) {
console.error('Not saving logs. Please set ARTIFACTS_DIR env variable');
return;
}
await fs.mkdir(ARTIFACTS_DIR, { recursive: true });
const { logsDir } = bootstrap;
await fs.rename(logsDir, path.join(ARTIFACTS_DIR, 'logs'));
}
// Can happen if electron exits prematurely
process.on('unhandledRejection', reason => {
console.error('Unhandled rejection:');

View File

@ -9,10 +9,11 @@ import {
EnvelopeType,
ReceiptType,
} from '@signalapp/mock-server';
import type { App } from './fixtures';
import {
Bootstrap,
debug,
saveLogs,
stats,
RUN_COUNT,
GROUP_SIZE,
@ -44,9 +45,11 @@ const LAST_MESSAGE = 'start sending messages now';
.pinGroup(group)
);
const app = await bootstrap.link();
let app: App | undefined;
try {
app = await bootstrap.link();
const { server, desktop } = bootstrap;
const [first] = members;
@ -179,10 +182,10 @@ const LAST_MESSAGE = 'start sending messages now';
console.log('stats info=%j', { delta: stats(deltaList, [99, 99.8]) });
} catch (error) {
await saveLogs(bootstrap);
await bootstrap.saveLogs();
throw error;
} finally {
await app.close();
await app?.close();
await bootstrap.teardown();
}
})();

View File

@ -6,14 +6,8 @@ import assert from 'assert';
import { ReceiptType } from '@signalapp/mock-server';
import {
Bootstrap,
debug,
saveLogs,
stats,
RUN_COUNT,
DISCARD_COUNT,
} from './fixtures';
import type { App } from './fixtures';
import { Bootstrap, debug, stats, RUN_COUNT, DISCARD_COUNT } from './fixtures';
const CONVERSATION_SIZE = 500; // messages
@ -25,9 +19,11 @@ const LAST_MESSAGE = 'start sending messages now';
});
await bootstrap.init();
const app = await bootstrap.link();
let app: App | undefined;
try {
app = await bootstrap.link();
const { server, contacts, phone, desktop } = bootstrap;
const [first] = contacts;
@ -136,10 +132,10 @@ const LAST_MESSAGE = 'start sending messages now';
console.log('stats info=%j', { delta: stats(deltaList, [99, 99.8]) });
} catch (error) {
await saveLogs(bootstrap);
await bootstrap.saveLogs();
throw error;
} finally {
await app.close();
await app?.close();
await bootstrap.teardown();
}
})();

View File

@ -4,7 +4,7 @@
import { ReceiptType } from '@signalapp/mock-server';
import { debug, Bootstrap, saveLogs, stats, RUN_COUNT } from './fixtures';
import { debug, Bootstrap, stats, RUN_COUNT } from './fixtures';
const MESSAGE_BATCH_SIZE = 1000; // messages
@ -128,7 +128,7 @@ const ENABLE_RECEIPTS = Boolean(process.env.ENABLE_RECEIPTS);
console.log('stats info=%j', { messagesPerSec: stats(messagesPerSec) });
}
} catch (error) {
await saveLogs(bootstrap);
await bootstrap.saveLogs();
throw error;
} finally {
await bootstrap.teardown();

View File

@ -4,7 +4,8 @@
import { StorageState } from '@signalapp/mock-server';
import { Bootstrap, saveLogs } from './fixtures';
import type { App } from './fixtures';
import { Bootstrap } from './fixtures';
const CONTACT_COUNT = 1000;
@ -43,8 +44,9 @@ const CONTACT_COUNT = 1000;
await phone.setStorageState(state);
const start = Date.now();
const app = await bootstrap.link();
let app: App | undefined;
try {
app = await bootstrap.link();
const window = await app.getWindow();
const leftPane = window.locator('.left-pane-wrapper');
@ -58,10 +60,10 @@ const CONTACT_COUNT = 1000;
const duration = Date.now() - start;
console.log(`Took: ${(duration / 1000).toFixed(2)} seconds`);
} catch (error) {
await saveLogs(bootstrap);
await bootstrap.saveLogs();
throw error;
} finally {
await app.close();
await app?.close();
await bootstrap.teardown();
}
})();

View File

@ -8,7 +8,7 @@ import os from 'os';
import createDebug from 'debug';
import type { Device, PrimaryDevice } from '@signalapp/mock-server';
import { Server, loadCertificates } from '@signalapp/mock-server';
import { Server, UUIDKind, loadCertificates } from '@signalapp/mock-server';
import { MAX_READ_KEYS as MAX_STORAGE_READ_KEYS } from '../services/storageConstants';
import * as durations from '../util/durations';
import { App } from './playwright';
@ -156,7 +156,7 @@ export class Bootstrap {
);
this.privPhone = await this.server.createPrimaryDevice({
profileName: 'Mock',
profileName: 'Myself',
contacts: this.contacts,
});
@ -206,10 +206,12 @@ export class Bootstrap {
await this.phone.addSingleUseKey(this.desktop, desktopKey);
for (const contact of this.contacts) {
// eslint-disable-next-line no-await-in-loop
const contactKey = await this.desktop.popSingleUseKey();
// eslint-disable-next-line no-await-in-loop
await contact.addSingleUseKey(this.desktop, contactKey);
for (const uuidKind of [UUIDKind.ACI, UUIDKind.PNI]) {
// eslint-disable-next-line no-await-in-loop
const contactKey = await this.desktop.popSingleUseKey(uuidKind);
// eslint-disable-next-line no-await-in-loop
await contact.addSingleUseKey(this.desktop, contactKey, uuidKind);
}
}
await this.phone.waitForSync(this.desktop);
@ -254,6 +256,25 @@ export class Bootstrap {
return result;
}
public async saveLogs(): Promise<void> {
const { ARTIFACTS_DIR } = process.env;
if (!ARTIFACTS_DIR) {
// eslint-disable-next-line no-console
console.error('Not saving logs. Please set ARTIFACTS_DIR env variable');
return;
}
await fs.mkdir(ARTIFACTS_DIR, { recursive: true });
const outDir = await fs.mkdtemp(path.join(ARTIFACTS_DIR, 'logs-'));
// eslint-disable-next-line no-console
console.error(`Saving logs to ${outDir}`);
const { logsDir } = this;
await fs.rename(logsDir, path.join(outDir));
}
//
// Getters
//
@ -299,13 +320,19 @@ export class Bootstrap {
storageProfile: 'mock',
serverUrl: url,
storageUrl: url,
directoryUrl: url,
cdn: {
'0': url,
'2': url,
},
updatesEnabled: false,
directoryVersion: 3,
directoryV3Url: url,
directoryV3MRENCLAVE:
'51133fecb3fa18aaf0c8f64cb763656d3272d9faaacdb26ae7df082e414fb142',
directoryV3Root:
'-----BEGIN CERTIFICATE-----\nMIICjzCCAjSgAwIBAgIUImUM1lqdNInzg7SVUr9QGzknBqwwCgYIKoZIzj0EAwIw\naDEaMBgGA1UEAwwRSW50ZWwgU0dYIFJvb3QgQ0ExGjAYBgNVBAoMEUludGVsIENv\ncnBvcmF0aW9uMRQwEgYDVQQHDAtTYW50YSBDbGFyYTELMAkGA1UECAwCQ0ExCzAJ\nBgNVBAYTAlVTMB4XDTE4MDUyMTEwNDUxMFoXDTQ5MTIzMTIzNTk1OVowaDEaMBgG\nA1UEAwwRSW50ZWwgU0dYIFJvb3QgQ0ExGjAYBgNVBAoMEUludGVsIENvcnBvcmF0\naW9uMRQwEgYDVQQHDAtTYW50YSBDbGFyYTELMAkGA1UECAwCQ0ExCzAJBgNVBAYT\nAlVTMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEC6nEwMDIYZOj/iPWsCzaEKi7\n1OiOSLRFhWGjbnBVJfVnkY4u3IjkDYYL0MxO4mqsyYjlBalTVYxFP2sJBK5zlKOB\nuzCBuDAfBgNVHSMEGDAWgBQiZQzWWp00ifODtJVSv1AbOScGrDBSBgNVHR8ESzBJ\nMEegRaBDhkFodHRwczovL2NlcnRpZmljYXRlcy50cnVzdGVkc2VydmljZXMuaW50\nZWwuY29tL0ludGVsU0dYUm9vdENBLmRlcjAdBgNVHQ4EFgQUImUM1lqdNInzg7SV\nUr9QGzknBqwwDgYDVR0PAQH/BAQDAgEGMBIGA1UdEwEB/wQIMAYBAf8CAQEwCgYI\nKoZIzj0EAwIDSQAwRgIhAOW/5QkR+S9CiSDcNoowLuPRLsWGf/Yi7GSX94BgwTwg\nAiEA4J0lrHoMs+Xo5o/sX6O9QWxHRAvZUGOdRQ7cvqRXaqI=\n-----END CERTIFICATE-----\n',
...this.options.extraConfig,
});
}

View File

@ -0,0 +1,240 @@
// Copyright 2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { assert } from 'chai';
import type { Group } from '@signalapp/mock-server';
import { UUIDKind } from '@signalapp/mock-server';
import createDebug from 'debug';
import * as durations from '../../util/durations';
import { Bootstrap } from '../bootstrap';
import type { App } from '../bootstrap';
export const debug = createDebug('mock:test:gv2');
describe('gv2', function needsName() {
this.timeout(durations.MINUTE);
let bootstrap: Bootstrap;
let app: App;
let group: Group;
beforeEach(async () => {
bootstrap = new Bootstrap();
await bootstrap.init();
const { contacts } = bootstrap;
const [first, second] = contacts;
group = await first.createGroup({
title: 'Invite by PNI',
members: [first, second],
});
app = await bootstrap.link();
const { desktop } = bootstrap;
group = await first.inviteToGroup(group, desktop, {
uuidKind: UUIDKind.PNI,
});
// Verify that created group has pending member
assert.strictEqual(group.state?.members?.length, 2);
assert(!group.getMemberByUUID(desktop.uuid));
assert(!group.getMemberByUUID(desktop.pni));
assert(!group.getPendingMemberByUUID(desktop.uuid));
assert(group.getPendingMemberByUUID(desktop.pni));
const window = await app.getWindow();
const leftPane = window.locator('.left-pane-wrapper');
debug('Opening group');
await leftPane
.locator(
'_react=ConversationListItem' +
`[title = ${JSON.stringify(group.title)}]`
)
.click();
});
afterEach(async function after() {
if (this.currentTest?.state !== 'passed') {
await bootstrap.saveLogs();
}
await app.close();
await bootstrap.teardown();
});
it('should accept PNI invite and modify the group state', async () => {
const { phone, contacts, desktop } = bootstrap;
const [first, second] = contacts;
const window = await app.getWindow();
const conversationStack = window.locator('.conversation-stack');
debug('Accepting');
await conversationStack
.locator('.module-message-request-actions button >> "Accept"')
.click();
group = await phone.waitForGroupUpdate(group);
assert.strictEqual(group.revision, 2);
assert.strictEqual(group.state?.members?.length, 3);
assert(group.getMemberByUUID(desktop.uuid));
assert(!group.getMemberByUUID(desktop.pni));
assert(!group.getPendingMemberByUUID(desktop.uuid));
assert(!group.getPendingMemberByUUID(desktop.pni));
debug('Checking that notifications are present');
await window
.locator(`"${first.profileName} invited you to the group."`)
.waitFor();
await window
.locator(
`"You accepted an invitation to the group from ${first.profileName}."`
)
.waitFor();
debug('Invite PNI again');
group = await second.inviteToGroup(group, desktop, {
uuidKind: UUIDKind.PNI,
});
assert(group.getMemberByUUID(desktop.uuid));
assert(group.getPendingMemberByUUID(desktop.pni));
await window
.locator(`"${second.profileName} invited you to the group."`)
.waitFor();
debug('Verify that message request state is not visible');
await conversationStack
.locator('.module-message-request-actions button >> "Accept"')
.waitFor({ state: 'hidden' });
debug('Leave the group through settings');
await conversationStack
.locator('button.module-ConversationHeader__button--more')
.click();
await conversationStack
.locator('.react-contextmenu-item >> "Group settings"')
.click();
await conversationStack
.locator('.conversation-details-panel >> "Leave group"')
.click();
await window.locator('.module-Modal button >> "Leave"').click();
debug('Waiting for final group update');
group = await phone.waitForGroupUpdate(group);
assert.strictEqual(group.revision, 4);
assert.strictEqual(group.state?.members?.length, 2);
assert(!group.getMemberByUUID(desktop.uuid));
assert(!group.getMemberByUUID(desktop.pni));
assert(!group.getPendingMemberByUUID(desktop.uuid));
assert(group.getPendingMemberByUUID(desktop.pni));
});
it('should decline PNI invite and modify the group state', async () => {
const { phone, desktop } = bootstrap;
const window = await app.getWindow();
const conversationStack = window.locator('.conversation-stack');
debug('Declining');
await conversationStack
.locator('.module-message-request-actions button >> "Delete"')
.click();
debug('waiting for confirmation modal');
await window.locator('.module-Modal button >> "Delete and Leave"').click();
group = await phone.waitForGroupUpdate(group);
assert.strictEqual(group.revision, 2);
assert.strictEqual(group.state?.members?.length, 2);
assert(!group.getMemberByUUID(desktop.uuid));
assert(!group.getMemberByUUID(desktop.pni));
assert(!group.getPendingMemberByUUID(desktop.uuid));
assert(!group.getPendingMemberByUUID(desktop.pni));
});
it('should accept ACI invite with extra PNI on the invite list', async () => {
const { phone, contacts, desktop } = bootstrap;
const [first, second] = contacts;
const window = await app.getWindow();
debug('Sending another invite');
// Invite ACI from another contact
group = await second.inviteToGroup(group, desktop, {
uuidKind: UUIDKind.ACI,
});
const conversationStack = window.locator('.conversation-stack');
debug('Accepting');
await conversationStack
.locator('.module-message-request-actions button >> "Accept"')
.click();
debug('Verifying notifications');
await window
.locator(`"${first.profileName} invited you to the group."`)
.waitFor();
await window.locator('"You were invited to the group."').waitFor();
await window
.locator(
`"You accepted an invitation to the group from ${second.profileName}."`
)
.waitFor();
group = await phone.waitForGroupUpdate(group);
assert.strictEqual(group.revision, 3);
assert.strictEqual(group.state?.members?.length, 3);
assert(group.getMemberByUUID(desktop.uuid));
assert(!group.getMemberByUUID(desktop.pni));
assert(!group.getPendingMemberByUUID(desktop.uuid));
assert(group.getPendingMemberByUUID(desktop.pni));
});
it('should decline ACI invite with extra PNI on the invite list', async () => {
const { phone, contacts, desktop } = bootstrap;
const [, second] = contacts;
const window = await app.getWindow();
debug('Sending another invite');
// Invite ACI from another contact
group = await second.inviteToGroup(group, desktop, {
uuidKind: UUIDKind.ACI,
});
const conversationStack = window.locator('.conversation-stack');
debug('Declining');
await conversationStack
.locator('.module-message-request-actions button >> "Delete"')
.click();
debug('waiting for confirmation modal');
await window.locator('.module-Modal button >> "Delete and Leave"').click();
group = await phone.waitForGroupUpdate(group);
assert.strictEqual(group.revision, 3);
assert.strictEqual(group.state?.members?.length, 2);
assert(!group.getMemberByUUID(desktop.uuid));
assert(!group.getMemberByUUID(desktop.pni));
assert(!group.getPendingMemberByUUID(desktop.uuid));
assert(group.getPendingMemberByUUID(desktop.pni));
});
});

View File

@ -63,7 +63,11 @@ describe('gv2', function needsName() {
app = await bootstrap.link();
});
afterEach(async () => {
afterEach(async function after() {
if (this.currentTest?.state !== 'passed') {
await bootstrap.saveLogs();
}
await app.close();
await bootstrap.teardown();
});

View File

@ -17,7 +17,15 @@ describe('storage service', function needsName() {
({ bootstrap, app } = await initStorage());
});
afterEach(async () => {
afterEach(async function after() {
if (!bootstrap) {
return;
}
if (this.currentTest?.state !== 'passed') {
await bootstrap.saveLogs();
}
await app.close();
await bootstrap.teardown();
});

View File

@ -20,7 +20,15 @@ describe('storage service', function needsName() {
({ bootstrap, app } = await initStorage());
});
afterEach(async () => {
afterEach(async function after() {
if (!bootstrap) {
return;
}
if (this.currentTest?.state !== 'passed') {
await bootstrap.saveLogs();
}
await app.close();
await bootstrap.teardown();
});

View File

@ -39,63 +39,68 @@ export async function initStorage(
await bootstrap.init();
// Populate storage service
const { contacts, phone } = bootstrap;
try {
// Populate storage service
const { contacts, phone } = bootstrap;
const [firstContact] = contacts;
const [firstContact] = contacts;
const members = [...contacts].slice(0, GROUP_SIZE);
const members = [...contacts].slice(0, GROUP_SIZE);
const group = await phone.createGroup({
title: 'Mock Group',
members: [phone, ...members],
});
let state = StorageState.getEmpty();
state = state.updateAccount({
profileKey: phone.profileKey.serialize(),
e164: phone.device.number,
});
state = state
.addGroup(group, {
whitelisted: true,
})
.pinGroup(group);
for (const contact of contacts) {
state = state.addContact(contact, {
identityState: Proto.ContactRecord.IdentityState.VERIFIED,
whitelisted: true,
identityKey: contact.publicKey.serialize(),
profileKey: contact.profileKey.serialize(),
const group = await phone.createGroup({
title: 'Mock Group',
members: [phone, ...members],
});
let state = StorageState.getEmpty();
state = state.updateAccount({
profileKey: phone.profileKey.serialize(),
e164: phone.device.number,
});
state = state
.addGroup(group, {
whitelisted: true,
})
.pinGroup(group);
for (const contact of contacts) {
state = state.addContact(contact, {
identityState: Proto.ContactRecord.IdentityState.VERIFIED,
whitelisted: true,
identityKey: contact.publicKey.serialize(),
profileKey: contact.profileKey.serialize(),
});
}
state = state.pin(firstContact);
await phone.setStorageState(state);
// Link new device
const app = await bootstrap.link();
const { desktop } = bootstrap;
// Send a message to the group and the first contact
const contactSend = contacts[0].sendText(desktop, 'hello from contact', {
timestamp: bootstrap.getTimestamp(),
sealed: true,
});
const groupSend = members[0].sendText(desktop, 'hello in group', {
timestamp: bootstrap.getTimestamp(),
sealed: true,
group,
});
await Promise.all([contactSend, groupSend]);
return { bootstrap, app, group, members };
} catch (error) {
await bootstrap.saveLogs();
throw error;
}
state = state.pin(firstContact);
await phone.setStorageState(state);
// Link new device
const app = await bootstrap.link();
const { desktop } = bootstrap;
// Send a message to the group and the first contact
const contactSend = contacts[0].sendText(desktop, 'hello from contact', {
timestamp: bootstrap.getTimestamp(),
sealed: true,
});
const groupSend = members[0].sendText(desktop, 'hello in group', {
timestamp: bootstrap.getTimestamp(),
sealed: true,
group,
});
await Promise.all([contactSend, groupSend]);
return { bootstrap, app, group, members };
}

View File

@ -22,7 +22,15 @@ describe('storage service', function needsName() {
({ bootstrap, app } = await initStorage());
});
afterEach(async () => {
afterEach(async function after() {
if (!bootstrap) {
return;
}
if (this.currentTest?.state !== 'passed') {
await bootstrap.saveLogs();
}
await app.close();
await bootstrap.teardown();
});

View File

@ -17,7 +17,15 @@ describe('storage service', function needsName() {
({ bootstrap, app } = await initStorage());
});
afterEach(async () => {
afterEach(async function after() {
if (!bootstrap) {
return;
}
if (this.currentTest?.state !== 'passed') {
await bootstrap.saveLogs();
}
await app.close();
await bootstrap.teardown();
});

View File

@ -21,7 +21,15 @@ describe('storage service', function needsName() {
({ bootstrap, app, group } = await initStorage());
});
afterEach(async () => {
afterEach(async function after() {
if (!bootstrap) {
return;
}
if (this.currentTest?.state !== 'passed') {
await bootstrap.saveLogs();
}
await app.close();
await bootstrap.teardown();
});

View File

@ -5,7 +5,7 @@ import PQueue from 'p-queue';
import { omit } from 'lodash';
import EventTarget from './EventTarget';
import type { WebAPIType, GroupCredentialType } from './WebAPI';
import type { WebAPIType } from './WebAPI';
import { HTTPError } from './Errors';
import type { KeyPairType } from './Types.d';
import ProvisioningCipher from './ProvisioningCipher';
@ -755,14 +755,6 @@ export default class AccountManager extends EventTarget {
});
}
async getGroupCredentials(
startDay: number,
endDay: number,
uuidKind: UUIDKind
): Promise<Array<GroupCredentialType>> {
return this.server.getGroupCredentials(startDay, endDay, uuidKind);
}
// Takes the same object returned by generateKeys
async confirmKeys(
keys: GeneratedKeysType,

View File

@ -759,7 +759,9 @@ export default class MessageReceiver
// Proto.Envelope fields
type: decoded.type,
source: decoded.source || item.source,
sourceUuid: decoded.sourceUuid || item.sourceUuid,
sourceUuid: decoded.sourceUuid
? UUID.cast(decoded.sourceUuid)
: item.sourceUuid,
sourceDevice: decoded.sourceDevice || item.sourceDevice,
destinationUuid: new UUID(
decoded.destinationUuid || item.destinationUuid || ourUuid.toString()

View File

@ -3,7 +3,7 @@
import type { SignalService as Proto } from '../protobuf';
import type { IncomingWebSocketRequest } from './WebsocketResources';
import type { UUID } from '../types/UUID';
import type { UUID, UUIDStringType } from '../types/UUID';
import type { TextAttachmentType } from '../types/Attachment';
import { GiftBadgeStates } from '../components/conversation/Message';
import { MIMEType } from '../types/MIME';
@ -84,7 +84,7 @@ export type ProcessedEnvelope = Readonly<{
// Mostly from Proto.Envelope except for null/undefined
type: Proto.Envelope.Type;
source?: string;
sourceUuid?: string;
sourceUuid?: UUIDStringType;
sourceDevice?: number;
destinationUuid: UUID;
timestamp: number;

View File

@ -490,7 +490,7 @@ const URL_CALLS = {
directoryAuthV2: 'v2/directory/auth',
discovery: 'v1/discovery',
getGroupAvatarUpload: 'v1/groups/avatar/form',
getGroupCredentials: 'v1/certificate/group',
getGroupCredentials: 'v1/certificate/auth/group',
getIceServers: 'v1/accounts/turn',
getStickerPackUpload: 'v1/sticker/pack/form',
groupLog: 'v1/groups/logs',
@ -718,6 +718,9 @@ export type ProfileType = Readonly<{
unrestrictedUnidentifiedAccess?: string;
uuid?: string;
credential?: string;
// Only present when `credentialType` is `pni`
pniCredential?: string;
capabilities?: CapabilitiesType;
paymentAddress?: string;
badges?: unknown;
@ -769,7 +772,7 @@ export type GetUuidsForE164sV2OptionsType = Readonly<{
type GetProfileCommonOptionsType = Readonly<
{
userLanguages: ReadonlyArray<string>;
credentialType?: 'pni' | 'profileKey';
credentialType?: 'pni' | 'expiringProfileKey';
} & (
| {
profileKeyVersion?: undefined;
@ -792,6 +795,11 @@ export type GetProfileUnauthOptionsType = GetProfileCommonOptionsType &
accessKey: string;
}>;
export type GetGroupCredentialsOptionsType = Readonly<{
startDayInMs: number;
endDayInMs: number;
}>;
export type WebAPIType = {
startRegistration(): unknown;
finishRegistration(baton: unknown): void;
@ -819,9 +827,7 @@ export type WebAPIType = {
) => Promise<Proto.GroupJoinInfo>;
getGroupAvatar: (key: string) => Promise<Uint8Array>;
getGroupCredentials: (
startDay: number,
endDay: number,
uuidKind: UUIDKind
options: GetGroupCredentialsOptionsType
) => Promise<Array<GroupCredentialType>>;
getGroupExternalCredential: (
options: GroupCredentialsType
@ -1580,7 +1586,7 @@ export function initialize({
{
profileKeyVersion,
profileKeyCredentialRequest,
credentialType = 'profileKey',
credentialType = 'expiringProfileKey',
}: GetProfileCommonOptionsType
) {
let profileUrl = `/${identifier}`;
@ -2509,14 +2515,17 @@ export function initialize({
credentials: Array<GroupCredentialType>;
};
async function getGroupCredentials(
startDay: number,
endDay: number,
uuidKind: UUIDKind
): Promise<Array<GroupCredentialType>> {
async function getGroupCredentials({
startDayInMs,
endDayInMs,
}: GetGroupCredentialsOptionsType): Promise<Array<GroupCredentialType>> {
const startDayInSeconds = startDayInMs / durations.SECOND;
const endDayInSeconds = endDayInMs / durations.SECOND;
const response = (await _ajax({
call: 'getGroupCredentials',
urlParameters: `/${startDay}/${endDay}?${uuidKindToQuery(uuidKind)}`,
urlParameters:
`?redemptionStartSeconds=${startDayInSeconds}&` +
`redemptionEndSeconds=${endDayInSeconds}`,
httpType: 'GET',
responseType: 'json',
})) as CredentialResponseType;

View File

@ -5,6 +5,7 @@
import type { PublicKey } from '@signalapp/libsignal-client';
import type { SignalService as Proto } from '../protobuf';
import type { UUIDStringType } from '../types/UUID';
import type {
ProcessedEnvelope,
ProcessedDataMessage,
@ -129,7 +130,7 @@ export type DeliveryEventData = Readonly<{
timestamp: number;
envelopeTimestamp: number;
source?: string;
sourceUuid?: string;
sourceUuid?: UUIDStringType;
sourceDevice?: number;
}>;
@ -166,7 +167,7 @@ export class DecryptionErrorEvent extends ConfirmableEvent {
export type RetryRequestEventData = Readonly<{
groupId?: string;
ratchetKey?: PublicKey;
requesterUuid: string;
requesterUuid: UUIDStringType;
requesterDevice: number;
senderDevice: number;
sentAt: number;
@ -204,7 +205,7 @@ export class SentEvent extends ConfirmableEvent {
export type ProfileKeyUpdateData = Readonly<{
source?: string;
sourceUuid?: string;
sourceUuid?: UUIDStringType;
profileKey: string;
}>;
@ -219,7 +220,7 @@ export class ProfileKeyUpdateEvent extends ConfirmableEvent {
export type MessageEventData = Readonly<{
source?: string;
sourceUuid?: string;
sourceUuid?: UUIDStringType;
sourceDevice?: number;
timestamp: number;
serverGuid?: string;
@ -243,7 +244,7 @@ export type ReadOrViewEventData = Readonly<{
timestamp: number;
envelopeTimestamp: number;
source?: string;
sourceUuid?: string;
sourceUuid?: UUIDStringType;
sourceDevice?: number;
}>;
@ -276,14 +277,14 @@ export class ConfigurationEvent extends ConfirmableEvent {
export type ViewOnceOpenSyncOptions = {
source?: string;
sourceUuid?: string;
sourceUuid?: UUIDStringType;
timestamp?: number;
};
export class ViewOnceOpenSyncEvent extends ConfirmableEvent {
public readonly source?: string;
public readonly sourceUuid?: string;
public readonly sourceUuid?: UUIDStringType;
public readonly timestamp?: number;

View File

@ -1,7 +1,10 @@
// Copyright 2020-2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import type { ProfileKeyCredentialRequestContext } from '@signalapp/libsignal-client/zkgroup';
import type {
ProfileKeyCredentialRequestContext,
ClientZkProfileOperations,
} from '@signalapp/libsignal-client/zkgroup';
import { SEALED_SENDER } from '../types/SealedSender';
import * as Errors from '../types/errors';
import type {
@ -11,12 +14,15 @@ import type {
import { HTTPError } from '../textsecure/Errors';
import { Address } from '../types/Address';
import { QualifiedAddress } from '../types/QualifiedAddress';
import { UUIDKind } from '../types/UUID';
import * as Bytes from '../Bytes';
import { trimForDisplay, verifyAccessKey, decryptProfile } from '../Crypto';
import {
generateProfileKeyCredentialRequest,
generatePNICredentialRequest,
getClientZkProfileOperations,
handleProfileKeyCredential,
handleProfileKeyPNICredential,
} from './zkgroup';
import { isMe } from './whatTypeOfConversation';
import type { ConversationModel } from '../models/conversations';
@ -25,6 +31,68 @@ import { getUserLanguages } from './userLanguages';
import { parseBadgesFromServer } from '../badges/parseBadgesFromServer';
import { strictAssert } from './assert';
async function maybeGetPNICredential(
c: ConversationModel,
{
clientZkProfileCipher,
profileKey,
profileKeyVersion,
userLanguages,
}: {
clientZkProfileCipher: ClientZkProfileOperations;
profileKey: string;
profileKeyVersion: string;
userLanguages: ReadonlyArray<string>;
}
): Promise<void> {
// Already present and up-to-date
if (c.get('pniCredential')) {
return;
}
strictAssert(isMe(c.attributes), 'Has to fetch PNI credential for ourselves');
log.info('maybeGetPNICredential: requesting PNI credential');
const { storage, messaging } = window.textsecure;
strictAssert(
messaging,
'maybeGetPNICredential: window.textsecure.messaging not available'
);
const ourACI = storage.user.getCheckedUuid(UUIDKind.ACI);
const ourPNI = storage.user.getCheckedUuid(UUIDKind.PNI);
const {
requestHex: profileKeyCredentialRequestHex,
context: profileCredentialRequestContext,
} = generatePNICredentialRequest(
clientZkProfileCipher,
ourACI.toString(),
ourPNI.toString(),
profileKey
);
const profile = await messaging.getProfile(ourACI, {
userLanguages,
profileKeyVersion,
profileKeyCredentialRequest: profileKeyCredentialRequestHex,
credentialType: 'pni',
});
strictAssert(
profile.pniCredential,
'We must get the credential for ourselves'
);
const pniCredential = handleProfileKeyPNICredential(
clientZkProfileCipher,
profileCredentialRequestContext,
profile.pniCredential
);
c.set({ pniCredential });
log.info('maybeGetPNICredential: updated PNI credential');
}
async function doGetProfile(c: ConversationModel): Promise<void> {
const idForLogging = c.idForLogging();
const { messaging } = window.textsecure;
@ -168,6 +236,22 @@ async function doGetProfile(c: ConversationModel): Promise<void> {
}
}
if (isMe(c.attributes) && profileKey && profileKeyVersion) {
try {
await maybeGetPNICredential(c, {
clientZkProfileCipher,
profileKey,
profileKeyVersion,
userLanguages,
});
} catch (error) {
log.warn(
'getProfile failed to get our own PNI credential',
Errors.toLogFormat(error)
);
}
}
if (profile.identityKey) {
const identityKey = Bytes.fromBase64(profile.identityKey);
const changed = await window.textsecure.storage.protocol.saveIdentity(
@ -285,12 +369,15 @@ async function doGetProfile(c: ConversationModel): Promise<void> {
if (profileCredentialRequestContext) {
if (profile.credential) {
const profileKeyCredential = handleProfileKeyCredential(
const {
credential: profileKeyCredential,
expiration: profileKeyCredentialExpiration,
} = handleProfileKeyCredential(
clientZkProfileCipher,
profileCredentialRequestContext,
profile.credential
);
c.set({ profileKeyCredential });
c.set({ profileKeyCredential, profileKeyCredentialExpiration });
} else {
c.unset('profileKeyCredential');
}

View File

@ -18,6 +18,7 @@ import { parseIntOrThrow } from './parseIntOrThrow';
import * as RemoteConfig from '../RemoteConfig';
import { Address } from '../types/Address';
import { QualifiedAddress } from '../types/QualifiedAddress';
import { UUID } from '../types/UUID';
import { ToastDecryptionError } from '../components/ToastDecryptionError';
import { showToast } from './showToast';
import * as Errors from '../types/errors';
@ -287,7 +288,7 @@ async function sendDistributionMessageOrNullMessage(
const group = window.ConversationController.get(groupId);
const distributionId = group?.get('senderKeyInfo')?.distributionId;
if (group && !group.hasMember(requesterUuid)) {
if (group && !group.hasMember(new UUID(requesterUuid))) {
throw new Error(
`sendDistributionMessageOrNullMessage/${logId}: Requester ${requesterUuid} is not a member of ${conversation.idForLogging()}`
);
@ -429,7 +430,7 @@ async function maybeAddSenderKeyDistributionMessage({
};
}
if (!conversation.hasMember(requesterUuid)) {
if (!conversation.hasMember(new UUID(requesterUuid))) {
throw new Error(
`maybeAddSenderKeyDistributionMessage/${logId}: Recipient ${requesterUuid} is not a member of ${conversation.idForLogging()}`
);

View File

@ -23,6 +23,7 @@ import { Address } from '../types/Address';
import { QualifiedAddress } from '../types/QualifiedAddress';
import { UUID } from '../types/UUID';
import { getValue, isEnabled } from '../RemoteConfig';
import type { UUIDStringType } from '../types/UUID';
import { isRecord } from './isRecord';
import { isOlderThan } from './timestamp';
@ -80,7 +81,7 @@ const ZERO_ACCESS_KEY = Bytes.toBase64(new Uint8Array(ACCESS_KEY_LENGTH));
export type SenderKeyTargetType = {
getGroupId: () => string | undefined;
getMembers: () => Array<ConversationModel>;
hasMember: (id: string) => boolean;
hasMember: (uuid: UUIDStringType) => boolean;
idForLogging: () => string;
isGroupV2: () => boolean;
isValid: () => boolean;
@ -1145,10 +1146,12 @@ function partialDeviceComparator(
);
}
function getUuidsFromDevices(devices: Array<DeviceType>): Array<string> {
const uuids = new Set<string>();
function getUuidsFromDevices(
devices: Array<DeviceType>
): Array<UUIDStringType> {
const uuids = new Set<UUIDStringType>();
devices.forEach(device => {
uuids.add(device.identifier);
uuids.add(UUID.checkedLookup(device.identifier).toString());
});
return Array.from(uuids);
@ -1160,9 +1163,9 @@ export function _analyzeSenderKeyDevices(
isPartialSend?: boolean
): {
newToMemberDevices: Array<DeviceType>;
newToMemberUuids: Array<string>;
newToMemberUuids: Array<UUIDStringType>;
removedFromMemberDevices: Array<DeviceType>;
removedFromMemberUuids: Array<string>;
removedFromMemberUuids: Array<UUIDStringType>;
} {
const newToMemberDevices = differenceWith<DeviceType, DeviceType>(
devicesForSend,

View File

@ -1,9 +1,12 @@
// Copyright 2020-2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import type { ProfileKeyCredentialRequestContext } from '@signalapp/libsignal-client/zkgroup';
import type {
ProfileKeyCredentialRequestContext,
PniCredentialRequestContext,
} from '@signalapp/libsignal-client/zkgroup';
import {
AuthCredential,
AuthCredentialWithPni,
ClientZkAuthOperations,
ClientZkGroupCipher,
ClientZkProfileOperations,
@ -11,9 +14,12 @@ import {
GroupSecretParams,
ProfileKey,
ProfileKeyCiphertext,
ProfileKeyCredential,
ExpiringProfileKeyCredential,
ProfileKeyCredentialPresentation,
ProfileKeyCredentialResponse,
ExpiringProfileKeyCredentialResponse,
PniCredential,
PniCredentialResponse,
PniCredentialPresentation,
ServerPublicParams,
UuidCiphertext,
NotarySignature,
@ -32,26 +38,45 @@ export function decryptGroupBlob(
return clientZkGroupCipher.decryptBlob(Buffer.from(ciphertext));
}
export function decryptProfileKeyCredentialPresentation(
clientZkGroupCipher: ClientZkGroupCipher,
export function decodeProfileKeyCredentialPresentation(
presentationBuffer: Uint8Array
): { profileKey: Uint8Array; uuid: UUIDStringType } {
): { profileKey: Uint8Array; userId: Uint8Array } {
const presentation = new ProfileKeyCredentialPresentation(
Buffer.from(presentationBuffer)
);
const uuidCiphertext = presentation.getUuidCiphertext();
const uuid = clientZkGroupCipher.decryptUuid(uuidCiphertext);
const userId = presentation.getUuidCiphertext().serialize();
const profileKey = presentation.getProfileKeyCiphertext().serialize();
return {
profileKey,
userId,
};
}
export function decryptPniCredentialPresentation(
clientZkGroupCipher: ClientZkGroupCipher,
presentationBuffer: Uint8Array
): { profileKey: Uint8Array; pni: UUIDStringType; aci: UUIDStringType } {
const presentation = new PniCredentialPresentation(
Buffer.from(presentationBuffer)
);
const pniCiphertext = presentation.getPniCiphertext();
const aciCiphertext = presentation.getAciCiphertext();
const aci = clientZkGroupCipher.decryptUuid(aciCiphertext);
const pni = clientZkGroupCipher.decryptUuid(pniCiphertext);
const profileKeyCiphertext = presentation.getProfileKeyCiphertext();
const profileKey = clientZkGroupCipher.decryptProfileKey(
profileKeyCiphertext,
uuid
aci
);
return {
profileKey: profileKey.serialize(),
uuid: UUID.cast(uuid),
aci: UUID.cast(aci),
pni: UUID.cast(pni),
};
}
@ -129,9 +154,11 @@ export function encryptGroupBlob(
export function encryptUuid(
clientZkGroupCipher: ClientZkGroupCipher,
uuidPlaintext: UUIDStringType
uuidPlaintext: UUID
): Uint8Array {
const uuidCiphertext = clientZkGroupCipher.encryptUuid(uuidPlaintext);
const uuidCiphertext = clientZkGroupCipher.encryptUuid(
uuidPlaintext.toString()
);
return uuidCiphertext.serialize();
}
@ -158,22 +185,46 @@ export function generateProfileKeyCredentialRequest(
};
}
export function generatePNICredentialRequest(
clientZkProfileCipher: ClientZkProfileOperations,
aci: UUIDStringType,
pni: UUIDStringType,
profileKeyBase64: string
): { context: PniCredentialRequestContext; requestHex: string } {
const profileKeyArray = Buffer.from(profileKeyBase64, 'base64');
const profileKey = new ProfileKey(profileKeyArray);
const context = clientZkProfileCipher.createPniCredentialRequestContext(
aci,
pni,
profileKey
);
const request = context.getRequest();
const requestArray = request.serialize();
return {
context,
requestHex: requestArray.toString('hex'),
};
}
export function getAuthCredentialPresentation(
clientZkAuthOperations: ClientZkAuthOperations,
authCredentialBase64: string,
groupSecretParamsBase64: string
): Uint8Array {
const authCredential = new AuthCredential(
const authCredential = new AuthCredentialWithPni(
Buffer.from(authCredentialBase64, 'base64')
);
const secretParams = new GroupSecretParams(
Buffer.from(groupSecretParamsBase64, 'base64')
);
const presentation = clientZkAuthOperations.createAuthCredentialPresentation(
secretParams,
authCredential
);
const presentation =
clientZkAuthOperations.createAuthCredentialWithPniPresentation(
secretParams,
authCredential
);
return presentation.serialize();
}
@ -186,7 +237,7 @@ export function createProfileKeyCredentialPresentation(
profileKeyCredentialBase64,
'base64'
);
const profileKeyCredential = new ProfileKeyCredential(
const profileKeyCredential = new ExpiringProfileKeyCredential(
profileKeyCredentialArray
);
const secretParams = new GroupSecretParams(
@ -194,7 +245,7 @@ export function createProfileKeyCredentialPresentation(
);
const presentation =
clientZkProfileCipher.createProfileKeyCredentialPresentation(
clientZkProfileCipher.createExpiringProfileKeyCredentialPresentation(
secretParams,
profileKeyCredential
);
@ -202,6 +253,25 @@ export function createProfileKeyCredentialPresentation(
return presentation.serialize();
}
export function createPNICredentialPresentation(
clientZkProfileCipher: ClientZkProfileOperations,
pniCredentialBase64: string,
groupSecretParamsBase64: string
): Uint8Array {
const pniCredentialArray = Buffer.from(pniCredentialBase64, 'base64');
const pniCredential = new PniCredential(pniCredentialArray);
const secretParams = new GroupSecretParams(
Buffer.from(groupSecretParamsBase64, 'base64')
);
const presentation = clientZkProfileCipher.createPniCredentialPresentation(
secretParams,
pniCredential
);
return presentation.serialize();
}
export function getClientZkAuthOperations(
serverPublicParamsBase64: string
): ClientZkAuthOperations {
@ -236,15 +306,39 @@ export function handleProfileKeyCredential(
clientZkProfileCipher: ClientZkProfileOperations,
context: ProfileKeyCredentialRequestContext,
responseBase64: string
): string {
const response = new ProfileKeyCredentialResponse(
): { credential: string; expiration: number } {
const response = new ExpiringProfileKeyCredentialResponse(
Buffer.from(responseBase64, 'base64')
);
const profileKeyCredential =
clientZkProfileCipher.receiveProfileKeyCredential(context, response);
clientZkProfileCipher.receiveExpiringProfileKeyCredential(
context,
response
);
const credentialArray = profileKeyCredential.serialize();
return {
credential: credentialArray.toString('base64'),
expiration: profileKeyCredential.getExpirationTime().getTime(),
};
}
export function handleProfileKeyPNICredential(
clientZkProfileCipher: ClientZkProfileOperations,
context: PniCredentialRequestContext,
responseBase64: string
): string {
const response = new PniCredentialResponse(
Buffer.from(responseBase64, 'base64')
);
const pniCredential = clientZkProfileCipher.receivePniCredential(
context,
response
);
const credentialArray = pniCredential.serialize();
return credentialArray.toString('base64');
}

View File

@ -1263,7 +1263,7 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
const ourUuid = window.textsecure.storage.user.getUuid(UUIDKind.ACI);
if (
!isGroup(this.model.attributes) ||
(ourUuid && this.model.hasMember(ourUuid.toString()))
(ourUuid && this.model.hasMember(ourUuid))
) {
strictAssert(
this.model.throttledGetProfiles !== undefined,

View File

@ -1745,28 +1745,20 @@
"@react-spring/shared" "~9.4.5"
"@react-spring/types" "~9.4.5"
"@signalapp/libsignal-client@0.17.0":
version "0.17.0"
resolved "https://registry.yarnpkg.com/@signalapp/libsignal-client/-/libsignal-client-0.17.0.tgz#ffe6763d80f56148b45192bca29deb16f9a0aea8"
integrity sha512-O5bd/BURWnybh6KhRYSO3NmNb1/oySu5yJx5ELy3QsfeFvpMnTkr0/PcXd0MCvRiaoN+/a0TsnywMO43t6Nxsw==
"@signalapp/libsignal-client@0.18.1", "@signalapp/libsignal-client@^0.18.1":
version "0.18.1"
resolved "https://registry.yarnpkg.com/@signalapp/libsignal-client/-/libsignal-client-0.18.1.tgz#6b499cdcc952f1981c6367f68484cf3275be3b31"
integrity sha512-43NcTYpahImlWHBDaNFmn7QaeXZHkFkTtb4m+ZWgzU0mkS1M8V+orGen2XuDvNiu+9HQmW4Lg7FV1deXhWtIRA==
dependencies:
node-gyp-build "^4.2.3"
uuid "^8.3.0"
"@signalapp/libsignal-client@^0.16.0":
version "0.16.0"
resolved "https://registry.yarnpkg.com/@signalapp/libsignal-client/-/libsignal-client-0.16.0.tgz#7acba54b7ba05f513cdcf7f555efa1ccc6ce0145"
integrity sha512-/5EzlAcQoQReDomqV6VTtin5tvqvdUxoe8knSiz+L1kcLSlHA0So0zTR9WAdfQQ69t4q69vhaS4pu5yVI28YHA==
"@signalapp/mock-server@2.0.1":
version "2.0.1"
resolved "https://registry.yarnpkg.com/@signalapp/mock-server/-/mock-server-2.0.1.tgz#0ecee7a0060181546e6b0c1b8e8c6f361fb2d7fe"
integrity sha512-YB0MIUzW8D1NirKpxxNXgEYuvK/OWbFo3djsBA4GqEUBIsJmdYcd4auHSqV3gKE/eSRoFQ0Z//eJNiqtsHbSEw==
dependencies:
node-gyp-build "^4.2.3"
uuid "^8.3.0"
"@signalapp/mock-server@1.5.1":
version "1.5.1"
resolved "https://registry.yarnpkg.com/@signalapp/mock-server/-/mock-server-1.5.1.tgz#e37a4505c037a3e85901cd00443d565cf9c2fe90"
integrity sha512-PqRrLhGPtKoTOeHj/L4tUlNkwXZ8MJMU3G7DaaVRAD+g+bpjpeb/ru73iH35K209wRdn4s7/hGUMaeRd6yKFxA==
dependencies:
"@signalapp/libsignal-client" "^0.16.0"
"@signalapp/libsignal-client" "^0.18.1"
debug "^4.3.2"
long "^4.0.0"
micro "^9.3.4"