Fetch PNI group credentials
This commit is contained in:
parent
b9ba732724
commit
a450e13a99
61 changed files with 1911 additions and 875 deletions
8
.github/workflows/ci.yml
vendored
8
.github/workflows/ci.yml
vendored
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.`
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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) {
|
||||
|
|
613
ts/groups.ts
613
ts/groups.ts
File diff suppressed because it is too large
Load diff
|
@ -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
4
ts/model-types.d.ts
vendored
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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) &&
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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: (
|
||||
|
|
|
@ -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')
|
||||
`
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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 }],
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
})();
|
||||
|
|
|
@ -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:');
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
})();
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
})();
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
})();
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
|
|
240
ts/test-mock/gv2/accept_invite_test.ts
Normal file
240
ts/test-mock/gv2/accept_invite_test.ts
Normal 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));
|
||||
});
|
||||
});
|
|
@ -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();
|
||||
});
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
|
|
|
@ -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 };
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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()
|
||||
|
|
4
ts/textsecure/Types.d.ts
vendored
4
ts/textsecure/Types.d.ts
vendored
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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');
|
||||
}
|
||||
|
|
|
@ -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()}`
|
||||
);
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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');
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
26
yarn.lock
26
yarn.lock
|
@ -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"
|
||||
|
|
Loading…
Add table
Reference in a new issue