Support phone number sharing flag on profile
This commit is contained in:
parent
23f39a0dc7
commit
d71da5c486
16 changed files with 155 additions and 33 deletions
1
ts/model-types.d.ts
vendored
1
ts/model-types.d.ts
vendored
|
@ -333,6 +333,7 @@ export type ConversationAttributesType = {
|
|||
messageRequestResponseType?: number;
|
||||
muteExpiresAt?: number;
|
||||
dontNotifyForMentionsIfMuted?: boolean;
|
||||
notSharingPhoneNumber?: boolean;
|
||||
profileAvatar?: ContactAvatarType | null;
|
||||
profileKeyCredential?: string | null;
|
||||
profileKeyCredentialExpiration?: number | null;
|
||||
|
|
|
@ -1858,7 +1858,7 @@ export class ConversationModel extends window.Backbone
|
|||
this.set('e164', e164 || undefined);
|
||||
|
||||
// This user changed their phone number
|
||||
if (oldValue && e164) {
|
||||
if (oldValue && e164 && !this.get('notSharingPhoneNumber')) {
|
||||
void this.addChangeNumberNotification(oldValue, e164);
|
||||
}
|
||||
|
||||
|
|
|
@ -427,6 +427,24 @@ async function doGetProfile(c: ConversationModel): Promise<void> {
|
|||
c.unset('aboutEmoji');
|
||||
}
|
||||
|
||||
if (profile.phoneNumberSharing) {
|
||||
if (decryptionKey) {
|
||||
const decrypted = decryptProfile(
|
||||
Bytes.fromBase64(profile.phoneNumberSharing),
|
||||
decryptionKey
|
||||
);
|
||||
|
||||
// It should be one byte, but be conservative about it and only
|
||||
// set `notSharingPhoneNumber` to `true` in all cases except [0x01].
|
||||
c.set(
|
||||
'notSharingPhoneNumber',
|
||||
decrypted.length !== 1 || decrypted[0] !== 1
|
||||
);
|
||||
}
|
||||
} else {
|
||||
c.unset('notSharingPhoneNumber');
|
||||
}
|
||||
|
||||
if (profile.paymentAddress && isMe(c.attributes)) {
|
||||
await window.storage.put('paymentAddress', profile.paymentAddress);
|
||||
}
|
||||
|
|
|
@ -2,13 +2,18 @@
|
|||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import * as Errors from '../types/errors';
|
||||
import { getConversation } from '../util/getConversation';
|
||||
import { DAY } from '../util/durations';
|
||||
import { drop } from '../util/drop';
|
||||
import { BackOff, FIBONACCI_TIMEOUTS } from '../util/BackOff';
|
||||
import { checkForUsername } from '../util/lookupConversationWithoutServiceId';
|
||||
import { storageJobQueue } from '../util/JobQueue';
|
||||
import { getProfile } from '../util/getProfile';
|
||||
import { isSharingPhoneNumberWithEverybody } from '../util/phoneNumberSharingMode';
|
||||
import * as log from '../logging/log';
|
||||
import { resolveUsernameByLink } from './username';
|
||||
import { runStorageServiceSyncJob } from './storage';
|
||||
import { writeProfile } from './writeProfile';
|
||||
|
||||
const CHECK_INTERVAL = DAY;
|
||||
|
||||
|
@ -59,6 +64,11 @@ class UsernameIntegrityService {
|
|||
}
|
||||
|
||||
private async check(): Promise<void> {
|
||||
await this.checkUsername();
|
||||
await this.checkPhoneNumberSharing();
|
||||
}
|
||||
|
||||
private async checkUsername(): Promise<void> {
|
||||
const me = window.ConversationController.getOurConversationOrThrow();
|
||||
const username = me.get('username');
|
||||
const aci = me.getAci();
|
||||
|
@ -100,6 +110,51 @@ class UsernameIntegrityService {
|
|||
log.info('usernameIntegrity: check pass');
|
||||
}
|
||||
}
|
||||
|
||||
private async checkPhoneNumberSharing(): Promise<void> {
|
||||
const me = window.ConversationController.getOurConversationOrThrow();
|
||||
|
||||
await getProfile(me.getServiceId(), me.get('e164'));
|
||||
|
||||
{
|
||||
const localValue = isSharingPhoneNumberWithEverybody();
|
||||
const remoteValue = !me.get('notSharingPhoneNumber');
|
||||
if (localValue === remoteValue) {
|
||||
return;
|
||||
}
|
||||
|
||||
log.warn(
|
||||
'usernameIntegrity: phone number sharing mode conflict, running ' +
|
||||
`storage service sync (local: ${localValue}, remote: ${remoteValue})`
|
||||
);
|
||||
|
||||
runStorageServiceSyncJob();
|
||||
}
|
||||
|
||||
window.Whisper.events.once('storageService:syncComplete', () => {
|
||||
const localValue = isSharingPhoneNumberWithEverybody();
|
||||
const remoteValue = !me.get('notSharingPhoneNumber');
|
||||
if (localValue === remoteValue) {
|
||||
log.info(
|
||||
'usernameIntegrity: phone number sharing mode conflict resolved by ' +
|
||||
'storage service sync'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
log.warn(
|
||||
'usernameIntegrity: phone number sharing mode conflict not resolved, ' +
|
||||
'updating profile'
|
||||
);
|
||||
|
||||
drop(
|
||||
writeProfile(getConversation(me), {
|
||||
oldAvatar: undefined,
|
||||
newAvatar: undefined,
|
||||
})
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export const usernameIntegrity = new UsernameIntegrityService();
|
||||
|
|
|
@ -18,9 +18,9 @@ export async function writeProfile(
|
|||
conversation: ConversationType,
|
||||
avatar: AvatarUpdateType
|
||||
): Promise<void> {
|
||||
const { messaging } = window.textsecure;
|
||||
if (!messaging) {
|
||||
throw new Error('messaging is not available!');
|
||||
const { server } = window.textsecure;
|
||||
if (!server) {
|
||||
throw new Error('server is not available!');
|
||||
}
|
||||
|
||||
// Before we write anything we request the user's profile so that we can
|
||||
|
@ -50,7 +50,7 @@ export async function writeProfile(
|
|||
conversation,
|
||||
avatar
|
||||
);
|
||||
const avatarRequestHeaders = await messaging.putProfile(profileData);
|
||||
const avatarRequestHeaders = await server.putProfile(profileData);
|
||||
|
||||
// Upload the avatar if provided
|
||||
// delete existing files on disk if avatar has been removed
|
||||
|
@ -68,7 +68,7 @@ export async function writeProfile(
|
|||
log.info('writeProfile: not updating avatar');
|
||||
} else if (avatarRequestHeaders && encryptedAvatarData && newAvatar) {
|
||||
log.info('writeProfile: uploading new avatar');
|
||||
const avatarUrl = await messaging.uploadAvatar(
|
||||
const avatarUrl = await server.uploadAvatar(
|
||||
avatarRequestHeaders,
|
||||
encryptedAvatarData
|
||||
);
|
||||
|
|
|
@ -308,6 +308,7 @@ export type ConversationType = ReadonlyDeep<
|
|||
typingContactIdTimestamps?: Record<string, number>;
|
||||
recentMediaItems?: ReadonlyArray<MediaItemType>;
|
||||
profileSharing?: boolean;
|
||||
notSharingPhoneNumber?: boolean;
|
||||
|
||||
shouldShowDraft?: boolean;
|
||||
// Full information for re-hydrating composition area
|
||||
|
|
|
@ -34,9 +34,7 @@ import type {
|
|||
GetProfileUnauthOptionsType,
|
||||
GroupCredentialsType,
|
||||
GroupLogResponseType,
|
||||
ProfileRequestDataType,
|
||||
ProxiedRequestOptionsType,
|
||||
UploadAvatarHeadersType,
|
||||
WebAPIType,
|
||||
} from './WebAPI';
|
||||
import createTaskWithTimeout from './TaskWithTimeout';
|
||||
|
@ -2232,17 +2230,4 @@ export default class MessageSender {
|
|||
): Promise<void> {
|
||||
return this.server.sendChallengeResponse(challengeResponse);
|
||||
}
|
||||
|
||||
async putProfile(
|
||||
jsonData: Readonly<ProfileRequestDataType>
|
||||
): Promise<UploadAvatarHeadersType | undefined> {
|
||||
return this.server.putProfile(jsonData);
|
||||
}
|
||||
|
||||
async uploadAvatar(
|
||||
requestHeaders: Readonly<UploadAvatarHeadersType>,
|
||||
avatarData: Readonly<Uint8Array>
|
||||
): Promise<string> {
|
||||
return this.server.uploadAvatar(requestHeaders, avatarData);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -674,6 +674,7 @@ export type ProfileRequestDataType = {
|
|||
commitment: string;
|
||||
name: string;
|
||||
paymentAddress: string | null;
|
||||
phoneNumberSharing: string | null;
|
||||
version: string;
|
||||
};
|
||||
|
||||
|
@ -706,6 +707,7 @@ export type ProfileType = Readonly<{
|
|||
about?: string;
|
||||
aboutEmoji?: string;
|
||||
avatar?: string;
|
||||
phoneNumberSharing?: string;
|
||||
unidentifiedAccess?: string;
|
||||
unrestrictedUnidentifiedAccess?: string;
|
||||
uuid?: string;
|
||||
|
|
|
@ -21,6 +21,7 @@ import type { ConversationType } from '../state/ducks/conversations';
|
|||
import type { AuthorizeArtCreatorDataType } from '../state/ducks/globalModals';
|
||||
import { calling } from '../services/calling';
|
||||
import { resolveUsernameByLinkBase64 } from '../services/username';
|
||||
import { writeProfile } from '../services/writeProfile';
|
||||
import { getConversationsWithCustomColorSelector } from '../state/selectors/conversations';
|
||||
import { getCustomColors } from '../state/selectors/items';
|
||||
import { themeChanged } from '../shims/themeChanged';
|
||||
|
@ -41,6 +42,7 @@ import type { NotificationClickData } from '../services/notifications';
|
|||
import { StoryViewModeType, StoryViewTargetType } from '../types/Stories';
|
||||
import { isValidE164 } from './isValidE164';
|
||||
import { fromWebSafeBase64 } from './webSafeBase64';
|
||||
import { getConversation } from './getConversation';
|
||||
|
||||
type SentMediaQualityType = 'standard' | 'high';
|
||||
type ThemeType = 'light' | 'dark' | 'system';
|
||||
|
@ -235,6 +237,14 @@ export function createIPCEvents(
|
|||
setPhoneNumberDiscoverabilitySetting,
|
||||
setPhoneNumberSharingSetting: async (newValue: PhoneNumberSharingMode) => {
|
||||
const account = window.ConversationController.getOurConversationOrThrow();
|
||||
|
||||
// writeProfile fetches the latest profile first so do it before updating
|
||||
// local data to prevent triggering a conflict.
|
||||
await writeProfile(getConversation(account), {
|
||||
oldAvatar: undefined,
|
||||
newAvatar: undefined,
|
||||
});
|
||||
|
||||
const promises = new Array<Promise<void>>();
|
||||
promises.push(window.storage.put('phoneNumberSharingMode', newValue));
|
||||
if (newValue === PhoneNumberSharingMode.Everybody) {
|
||||
|
|
|
@ -12,6 +12,7 @@ import {
|
|||
} from '../Crypto';
|
||||
import type { AvatarUpdateType } from '../types/Avatar';
|
||||
import { deriveProfileKeyCommitment, deriveProfileKeyVersion } from './zkgroup';
|
||||
import { isSharingPhoneNumberWithEverybody } from './phoneNumberSharingMode';
|
||||
|
||||
export async function encryptProfileData(
|
||||
conversation: ConversationType,
|
||||
|
@ -56,6 +57,11 @@ export async function encryptProfileData(
|
|||
)
|
||||
: null;
|
||||
|
||||
const encryptedPhoneNumberSharing = encryptProfile(
|
||||
new Uint8Array([isSharingPhoneNumberWithEverybody() ? 1 : 0]),
|
||||
keyBuffer
|
||||
);
|
||||
|
||||
const encryptedAvatarData = newAvatar
|
||||
? encryptProfile(newAvatar, keyBuffer)
|
||||
: undefined;
|
||||
|
@ -72,6 +78,7 @@ export async function encryptProfileData(
|
|||
avatar: Boolean(newAvatar),
|
||||
sameAvatar,
|
||||
commitment: deriveProfileKeyCommitment(profileKey, serviceId),
|
||||
phoneNumberSharing: Bytes.toBase64(encryptedPhoneNumberSharing),
|
||||
};
|
||||
|
||||
return [profileData, encryptedAvatarData];
|
||||
|
|
|
@ -8,6 +8,7 @@ import { parseAndFormatPhoneNumber } from './libphonenumberInstance';
|
|||
import { WEEK } from './durations';
|
||||
import { fuseGetFnRemoveDiacritics, getCachedFuseIndex } from './fuse';
|
||||
import { countConversationUnreadStats, hasUnread } from './countUnreadStats';
|
||||
import { getE164 } from './getE164';
|
||||
|
||||
// Fuse.js scores have order of 0.01
|
||||
const ACTIVE_AT_SCORE_FACTOR = (1 / WEEK) * 0.01;
|
||||
|
@ -46,7 +47,13 @@ const FUSE_OPTIONS: Fuse.IFuseOptions<ConversationType> = {
|
|||
weight: 0.5,
|
||||
},
|
||||
],
|
||||
getFn: fuseGetFnRemoveDiacritics,
|
||||
getFn: (convo, path) => {
|
||||
if (path === 'e164' || (path.length === 1 && path[0] === 'e164')) {
|
||||
return getE164(convo) ?? '';
|
||||
}
|
||||
|
||||
return fuseGetFnRemoveDiacritics(convo, path);
|
||||
},
|
||||
};
|
||||
|
||||
type CommandRunnerType = (
|
||||
|
|
|
@ -210,6 +210,7 @@ export function getConversation(model: ConversationModel): ConversationType {
|
|||
phoneNumber: getNumber(attributes),
|
||||
profileName: getProfileName(attributes),
|
||||
profileSharing: attributes.profileSharing,
|
||||
notSharingPhoneNumber: attributes.notSharingPhoneNumber,
|
||||
publicParams: attributes.publicParams,
|
||||
secretParams: attributes.secretParams,
|
||||
shouldShowDraft,
|
||||
|
|
26
ts/util/getE164.ts
Normal file
26
ts/util/getE164.ts
Normal file
|
@ -0,0 +1,26 @@
|
|||
// Copyright 2023 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import type { ConversationAttributesType } from '../model-types.d';
|
||||
import type { ConversationType } from '../state/ducks/conversations';
|
||||
import { isInSystemContacts } from './isInSystemContacts';
|
||||
|
||||
export function getE164(
|
||||
attributes: Pick<
|
||||
ConversationAttributesType | ConversationType,
|
||||
| 'type'
|
||||
| 'name'
|
||||
| 'systemGivenName'
|
||||
| 'systemFamilyName'
|
||||
| 'e164'
|
||||
| 'notSharingPhoneNumber'
|
||||
>
|
||||
): string | undefined {
|
||||
const { e164, notSharingPhoneNumber = false } = attributes;
|
||||
|
||||
if (notSharingPhoneNumber && !isInSystemContacts(attributes)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return e164;
|
||||
}
|
|
@ -8,6 +8,7 @@ import type {
|
|||
import { combineNames } from './combineNames';
|
||||
import { getRegionCodeForNumber } from './libphonenumberUtil';
|
||||
import { isDirectConversation } from './whatTypeOfConversation';
|
||||
import { getE164 } from './getE164';
|
||||
|
||||
export function getTitle(
|
||||
attributes: ConversationRenderInfoType,
|
||||
|
@ -107,13 +108,16 @@ export function getSystemName(
|
|||
}
|
||||
|
||||
export function getNumber(
|
||||
attributes: Pick<ConversationAttributesType, 'e164' | 'type'>
|
||||
attributes: Pick<
|
||||
ConversationAttributesType,
|
||||
'e164' | 'type' | 'notSharingPhoneNumber'
|
||||
>
|
||||
): string | undefined {
|
||||
if (!isDirectConversation(attributes)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const { e164 } = attributes;
|
||||
const e164 = getE164(attributes);
|
||||
if (!e164) {
|
||||
return '';
|
||||
}
|
||||
|
|
|
@ -11,6 +11,7 @@ import {
|
|||
getSourceServiceId,
|
||||
} from '../messages/helpers';
|
||||
import { isDirectConversation, isGroupV2 } from './whatTypeOfConversation';
|
||||
import { getE164 } from './getE164';
|
||||
|
||||
export function getMessageIdForLogging(
|
||||
message: Pick<
|
||||
|
@ -29,8 +30,8 @@ export function getConversationIdForLogging(
|
|||
conversation: ConversationAttributesType
|
||||
): string {
|
||||
if (isDirectConversation(conversation)) {
|
||||
const { serviceId, pni, e164, id } = conversation;
|
||||
return `${serviceId || pni || e164} (${id})`;
|
||||
const { serviceId, pni, id } = conversation;
|
||||
return `${serviceId || pni || getE164(conversation)} (${id})`;
|
||||
}
|
||||
if (isGroupV2(conversation)) {
|
||||
return `groupv2(${conversation.groupId})`;
|
||||
|
|
|
@ -19,13 +19,7 @@ export const parsePhoneNumberSharingMode = makeEnumParser(
|
|||
PhoneNumberSharingMode.Everybody
|
||||
);
|
||||
|
||||
export const shouldSharePhoneNumberWith = (
|
||||
conversation: ConversationAttributesType
|
||||
): boolean => {
|
||||
if (!isDirectConversation(conversation) || isMe(conversation)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
export const isSharingPhoneNumberWithEverybody = (): boolean => {
|
||||
const phoneNumberSharingMode = parsePhoneNumberSharingMode(
|
||||
window.storage.get('phoneNumberSharingMode')
|
||||
);
|
||||
|
@ -40,3 +34,13 @@ export const shouldSharePhoneNumberWith = (
|
|||
throw missingCaseError(phoneNumberSharingMode);
|
||||
}
|
||||
};
|
||||
|
||||
export const shouldSharePhoneNumberWith = (
|
||||
conversation: ConversationAttributesType
|
||||
): boolean => {
|
||||
if (!isDirectConversation(conversation) || isMe(conversation)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return isSharingPhoneNumberWithEverybody();
|
||||
};
|
||||
|
|
Loading…
Reference in a new issue