mirror of
https://github.com/signalapp/Signal-Desktop.git
synced 2026-08-14 08:52:55 +01:00
Persist timestamp along with blocked contacts and groups
Co-authored-by: Scott Nonnenberg <scott@signal.org>
This commit is contained in:
co-authored by
Scott Nonnenberg
parent
5d9a876db2
commit
58f0ac9e6b
@@ -8540,6 +8540,10 @@
|
||||
"messageformat": "Blocked Groups",
|
||||
"description": "Header for groups section on the blocked preferences subpage"
|
||||
},
|
||||
"icu:Preferences--blocked--blocked-on": {
|
||||
"messageformat": "Blocked on {blockedAt}",
|
||||
"description": "A second line shown under the contact's name in block list if we have a timestamp for that blocked contact or group"
|
||||
},
|
||||
"icu:Preferences--signal-backups": {
|
||||
"messageformat": "Signal Secure Backups",
|
||||
"description": "Feature name for message backups using the Signal service."
|
||||
|
||||
@@ -275,6 +275,7 @@ message Contact {
|
||||
optional AvatarColor avatarColor = 21;
|
||||
// Opaque blob containing key transparency data for the contact
|
||||
optional bytes keyTransparencyData = 22;
|
||||
uint64 blockedAtTimestamp = 23; // if `blocked` is true, 0 means unknown block time
|
||||
}
|
||||
|
||||
message Group {
|
||||
@@ -291,6 +292,7 @@ message Group {
|
||||
GroupSnapshot snapshot = 5;
|
||||
bool blocked = 6;
|
||||
optional AvatarColor avatarColor = 7;
|
||||
uint64 blockedAtTimestamp = 8; // if `blocked` is true, 0 means unknown block time
|
||||
|
||||
// These are simply plaintext copies of the groups proto from Groups.proto.
|
||||
// They should be kept completely in-sync with Groups.proto.
|
||||
@@ -413,7 +415,7 @@ message CallLink {
|
||||
string name = 3;
|
||||
Restrictions restrictions = 4;
|
||||
uint64 expirationMs = 5;
|
||||
reserved 6; // was epoch, never used
|
||||
reserved /*epoch*/ 6;
|
||||
}
|
||||
|
||||
message AdHocCall {
|
||||
@@ -1083,7 +1085,6 @@ message GroupChangeChatUpdate {
|
||||
GroupExpirationTimerUpdate groupExpirationTimerUpdate = 34;
|
||||
GroupMemberLabelAccessLevelChangeUpdate groupMemberLabelAccessLevelChangeUpdate = 35;
|
||||
GroupTerminateChangeUpdate groupTerminateChangeUpdate = 36;
|
||||
// next: 37
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1319,6 +1320,12 @@ message StickerPack {
|
||||
|
||||
message ChatStyle {
|
||||
message Gradient {
|
||||
// Color ordering:
|
||||
// 0 degrees: bottom-to-top
|
||||
// 90 degrees: left-to-right
|
||||
// 180 degrees: top-to-bottom
|
||||
// 270 degrees: right-to-left
|
||||
|
||||
uint32 angle = 1; // degrees
|
||||
repeated fixed32 colors = 2; // 0xAARRGGBB
|
||||
repeated float positions = 3; // percent from 0 to 1
|
||||
|
||||
@@ -579,10 +579,29 @@ message SyncMessage {
|
||||
}
|
||||
|
||||
message Blocked {
|
||||
repeated string numbers = 1;
|
||||
repeated string numbers = 1; // deprecated: this field will be removed in a future release.
|
||||
repeated string acis = 3;
|
||||
repeated bytes groupIds = 2;
|
||||
repeated bytes acisBinary = 4; // 16-byte UUID
|
||||
repeated bytes groupIds = 2; // deprecated: this field will be removed in a future release.
|
||||
repeated bytes acisBinary = 4; // deprecated: this field will be removed in a future release.
|
||||
|
||||
message BlockedE164 {
|
||||
optional string e164 = 1;
|
||||
optional uint64 timestamp = 2;
|
||||
}
|
||||
|
||||
message BlockedAci {
|
||||
optional bytes aciBinary = 1; // 16-byte UUID
|
||||
optional uint64 timestamp = 2;
|
||||
}
|
||||
|
||||
message BlockedGroup {
|
||||
optional bytes groupId = 1;
|
||||
optional uint64 timestamp = 2;
|
||||
}
|
||||
|
||||
repeated BlockedE164 blockedE164s = 5;
|
||||
repeated BlockedAci blockedAcis = 6;
|
||||
repeated BlockedGroup blockedGroups = 7;
|
||||
}
|
||||
|
||||
message Request {
|
||||
|
||||
@@ -144,7 +144,8 @@ message ContactRecord {
|
||||
optional AvatarColor avatarColor = 24;
|
||||
bytes aciBinary = 25; // 16-byte UUID
|
||||
bytes pniBinary = 26; // 16-byte UUID
|
||||
// Next ID: 27
|
||||
uint64 blockedAtTimestamp = 27; // if `blocked` is true, 0 means unknown block time
|
||||
// Next ID: 28
|
||||
}
|
||||
|
||||
message GroupV1Record {
|
||||
@@ -175,6 +176,7 @@ message GroupV2Record {
|
||||
StorySendMode storySendMode = 10;
|
||||
optional AvatarColor avatarColor = 11;
|
||||
bytes verifiedNameHash = 12; // SHA-256 of UTF-8 encoded decrypted group title that was last verified
|
||||
uint64 blockedAtTimestamp = 13; // if `blocked` is true, 0 means unknown block time
|
||||
}
|
||||
|
||||
message Payments {
|
||||
@@ -306,6 +308,7 @@ message AccountRecord {
|
||||
optional uint64 releaseNotesChatMutedUntilTimestamp = 49;
|
||||
optional bool releaseNotesChatBlocked = 50;
|
||||
optional bool releaseNotesChatMarkedUnread = 51;
|
||||
optional uint64 releaseNotesChatBlockedAt = 52; // only set if known (>0)
|
||||
}
|
||||
|
||||
message StoryDistributionListRecord {
|
||||
|
||||
@@ -1332,6 +1332,22 @@ export class ConversationController {
|
||||
),
|
||||
});
|
||||
|
||||
if (obsolete.isBlocked()) {
|
||||
const e164 = obsolete.get('e164');
|
||||
const e164Block = e164
|
||||
? itemStorage.blocked.getBlockedNumbers().get(e164)
|
||||
: undefined;
|
||||
|
||||
const serviceId = obsolete.get('serviceId');
|
||||
const serviceIdBlock = serviceId
|
||||
? itemStorage.blocked.getBlockedServiceIds().get(serviceId)
|
||||
: undefined;
|
||||
|
||||
const timestamp = serviceIdBlock?.blockedAt ?? e164Block?.blockedAt;
|
||||
|
||||
current.block({ viaStorageServiceSync: false, timestamp });
|
||||
}
|
||||
|
||||
const obsoleteExpireTimer = obsolete.get('expireTimer');
|
||||
const currentExpireTimer = current.get('expireTimer');
|
||||
if (
|
||||
|
||||
@@ -1454,7 +1454,9 @@ async function startApp(): Promise<void> {
|
||||
|
||||
log.info('Blocked uuids cleanup: starting...');
|
||||
const blockedUuids = itemStorage.get(BLOCKED_UUIDS_ID, []);
|
||||
const blockedAcis = blockedUuids.filter(isAciString);
|
||||
const blockedAcis = blockedUuids.filter(item =>
|
||||
isAciString(item.serviceId)
|
||||
);
|
||||
const diff = blockedUuids.length - blockedAcis.length;
|
||||
if (diff > 0) {
|
||||
log.warn(
|
||||
@@ -1463,14 +1465,20 @@ async function startApp(): Promise<void> {
|
||||
await itemStorage.put(BLOCKED_UUIDS_ID, blockedAcis);
|
||||
}
|
||||
|
||||
if (blockedAcis.some(isSignalServiceId)) {
|
||||
const signalItem = blockedAcis.find(item =>
|
||||
isSignalServiceId(item.serviceId)
|
||||
);
|
||||
if (signalItem) {
|
||||
log.warn(
|
||||
'Release notes chat block migration: found in blocked list. Moving.'
|
||||
);
|
||||
await itemStorage.blocked.setReleaseNotesChatBlocked(true);
|
||||
await itemStorage.blocked.setReleaseNotesChatBlocked(
|
||||
true,
|
||||
signalItem.blockedAt
|
||||
);
|
||||
await itemStorage.put(
|
||||
BLOCKED_UUIDS_ID,
|
||||
blockedAcis.filter(aci => !isSignalServiceId(aci))
|
||||
blockedAcis.filter(item => !isSignalServiceId(item.serviceId))
|
||||
);
|
||||
log.info('Release notes chat block migration: complete');
|
||||
}
|
||||
|
||||
@@ -1055,47 +1055,59 @@ Internal.args = {
|
||||
|
||||
export const PrivacyBlocked1Contact = Template.bind({});
|
||||
PrivacyBlocked1Contact.args = {
|
||||
blockedContacts: [getDefaultConversation()],
|
||||
blockedContacts: [
|
||||
{ conversation: getDefaultConversation(), blockedAt: undefined },
|
||||
],
|
||||
settingsLocation: { page: SettingsPage.Privacy },
|
||||
};
|
||||
|
||||
export const PrivacyBlocked1Group = Template.bind({});
|
||||
PrivacyBlocked1Group.args = {
|
||||
blockedGroups: [getDefaultConversation()],
|
||||
blockedGroups: [
|
||||
{ conversation: getDefaultConversation(), blockedAt: undefined },
|
||||
],
|
||||
settingsLocation: { page: SettingsPage.Privacy },
|
||||
};
|
||||
|
||||
export const PrivacyBlocked1Both = Template.bind({});
|
||||
PrivacyBlocked1Both.args = {
|
||||
blockedContacts: [getDefaultConversation()],
|
||||
blockedGroups: [getDefaultConversation()],
|
||||
export const PrivacyBlocked1BothWithBlockedAt = Template.bind({});
|
||||
PrivacyBlocked1BothWithBlockedAt.args = {
|
||||
blockedContacts: [
|
||||
{ conversation: getDefaultConversation(), blockedAt: Date.now() },
|
||||
],
|
||||
blockedGroups: [
|
||||
{ conversation: getDefaultConversation(), blockedAt: Date.now() - DAY },
|
||||
],
|
||||
settingsLocation: { page: SettingsPage.Privacy },
|
||||
};
|
||||
|
||||
export const PrivacyBlockedManyContacts = Template.bind({});
|
||||
PrivacyBlockedManyContacts.args = {
|
||||
blockedContacts: new Array(55)
|
||||
.fill(undefined)
|
||||
.map(() => getDefaultConversation()),
|
||||
blockedContacts: new Array(55).fill(undefined).map(() => ({
|
||||
conversation: getDefaultConversation(),
|
||||
blockedAt: undefined,
|
||||
})),
|
||||
settingsLocation: { page: SettingsPage.Privacy },
|
||||
};
|
||||
|
||||
export const PrivacyBlockedManyGroups = Template.bind({});
|
||||
PrivacyBlockedManyGroups.args = {
|
||||
blockedGroups: new Array(55)
|
||||
.fill(undefined)
|
||||
.map(() => getDefaultConversation()),
|
||||
blockedGroups: new Array(55).fill(undefined).map(() => ({
|
||||
conversation: getDefaultConversation(),
|
||||
blockedAt: undefined,
|
||||
})),
|
||||
settingsLocation: { page: SettingsPage.Privacy },
|
||||
};
|
||||
|
||||
export const PrivacyBlockedManyBoth = Template.bind({});
|
||||
PrivacyBlockedManyBoth.args = {
|
||||
blockedContacts: new Array(20)
|
||||
.fill(undefined)
|
||||
.map(() => getDefaultConversation()),
|
||||
blockedGroups: new Array(20)
|
||||
.fill(undefined)
|
||||
.map(() => getDefaultConversation()),
|
||||
blockedContacts: new Array(20).fill(undefined).map(() => ({
|
||||
conversation: getDefaultConversation(),
|
||||
blockedAt: undefined,
|
||||
})),
|
||||
blockedGroups: new Array(20).fill(undefined).map(() => ({
|
||||
conversation: getDefaultConversation(),
|
||||
blockedAt: undefined,
|
||||
})),
|
||||
settingsLocation: { page: SettingsPage.Privacy },
|
||||
};
|
||||
|
||||
|
||||
@@ -103,12 +103,18 @@ import { TitlebarDragArea } from './TitlebarDragArea.dom.tsx';
|
||||
import type { PreferredBadgeSelectorType } from '../state/selectors/badges.preload.ts';
|
||||
import { Emoji } from '../axo/emoji.std.ts';
|
||||
import { AxoConfirmDialog } from '../axo/AxoConfirmDialog.dom.tsx';
|
||||
import moment from 'moment';
|
||||
|
||||
const { isNumber, noop, partition } = lodash;
|
||||
|
||||
type CheckboxChangeHandlerType = (value: boolean) => unknown;
|
||||
type SelectChangeHandlerType<T = string | number> = (value: T) => unknown;
|
||||
|
||||
export type BlockedConversation = {
|
||||
conversation: ConversationType;
|
||||
blockedAt: number | undefined;
|
||||
};
|
||||
|
||||
export type PropsDataType = {
|
||||
// Settings
|
||||
backupKey: string | undefined;
|
||||
@@ -127,8 +133,8 @@ export type PropsDataType = {
|
||||
pauseBackupMediaDownload: VoidFunction;
|
||||
cancelBackupMediaDownload: VoidFunction;
|
||||
resumeBackupMediaDownload: VoidFunction;
|
||||
blockedContacts: ReadonlyArray<ConversationType>;
|
||||
blockedGroups: ReadonlyArray<ConversationType>;
|
||||
blockedContacts: ReadonlyArray<BlockedConversation>;
|
||||
blockedGroups: ReadonlyArray<BlockedConversation>;
|
||||
customColors: Record<string, CustomColorType>;
|
||||
defaultConversationColor: DefaultConversationColorType;
|
||||
deviceName?: string;
|
||||
@@ -2252,20 +2258,29 @@ export function Preferences({
|
||||
</SettingsRow>
|
||||
{blockedContacts.length > 0 ? (
|
||||
<SettingsRow title={i18n('icu:Preferences--blocked-users')}>
|
||||
{blockedContacts.map(item => {
|
||||
{blockedContacts.map(({ conversation, blockedAt }) => {
|
||||
return (
|
||||
<div className={tw('flex w-full items-center px-[14px]')}>
|
||||
<div className={tw('p-2')}>
|
||||
<Avatar
|
||||
conversationType={item.type}
|
||||
badge={getPreferredBadge(item.badges)}
|
||||
conversationType={conversation.type}
|
||||
badge={getPreferredBadge(conversation.badges)}
|
||||
i18n={i18n}
|
||||
size={AvatarSize.THIRTY_SIX}
|
||||
theme={theme}
|
||||
{...item}
|
||||
{...conversation}
|
||||
/>
|
||||
</div>
|
||||
<div>{item.title}</div>
|
||||
<div className={tw('flex flex-col')}>
|
||||
<div>{conversation.title}</div>
|
||||
{isNumber(blockedAt) && blockedAt > 0 ? (
|
||||
<div className={tw('type-body-small text-secondary')}>
|
||||
{i18n('icu:Preferences--blocked--blocked-on', {
|
||||
blockedAt: moment(blockedAt).format('ll'),
|
||||
})}
|
||||
</div>
|
||||
) : undefined}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
@@ -2273,20 +2288,29 @@ export function Preferences({
|
||||
) : undefined}
|
||||
{blockedGroups.length > 0 ? (
|
||||
<SettingsRow title={i18n('icu:Preferences--blocked-groups')}>
|
||||
{blockedGroups.map(item => {
|
||||
{blockedGroups.map(({ conversation, blockedAt }) => {
|
||||
return (
|
||||
<div className={tw('flex w-full items-center px-[14px]')}>
|
||||
<div className={tw('p-2')}>
|
||||
<Avatar
|
||||
conversationType={item.type}
|
||||
badge={getPreferredBadge(item.badges)}
|
||||
conversationType={conversation.type}
|
||||
badge={getPreferredBadge(conversation.badges)}
|
||||
i18n={i18n}
|
||||
size={AvatarSize.THIRTY_SIX}
|
||||
theme={theme}
|
||||
{...item}
|
||||
{...conversation}
|
||||
/>
|
||||
</div>{' '}
|
||||
<div className={tw('flex flex-col')}>
|
||||
<div>{conversation.title}</div>
|
||||
{isNumber(blockedAt) && blockedAt > 0 ? (
|
||||
<div className={tw('type-body-small text-secondary')}>
|
||||
{i18n('icu:Preferences--blocked--blocked-on', {
|
||||
blockedAt: moment(blockedAt).format('ll'),
|
||||
})}
|
||||
</div>
|
||||
) : undefined}{' '}
|
||||
</div>
|
||||
<div>{item.title}</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -2518,7 +2518,7 @@ export async function initiateMigrationToGroupV2(
|
||||
});
|
||||
|
||||
if (itemStorage.blocked.isGroupBlocked(previousGroupV1Id)) {
|
||||
await itemStorage.blocked.addBlockedGroup(groupId);
|
||||
await itemStorage.blocked.addBlockedGroup(groupId, undefined);
|
||||
}
|
||||
|
||||
// Save these most recent updates to conversation
|
||||
@@ -2862,7 +2862,7 @@ export async function respondToGroupV2Migration({
|
||||
);
|
||||
|
||||
if (itemStorage.blocked.isGroupBlocked(previousGroupV1Id)) {
|
||||
await itemStorage.blocked.addBlockedGroup(groupId);
|
||||
await itemStorage.blocked.addBlockedGroup(groupId, undefined);
|
||||
}
|
||||
|
||||
if (wereWePreviouslyAMember) {
|
||||
@@ -3001,7 +3001,7 @@ export async function respondToGroupV2Migration({
|
||||
});
|
||||
|
||||
if (itemStorage.blocked.isGroupBlocked(previousGroupV1Id)) {
|
||||
await itemStorage.blocked.addBlockedGroup(groupId);
|
||||
await itemStorage.blocked.addBlockedGroup(groupId, undefined);
|
||||
}
|
||||
|
||||
// Save these most recent updates to conversation
|
||||
|
||||
@@ -73,6 +73,7 @@ export async function onResponse(
|
||||
receivedAtCounter,
|
||||
receivedAtMs,
|
||||
timestamp: sentAt,
|
||||
blockedAt: sentAt,
|
||||
})
|
||||
);
|
||||
|
||||
|
||||
@@ -1004,7 +1004,13 @@ export class ConversationModel {
|
||||
return isBlocked(this.attributes);
|
||||
}
|
||||
|
||||
block({ viaStorageServiceSync = false } = {}): void {
|
||||
block({
|
||||
viaStorageServiceSync,
|
||||
timestamp,
|
||||
}: {
|
||||
viaStorageServiceSync: boolean;
|
||||
timestamp: number | undefined;
|
||||
}): void {
|
||||
if (isMe(this.attributes)) {
|
||||
log.error(`${this.idForLogging()}: Refusing to block Note to Self`);
|
||||
return;
|
||||
@@ -1015,22 +1021,22 @@ export class ConversationModel {
|
||||
|
||||
const serviceId = this.getServiceId();
|
||||
if (isSignalConversation(this)) {
|
||||
drop(itemStorage.blocked.setReleaseNotesChatBlocked(true));
|
||||
drop(itemStorage.blocked.setReleaseNotesChatBlocked(true, timestamp));
|
||||
blocked = true;
|
||||
} else if (serviceId && isAciString(serviceId)) {
|
||||
drop(itemStorage.blocked.addBlockedServiceId(serviceId));
|
||||
drop(itemStorage.blocked.addBlockedServiceId(serviceId, timestamp));
|
||||
blocked = true;
|
||||
}
|
||||
|
||||
const e164 = this.get('e164');
|
||||
if (e164) {
|
||||
drop(itemStorage.blocked.addBlockedNumber(e164));
|
||||
drop(itemStorage.blocked.addBlockedNumber(e164, timestamp));
|
||||
blocked = true;
|
||||
}
|
||||
|
||||
const groupId = this.get('groupId');
|
||||
if (groupId) {
|
||||
drop(itemStorage.blocked.addBlockedGroup(groupId));
|
||||
drop(itemStorage.blocked.addBlockedGroup(groupId, timestamp));
|
||||
blocked = true;
|
||||
}
|
||||
|
||||
@@ -1049,7 +1055,7 @@ export class ConversationModel {
|
||||
|
||||
const serviceId = this.getServiceId();
|
||||
if (serviceId && isSignalServiceId(serviceId)) {
|
||||
drop(itemStorage.blocked.setReleaseNotesChatBlocked(false));
|
||||
drop(itemStorage.blocked.setReleaseNotesChatBlocked(false, undefined));
|
||||
unblocked = true;
|
||||
} else if (serviceId && isAciString(serviceId)) {
|
||||
drop(itemStorage.blocked.removeBlockedServiceId(serviceId));
|
||||
@@ -1117,10 +1123,11 @@ export class ConversationModel {
|
||||
? {
|
||||
source: MessageRequestResponseSource.STORAGE_SERVICE,
|
||||
learnedAtMs: Date.now(),
|
||||
blockedAt: undefined,
|
||||
}
|
||||
: {
|
||||
source: MessageRequestResponseSource.LOCAL,
|
||||
timestamp: Date.now(),
|
||||
blockedAt: Date.now(),
|
||||
},
|
||||
{ shouldSave: false }
|
||||
);
|
||||
@@ -2198,8 +2205,24 @@ export class ConversationModel {
|
||||
return;
|
||||
}
|
||||
|
||||
const wasBlockedByNumber = oldValue
|
||||
? itemStorage.blocked.getBlockedNumbers().get(oldValue)
|
||||
: undefined;
|
||||
const serviceId = this.get('serviceId');
|
||||
const wasBlockedByServiceId = serviceId
|
||||
? itemStorage.blocked.getBlockedServiceIds().get(serviceId)
|
||||
: undefined;
|
||||
|
||||
this.set({ e164: e164 || undefined });
|
||||
|
||||
if (wasBlockedByNumber || wasBlockedByServiceId) {
|
||||
this.block({
|
||||
viaStorageServiceSync: false,
|
||||
timestamp:
|
||||
wasBlockedByNumber?.blockedAt ?? wasBlockedByServiceId?.blockedAt,
|
||||
});
|
||||
}
|
||||
|
||||
// This user changed their phone number
|
||||
if (oldValue && e164) {
|
||||
void this.addChangeNumberNotification(oldValue, e164);
|
||||
@@ -2216,11 +2239,27 @@ export class ConversationModel {
|
||||
return;
|
||||
}
|
||||
|
||||
const wasBlockedByServiceId = oldValue
|
||||
? itemStorage.blocked.getBlockedServiceIds().get(oldValue)
|
||||
: undefined;
|
||||
const e164 = this.get('e164');
|
||||
const wasBlockedByNumber = e164
|
||||
? itemStorage.blocked.getBlockedNumbers().get(e164)
|
||||
: undefined;
|
||||
|
||||
this.set({
|
||||
serviceId: serviceId
|
||||
? normalizeServiceId(serviceId, 'Conversation.updateServiceId')
|
||||
: undefined,
|
||||
});
|
||||
|
||||
if (wasBlockedByServiceId || wasBlockedByNumber) {
|
||||
this.block({
|
||||
viaStorageServiceSync: false,
|
||||
timestamp:
|
||||
wasBlockedByNumber?.blockedAt ?? wasBlockedByServiceId?.blockedAt,
|
||||
});
|
||||
}
|
||||
drop(DataWriter.updateConversation(this.attributes));
|
||||
window.ConversationController.idUpdated(this, 'serviceId', oldValue);
|
||||
|
||||
@@ -2464,9 +2503,9 @@ export class ConversationModel {
|
||||
const { source } = responseInfo;
|
||||
switch (source) {
|
||||
case MessageRequestResponseSource.LOCAL:
|
||||
receivedAtMs = responseInfo.timestamp;
|
||||
receivedAtMs = responseInfo.blockedAt;
|
||||
receivedAtCounter = incrementMessageCounter();
|
||||
timestamp = responseInfo.timestamp;
|
||||
timestamp = responseInfo.blockedAt;
|
||||
break;
|
||||
case MessageRequestResponseSource.MRR_SYNC:
|
||||
receivedAtMs = responseInfo.receivedAtMs;
|
||||
@@ -2607,7 +2646,10 @@ export class ConversationModel {
|
||||
isSpam?: boolean;
|
||||
}) => {
|
||||
if (isBlock) {
|
||||
this.block({ viaStorageServiceSync });
|
||||
this.block({
|
||||
viaStorageServiceSync,
|
||||
timestamp: responseInfo.blockedAt,
|
||||
});
|
||||
}
|
||||
|
||||
if (isBlock || isDelete) {
|
||||
|
||||
@@ -1324,6 +1324,10 @@ export class BackupExportStream extends Readable {
|
||||
|
||||
strictAssert(recipientId != null, 'recipientId must exist');
|
||||
|
||||
const blockedItem = convo.serviceId
|
||||
? itemStorage.blocked.getBlockedServiceIds().get(convo.serviceId)
|
||||
: undefined;
|
||||
|
||||
return {
|
||||
id: recipientId,
|
||||
destination: {
|
||||
@@ -1332,8 +1336,9 @@ export class BackupExportStream extends Readable {
|
||||
pni,
|
||||
e164,
|
||||
username: convo.username || null,
|
||||
blocked: convo.serviceId
|
||||
? itemStorage.blocked.isServiceIdBlocked(convo.serviceId)
|
||||
blocked: Boolean(blockedItem),
|
||||
blockedAtTimestamp: blockedItem?.blockedAt
|
||||
? BigInt(blockedItem.blockedAt)
|
||||
: null,
|
||||
visibility,
|
||||
registration: convo.discoveredUnregisteredAt
|
||||
@@ -1399,6 +1404,11 @@ export class BackupExportStream extends Readable {
|
||||
const recipientId = this.#getNewRecipientId({
|
||||
id: convo.id,
|
||||
});
|
||||
|
||||
const blockedItem = convo.groupId
|
||||
? itemStorage.blocked.getBlockedGroups().get(convo.groupId)
|
||||
: undefined;
|
||||
|
||||
return {
|
||||
id: recipientId,
|
||||
destination: {
|
||||
@@ -1407,9 +1417,10 @@ export class BackupExportStream extends Readable {
|
||||
whitelisted: convo.profileSharing ?? null,
|
||||
hideStory: convo.hideStory === true,
|
||||
storySendMode,
|
||||
blocked: convo.groupId
|
||||
? itemStorage.blocked.isGroupBlocked(convo.groupId)
|
||||
: false,
|
||||
blocked: Boolean(blockedItem),
|
||||
blockedAtTimestamp: blockedItem?.blockedAt
|
||||
? BigInt(blockedItem.blockedAt)
|
||||
: null,
|
||||
avatarColor: toAvatarColor(convo.color) ?? null,
|
||||
snapshot: {
|
||||
title: {
|
||||
|
||||
@@ -1171,10 +1171,20 @@ export class BackupImportStream extends Writable {
|
||||
|
||||
if (contact.blocked) {
|
||||
if (serviceId) {
|
||||
await itemStorage.blocked.addBlockedServiceId(serviceId);
|
||||
await itemStorage.blocked.addBlockedServiceId(
|
||||
serviceId,
|
||||
contact.blockedAtTimestamp
|
||||
? getCheckedTimestampFromLong(contact.blockedAtTimestamp)
|
||||
: undefined
|
||||
);
|
||||
}
|
||||
if (e164) {
|
||||
await itemStorage.blocked.addBlockedNumber(e164);
|
||||
await itemStorage.blocked.addBlockedNumber(
|
||||
e164,
|
||||
contact.blockedAtTimestamp
|
||||
? getCheckedTimestampFromLong(contact.blockedAtTimestamp)
|
||||
: undefined
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1355,7 +1365,12 @@ export class BackupImportStream extends Writable {
|
||||
};
|
||||
|
||||
if (group.blocked) {
|
||||
await itemStorage.blocked.addBlockedGroup(groupId);
|
||||
await itemStorage.blocked.addBlockedGroup(
|
||||
groupId,
|
||||
group.blockedAtTimestamp
|
||||
? getCheckedTimestampFromLong(group.blockedAtTimestamp)
|
||||
: undefined
|
||||
);
|
||||
}
|
||||
|
||||
return attrs;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Copyright 2020 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import lodash, { omit, partition, without } from 'lodash';
|
||||
import lodash, { isNumber, omit, partition, without } from 'lodash';
|
||||
|
||||
import { ServiceId } from '@signalapp/libsignal-client';
|
||||
import { uuidToBytes, bytesToUuid } from '../util/uuidToBytes.std.ts';
|
||||
@@ -159,6 +159,13 @@ export type MergeResultType = Readonly<{
|
||||
details: ReadonlyArray<string>;
|
||||
}>;
|
||||
|
||||
function makeBigInt(value: number | undefined): bigint | undefined {
|
||||
if (!isNumber(value)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return BigInt(value);
|
||||
}
|
||||
function toRecordVerified(verified: number): Proto.ContactRecord.IdentityState {
|
||||
const VERIFIED_ENUM = signalProtocolStore.VerifiedStatus;
|
||||
const STATE_ENUM = Proto.ContactRecord.IdentityState;
|
||||
@@ -304,6 +311,7 @@ export async function toContactRecord(
|
||||
const username = conversation.get('username');
|
||||
const ourID = window.ConversationController.getOurConversationId();
|
||||
const pni = conversation.getPni();
|
||||
const e164 = conversation.get('e164');
|
||||
|
||||
const profileKey = conversation.get('profileKey');
|
||||
const serviceId = aci ?? pni;
|
||||
@@ -312,8 +320,15 @@ export async function toContactRecord(
|
||||
const nicknameFamilyName = conversation.get('nicknameFamilyName');
|
||||
const hideStory = conversation.get('hideStory');
|
||||
|
||||
const blockedServiceId = serviceId
|
||||
? itemStorage.blocked.getBlockedServiceIds().get(serviceId)
|
||||
: undefined;
|
||||
const blockedE164 = e164
|
||||
? itemStorage.blocked.getBlockedNumbers().get(e164)
|
||||
: undefined;
|
||||
|
||||
return {
|
||||
e164: conversation.get('e164') ?? null,
|
||||
e164: e164 ?? null,
|
||||
aciBinary:
|
||||
isProtoBinaryEncodingEnabled() && aci
|
||||
? toAciObject(aci).getRawUuidBytes()
|
||||
@@ -347,7 +362,11 @@ export async function toContactRecord(
|
||||
systemGivenName: conversation.get('systemGivenName') || null,
|
||||
systemFamilyName: conversation.get('systemFamilyName') || null,
|
||||
systemNickname: conversation.get('systemNickname') || null,
|
||||
blocked: conversation.isBlocked(),
|
||||
blocked: Boolean(blockedServiceId || blockedE164),
|
||||
blockedAtTimestamp:
|
||||
makeBigInt(blockedServiceId?.blockedAt) ??
|
||||
makeBigInt(blockedE164?.blockedAt) ??
|
||||
null,
|
||||
hidden: conversation.get('removalStage') !== undefined,
|
||||
whitelisted: Boolean(conversation.get('profileSharing')),
|
||||
archived: Boolean(conversation.get('isArchived')),
|
||||
@@ -542,6 +561,11 @@ export function toAccountRecord({
|
||||
storyViewReceiptsEnabledValue = Proto.OptionalBool.UNSET;
|
||||
}
|
||||
|
||||
const releaseNotesChatBlocked = signalConversation?.isBlocked() ?? null;
|
||||
const releaseNotesChatBlockedAt = releaseNotesChatBlocked
|
||||
? itemStorage.blocked.whenWasReleaseNotesChatBlocked()
|
||||
: undefined;
|
||||
|
||||
return {
|
||||
profileKey: profileKey ? Bytes.fromBase64(profileKey) : null,
|
||||
givenName: ourConversation.get('profileName') || null,
|
||||
@@ -617,7 +641,10 @@ export function toAccountRecord({
|
||||
signalConversation?.get('muteExpiresAt'),
|
||||
MAX_VALUE
|
||||
),
|
||||
releaseNotesChatBlocked: signalConversation?.isBlocked() ?? null,
|
||||
releaseNotesChatBlocked,
|
||||
releaseNotesChatBlockedAt: releaseNotesChatBlockedAt
|
||||
? BigInt(releaseNotesChatBlockedAt)
|
||||
: null,
|
||||
|
||||
$unknown: conversationUnknownFieldsToRecord(ourConversation),
|
||||
};
|
||||
@@ -650,13 +677,19 @@ export function toGroupV2Record(
|
||||
throw missingCaseError(localStorySendMode);
|
||||
}
|
||||
|
||||
const groupId = conversation.get('groupId');
|
||||
const avatarColor = conversation.get('colorFromPrimary');
|
||||
const masterKey = conversation.get('masterKey');
|
||||
const verifiedNameHash = conversation.get('groupVerifiedNameHash');
|
||||
|
||||
const blockedItem = groupId
|
||||
? itemStorage.blocked.getBlockedGroups().get(groupId)
|
||||
: undefined;
|
||||
|
||||
return {
|
||||
masterKey: masterKey != null ? Bytes.fromBase64(masterKey) : null,
|
||||
blocked: conversation.isBlocked(),
|
||||
blocked: Boolean(blockedItem),
|
||||
blockedAtTimestamp: makeBigInt(blockedItem?.blockedAt) ?? null,
|
||||
whitelisted: Boolean(conversation.get('profileSharing')),
|
||||
archived: Boolean(conversation.get('isArchived')),
|
||||
markedUnread: Boolean(conversation.get('markedUnread')),
|
||||
@@ -924,7 +957,11 @@ export function toNotificationProfileRecord(
|
||||
}
|
||||
|
||||
async function applyMessageRequestState(
|
||||
record: { blocked: boolean; whitelisted: boolean },
|
||||
record: {
|
||||
blocked: boolean;
|
||||
blockedAtTimestamp: bigint | undefined;
|
||||
whitelisted: boolean;
|
||||
},
|
||||
conversation: ConversationModel
|
||||
): Promise<void> {
|
||||
const messageRequestEnum = Proto.SyncMessage.MessageRequestResponse.Type;
|
||||
@@ -935,6 +972,7 @@ async function applyMessageRequestState(
|
||||
{
|
||||
source: MessageRequestResponseSource.STORAGE_SERVICE,
|
||||
learnedAtMs: Date.now(),
|
||||
blockedAt: dropNull(toNumber(record.blockedAtTimestamp)),
|
||||
},
|
||||
{ shouldSave: false }
|
||||
);
|
||||
@@ -946,6 +984,7 @@ async function applyMessageRequestState(
|
||||
{
|
||||
source: MessageRequestResponseSource.STORAGE_SERVICE,
|
||||
learnedAtMs: Date.now(),
|
||||
blockedAt: undefined,
|
||||
},
|
||||
{ shouldSave: false }
|
||||
);
|
||||
@@ -1601,6 +1640,7 @@ export async function mergeAccountRecord(
|
||||
automaticKeyVerificationDisabled,
|
||||
releaseNotesChatArchived,
|
||||
releaseNotesChatBlocked,
|
||||
releaseNotesChatBlockedAt,
|
||||
releaseNotesChatMarkedUnread,
|
||||
releaseNotesChatMutedUntilTimestamp,
|
||||
} = accountRecord;
|
||||
@@ -2056,6 +2096,7 @@ export async function mergeAccountRecord(
|
||||
{
|
||||
blocked: releaseNotesChatBlocked,
|
||||
whitelisted: !releaseNotesChatBlocked,
|
||||
blockedAtTimestamp: dropNull(releaseNotesChatBlockedAt),
|
||||
},
|
||||
signalConversation
|
||||
);
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
// Copyright 2026 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { getById, createOrUpdate } from '../util.std.ts';
|
||||
import { toLogFormat } from '../../types/errors.std.ts';
|
||||
|
||||
import type { ServiceIdString } from '../../types/ServiceId.std.ts';
|
||||
import type { WritableDB } from '../Interface.std.ts';
|
||||
import type { LoggerType } from '../../types/Logging.std.ts';
|
||||
|
||||
const ITEMS_TABLE = 'items';
|
||||
const BLOCKED_NUMBER_KEY = 'blocked';
|
||||
const BLOCKED_SERVICE_IDS_KEY = 'blocked-uuids';
|
||||
const BLOCKED_GROUP_KEY = 'blocked-groups';
|
||||
|
||||
// Old types
|
||||
|
||||
type OldBlockedNumberList = ReadonlyArray<string>;
|
||||
type OldBlockedServiceIdsList = ReadonlyArray<ServiceIdString>;
|
||||
type OldBlockedGroupList = ReadonlyArray<string>;
|
||||
|
||||
type Item<T> = {
|
||||
id: string;
|
||||
value: T;
|
||||
};
|
||||
|
||||
// New types:
|
||||
|
||||
type BlockedNumber = {
|
||||
blockedAt: number | undefined;
|
||||
e164: string;
|
||||
};
|
||||
type BlockedNumberList = ReadonlyArray<BlockedNumber>;
|
||||
|
||||
type BlockedServiceId = {
|
||||
blockedAt: number | undefined;
|
||||
serviceId: ServiceIdString;
|
||||
};
|
||||
type BlockedServiceIdList = ReadonlyArray<BlockedServiceId>;
|
||||
|
||||
type BlockedGroup = {
|
||||
blockedAt: number | undefined;
|
||||
groupId: string;
|
||||
};
|
||||
type BlockedGroupList = ReadonlyArray<BlockedGroup>;
|
||||
|
||||
export default function updateToSchemaVersion1770(
|
||||
db: WritableDB,
|
||||
logger: LoggerType
|
||||
): void {
|
||||
const logId = 'updateToSchemaVersion1770';
|
||||
|
||||
try {
|
||||
const blockedNumbers = getById(db, ITEMS_TABLE, BLOCKED_NUMBER_KEY) as Item<
|
||||
OldBlockedNumberList | undefined
|
||||
>;
|
||||
if (blockedNumbers?.value) {
|
||||
const updatedNumbers: BlockedNumberList = blockedNumbers.value.map(
|
||||
e164 => ({
|
||||
e164,
|
||||
blockedAt: undefined,
|
||||
})
|
||||
);
|
||||
|
||||
const item: Item<BlockedNumberList> = {
|
||||
id: BLOCKED_NUMBER_KEY,
|
||||
value: updatedNumbers,
|
||||
};
|
||||
createOrUpdate(db, ITEMS_TABLE, item);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
`${logId}: Failed to update '${BLOCKED_NUMBER_KEY}' item`,
|
||||
toLogFormat(error)
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
const blockedServiceIds = getById(
|
||||
db,
|
||||
ITEMS_TABLE,
|
||||
BLOCKED_SERVICE_IDS_KEY
|
||||
) as Item<OldBlockedServiceIdsList | undefined>;
|
||||
if (blockedServiceIds?.value) {
|
||||
const updatedServiceIds: BlockedServiceIdList =
|
||||
blockedServiceIds.value.map(serviceId => ({
|
||||
serviceId,
|
||||
blockedAt: undefined,
|
||||
}));
|
||||
|
||||
const item: Item<BlockedServiceIdList> = {
|
||||
id: BLOCKED_SERVICE_IDS_KEY,
|
||||
value: updatedServiceIds,
|
||||
};
|
||||
createOrUpdate(db, ITEMS_TABLE, item);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
`${logId}: Failed to update '${BLOCKED_SERVICE_IDS_KEY}' item`,
|
||||
toLogFormat(error)
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
const blockedGroups = getById(db, ITEMS_TABLE, BLOCKED_GROUP_KEY) as Item<
|
||||
OldBlockedGroupList | undefined
|
||||
>;
|
||||
if (blockedGroups?.value) {
|
||||
const updatedGroups: BlockedGroupList = blockedGroups.value.map(
|
||||
groupId => ({
|
||||
groupId,
|
||||
blockedAt: undefined,
|
||||
})
|
||||
);
|
||||
|
||||
const item: Item<BlockedGroupList> = {
|
||||
id: BLOCKED_GROUP_KEY,
|
||||
value: updatedGroups,
|
||||
};
|
||||
createOrUpdate(db, ITEMS_TABLE, item);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
`${logId}: Failed to update '${BLOCKED_GROUP_KEY}' item`,
|
||||
toLogFormat(error)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -153,6 +153,7 @@ import updateToSchemaVersion1730 from './1730-protected-attachments-dedupe-token
|
||||
import updateToSchemaVersion1740 from './1740-cleanup-groups.node.ts';
|
||||
import updateToSchemaVersion1750 from './1750-fts-url.std.ts';
|
||||
import updateToSchemaVersion1760 from './1760-delete-story-reply-attachment.std.ts';
|
||||
import updateToSchemaVersion1770 from './1770-add-blocked-at.std.ts';
|
||||
|
||||
import { DataWriter } from '../Server.node.ts';
|
||||
import { strictAssert } from '../../util/assert.std.ts';
|
||||
@@ -1662,6 +1663,7 @@ export const SCHEMA_VERSIONS: ReadonlyArray<SchemaUpdateType> = [
|
||||
{ version: 1670, update: updateToSchemaVersion1670 },
|
||||
{ version: 1680, update: updateToSchemaVersion1680 },
|
||||
{ version: 1690, update: updateToSchemaVersion1690 },
|
||||
|
||||
{ version: 1700, update: updateToSchemaVersion1700 },
|
||||
{ version: 1710, update: updateToSchemaVersion1710 },
|
||||
{ version: 1720, update: updateToSchemaVersion1720 },
|
||||
@@ -1669,6 +1671,7 @@ export const SCHEMA_VERSIONS: ReadonlyArray<SchemaUpdateType> = [
|
||||
{ version: 1740, update: updateToSchemaVersion1740 },
|
||||
{ version: 1750, update: updateToSchemaVersion1750 },
|
||||
{ version: 1760, update: updateToSchemaVersion1760 },
|
||||
{ version: 1770, update: updateToSchemaVersion1770 },
|
||||
];
|
||||
|
||||
class DBVersionFromFutureError extends Error {
|
||||
|
||||
@@ -3770,7 +3770,7 @@ async function syncMessageRequestResponse(
|
||||
response,
|
||||
{
|
||||
source: MessageRequestResponseSource.LOCAL,
|
||||
timestamp: Date.now(),
|
||||
blockedAt: Date.now(),
|
||||
},
|
||||
{ shouldSave }
|
||||
);
|
||||
@@ -4009,7 +4009,7 @@ function acceptConversation(
|
||||
messageRequestEnum.ACCEPT,
|
||||
{
|
||||
source: MessageRequestResponseSource.LOCAL,
|
||||
timestamp: Date.now(),
|
||||
blockedAt: Date.now(),
|
||||
},
|
||||
{ shouldSave: true }
|
||||
);
|
||||
@@ -4085,7 +4085,7 @@ function blockConversation(
|
||||
messageRequestEnum.BLOCK,
|
||||
{
|
||||
source: MessageRequestResponseSource.LOCAL,
|
||||
timestamp: Date.now(),
|
||||
blockedAt: Date.now(),
|
||||
},
|
||||
{ shouldSave: true }
|
||||
);
|
||||
|
||||
@@ -9,7 +9,6 @@ import type { MutableRefObject, JSX } from 'react';
|
||||
|
||||
import { useItemsActions } from '../ducks/items.preload.ts';
|
||||
import { useConversationsActions } from '../ducks/conversations.preload.ts';
|
||||
import type { ConversationType } from '../ducks/conversations.preload.ts';
|
||||
import {
|
||||
getConversationSelector,
|
||||
getConversationsWithCustomColorSelector,
|
||||
@@ -100,29 +99,30 @@ import {
|
||||
SmartNotificationProfilesCreateFlow,
|
||||
SmartNotificationProfilesHome,
|
||||
} from './PreferencesNotificationProfiles.preload.tsx';
|
||||
|
||||
import type { SettingsLocation } from '../../types/Nav.std.ts';
|
||||
import type { StorageAccessType } from '../../types/Storage.d.ts';
|
||||
import type { ThemeType } from '../../util/preload.preload.ts';
|
||||
import type { WidthBreakpoint } from '../../components/_util.std.ts';
|
||||
import { isLocalBackupsEnabled } from '../../util/isLocalBackupsEnabled.preload.ts';
|
||||
import { getBackupKeyHash } from '../../services/backups/crypto.preload.ts';
|
||||
import { Emoji } from '../../axo/emoji.std.ts';
|
||||
import { AppProvider } from '../../windows/AppProvider.dom.tsx';
|
||||
import { useMegaphonesActions } from '../ducks/megaphones.preload.ts';
|
||||
import { DialogType } from '../../types/Dialogs.std.ts';
|
||||
import { promptOSAuth } from '../../util/promptOSAuth.preload.ts';
|
||||
import type { StateType } from '../reducer.preload.ts';
|
||||
import {
|
||||
pauseBackupMediaDownload,
|
||||
resumeBackupMediaDownload,
|
||||
cancelBackupMediaDownload,
|
||||
} from '../../util/backupMediaDownload.preload.ts';
|
||||
|
||||
import type { SettingsLocation } from '../../types/Nav.std.ts';
|
||||
import type { StorageAccessType } from '../../types/Storage.d.ts';
|
||||
import type { ThemeType } from '../../util/preload.preload.ts';
|
||||
import type { WidthBreakpoint } from '../../components/_util.std.ts';
|
||||
import type { StateType } from '../reducer.preload.ts';
|
||||
import { DonationsErrorBoundary } from '../../components/DonationsErrorBoundary.dom.tsx';
|
||||
import type { SmartPreferencesChatFoldersPageProps } from './PreferencesChatFoldersPage.preload.tsx';
|
||||
import type { SmartPreferencesEditChatFolderPageProps } from './PreferencesEditChatFolderPage.preload.tsx';
|
||||
import type { ExternalProps as SmartNotificationProfilesProps } from './PreferencesNotificationProfiles.preload.tsx';
|
||||
import { useMegaphonesActions } from '../ducks/megaphones.preload.ts';
|
||||
import type { ZoomFactorType } from '../../types/StorageKeys.std.ts';
|
||||
import { isLocalBackupsEnabled } from '../../util/isLocalBackupsEnabled.preload.ts';
|
||||
import { getBackupKeyHash } from '../../services/backups/crypto.preload.ts';
|
||||
import { Emoji } from '../../axo/emoji.std.ts';
|
||||
import { AppProvider } from '../../windows/AppProvider.dom.tsx';
|
||||
import type { BlockedConversation } from '../../components/Preferences.dom.tsx';
|
||||
|
||||
const DEFAULT_NOTIFICATION_SETTING = 'message';
|
||||
|
||||
@@ -591,29 +591,43 @@ export function SmartPreferences(): JSX.Element | null {
|
||||
const defaultConversationColor =
|
||||
items.defaultConversationColor || DEFAULT_CONVERSATION_COLOR;
|
||||
|
||||
const blockedContacts: Array<ConversationType> = useMemo(() => {
|
||||
const result = new Set<ConversationType>();
|
||||
const blockedContacts: Array<BlockedConversation> = useMemo(() => {
|
||||
const result = new Map<string, BlockedConversation>();
|
||||
|
||||
(items['blocked-uuids'] ?? []).forEach(item => {
|
||||
result.add(conversationSelector(item));
|
||||
const conversation = conversationSelector(item.serviceId);
|
||||
result.set(conversation.id, {
|
||||
conversation,
|
||||
blockedAt: item.blockedAt,
|
||||
});
|
||||
});
|
||||
(items.blocked ?? []).forEach(item => {
|
||||
const conversation = conversationSelector(item);
|
||||
if (!result.has(conversation)) {
|
||||
result.add(conversation);
|
||||
const conversation = conversationSelector(item.e164);
|
||||
if (!result.has(conversation.id)) {
|
||||
result.set(conversation.id, {
|
||||
conversation,
|
||||
blockedAt: item.blockedAt,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
if (items.releaseNotesChatBlocked) {
|
||||
result.add(conversationSelector(SIGNAL_ACI));
|
||||
const conversation = conversationSelector(SIGNAL_ACI);
|
||||
result.set(conversation.id, {
|
||||
conversation,
|
||||
blockedAt: undefined /* TODO */,
|
||||
});
|
||||
}
|
||||
|
||||
return Array.from(result);
|
||||
return Array.from(result.values());
|
||||
}, [items, conversationSelector]);
|
||||
const blockedGroups: Array<ConversationType> = useMemo(() => {
|
||||
const result: Array<ConversationType> = [];
|
||||
const blockedGroups: Array<BlockedConversation> = useMemo(() => {
|
||||
const result: Array<BlockedConversation> = [];
|
||||
(items['blocked-groups'] ?? []).forEach(item => {
|
||||
result.push(conversationSelector(item));
|
||||
result.push({
|
||||
conversation: conversationSelector(item.groupId),
|
||||
blockedAt: item.blockedAt,
|
||||
});
|
||||
});
|
||||
return result;
|
||||
}, [items, conversationSelector]);
|
||||
|
||||
@@ -2,14 +2,15 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { assert } from 'chai';
|
||||
import sinon from 'sinon';
|
||||
import { v4 as generateUuid } from 'uuid';
|
||||
import { Aci } from '@signalapp/libsignal-client';
|
||||
|
||||
import MessageReceiver from '../textsecure/MessageReceiver.preload.ts';
|
||||
import {
|
||||
IncomingWebSocketRequest,
|
||||
ServerRequestType,
|
||||
} from '../textsecure/WebsocketResources.preload.ts';
|
||||
import type { DecryptionErrorEvent } from '../textsecure/messageReceiverEvents.std.ts';
|
||||
import type { AciString } from '../types/ServiceId.std.ts';
|
||||
import { toAciObject } from '../util/ServiceId.node.ts';
|
||||
import { SignalService as Proto } from '../protobuf/index.std.ts';
|
||||
import * as Crypto from '../Crypto.node.ts';
|
||||
@@ -17,6 +18,25 @@ import { toBase64 } from '../Bytes.std.ts';
|
||||
import { signalProtocolStore } from '../SignalProtocolStore.preload.ts';
|
||||
import { itemStorage } from '../textsecure/Storage.preload.ts';
|
||||
import { generateAci } from '../test-helpers/serviceIdUtils.std.ts';
|
||||
import { DataWriter } from '../sql/Client.preload.ts';
|
||||
|
||||
import type { DecryptionErrorEvent } from '../textsecure/messageReceiverEvents.std.ts';
|
||||
import {
|
||||
normalizePni,
|
||||
normalizeServiceId,
|
||||
type AciString,
|
||||
} from '../types/ServiceId.std.ts';
|
||||
import type { ProcessedEnvelope } from '../textsecure/Types.d.ts';
|
||||
import type { ConversationModel } from '../models/conversations.preload.ts';
|
||||
import type {
|
||||
ConversationAttributesType,
|
||||
ConversationAttributesTypeType,
|
||||
} from '../model-types.d.ts';
|
||||
import {
|
||||
ReceivedTimestampMs,
|
||||
SentTimestampMs,
|
||||
ServerTimestampMs,
|
||||
} from '@signalapp/types';
|
||||
|
||||
describe('MessageReceiver', () => {
|
||||
const someAci = generateAci();
|
||||
@@ -25,6 +45,13 @@ describe('MessageReceiver', () => {
|
||||
let oldAci: AciString | undefined;
|
||||
let oldDeviceId: number | undefined;
|
||||
|
||||
const fakeTrustRootPublicKey = Crypto.getRandomBytes(33);
|
||||
fakeTrustRootPublicKey.set([5], 0); // first byte is the key type (5)
|
||||
|
||||
before(async () => {
|
||||
await window.ConversationController.load();
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
oldAci = itemStorage.user.getAci();
|
||||
oldDeviceId = itemStorage.user.getDeviceId();
|
||||
@@ -41,9 +68,6 @@ describe('MessageReceiver', () => {
|
||||
|
||||
describe('connecting', () => {
|
||||
it('generates decryption-error event when it cannot decrypt', async () => {
|
||||
const fakeTrustRootPublicKey = Crypto.getRandomBytes(33);
|
||||
fakeTrustRootPublicKey.set([5], 0); // first byte is the key type (5)
|
||||
|
||||
const messageReceiver = new MessageReceiver({
|
||||
storage: itemStorage,
|
||||
serverTrustRoots: [toBase64(fakeTrustRootPublicKey)],
|
||||
@@ -96,4 +120,356 @@ describe('MessageReceiver', () => {
|
||||
await messageReceiver.drain();
|
||||
});
|
||||
});
|
||||
|
||||
describe('handleBlocked', () => {
|
||||
const now = Date.now();
|
||||
|
||||
const ME_E164 = '+18005551110';
|
||||
const E164_1 = '+18005551111';
|
||||
const E164_2 = '+18005551112';
|
||||
const E164_3 = '+18005551113';
|
||||
const ME_UUID = generateUuid();
|
||||
const UUID_1 = generateUuid();
|
||||
const UUID_2 = generateUuid();
|
||||
const UUID_3 = generateUuid();
|
||||
const GROUP_1 = Crypto.getRandomBytes(32).toBase64();
|
||||
const GROUP_2 = Crypto.getRandomBytes(32).toBase64();
|
||||
const GROUP_3 = Crypto.getRandomBytes(32).toBase64();
|
||||
|
||||
function addConversation(
|
||||
identifier: string,
|
||||
type: ConversationAttributesTypeType = 'private',
|
||||
additionalAttributes?: Partial<ConversationAttributesType>
|
||||
): ConversationModel {
|
||||
const conversation = window.ConversationController.getOrCreate(
|
||||
identifier,
|
||||
type,
|
||||
additionalAttributes
|
||||
);
|
||||
conversation.applyMessageRequestResponse = sinon.spy();
|
||||
return conversation;
|
||||
}
|
||||
|
||||
beforeEach(async () => {
|
||||
await DataWriter._removeAllConversations();
|
||||
window.ConversationController.reset();
|
||||
await window.ConversationController.load();
|
||||
|
||||
await DataWriter.removeAllItems();
|
||||
itemStorage.reset();
|
||||
await itemStorage.fetch();
|
||||
|
||||
const e1 = addConversation(E164_1);
|
||||
e1.block({ viaStorageServiceSync: false, timestamp: now + 1 });
|
||||
const e2 = addConversation(E164_2);
|
||||
e2.block({ viaStorageServiceSync: false, timestamp: now + 2 });
|
||||
addConversation(E164_3);
|
||||
|
||||
const u1 = addConversation(UUID_1);
|
||||
u1.block({ viaStorageServiceSync: false, timestamp: now + 10 + 1 });
|
||||
const u2 = addConversation(UUID_2);
|
||||
u2.block({ viaStorageServiceSync: false, timestamp: now + 10 + 2 });
|
||||
addConversation(UUID_3);
|
||||
|
||||
const g1 = addConversation(GROUP_1, 'group');
|
||||
g1.block({ viaStorageServiceSync: false, timestamp: now + 20 + 1 });
|
||||
const g2 = addConversation(GROUP_2, 'group');
|
||||
g2.block({ viaStorageServiceSync: false, timestamp: now + 20 + 2 });
|
||||
addConversation(GROUP_3, 'group');
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await DataWriter._removeAllConversations();
|
||||
window.ConversationController.reset();
|
||||
await window.ConversationController.load();
|
||||
|
||||
await DataWriter.removeAllItems();
|
||||
itemStorage.reset();
|
||||
await itemStorage.fetch();
|
||||
});
|
||||
|
||||
it('handles modern fields', async () => {
|
||||
const messageReceiver = new MessageReceiver({
|
||||
storage: itemStorage,
|
||||
serverTrustRoots: [toBase64(fakeTrustRootPublicKey)],
|
||||
});
|
||||
|
||||
const processedEnvelope: ProcessedEnvelope = {
|
||||
id: generateUuid(),
|
||||
receivedAtCounter: 1,
|
||||
receivedAtDate: ReceivedTimestampMs.fromNumber(now - 1),
|
||||
messageAgeSec: 1,
|
||||
|
||||
type: Proto.Envelope.Type.DOUBLE_RATCHET,
|
||||
source: ME_E164,
|
||||
sourceServiceId: normalizeServiceId(ME_UUID, 'test1'),
|
||||
sourceDevice: 1,
|
||||
destinationServiceId: normalizeServiceId(ME_UUID, 'test2'),
|
||||
updatedPni: normalizePni(generateUuid(), 'test3'),
|
||||
timestamp: SentTimestampMs.fromNumber(now - 2),
|
||||
content: Crypto.getRandomBytes(200),
|
||||
serverGuid: generateUuid(),
|
||||
serverTimestamp: ServerTimestampMs.fromNumber(now - 3),
|
||||
groupId: undefined,
|
||||
urgent: false,
|
||||
story: false,
|
||||
reportingToken: undefined,
|
||||
};
|
||||
const blocked: Proto.SyncMessage.Blocked = {
|
||||
numbers: [],
|
||||
acis: [],
|
||||
groupIds: [],
|
||||
acisBinary: [],
|
||||
blockedE164s: [
|
||||
{ e164: E164_1, timestamp: BigInt(now + 30 + 1), $unknown: [] },
|
||||
{ e164: E164_3, timestamp: BigInt(now + 30 + 3), $unknown: [] },
|
||||
],
|
||||
blockedAcis: [
|
||||
{
|
||||
aciBinary: Aci.parseFromServiceIdString(UUID_1).getRawUuidBytes(),
|
||||
timestamp: BigInt(now + 40 + 1),
|
||||
$unknown: [],
|
||||
},
|
||||
{
|
||||
aciBinary: Aci.parseFromServiceIdString(UUID_3).getRawUuidBytes(),
|
||||
timestamp: BigInt(now + 40 + 3),
|
||||
$unknown: [],
|
||||
},
|
||||
],
|
||||
blockedGroups: [
|
||||
{
|
||||
groupId: Uint8Array.fromBase64(GROUP_1),
|
||||
timestamp: BigInt(now + 50 + 1),
|
||||
$unknown: [],
|
||||
},
|
||||
{
|
||||
groupId: Uint8Array.fromBase64(GROUP_3),
|
||||
timestamp: BigInt(now + 50 + 3),
|
||||
$unknown: [],
|
||||
},
|
||||
],
|
||||
$unknown: [],
|
||||
};
|
||||
|
||||
await messageReceiver._handleBlocked(processedEnvelope, blocked);
|
||||
|
||||
const e1 = window.ConversationController.get(E164_1);
|
||||
assert.isTrue(e1?.isBlocked(), 'e1 should be blocked');
|
||||
const e1BlockItem = itemStorage.blocked.getBlockedNumbers().get(E164_1);
|
||||
assert.strictEqual(
|
||||
e1BlockItem?.blockedAt,
|
||||
now + 30 + 1,
|
||||
'e1 should have an updated blockedAt'
|
||||
);
|
||||
|
||||
const e2 = window.ConversationController.get(E164_2);
|
||||
assert.isFalse(e2?.isBlocked(), 'e2 should not be blocked');
|
||||
const e3 = window.ConversationController.get(E164_3);
|
||||
assert.isTrue(e3?.isBlocked(), 'e3 should be blocked');
|
||||
const e3BlockItem = itemStorage.blocked.getBlockedNumbers().get(E164_3);
|
||||
assert.strictEqual(
|
||||
e3BlockItem?.blockedAt,
|
||||
now + 30 + 3,
|
||||
'e3 should take new blockedAt from sync'
|
||||
);
|
||||
|
||||
const u1 = window.ConversationController.get(UUID_1);
|
||||
assert.isTrue(u1?.isBlocked(), 'u1 should be blocked');
|
||||
const u1BlockItem = itemStorage.blocked
|
||||
.getBlockedServiceIds()
|
||||
.get(UUID_1);
|
||||
assert.strictEqual(
|
||||
u1BlockItem?.blockedAt,
|
||||
now + 40 + 1,
|
||||
'u1 should have an updated blockedAt'
|
||||
);
|
||||
|
||||
const u2 = window.ConversationController.get(UUID_2);
|
||||
assert.isFalse(u2?.isBlocked(), 'u2 should not be blocked');
|
||||
|
||||
const u3 = window.ConversationController.get(UUID_3);
|
||||
assert.isTrue(u3?.isBlocked(), 'u3 should be blocked');
|
||||
const u3BlockItem = itemStorage.blocked
|
||||
.getBlockedServiceIds()
|
||||
.get(UUID_3);
|
||||
assert.strictEqual(
|
||||
u3BlockItem?.blockedAt,
|
||||
now + 40 + 3,
|
||||
'u3 should take new blockedAt from sync'
|
||||
);
|
||||
|
||||
const g1 = window.ConversationController.get(GROUP_1);
|
||||
assert.isTrue(g1?.isBlocked(), 'g1 should be blocked');
|
||||
const g1BlockItem = itemStorage.blocked.getBlockedGroups().get(GROUP_1);
|
||||
assert.strictEqual(
|
||||
g1BlockItem?.blockedAt,
|
||||
now + 50 + 1,
|
||||
'g1 should have an updated blockedAt'
|
||||
);
|
||||
|
||||
const g2 = window.ConversationController.get(GROUP_2);
|
||||
assert.isFalse(g2?.isBlocked(), 'g2 should not be blocked');
|
||||
|
||||
const g3 = window.ConversationController.get(GROUP_3);
|
||||
assert.isTrue(g3?.isBlocked(), 'g3 should be blocked');
|
||||
const g3BlockItem = itemStorage.blocked.getBlockedGroups().get(GROUP_3);
|
||||
assert.strictEqual(
|
||||
g3BlockItem?.blockedAt,
|
||||
now + 50 + 3,
|
||||
'g3 should take new blockedAt from sync'
|
||||
);
|
||||
});
|
||||
|
||||
it('handles legacy fields', async () => {
|
||||
const messageReceiver = new MessageReceiver({
|
||||
storage: itemStorage,
|
||||
serverTrustRoots: [toBase64(fakeTrustRootPublicKey)],
|
||||
});
|
||||
|
||||
const processedEnvelope: ProcessedEnvelope = {
|
||||
id: generateUuid(),
|
||||
receivedAtCounter: 1,
|
||||
receivedAtDate: ReceivedTimestampMs.fromNumber(now - 1),
|
||||
messageAgeSec: 1,
|
||||
|
||||
type: Proto.Envelope.Type.DOUBLE_RATCHET,
|
||||
source: ME_E164,
|
||||
sourceServiceId: normalizeServiceId(ME_UUID, 'test1'),
|
||||
sourceDevice: 1,
|
||||
destinationServiceId: normalizeServiceId(ME_UUID, 'test2'),
|
||||
updatedPni: normalizePni(generateUuid(), 'test3'),
|
||||
timestamp: SentTimestampMs.fromNumber(now - 2),
|
||||
content: Crypto.getRandomBytes(200),
|
||||
serverGuid: generateUuid(),
|
||||
serverTimestamp: ServerTimestampMs.fromNumber(now - 3),
|
||||
groupId: undefined,
|
||||
urgent: false,
|
||||
story: false,
|
||||
reportingToken: undefined,
|
||||
};
|
||||
const blocked: Proto.SyncMessage.Blocked = {
|
||||
numbers: [E164_1, E164_3],
|
||||
acis: [UUID_1, UUID_3],
|
||||
groupIds: [
|
||||
Uint8Array.fromBase64(GROUP_1),
|
||||
Uint8Array.fromBase64(GROUP_3),
|
||||
],
|
||||
acisBinary: [],
|
||||
blockedE164s: [],
|
||||
blockedAcis: [],
|
||||
blockedGroups: [],
|
||||
$unknown: [],
|
||||
};
|
||||
|
||||
await messageReceiver._handleBlocked(processedEnvelope, blocked);
|
||||
|
||||
const e1 = window.ConversationController.get(E164_1);
|
||||
assert.isTrue(e1?.isBlocked(), 'e1 should be blocked');
|
||||
const e1BlockItem = itemStorage.blocked.getBlockedNumbers().get(E164_1);
|
||||
assert.strictEqual(
|
||||
e1BlockItem?.blockedAt,
|
||||
now + 1,
|
||||
'e1 should keep its blockedAt'
|
||||
);
|
||||
|
||||
const e2 = window.ConversationController.get(E164_2);
|
||||
assert.isFalse(e2?.isBlocked(), 'e2 should not be blocked');
|
||||
const e3 = window.ConversationController.get(E164_3);
|
||||
assert.isTrue(e3?.isBlocked(), 'e3 should be blocked');
|
||||
const e3BlockItem = itemStorage.blocked.getBlockedNumbers().get(E164_3);
|
||||
assert.isUndefined(e3BlockItem?.blockedAt, 'e3 should have no blockedAt');
|
||||
|
||||
const u1 = window.ConversationController.get(UUID_1);
|
||||
assert.isTrue(u1?.isBlocked(), 'u1 should be blocked');
|
||||
const u1BlockItem = itemStorage.blocked
|
||||
.getBlockedServiceIds()
|
||||
.get(UUID_1);
|
||||
assert.strictEqual(
|
||||
u1BlockItem?.blockedAt,
|
||||
now + 10 + 1,
|
||||
'u1 should keep its blockedAt'
|
||||
);
|
||||
|
||||
const u2 = window.ConversationController.get(UUID_2);
|
||||
assert.isFalse(u2?.isBlocked(), 'u2 should not be blocked');
|
||||
|
||||
const u3 = window.ConversationController.get(UUID_3);
|
||||
assert.isTrue(u3?.isBlocked(), 'u3 should be blocked');
|
||||
const u3BlockItem = itemStorage.blocked
|
||||
.getBlockedServiceIds()
|
||||
.get(UUID_3);
|
||||
assert.isUndefined(u3BlockItem?.blockedAt, 'u3 should have no blockedAt');
|
||||
|
||||
const g1 = window.ConversationController.get(GROUP_1);
|
||||
assert.isTrue(g1?.isBlocked(), 'g1 should be blocked');
|
||||
const g1BlockItem = itemStorage.blocked.getBlockedGroups().get(GROUP_1);
|
||||
assert.strictEqual(
|
||||
g1BlockItem?.blockedAt,
|
||||
now + 20 + 1,
|
||||
'g1 should keep its blockedAt'
|
||||
);
|
||||
|
||||
const g2 = window.ConversationController.get(GROUP_2);
|
||||
assert.isFalse(g2?.isBlocked(), 'g2 should not be blocked');
|
||||
|
||||
const g3 = window.ConversationController.get(GROUP_3);
|
||||
assert.isTrue(g3?.isBlocked(), 'g3 should be blocked');
|
||||
const g3BlockItem = itemStorage.blocked.getBlockedGroups().get(GROUP_3);
|
||||
assert.isUndefined(g3BlockItem?.blockedAt, 'g3 should have no blockedAt');
|
||||
});
|
||||
|
||||
it('handles legacy fields with acisBinary set', async () => {
|
||||
const messageReceiver = new MessageReceiver({
|
||||
storage: itemStorage,
|
||||
serverTrustRoots: [toBase64(fakeTrustRootPublicKey)],
|
||||
});
|
||||
|
||||
const processedEnvelope: ProcessedEnvelope = {
|
||||
id: generateUuid(),
|
||||
receivedAtCounter: 1,
|
||||
receivedAtDate: ReceivedTimestampMs.fromNumber(now - 1),
|
||||
messageAgeSec: 1,
|
||||
|
||||
type: Proto.Envelope.Type.DOUBLE_RATCHET,
|
||||
source: ME_E164,
|
||||
sourceServiceId: normalizeServiceId(ME_UUID, 'test1'),
|
||||
sourceDevice: 1,
|
||||
destinationServiceId: normalizeServiceId(ME_UUID, 'test2'),
|
||||
updatedPni: normalizePni(generateUuid(), 'test3'),
|
||||
timestamp: SentTimestampMs.fromNumber(now - 2),
|
||||
content: Crypto.getRandomBytes(200),
|
||||
serverGuid: generateUuid(),
|
||||
serverTimestamp: ServerTimestampMs.fromNumber(now - 3),
|
||||
groupId: undefined,
|
||||
urgent: false,
|
||||
story: false,
|
||||
reportingToken: undefined,
|
||||
};
|
||||
const blocked: Proto.SyncMessage.Blocked = {
|
||||
numbers: [E164_1, E164_2],
|
||||
acis: [],
|
||||
groupIds: [
|
||||
Uint8Array.fromBase64(GROUP_1),
|
||||
Uint8Array.fromBase64(GROUP_2),
|
||||
],
|
||||
acisBinary: [
|
||||
Aci.parseFromServiceIdString(UUID_1).getRawUuidBytes(),
|
||||
Aci.parseFromServiceIdString(UUID_3).getRawUuidBytes(),
|
||||
],
|
||||
blockedE164s: [],
|
||||
blockedAcis: [],
|
||||
blockedGroups: [],
|
||||
$unknown: [],
|
||||
};
|
||||
|
||||
await messageReceiver._handleBlocked(processedEnvelope, blocked);
|
||||
|
||||
const u1 = window.ConversationController.get(UUID_1);
|
||||
assert.isTrue(u1?.isBlocked(), 'u1 should be blocked');
|
||||
const u2 = window.ConversationController.get(UUID_2);
|
||||
assert.isFalse(u2?.isBlocked(), 'u2 should not be blocked');
|
||||
const u3 = window.ConversationController.get(UUID_3);
|
||||
assert.isTrue(u3?.isBlocked(), 'u3 should be blocked');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -108,7 +108,11 @@ describe('backup/conversations', () => {
|
||||
}
|
||||
);
|
||||
|
||||
await itemStorage.blocked.addBlockedGroup(blockedGroupInfo.groupId);
|
||||
const timestamp = Date.now();
|
||||
await itemStorage.blocked.addBlockedGroup(
|
||||
blockedGroupInfo.groupId,
|
||||
timestamp
|
||||
);
|
||||
|
||||
await symmetricRoundtripHarness([]);
|
||||
|
||||
@@ -116,6 +120,15 @@ describe('backup/conversations', () => {
|
||||
blockedGroupInfo.groupId
|
||||
);
|
||||
assert.isTrue(blockedGroupAfter?.isBlocked());
|
||||
const blockedGroupItem = itemStorage.blocked
|
||||
.getBlockedGroups()
|
||||
.get(blockedGroupInfo.groupId);
|
||||
assert.strictEqual(
|
||||
blockedGroupItem?.blockedAt,
|
||||
timestamp,
|
||||
'Timestamp on blocked group should be rountripped'
|
||||
);
|
||||
|
||||
const unblockedGroupAfter = window.ConversationController.get(
|
||||
unblockedGroupInfo.groupId
|
||||
);
|
||||
|
||||
@@ -202,6 +202,7 @@ function* createRecords({
|
||||
contact: {
|
||||
aci: chatAci,
|
||||
blocked: false,
|
||||
blockedAtTimestamp: null,
|
||||
visibility: Backups.Contact.Visibility.VISIBLE,
|
||||
registration: {
|
||||
registered: {},
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
// Copyright 2026 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { assert } from 'chai';
|
||||
import { v4 as generateUuid } from 'uuid';
|
||||
|
||||
import type { WritableDB } from '../../sql/Interface.std.ts';
|
||||
import {
|
||||
createDB,
|
||||
updateToVersion,
|
||||
insertData,
|
||||
getTableData,
|
||||
} from './helpers.node.ts';
|
||||
import { getRandomBytes } from '../../Crypto.node.ts';
|
||||
import { sortBy } from 'lodash';
|
||||
|
||||
describe('SQL/updateToSchemaVersion1770', () => {
|
||||
let db: WritableDB;
|
||||
|
||||
beforeEach(() => {
|
||||
db = createDB();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
db.close();
|
||||
});
|
||||
|
||||
const E164_1 = '+18005551111';
|
||||
const E164_2 = '+18005551112';
|
||||
const UUID_1 = generateUuid();
|
||||
const UUID_2 = generateUuid();
|
||||
const GROUP_1 = getRandomBytes(32).toBase64();
|
||||
const GROUP_2 = getRandomBytes(32).toBase64();
|
||||
|
||||
it('removes the cached attachment but preserves the author', () => {
|
||||
updateToVersion(db, 1760);
|
||||
insertData(db, 'items', [
|
||||
{
|
||||
id: 'blocked',
|
||||
json: {
|
||||
id: 'blocked',
|
||||
value: [E164_1, E164_2],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'blocked-groups',
|
||||
json: {
|
||||
id: 'blocked-groups',
|
||||
value: [GROUP_1, GROUP_2],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'blocked-uuids',
|
||||
json: {
|
||||
id: 'blocked-uuids',
|
||||
value: [UUID_1, UUID_2],
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
updateToVersion(db, 1770);
|
||||
|
||||
assert.deepStrictEqual(sortBy(getTableData(db, 'items'), 'id'), [
|
||||
{
|
||||
id: 'blocked',
|
||||
json: {
|
||||
id: 'blocked',
|
||||
value: [{ e164: E164_1 }, { e164: E164_2 }],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'blocked-groups',
|
||||
json: {
|
||||
id: 'blocked-groups',
|
||||
value: [{ groupId: GROUP_1 }, { groupId: GROUP_2 }],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'blocked-uuids',
|
||||
json: {
|
||||
id: 'blocked-uuids',
|
||||
value: [{ serviceId: UUID_1 }, { serviceId: UUID_2 }],
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -88,7 +88,7 @@ import type { EventHandler } from './EventTarget.std.ts';
|
||||
import EventTarget from './EventTarget.std.ts';
|
||||
import type { IncomingWebSocketRequest } from './WebsocketResources.preload.ts';
|
||||
import { ServerRequestType } from './WebsocketResources.preload.ts';
|
||||
import { type Storage } from './Storage.preload.ts';
|
||||
import { itemStorage, type Storage } from './Storage.preload.ts';
|
||||
import { accountManager } from './AccountManager.preload.ts';
|
||||
import { WarnOnlyError } from './Errors.std.ts';
|
||||
import * as Bytes from '../Bytes.std.ts';
|
||||
@@ -182,6 +182,7 @@ import {
|
||||
SentTimestampMs,
|
||||
ServerTimestampMs,
|
||||
} from '@signalapp/types';
|
||||
import type { ConversationAttributesTypeType } from '../model-types.d.ts';
|
||||
|
||||
const { isBoolean, isNumber, isString, noop } = lodash;
|
||||
|
||||
@@ -3056,7 +3057,7 @@ export default class MessageReceiver
|
||||
return this.#handleContacts(envelope, syncMessage.content.contacts);
|
||||
}
|
||||
if (syncMessage.content?.blocked) {
|
||||
return this.#handleBlocked(envelope, syncMessage.content.blocked);
|
||||
return this._handleBlocked(envelope, syncMessage.content.blocked);
|
||||
}
|
||||
if (syncMessage.content?.request) {
|
||||
log.info('Got SyncMessage Request');
|
||||
@@ -3947,7 +3948,8 @@ export default class MessageReceiver
|
||||
|
||||
// This function calls applyMessageRequestResponse before setting storage so
|
||||
// proper before/after logic can be applied within that function.
|
||||
async #handleBlocked(
|
||||
// Exposed only for testing.
|
||||
async _handleBlocked(
|
||||
envelope: ProcessedEnvelope,
|
||||
blocked: Proto.SyncMessage.Blocked
|
||||
): Promise<void> {
|
||||
@@ -3961,24 +3963,241 @@ export default class MessageReceiver
|
||||
receivedAtCounter: envelope.receivedAtCounter,
|
||||
receivedAtMs: envelope.receivedAtDate,
|
||||
timestamp: envelope.timestamp,
|
||||
blockedAt: undefined,
|
||||
};
|
||||
|
||||
const areModernFieldsUsed =
|
||||
blocked.blockedE164s.length ||
|
||||
blocked.blockedAcis.length ||
|
||||
blocked.blockedGroups.length;
|
||||
const areLegacyFieldsUsed =
|
||||
blocked.numbers.length || blocked.acis.length || blocked.groupIds.length;
|
||||
|
||||
if (areModernFieldsUsed || !areLegacyFieldsUsed) {
|
||||
log.info(`${logId}: Using modern fields`);
|
||||
|
||||
{
|
||||
const previous = this.#storage.get('blocked', []);
|
||||
const updatedBlocked = blocked.blockedE164s
|
||||
.map(item => {
|
||||
if (!item.e164) {
|
||||
return;
|
||||
}
|
||||
|
||||
return {
|
||||
e164: item.e164,
|
||||
blockedAt: item.timestamp
|
||||
? TimestampMs.fromBigInt(item.timestamp)
|
||||
: undefined,
|
||||
};
|
||||
})
|
||||
.filter(isNotNil);
|
||||
|
||||
const { added, removed } = diffArraysAsSets(
|
||||
previous.map(item => item.e164),
|
||||
updatedBlocked.map(item => item.e164)
|
||||
);
|
||||
|
||||
if (removed.length) {
|
||||
await Promise.all(
|
||||
removed.map(getAndApply(messageRequestEnum.ACCEPT))
|
||||
);
|
||||
}
|
||||
|
||||
await Promise.all(
|
||||
updatedBlocked.map(async item => {
|
||||
if (!item.e164 || !added.includes(item.e164)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const conversation = window.ConversationController.getOrCreate(
|
||||
item.e164,
|
||||
'private'
|
||||
);
|
||||
await conversation.applyMessageRequestResponse(
|
||||
messageRequestEnum.BLOCK,
|
||||
{
|
||||
...responseInfo,
|
||||
blockedAt: item.blockedAt,
|
||||
}
|
||||
);
|
||||
})
|
||||
);
|
||||
|
||||
log.info(`${logId}: New e164 blocks:`, added);
|
||||
log.info(`${logId}: New e164 unblocks:`, removed);
|
||||
|
||||
await this.#storage.put('blocked', updatedBlocked);
|
||||
itemStorage.blocked.setBlockedNumbers();
|
||||
}
|
||||
|
||||
{
|
||||
const updatedBlocked = blocked.blockedAcis
|
||||
.map((item, index) => {
|
||||
try {
|
||||
const aci = fromAciUuidBytes(item.aciBinary);
|
||||
if (!aci) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
serviceId: aci,
|
||||
blockedAt: item.timestamp
|
||||
? TimestampMs.fromBigInt(item.timestamp)
|
||||
: undefined,
|
||||
};
|
||||
} catch (error) {
|
||||
log.warn(
|
||||
`${logId}: ACI ${index} was malformed`,
|
||||
Errors.toLogFormat(error)
|
||||
);
|
||||
return undefined;
|
||||
}
|
||||
})
|
||||
.filter(isNotNil);
|
||||
|
||||
const previous = this.#storage.get('blocked-uuids', []);
|
||||
const { added, removed } = diffArraysAsSets(
|
||||
previous.map(item => item.serviceId),
|
||||
updatedBlocked.map(item => item.serviceId)
|
||||
);
|
||||
|
||||
if (removed.length) {
|
||||
await Promise.all(
|
||||
removed.map(getAndApply(messageRequestEnum.ACCEPT))
|
||||
);
|
||||
}
|
||||
|
||||
await Promise.all(
|
||||
updatedBlocked.map(async item => {
|
||||
if (!added.includes(item.serviceId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const conversation = window.ConversationController.getOrCreate(
|
||||
item.serviceId,
|
||||
'private'
|
||||
);
|
||||
await conversation.applyMessageRequestResponse(
|
||||
messageRequestEnum.BLOCK,
|
||||
{
|
||||
...responseInfo,
|
||||
blockedAt: item.blockedAt,
|
||||
}
|
||||
);
|
||||
})
|
||||
);
|
||||
|
||||
log.info(`${logId}: New aci blocks:`, added);
|
||||
log.info(`${logId}: New aci unblocks:`, removed);
|
||||
|
||||
await this.#storage.put('blocked-uuids', updatedBlocked);
|
||||
itemStorage.blocked.setBlockedServiceIds();
|
||||
}
|
||||
|
||||
{
|
||||
const updatedBlocked = blocked.blockedGroups
|
||||
.map((item, index) => {
|
||||
const { groupId, timestamp } = item;
|
||||
|
||||
if (groupId?.byteLength !== GROUPV2_ID_LENGTH) {
|
||||
log.error(
|
||||
`${logId}: Received invalid groupId value at index ${index}`
|
||||
);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
groupId: Bytes.toBase64(groupId),
|
||||
blockedAt: timestamp
|
||||
? TimestampMs.fromBigInt(timestamp)
|
||||
: undefined,
|
||||
};
|
||||
})
|
||||
.filter(isNotNil);
|
||||
|
||||
const previous = this.#storage.get('blocked-groups', []);
|
||||
const { added, removed } = diffArraysAsSets(
|
||||
previous.map(item => item.groupId),
|
||||
updatedBlocked.map(item => item.groupId)
|
||||
);
|
||||
|
||||
if (removed.length) {
|
||||
await Promise.all(
|
||||
removed.map(async item => {
|
||||
const conversation = window.ConversationController.get(item);
|
||||
if (!conversation) {
|
||||
log.warn(`${logId}: Group groupv2(${item}) not found!`);
|
||||
return;
|
||||
}
|
||||
await conversation.applyMessageRequestResponse(
|
||||
messageRequestEnum.ACCEPT,
|
||||
responseInfo
|
||||
);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
await Promise.all(
|
||||
updatedBlocked.map(async item => {
|
||||
if (!added.includes(item.groupId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const conversation = window.ConversationController.get(
|
||||
item.groupId
|
||||
);
|
||||
if (!conversation) {
|
||||
log.warn(`${logId}: Group groupv2(${item.groupId}) not found!`);
|
||||
return;
|
||||
}
|
||||
await conversation.applyMessageRequestResponse(
|
||||
messageRequestEnum.BLOCK,
|
||||
{ ...responseInfo, blockedAt: item.blockedAt }
|
||||
);
|
||||
})
|
||||
);
|
||||
|
||||
log.info(
|
||||
`${logId}: New groupId blocks:`,
|
||||
added.map(groupId => `groupv2(${groupId})`)
|
||||
);
|
||||
log.info(
|
||||
`${logId}: New groupId unblocks:`,
|
||||
removed.map(groupId => `groupv2(${groupId})`)
|
||||
);
|
||||
|
||||
await this.#storage.put('blocked-groups', updatedBlocked);
|
||||
itemStorage.blocked.setBlockedGroups();
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
log.info(`${logId}: Using legacy fields`);
|
||||
|
||||
function getAndApply(
|
||||
type: Proto.SyncMessage.MessageRequestResponse.Type
|
||||
type: Proto.SyncMessage.MessageRequestResponse.Type,
|
||||
convoType: ConversationAttributesTypeType = 'private'
|
||||
): (value: string) => Promise<void> {
|
||||
return async item => {
|
||||
const conversation = window.ConversationController.getOrCreate(
|
||||
item,
|
||||
'private'
|
||||
convoType
|
||||
);
|
||||
await conversation.applyMessageRequestResponse(type, responseInfo);
|
||||
};
|
||||
}
|
||||
|
||||
// If we get here, we need to be ready to keep all existing timestamps!
|
||||
|
||||
if (blocked.numbers) {
|
||||
const previous = this.#storage.get('blocked', []);
|
||||
|
||||
const { added, removed } = diffArraysAsSets(previous, blocked.numbers);
|
||||
const { added, removed } = diffArraysAsSets(
|
||||
previous.map(item => item.e164),
|
||||
blocked.numbers
|
||||
);
|
||||
if (added.length) {
|
||||
await Promise.all(added.map(getAndApply(messageRequestEnum.BLOCK)));
|
||||
}
|
||||
@@ -3988,7 +4207,13 @@ export default class MessageReceiver
|
||||
|
||||
log.info(`${logId}: New e164 blocks:`, added);
|
||||
log.info(`${logId}: New e164 unblocks:`, removed);
|
||||
await this.#storage.put('blocked', blocked.numbers);
|
||||
|
||||
const updatedBlocked = previous
|
||||
.filter(item => !removed.includes(item.e164))
|
||||
.concat(added.map(e164 => ({ e164, blockedAt: undefined })));
|
||||
|
||||
await this.#storage.put('blocked', updatedBlocked);
|
||||
itemStorage.blocked.setBlockedNumbers();
|
||||
}
|
||||
if (blocked.acisBinary?.length || blocked.acis?.length) {
|
||||
const previous = this.#storage.get('blocked-uuids', []);
|
||||
@@ -4028,7 +4253,10 @@ export default class MessageReceiver
|
||||
// Older desktops might send the release note serviceId incorrectly
|
||||
acis = acis.filter(aci => !isSignalServiceId(aci));
|
||||
|
||||
const { added, removed } = diffArraysAsSets(previous, acis);
|
||||
const { added, removed } = diffArraysAsSets(
|
||||
previous.map(item => item.serviceId),
|
||||
acis
|
||||
);
|
||||
if (added.length) {
|
||||
await Promise.all(added.map(getAndApply(messageRequestEnum.BLOCK)));
|
||||
}
|
||||
@@ -4038,7 +4266,13 @@ export default class MessageReceiver
|
||||
|
||||
log.info(`${logId}: New aci blocks:`, added);
|
||||
log.info(`${logId}: New aci unblocks:`, removed);
|
||||
await this.#storage.put('blocked-uuids', acis);
|
||||
|
||||
const updatedBlocked = previous
|
||||
.filter(item => !removed.includes(item.serviceId))
|
||||
.concat(added.map(serviceId => ({ serviceId, blockedAt: undefined })));
|
||||
|
||||
await this.#storage.put('blocked-uuids', updatedBlocked);
|
||||
itemStorage.blocked.setBlockedServiceIds();
|
||||
}
|
||||
|
||||
if (blocked.groupIds) {
|
||||
@@ -4053,7 +4287,10 @@ export default class MessageReceiver
|
||||
}
|
||||
});
|
||||
|
||||
const { added, removed } = diffArraysAsSets(previous, groupIds);
|
||||
const { added, removed } = diffArraysAsSets(
|
||||
previous.map(item => item.groupId),
|
||||
groupIds
|
||||
);
|
||||
if (added.length) {
|
||||
await Promise.all(
|
||||
added.map(async item => {
|
||||
@@ -4093,17 +4330,31 @@ export default class MessageReceiver
|
||||
`${logId}: New groupId unblocks:`,
|
||||
removed.map(groupId => `groupv2(${groupId})`)
|
||||
);
|
||||
await this.#storage.put('blocked-groups', groupIds);
|
||||
|
||||
const updatedBlocked = previous
|
||||
.filter(item => !removed.includes(item.groupId))
|
||||
.concat(added.map(groupId => ({ groupId, blockedAt: undefined })));
|
||||
|
||||
await this.#storage.put('blocked-groups', updatedBlocked);
|
||||
itemStorage.blocked.setBlockedGroups();
|
||||
}
|
||||
|
||||
this.#removeFromCache(envelope);
|
||||
}
|
||||
|
||||
#isBlocked(number: string): boolean {
|
||||
const conversation = window.ConversationController.get(number);
|
||||
if (conversation) {
|
||||
return conversation.isBlocked();
|
||||
}
|
||||
return this.#storage.blocked.isBlocked(number);
|
||||
}
|
||||
|
||||
#isServiceIdBlocked(serviceId: ServiceIdString): boolean {
|
||||
const conversation = window.ConversationController.get(serviceId);
|
||||
if (conversation) {
|
||||
return conversation.isBlocked();
|
||||
}
|
||||
return this.#storage.blocked.isServiceIdBlocked(serviceId);
|
||||
}
|
||||
|
||||
|
||||
@@ -103,6 +103,7 @@ import type {
|
||||
SendUnpinMessageType,
|
||||
} from '../types/PinnedMessage.std.ts';
|
||||
import type { Emoji } from '../axo/emoji.std.ts';
|
||||
import type { BlockedNumber } from '../types/StorageKeys.std.ts';
|
||||
|
||||
const log = createLogger('SendMessage');
|
||||
|
||||
@@ -1202,8 +1203,12 @@ export class MessageSender {
|
||||
|
||||
const blockedIdentifiers = new Set(
|
||||
concat(
|
||||
itemStorage.blocked.getBlockedServiceIds(),
|
||||
itemStorage.blocked.getBlockedNumbers()
|
||||
Array.from(itemStorage.blocked.getBlockedServiceIds().values()).map(
|
||||
item => item.serviceId
|
||||
),
|
||||
Array.from(itemStorage.blocked.getBlockedNumbers().values()).map(
|
||||
item => item.e164
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
@@ -2242,25 +2247,44 @@ export class MessageSender {
|
||||
|
||||
static getBlockSync(
|
||||
options: Readonly<{
|
||||
e164s: Array<string>;
|
||||
acis: Array<AciString>;
|
||||
groupIds: Array<Uint8Array<ArrayBuffer>>;
|
||||
e164s: ReadonlyArray<BlockedNumber>;
|
||||
acis: ReadonlyArray<{
|
||||
blockedAt: number | undefined;
|
||||
aci: AciString;
|
||||
}>;
|
||||
groupIds: ReadonlyArray<{
|
||||
blockedAt: number | undefined;
|
||||
groupId: Uint8Array<ArrayBuffer>;
|
||||
}>;
|
||||
}>
|
||||
): SingleProtoJobData {
|
||||
const myAci = itemStorage.user.getCheckedAci();
|
||||
|
||||
const blocked: Proto.SyncMessage.Blocked.Params = {
|
||||
numbers: options.e164s,
|
||||
numbers: options.e164s.map(item => item.e164),
|
||||
blockedE164s: options.e164s.map(item => ({
|
||||
timestamp: item.blockedAt ? BigInt(item.blockedAt) : null,
|
||||
e164: item.e164,
|
||||
})),
|
||||
acisBinary: null,
|
||||
acis: null,
|
||||
groupIds: options.groupIds,
|
||||
blockedAcis: options.acis.map(item => ({
|
||||
timestamp: item.blockedAt ? BigInt(item.blockedAt) : null,
|
||||
aci: item.aci,
|
||||
aciBinary: toAciObject(item.aci).getRawUuidBytes(),
|
||||
})),
|
||||
groupIds: options.groupIds.map(item => item.groupId),
|
||||
blockedGroups: options.groupIds.map(item => ({
|
||||
timestamp: item.blockedAt ? BigInt(item.blockedAt) : null,
|
||||
groupId: item.groupId,
|
||||
})),
|
||||
};
|
||||
if (isProtoBinaryEncodingEnabled()) {
|
||||
blocked.acisBinary = options.acis.map(aci =>
|
||||
toAciObject(aci).getRawUuidBytes()
|
||||
blocked.acisBinary = options.acis.map(item =>
|
||||
toAciObject(item.aci).getRawUuidBytes()
|
||||
);
|
||||
} else {
|
||||
blocked.acis = options.acis;
|
||||
blocked.acis = options.acis.map(item => item.aci);
|
||||
}
|
||||
|
||||
const syncMessage = MessageSender.padSyncMessage({
|
||||
|
||||
@@ -99,6 +99,7 @@ export class Storage implements StorageInterface {
|
||||
this.reset();
|
||||
|
||||
Object.assign(this.#items, await DataReader.getAllItems());
|
||||
this.blocked.load();
|
||||
|
||||
this.#ready = true;
|
||||
this.#callListeners();
|
||||
@@ -107,6 +108,7 @@ export class Storage implements StorageInterface {
|
||||
public reset(): void {
|
||||
this.#ready = false;
|
||||
this.#items = Object.create(null);
|
||||
this.blocked.reset();
|
||||
}
|
||||
|
||||
public getItemsState(): Partial<Access> {
|
||||
|
||||
@@ -1,16 +1,19 @@
|
||||
// Copyright 2016 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import lodash from 'lodash';
|
||||
|
||||
import { createLogger } from '../../logging/log.std.ts';
|
||||
import * as Bytes from '../../Bytes.std.ts';
|
||||
import { createLogger } from '../../logging/log.std.ts';
|
||||
import { isAciString } from '../../util/isAciString.std.ts';
|
||||
import { isSignalServiceId } from '../../types/SignalConversation.std.ts';
|
||||
import { isNotNil } from '../../util/isNotNil.std.ts';
|
||||
|
||||
import type { StorageInterface } from '../../types/Storage.d.ts';
|
||||
import type { AciString, ServiceIdString } from '../../types/ServiceId.std.ts';
|
||||
import { isSignalServiceId } from '../../types/SignalConversation.std.ts';
|
||||
|
||||
const { without } = lodash;
|
||||
import type {
|
||||
BlockedGroup,
|
||||
BlockedNumber,
|
||||
BlockedServiceId,
|
||||
} from '../../types/StorageKeys.std.ts';
|
||||
|
||||
const log = createLogger('Blocked');
|
||||
|
||||
@@ -18,63 +21,120 @@ const BLOCKED_NUMBERS_ID = 'blocked';
|
||||
export const BLOCKED_UUIDS_ID = 'blocked-uuids';
|
||||
const BLOCKED_GROUPS_ID = 'blocked-groups';
|
||||
const RELEASE_NOTES_CHAT_BLOCKED_ID = 'releaseNotesChatBlocked';
|
||||
const RELEASE_NOTES_CHAT_BLOCKED_AT_ID = 'releaseNotesChatBlockedAt';
|
||||
|
||||
export class Blocked {
|
||||
readonly #storage: StorageInterface;
|
||||
|
||||
readonly #blockedNumbers: Map<string, BlockedNumber>;
|
||||
readonly #blockedServiceIds: Map<string, BlockedServiceId>;
|
||||
readonly #blockedGroups: Map<string, BlockedGroup>;
|
||||
|
||||
constructor(storage: StorageInterface) {
|
||||
this.#storage = storage;
|
||||
|
||||
this.#blockedNumbers = new Map();
|
||||
this.#blockedServiceIds = new Map();
|
||||
this.#blockedGroups = new Map();
|
||||
|
||||
this.load();
|
||||
}
|
||||
|
||||
public getBlockedNumbers(): Array<string> {
|
||||
return this.#storage.get(BLOCKED_NUMBERS_ID, new Array<string>());
|
||||
public reset(): void {
|
||||
this.#blockedNumbers.clear();
|
||||
this.#blockedServiceIds.clear();
|
||||
this.#blockedGroups.clear();
|
||||
}
|
||||
|
||||
public isBlocked(number: string): boolean {
|
||||
return this.getBlockedNumbers().includes(number);
|
||||
public load(): void {
|
||||
this.setBlockedNumbers();
|
||||
this.setBlockedServiceIds();
|
||||
this.setBlockedGroups();
|
||||
}
|
||||
|
||||
public async addBlockedNumber(number: string): Promise<void> {
|
||||
const numbers = this.getBlockedNumbers();
|
||||
if (numbers.includes(number)) {
|
||||
public setBlockedNumbers(): void {
|
||||
const array = this.#storage.get(BLOCKED_NUMBERS_ID);
|
||||
this.#blockedNumbers.clear();
|
||||
array?.forEach(item => {
|
||||
this.#blockedNumbers.set(item.e164, item);
|
||||
});
|
||||
}
|
||||
public getBlockedNumbers(): ReadonlyMap<string, BlockedNumber> {
|
||||
return this.#blockedNumbers;
|
||||
}
|
||||
|
||||
public isBlocked(e164: string): boolean {
|
||||
return Boolean(this.#blockedNumbers.get(e164));
|
||||
}
|
||||
|
||||
public async addBlockedNumber(
|
||||
e164: string,
|
||||
blockedAt: number | undefined
|
||||
): Promise<void> {
|
||||
if (this.isBlocked(e164)) {
|
||||
return;
|
||||
}
|
||||
|
||||
log.info('adding', number, 'to blocked list');
|
||||
await this.#storage.put(BLOCKED_NUMBERS_ID, numbers.concat(number));
|
||||
log.info('adding', e164, 'to blocked list');
|
||||
|
||||
const data = { e164, blockedAt };
|
||||
this.#blockedNumbers.set(e164, data);
|
||||
|
||||
const array = this.#storage.get(BLOCKED_NUMBERS_ID);
|
||||
await this.#storage.put(BLOCKED_NUMBERS_ID, (array || []).concat(data));
|
||||
}
|
||||
|
||||
public async removeBlockedNumber(number: string): Promise<void> {
|
||||
const numbers = this.getBlockedNumbers();
|
||||
if (!numbers.includes(number)) {
|
||||
public async removeBlockedNumber(e164: string): Promise<void> {
|
||||
if (!this.isBlocked(e164)) {
|
||||
return;
|
||||
}
|
||||
|
||||
log.info('removing', number, 'from blocked list');
|
||||
await this.#storage.put(BLOCKED_NUMBERS_ID, without(numbers, number));
|
||||
log.info('removing', e164, 'from blocked list');
|
||||
|
||||
this.#blockedNumbers.delete(e164);
|
||||
|
||||
const array = this.#storage.get(BLOCKED_NUMBERS_ID);
|
||||
await this.#storage.put(
|
||||
BLOCKED_NUMBERS_ID,
|
||||
(array || []).filter(item => item.e164 !== e164)
|
||||
);
|
||||
}
|
||||
|
||||
public getBlockedServiceIds(): Array<ServiceIdString> {
|
||||
return this.#storage.get(BLOCKED_UUIDS_ID, new Array<ServiceIdString>());
|
||||
public setBlockedServiceIds(): void {
|
||||
const array = this.#storage.get(BLOCKED_UUIDS_ID);
|
||||
this.#blockedServiceIds.clear();
|
||||
array?.forEach(item => {
|
||||
this.#blockedServiceIds.set(item.serviceId, item);
|
||||
});
|
||||
}
|
||||
public getBlockedServiceIds(): ReadonlyMap<string, BlockedServiceId> {
|
||||
return this.#blockedServiceIds;
|
||||
}
|
||||
|
||||
public isServiceIdBlocked(serviceId: ServiceIdString): boolean {
|
||||
return this.getBlockedServiceIds().includes(serviceId);
|
||||
return Boolean(this.#blockedServiceIds.get(serviceId));
|
||||
}
|
||||
|
||||
public async addBlockedServiceId(serviceId: ServiceIdString): Promise<void> {
|
||||
public async addBlockedServiceId(
|
||||
serviceId: ServiceIdString,
|
||||
blockedAt: number | undefined
|
||||
): Promise<void> {
|
||||
if (isSignalServiceId(serviceId)) {
|
||||
log.error('Attempting to block release notes chat by serviceId');
|
||||
return;
|
||||
}
|
||||
|
||||
const serviceIds = this.getBlockedServiceIds();
|
||||
if (serviceIds.includes(serviceId)) {
|
||||
if (this.isServiceIdBlocked(serviceId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
log.info('adding', serviceId, 'to blocked list');
|
||||
await this.#storage.put(BLOCKED_UUIDS_ID, serviceIds.concat(serviceId));
|
||||
|
||||
const data = { serviceId, blockedAt };
|
||||
this.#blockedServiceIds.set(serviceId, data);
|
||||
|
||||
const array = this.#storage.get(BLOCKED_UUIDS_ID);
|
||||
await this.#storage.put(BLOCKED_UUIDS_ID, (array || []).concat(data));
|
||||
}
|
||||
|
||||
public async removeBlockedServiceId(
|
||||
@@ -85,61 +145,115 @@ export class Blocked {
|
||||
return;
|
||||
}
|
||||
|
||||
const numbers = this.getBlockedServiceIds();
|
||||
if (!numbers.includes(serviceId)) {
|
||||
if (!this.isServiceIdBlocked(serviceId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
log.info('removing', serviceId, 'from blocked list');
|
||||
await this.#storage.put(BLOCKED_UUIDS_ID, without(numbers, serviceId));
|
||||
|
||||
this.#blockedServiceIds.delete(serviceId);
|
||||
|
||||
const array = this.#storage.get(BLOCKED_UUIDS_ID);
|
||||
await this.#storage.put(
|
||||
BLOCKED_UUIDS_ID,
|
||||
(array || []).filter(item => item.serviceId !== serviceId)
|
||||
);
|
||||
}
|
||||
|
||||
public isReleaseNotesChatBlocked(): boolean {
|
||||
return this.#storage.get(RELEASE_NOTES_CHAT_BLOCKED_ID, false);
|
||||
}
|
||||
|
||||
public async setReleaseNotesChatBlocked(blocked: boolean): Promise<void> {
|
||||
await this.#storage.put(RELEASE_NOTES_CHAT_BLOCKED_ID, blocked);
|
||||
public whenWasReleaseNotesChatBlocked(): number | undefined {
|
||||
return this.#storage.get(RELEASE_NOTES_CHAT_BLOCKED_AT_ID, undefined);
|
||||
}
|
||||
|
||||
public getBlockedGroups(): Array<string> {
|
||||
return this.#storage.get(BLOCKED_GROUPS_ID, new Array<string>());
|
||||
public async setReleaseNotesChatBlocked(
|
||||
blocked: boolean,
|
||||
blockedAt: number | undefined
|
||||
): Promise<void> {
|
||||
await this.#storage.put(RELEASE_NOTES_CHAT_BLOCKED_ID, blocked);
|
||||
await this.#storage.put(
|
||||
RELEASE_NOTES_CHAT_BLOCKED_AT_ID,
|
||||
blocked ? blockedAt : undefined
|
||||
);
|
||||
}
|
||||
|
||||
public setBlockedGroups(): void {
|
||||
const array = this.#storage.get(BLOCKED_GROUPS_ID);
|
||||
this.#blockedGroups.clear();
|
||||
array?.forEach(item => {
|
||||
this.#blockedGroups.set(item.groupId, item);
|
||||
});
|
||||
}
|
||||
public getBlockedGroups(): ReadonlyMap<string, BlockedGroup> {
|
||||
return this.#blockedGroups;
|
||||
}
|
||||
|
||||
public isGroupBlocked(groupId: string): boolean {
|
||||
return this.getBlockedGroups().includes(groupId);
|
||||
return Boolean(this.#blockedGroups.get(groupId));
|
||||
}
|
||||
|
||||
public async addBlockedGroup(groupId: string): Promise<void> {
|
||||
const groupIds = this.getBlockedGroups();
|
||||
if (groupIds.includes(groupId)) {
|
||||
public async addBlockedGroup(
|
||||
groupId: string,
|
||||
blockedAt: number | undefined
|
||||
): Promise<void> {
|
||||
if (this.isGroupBlocked(groupId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
log.info(`adding group(${groupId}) to blocked list`);
|
||||
await this.#storage.put(BLOCKED_GROUPS_ID, groupIds.concat(groupId));
|
||||
|
||||
const data = { groupId, blockedAt };
|
||||
this.#blockedGroups.set(groupId, data);
|
||||
|
||||
const array = this.#storage.get(BLOCKED_GROUPS_ID);
|
||||
await this.#storage.put(BLOCKED_GROUPS_ID, (array || []).concat(data));
|
||||
}
|
||||
|
||||
public async removeBlockedGroup(groupId: string): Promise<void> {
|
||||
const groupIds = this.getBlockedGroups();
|
||||
if (!groupIds.includes(groupId)) {
|
||||
if (!this.isGroupBlocked(groupId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
log.info(`removing group(${groupId} from blocked list`);
|
||||
await this.#storage.put(BLOCKED_GROUPS_ID, without(groupIds, groupId));
|
||||
|
||||
this.#blockedGroups.delete(groupId);
|
||||
|
||||
const array = this.#storage.get(BLOCKED_GROUPS_ID);
|
||||
await this.#storage.put(
|
||||
BLOCKED_GROUPS_ID,
|
||||
(array || []).filter(item => item.groupId !== groupId)
|
||||
);
|
||||
}
|
||||
|
||||
public getBlockedData(): {
|
||||
e164s: Array<string>;
|
||||
acis: Array<AciString>;
|
||||
groupIds: Array<Uint8Array<ArrayBuffer>>;
|
||||
e164s: ReadonlyArray<BlockedNumber>;
|
||||
acis: ReadonlyArray<{
|
||||
blockedAt: number | undefined;
|
||||
aci: AciString;
|
||||
}>;
|
||||
groupIds: ReadonlyArray<{
|
||||
blockedAt: number | undefined;
|
||||
groupId: Uint8Array<ArrayBuffer>;
|
||||
}>;
|
||||
} {
|
||||
const e164s = this.getBlockedNumbers();
|
||||
const acis = this.getBlockedServiceIds().filter(item => isAciString(item));
|
||||
const groupIds = this.getBlockedGroups().map(item =>
|
||||
Bytes.fromBase64(item)
|
||||
);
|
||||
const e164s = Array.from(this.getBlockedNumbers().values());
|
||||
const acis = Array.from(this.getBlockedServiceIds().values())
|
||||
.map(item => {
|
||||
if (!isAciString(item.serviceId)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
blockedAt: item.blockedAt,
|
||||
aci: item.serviceId,
|
||||
};
|
||||
})
|
||||
.filter(isNotNil);
|
||||
const groupIds = Array.from(this.getBlockedGroups().values()).map(item => ({
|
||||
...item,
|
||||
groupId: Bytes.fromBase64(item.groupId),
|
||||
}));
|
||||
|
||||
return {
|
||||
e164s,
|
||||
|
||||
@@ -17,11 +17,12 @@ export enum MessageRequestResponseSource {
|
||||
export type MessageRequestResponseInfo =
|
||||
| {
|
||||
source: MessageRequestResponseSource.LOCAL;
|
||||
timestamp: number;
|
||||
blockedAt: number;
|
||||
}
|
||||
| {
|
||||
source: MessageRequestResponseSource.STORAGE_SERVICE;
|
||||
learnedAtMs: number;
|
||||
blockedAt: number | undefined;
|
||||
}
|
||||
| {
|
||||
source:
|
||||
@@ -30,4 +31,5 @@ export type MessageRequestResponseInfo =
|
||||
timestamp: number;
|
||||
receivedAtMs: number;
|
||||
receivedAtCounter: number;
|
||||
blockedAt: number | undefined;
|
||||
};
|
||||
|
||||
@@ -59,6 +59,19 @@ export type IdentityKeyMap = Record<
|
||||
}
|
||||
>;
|
||||
|
||||
export type BlockedGroup = {
|
||||
blockedAt: number | undefined;
|
||||
groupId: string;
|
||||
};
|
||||
export type BlockedServiceId = {
|
||||
blockedAt: number | undefined;
|
||||
serviceId: ServiceIdString;
|
||||
};
|
||||
export type BlockedNumber = {
|
||||
blockedAt: number | undefined;
|
||||
e164: string;
|
||||
};
|
||||
|
||||
export type StorageAccessType = {
|
||||
'always-relay-calls': boolean;
|
||||
'audio-notification': boolean;
|
||||
@@ -66,8 +79,8 @@ export type StorageAccessType = {
|
||||
'auto-download-attachment': AutoDownloadAttachmentType;
|
||||
autoConvertEmoji: boolean;
|
||||
'badge-count-muted-conversations': boolean;
|
||||
'blocked-groups': ReadonlyArray<string>;
|
||||
'blocked-uuids': ReadonlyArray<ServiceIdString>;
|
||||
'blocked-groups': ReadonlyArray<BlockedGroup>;
|
||||
'blocked-uuids': ReadonlyArray<BlockedServiceId>;
|
||||
'call-ringtone-notification': boolean;
|
||||
'call-system-notification': boolean;
|
||||
lastCallQualitySurveyTime: number;
|
||||
@@ -82,7 +95,7 @@ export type StorageAccessType = {
|
||||
audioMessage: boolean;
|
||||
attachmentMigration_isComplete: boolean;
|
||||
attachmentMigration_lastProcessedIndex: number;
|
||||
blocked: ReadonlyArray<string>;
|
||||
blocked: ReadonlyArray<BlockedNumber>;
|
||||
defaultConversationColor: DefaultConversationColorType;
|
||||
|
||||
customColors: CustomColorsItemType;
|
||||
@@ -229,6 +242,7 @@ export type StorageAccessType = {
|
||||
releaseNotesVersionWatermark: string;
|
||||
releaseNotesPreviousManifestHash: string;
|
||||
releaseNotesChatBlocked: boolean;
|
||||
releaseNotesChatBlockedAt: number | undefined;
|
||||
|
||||
// If present - we are downloading backup
|
||||
backupDownloadPath: string;
|
||||
@@ -433,6 +447,7 @@ export const STORAGE_KEYS_TO_PRESERVE_WHEN_PRIMARY = [
|
||||
'read-receipt-setting',
|
||||
'blocked',
|
||||
'releaseNotesChatBlocked',
|
||||
'releaseNotesChatBlockedAt',
|
||||
'device_name',
|
||||
'seenPinMessageDisappearingMessagesWarningCount',
|
||||
'usernameLastIntegrityCheck',
|
||||
|
||||
@@ -15,16 +15,19 @@ export function isBlocked(
|
||||
return itemStorage.blocked.isReleaseNotesChatBlocked();
|
||||
}
|
||||
|
||||
if (isAciString(serviceId)) {
|
||||
return itemStorage.blocked.isServiceIdBlocked(serviceId);
|
||||
if (
|
||||
isAciString(serviceId) &&
|
||||
itemStorage.blocked.isServiceIdBlocked(serviceId)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (e164) {
|
||||
return itemStorage.blocked.isBlocked(e164);
|
||||
if (e164 && itemStorage.blocked.isBlocked(e164)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (groupId) {
|
||||
return itemStorage.blocked.isGroupBlocked(groupId);
|
||||
if (groupId && itemStorage.blocked.isGroupBlocked(groupId)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
|
||||
Reference in New Issue
Block a user