Initial logic for release notes
Co-authored-by: ayumi-signal <143036029+ayumi-signal@users.noreply.github.com>
This commit is contained in:
parent
e2d12db443
commit
e624759003
13 changed files with 491 additions and 5 deletions
|
@ -33,6 +33,9 @@ export type ConfigKeyType =
|
||||||
| 'desktop.experimentalTransportEnabled.beta'
|
| 'desktop.experimentalTransportEnabled.beta'
|
||||||
| 'desktop.experimentalTransportEnabled.prod'
|
| 'desktop.experimentalTransportEnabled.prod'
|
||||||
| 'desktop.cdsiViaLibsignal'
|
| 'desktop.cdsiViaLibsignal'
|
||||||
|
| 'desktop.releaseNotes'
|
||||||
|
| 'desktop.releaseNotes.beta'
|
||||||
|
| 'desktop.releaseNotes.dev'
|
||||||
| 'global.attachments.maxBytes'
|
| 'global.attachments.maxBytes'
|
||||||
| 'global.attachments.maxReceiveBytes'
|
| 'global.attachments.maxReceiveBytes'
|
||||||
| 'global.calling.maxGroupCallRingSize'
|
| 'global.calling.maxGroupCallRingSize'
|
||||||
|
|
|
@ -198,6 +198,7 @@ import { restoreRemoteConfigFromStorage } from './RemoteConfig';
|
||||||
import { getParametersForRedux, loadAll } from './services/allLoaders';
|
import { getParametersForRedux, loadAll } from './services/allLoaders';
|
||||||
import { checkFirstEnvelope } from './util/checkFirstEnvelope';
|
import { checkFirstEnvelope } from './util/checkFirstEnvelope';
|
||||||
import { BLOCKED_UUIDS_ID } from './textsecure/storage/Blocked';
|
import { BLOCKED_UUIDS_ID } from './textsecure/storage/Blocked';
|
||||||
|
import { ReleaseNotesFetcher } from './services/releaseNotesFetcher';
|
||||||
|
|
||||||
export function isOverHourIntoPast(timestamp: number): boolean {
|
export function isOverHourIntoPast(timestamp: number): boolean {
|
||||||
return isNumber(timestamp) && isOlderThan(timestamp, HOUR);
|
return isNumber(timestamp) && isOlderThan(timestamp, HOUR);
|
||||||
|
@ -2160,6 +2161,8 @@ export async function startApp(): Promise<void> {
|
||||||
}
|
}
|
||||||
|
|
||||||
drop(usernameIntegrity.start());
|
drop(usernameIntegrity.start());
|
||||||
|
|
||||||
|
drop(ReleaseNotesFetcher.init(window.Whisper.events, newVersion));
|
||||||
}
|
}
|
||||||
|
|
||||||
let initialStartupCount = 0;
|
let initialStartupCount = 0;
|
||||||
|
|
|
@ -631,6 +631,19 @@ function HeaderMenu({
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
)}
|
)}
|
||||||
</SubMenu>
|
</SubMenu>
|
||||||
|
{conversation.isArchived ? (
|
||||||
|
<MenuItem onClick={onConversationUnarchive}>
|
||||||
|
{i18n('icu:moveConversationToInbox')}
|
||||||
|
</MenuItem>
|
||||||
|
) : (
|
||||||
|
<MenuItem onClick={onConversationArchive}>
|
||||||
|
{i18n('icu:archiveConversation')}
|
||||||
|
</MenuItem>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<MenuItem onClick={onConversationDeleteMessages}>
|
||||||
|
{i18n('icu:deleteConversation')}
|
||||||
|
</MenuItem>
|
||||||
</ContextMenu>
|
</ContextMenu>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -55,6 +55,7 @@ import {
|
||||||
getChangesForPropAtTimestamp,
|
getChangesForPropAtTimestamp,
|
||||||
} from '../../util/editHelpers';
|
} from '../../util/editHelpers';
|
||||||
import { getMessageSentTimestamp } from '../../util/getMessageSentTimestamp';
|
import { getMessageSentTimestamp } from '../../util/getMessageSentTimestamp';
|
||||||
|
import { isSignalConversation } from '../../util/isSignalConversation';
|
||||||
|
|
||||||
const MAX_CONCURRENT_ATTACHMENT_UPLOADS = 5;
|
const MAX_CONCURRENT_ATTACHMENT_UPLOADS = 5;
|
||||||
|
|
||||||
|
@ -88,6 +89,13 @@ export async function sendNormalMessage(
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isSignalConversation(messageConversation)) {
|
||||||
|
log.error(
|
||||||
|
`Message conversation '${messageConversation?.idForLogging()}' is the Signal serviceId, not sending`
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!isOutgoing(message.attributes)) {
|
if (!isOutgoing(message.attributes)) {
|
||||||
log.error(
|
log.error(
|
||||||
`message ${messageId} was not an outgoing message to begin with. This is probably a bogus job. Giving up on sending it`
|
`message ${messageId} was not an outgoing message to begin with. This is probably a bogus job. Giving up on sending it`
|
||||||
|
|
|
@ -5,6 +5,7 @@ import type { ConversationModel } from '../../models/conversations';
|
||||||
import type { LoggerType } from '../../types/Logging';
|
import type { LoggerType } from '../../types/Logging';
|
||||||
import { getRecipients } from '../../util/getRecipients';
|
import { getRecipients } from '../../util/getRecipients';
|
||||||
import { isConversationAccepted } from '../../util/isConversationAccepted';
|
import { isConversationAccepted } from '../../util/isConversationAccepted';
|
||||||
|
import { isSignalConversation } from '../../util/isSignalConversation';
|
||||||
import { getUntrustedConversationServiceIds } from './getUntrustedConversationServiceIds';
|
import { getUntrustedConversationServiceIds } from './getUntrustedConversationServiceIds';
|
||||||
|
|
||||||
export function shouldSendToConversation(
|
export function shouldSendToConversation(
|
||||||
|
@ -35,5 +36,12 @@ export function shouldSendToConversation(
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isSignalConversation(conversation.attributes)) {
|
||||||
|
log.info(
|
||||||
|
`conversation ${conversation.idForLogging()} is Signal conversation; refusing to send`
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1343,6 +1343,10 @@ export class ConversationModel extends window.Backbone
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isSignalConversation(this.attributes)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Coalesce multiple sendTypingMessage calls into one.
|
// Coalesce multiple sendTypingMessage calls into one.
|
||||||
//
|
//
|
||||||
// `lastIsTyping` is set to the last `isTyping` value passed to the
|
// `lastIsTyping` is set to the last `isTyping` value passed to the
|
||||||
|
|
330
ts/services/releaseNotesFetcher.ts
Normal file
330
ts/services/releaseNotesFetcher.ts
Normal file
|
@ -0,0 +1,330 @@
|
||||||
|
// Copyright 2024 Signal Messenger, LLC
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
import semver from 'semver';
|
||||||
|
import { last } from 'lodash';
|
||||||
|
|
||||||
|
import * as durations from '../util/durations';
|
||||||
|
import { clearTimeoutIfNecessary } from '../util/clearTimeoutIfNecessary';
|
||||||
|
import * as Registration from '../util/registration';
|
||||||
|
import * as log from '../logging/log';
|
||||||
|
import * as Errors from '../types/errors';
|
||||||
|
import { HTTPError } from '../textsecure/Errors';
|
||||||
|
import { drop } from '../util/drop';
|
||||||
|
import { strictAssert } from '../util/assert';
|
||||||
|
import type { MessageAttributesType } from '../model-types';
|
||||||
|
import { ReadStatus } from '../messages/MessageReadStatus';
|
||||||
|
import { incrementMessageCounter } from '../util/incrementMessageCounter';
|
||||||
|
import { SeenStatus } from '../MessageSeenStatus';
|
||||||
|
import { saveNewMessageBatcher } from '../util/messageBatcher';
|
||||||
|
import { generateMessageId } from '../util/generateMessageId';
|
||||||
|
import { BodyRange } from '../types/BodyRange';
|
||||||
|
import * as RemoteConfig from '../RemoteConfig';
|
||||||
|
import { isBeta, isProduction } from '../util/version';
|
||||||
|
import type {
|
||||||
|
ReleaseNotesManifestResponseType,
|
||||||
|
ReleaseNoteResponseType,
|
||||||
|
} from '../textsecure/WebAPI';
|
||||||
|
import type { WithRequiredProperties } from '../types/Util';
|
||||||
|
|
||||||
|
const FETCH_INTERVAL = 3 * durations.DAY;
|
||||||
|
const ERROR_RETRY_DELAY = 3 * durations.HOUR;
|
||||||
|
const NEXT_FETCH_TIME_STORAGE_KEY = 'releaseNotesNextFetchTime';
|
||||||
|
const PREVIOUS_MANIFEST_HASH_STORAGE_KEY = 'releaseNotesPreviousManifestHash';
|
||||||
|
const VERSION_WATERMARK_STORAGE_KEY = 'releaseNotesVersionWatermark';
|
||||||
|
|
||||||
|
type MinimalEventsType = {
|
||||||
|
on(event: 'timetravel', callback: () => void): void;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ManifestReleaseNoteType = WithRequiredProperties<
|
||||||
|
ReleaseNotesManifestResponseType['announcements'][0],
|
||||||
|
'desktopMinVersion'
|
||||||
|
>;
|
||||||
|
|
||||||
|
export type ReleaseNoteType = ReleaseNoteResponseType &
|
||||||
|
Pick<ReleaseNotesManifestResponseType['announcements'][0], 'ctaId' | 'link'>;
|
||||||
|
|
||||||
|
let initComplete = false;
|
||||||
|
|
||||||
|
export class ReleaseNotesFetcher {
|
||||||
|
private timeout: NodeJS.Timeout | undefined;
|
||||||
|
private isRunning = false;
|
||||||
|
|
||||||
|
protected async scheduleUpdateForNow(): Promise<void> {
|
||||||
|
const now = Date.now();
|
||||||
|
await window.textsecure.storage.put(NEXT_FETCH_TIME_STORAGE_KEY, now);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected setTimeoutForNextRun(): void {
|
||||||
|
const now = Date.now();
|
||||||
|
const time = window.textsecure.storage.get(
|
||||||
|
NEXT_FETCH_TIME_STORAGE_KEY,
|
||||||
|
now
|
||||||
|
);
|
||||||
|
|
||||||
|
log.info(
|
||||||
|
'ReleaseNotesFetcher: Next update scheduled for',
|
||||||
|
new Date(time).toISOString()
|
||||||
|
);
|
||||||
|
|
||||||
|
let waitTime = time - now;
|
||||||
|
if (waitTime < 0) {
|
||||||
|
waitTime = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
clearTimeoutIfNecessary(this.timeout);
|
||||||
|
this.timeout = setTimeout(() => this.runWhenOnline(), waitTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
private getOrInitializeVersionWatermark(): string {
|
||||||
|
const versionWatermark = window.textsecure.storage.get(
|
||||||
|
VERSION_WATERMARK_STORAGE_KEY
|
||||||
|
);
|
||||||
|
if (versionWatermark) {
|
||||||
|
return versionWatermark;
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info(
|
||||||
|
'ReleaseNotesFetcher: Initializing version high watermark to current version'
|
||||||
|
);
|
||||||
|
const currentVersion = window.getVersion();
|
||||||
|
drop(
|
||||||
|
window.textsecure.storage.put(
|
||||||
|
VERSION_WATERMARK_STORAGE_KEY,
|
||||||
|
currentVersion
|
||||||
|
)
|
||||||
|
);
|
||||||
|
return currentVersion;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getReleaseNote(
|
||||||
|
note: ManifestReleaseNoteType
|
||||||
|
): Promise<ReleaseNoteType | undefined> {
|
||||||
|
if (!window.textsecure.server) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { uuid, ctaId, link } = note;
|
||||||
|
const result = await window.textsecure.server.getReleaseNote({
|
||||||
|
uuid,
|
||||||
|
});
|
||||||
|
strictAssert(
|
||||||
|
result.uuid === uuid,
|
||||||
|
'UUID of localized release note should match requested UUID'
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...result,
|
||||||
|
uuid,
|
||||||
|
ctaId,
|
||||||
|
link,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private async processReleaseNotes(
|
||||||
|
notes: ReadonlyArray<ManifestReleaseNoteType>
|
||||||
|
): Promise<void> {
|
||||||
|
const sortedNotes = [...notes].sort(
|
||||||
|
(a: ManifestReleaseNoteType, b: ManifestReleaseNoteType) =>
|
||||||
|
semver.compare(a.desktopMinVersion, b.desktopMinVersion)
|
||||||
|
);
|
||||||
|
const hydratedNotes = [];
|
||||||
|
for (const note of sortedNotes) {
|
||||||
|
// eslint-disable-next-line no-await-in-loop
|
||||||
|
hydratedNotes.push(await this.getReleaseNote(note));
|
||||||
|
}
|
||||||
|
if (!hydratedNotes.length) {
|
||||||
|
log.warn('ReleaseNotesFetcher: No hydrated notes available, stopping');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info('ReleaseNotesFetcher: Ensuring Signal conversation');
|
||||||
|
const signalConversation =
|
||||||
|
await window.ConversationController.getOrCreateSignalConversation();
|
||||||
|
|
||||||
|
const messages: Array<MessageAttributesType> = [];
|
||||||
|
hydratedNotes.forEach(async (note, index) => {
|
||||||
|
if (!note) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { title, body } = note;
|
||||||
|
const messageBody = `${title}\n\n${body}`;
|
||||||
|
const bodyRanges = [
|
||||||
|
{ start: 0, length: title.length, style: BodyRange.Style.BOLD },
|
||||||
|
];
|
||||||
|
const timestamp = Date.now() + index;
|
||||||
|
|
||||||
|
const message: MessageAttributesType = {
|
||||||
|
...generateMessageId(incrementMessageCounter()),
|
||||||
|
body: messageBody,
|
||||||
|
bodyRanges,
|
||||||
|
conversationId: signalConversation.id,
|
||||||
|
readStatus: ReadStatus.Unread,
|
||||||
|
seenStatus: SeenStatus.Unseen,
|
||||||
|
received_at_ms: timestamp,
|
||||||
|
sent_at: timestamp,
|
||||||
|
serverTimestamp: timestamp,
|
||||||
|
sourceDevice: 1,
|
||||||
|
sourceServiceId: signalConversation.getServiceId(),
|
||||||
|
timestamp,
|
||||||
|
type: 'incoming',
|
||||||
|
};
|
||||||
|
|
||||||
|
window.MessageCache.toMessageAttributes(message);
|
||||||
|
signalConversation.trigger('newmessage', message);
|
||||||
|
|
||||||
|
messages.push(message);
|
||||||
|
});
|
||||||
|
|
||||||
|
await Promise.all(
|
||||||
|
messages.map(message => saveNewMessageBatcher.add(message))
|
||||||
|
);
|
||||||
|
|
||||||
|
signalConversation.set({ active_at: Date.now(), isArchived: false });
|
||||||
|
drop(signalConversation.updateUnread());
|
||||||
|
|
||||||
|
const newestNote = last(sortedNotes);
|
||||||
|
strictAssert(newestNote, 'processReleaseNotes requires at least 1 note');
|
||||||
|
|
||||||
|
const versionWatermark = newestNote.desktopMinVersion;
|
||||||
|
log.info(
|
||||||
|
`ReleaseNotesFetcher: Updating version watermark to ${versionWatermark}`
|
||||||
|
);
|
||||||
|
drop(
|
||||||
|
window.textsecure.storage.put(
|
||||||
|
VERSION_WATERMARK_STORAGE_KEY,
|
||||||
|
versionWatermark
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async scheduleForNextRun(): Promise<void> {
|
||||||
|
const now = Date.now();
|
||||||
|
const nextTime = now + FETCH_INTERVAL;
|
||||||
|
await window.textsecure.storage.put(NEXT_FETCH_TIME_STORAGE_KEY, nextTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async run(): Promise<void> {
|
||||||
|
if (this.isRunning) {
|
||||||
|
log.warn('ReleaseNotesFetcher: Already running, preventing reentrancy');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.isRunning = true;
|
||||||
|
log.info('ReleaseNotesFetcher: Starting');
|
||||||
|
try {
|
||||||
|
const versionWatermark = this.getOrInitializeVersionWatermark();
|
||||||
|
log.info(`ReleaseNotesFetcher: Version watermark is ${versionWatermark}`);
|
||||||
|
|
||||||
|
if (!window.textsecure.server) {
|
||||||
|
log.info('ReleaseNotesFetcher: WebAPI unavailable');
|
||||||
|
throw new Error('WebAPI unavailable');
|
||||||
|
}
|
||||||
|
|
||||||
|
const hash = await window.textsecure.server.getReleaseNotesManifestHash();
|
||||||
|
if (!hash) {
|
||||||
|
throw new Error('Release notes manifest hash missing');
|
||||||
|
}
|
||||||
|
|
||||||
|
const previousHash = window.textsecure.storage.get(
|
||||||
|
PREVIOUS_MANIFEST_HASH_STORAGE_KEY
|
||||||
|
);
|
||||||
|
if (hash !== previousHash) {
|
||||||
|
log.info('ReleaseNotesFetcher: Manifest hash changed, fetching');
|
||||||
|
const manifest =
|
||||||
|
await window.textsecure.server.getReleaseNotesManifest();
|
||||||
|
const validNotes = manifest.announcements.filter(
|
||||||
|
(note): note is ManifestReleaseNoteType =>
|
||||||
|
note.desktopMinVersion != null &&
|
||||||
|
semver.gt(note.desktopMinVersion, versionWatermark)
|
||||||
|
);
|
||||||
|
if (validNotes.length) {
|
||||||
|
log.info(
|
||||||
|
`ReleaseNotesFetcher: Processing ${validNotes.length} new release notes`
|
||||||
|
);
|
||||||
|
drop(this.processReleaseNotes(validNotes));
|
||||||
|
} else {
|
||||||
|
log.info('ReleaseNotesFetcher: No new release notes');
|
||||||
|
}
|
||||||
|
|
||||||
|
drop(
|
||||||
|
window.textsecure.storage.put(
|
||||||
|
PREVIOUS_MANIFEST_HASH_STORAGE_KEY,
|
||||||
|
hash
|
||||||
|
)
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
log.info('ReleaseNotesFetcher: Manifest hash unchanged');
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.scheduleForNextRun();
|
||||||
|
this.setTimeoutForNextRun();
|
||||||
|
} catch (error) {
|
||||||
|
const errorString =
|
||||||
|
error instanceof HTTPError
|
||||||
|
? error.code.toString()
|
||||||
|
: Errors.toLogFormat(error);
|
||||||
|
log.error(
|
||||||
|
`ReleaseNotesFetcher: Error, trying again later. ${errorString}`
|
||||||
|
);
|
||||||
|
setTimeout(() => this.setTimeoutForNextRun(), ERROR_RETRY_DELAY);
|
||||||
|
} finally {
|
||||||
|
this.isRunning = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private runWhenOnline() {
|
||||||
|
if (window.textsecure.server?.isOnline()) {
|
||||||
|
drop(this.run());
|
||||||
|
} else {
|
||||||
|
log.info(
|
||||||
|
'ReleaseNotesFetcher: We are offline; will fetch when we are next online'
|
||||||
|
);
|
||||||
|
const listener = () => {
|
||||||
|
window.Whisper.events.off('online', listener);
|
||||||
|
this.setTimeoutForNextRun();
|
||||||
|
};
|
||||||
|
window.Whisper.events.on('online', listener);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async init(
|
||||||
|
events: MinimalEventsType,
|
||||||
|
isNewVersion: boolean
|
||||||
|
): Promise<void> {
|
||||||
|
if (initComplete || !this.isEnabled()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
initComplete = true;
|
||||||
|
|
||||||
|
const listener = new ReleaseNotesFetcher();
|
||||||
|
|
||||||
|
if (isNewVersion) {
|
||||||
|
await listener.scheduleUpdateForNow();
|
||||||
|
}
|
||||||
|
listener.setTimeoutForNextRun();
|
||||||
|
|
||||||
|
events.on('timetravel', () => {
|
||||||
|
if (Registration.isDone()) {
|
||||||
|
listener.setTimeoutForNextRun();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public static isEnabled(): boolean {
|
||||||
|
const version = window.getVersion();
|
||||||
|
|
||||||
|
if (isProduction(version)) {
|
||||||
|
return RemoteConfig.isEnabled('desktop.releaseNotes');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isBeta(version)) {
|
||||||
|
return RemoteConfig.isEnabled('desktop.releaseNotes.beta');
|
||||||
|
}
|
||||||
|
|
||||||
|
return RemoteConfig.isEnabled('desktop.releaseNotes.dev');
|
||||||
|
}
|
||||||
|
}
|
|
@ -380,10 +380,6 @@ export const _getLeftPaneLists = (
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isSignalConversation(conversation)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// We always show pinned conversations
|
// We always show pinned conversations
|
||||||
if (conversation.isPinned) {
|
if (conversation.isPinned) {
|
||||||
pinnedConversations.push(conversation);
|
pinnedConversations.push(conversation);
|
||||||
|
|
|
@ -43,6 +43,7 @@ import { getKeysForServiceId } from './getKeysForServiceId';
|
||||||
import { SignalService as Proto } from '../protobuf';
|
import { SignalService as Proto } from '../protobuf';
|
||||||
import * as log from '../logging/log';
|
import * as log from '../logging/log';
|
||||||
import type { GroupSendToken } from '../types/GroupSendEndorsements';
|
import type { GroupSendToken } from '../types/GroupSendEndorsements';
|
||||||
|
import { isSignalServiceId } from '../util/isSignalConversation';
|
||||||
|
|
||||||
export const enum SenderCertificateMode {
|
export const enum SenderCertificateMode {
|
||||||
WithE164,
|
WithE164,
|
||||||
|
@ -686,6 +687,15 @@ export default class OutgoingMessage {
|
||||||
}
|
}
|
||||||
|
|
||||||
async sendToServiceId(serviceId: ServiceIdString): Promise<void> {
|
async sendToServiceId(serviceId: ServiceIdString): Promise<void> {
|
||||||
|
if (isSignalServiceId(serviceId)) {
|
||||||
|
this.registerError(
|
||||||
|
serviceId,
|
||||||
|
'Failed to send to Signal serviceId',
|
||||||
|
new Error("Can't send to Signal serviceId")
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const ourAci = window.textsecure.storage.user.getCheckedAci();
|
const ourAci = window.textsecure.storage.user.getCheckedAci();
|
||||||
const deviceIds = await window.textsecure.storage.protocol.getDeviceIds({
|
const deviceIds = await window.textsecure.storage.protocol.getDeviceIds({
|
||||||
|
|
|
@ -657,6 +657,8 @@ const URL_CALLS = {
|
||||||
callLinkCreateAuth: 'v1/call-link/create-auth',
|
callLinkCreateAuth: 'v1/call-link/create-auth',
|
||||||
registration: 'v1/registration',
|
registration: 'v1/registration',
|
||||||
registerCapabilities: 'v1/devices/capabilities',
|
registerCapabilities: 'v1/devices/capabilities',
|
||||||
|
releaseNotesManifest: 'dynamic/release-notes/release-notes-v2.json',
|
||||||
|
releaseNotes: 'static/release-notes',
|
||||||
reportMessage: 'v1/messages/report',
|
reportMessage: 'v1/messages/report',
|
||||||
setBackupId: 'v1/archives/backupid',
|
setBackupId: 'v1/archives/backupid',
|
||||||
setBackupSignatureKey: 'v1/archives/keys',
|
setBackupSignatureKey: 'v1/archives/keys',
|
||||||
|
@ -1209,6 +1211,56 @@ export type GetBackupInfoResponseType = z.infer<
|
||||||
typeof getBackupInfoResponseSchema
|
typeof getBackupInfoResponseSchema
|
||||||
>;
|
>;
|
||||||
|
|
||||||
|
export type GetReleaseNoteOptionsType = Readonly<{
|
||||||
|
uuid: string;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
export const releaseNoteSchema = z.object({
|
||||||
|
uuid: z.string(),
|
||||||
|
title: z.string(),
|
||||||
|
body: z.string(),
|
||||||
|
linkText: z.string().optional(),
|
||||||
|
callToActionText: z.string().optional(),
|
||||||
|
includeBoostMessage: z.boolean().optional().default(true),
|
||||||
|
bodyRanges: z
|
||||||
|
.array(
|
||||||
|
z.object({
|
||||||
|
style: z.string(),
|
||||||
|
start: z.number(),
|
||||||
|
length: z.number(),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.optional(),
|
||||||
|
media: z.string().optional(),
|
||||||
|
mediaHeight: z.coerce
|
||||||
|
.number()
|
||||||
|
.optional()
|
||||||
|
.transform(x => x || undefined),
|
||||||
|
mediaWidth: z.coerce
|
||||||
|
.number()
|
||||||
|
.optional()
|
||||||
|
.transform(x => x || undefined),
|
||||||
|
mediaContentType: z.string().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type ReleaseNoteResponseType = z.infer<typeof releaseNoteSchema>;
|
||||||
|
|
||||||
|
export const releaseNotesManifestSchema = z.object({
|
||||||
|
announcements: z
|
||||||
|
.object({
|
||||||
|
uuid: z.string(),
|
||||||
|
countries: z.string().optional(),
|
||||||
|
desktopMinVersion: z.string().optional(),
|
||||||
|
link: z.string().optional(),
|
||||||
|
ctaId: z.string().optional(),
|
||||||
|
})
|
||||||
|
.array(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type ReleaseNotesManifestResponseType = z.infer<
|
||||||
|
typeof releaseNotesManifestSchema
|
||||||
|
>;
|
||||||
|
|
||||||
export type CallLinkCreateAuthResponseType = Readonly<{
|
export type CallLinkCreateAuthResponseType = Readonly<{
|
||||||
credential: string;
|
credential: string;
|
||||||
}>;
|
}>;
|
||||||
|
@ -1339,6 +1391,11 @@ export type WebAPIType = {
|
||||||
getSenderCertificate: (
|
getSenderCertificate: (
|
||||||
withUuid?: boolean
|
withUuid?: boolean
|
||||||
) => Promise<GetSenderCertificateResultType>;
|
) => Promise<GetSenderCertificateResultType>;
|
||||||
|
getReleaseNote: (
|
||||||
|
options: GetReleaseNoteOptionsType
|
||||||
|
) => Promise<ReleaseNoteResponseType>;
|
||||||
|
getReleaseNotesManifest: () => Promise<ReleaseNotesManifestResponseType>;
|
||||||
|
getReleaseNotesManifestHash: () => Promise<string | undefined>;
|
||||||
getSticker: (packId: string, stickerId: number) => Promise<Uint8Array>;
|
getSticker: (packId: string, stickerId: number) => Promise<Uint8Array>;
|
||||||
getStickerPackManifest: (packId: string) => Promise<StickerPackManifestType>;
|
getStickerPackManifest: (packId: string) => Promise<StickerPackManifestType>;
|
||||||
getStorageCredentials: MessageSender['getStorageCredentials'];
|
getStorageCredentials: MessageSender['getStorageCredentials'];
|
||||||
|
@ -1807,6 +1864,9 @@ export function initialize({
|
||||||
getProfile,
|
getProfile,
|
||||||
getProfileUnauth,
|
getProfileUnauth,
|
||||||
getProvisioningResource,
|
getProvisioningResource,
|
||||||
|
getReleaseNote,
|
||||||
|
getReleaseNotesManifest,
|
||||||
|
getReleaseNotesManifestHash,
|
||||||
getTransferArchive,
|
getTransferArchive,
|
||||||
getSenderCertificate,
|
getSenderCertificate,
|
||||||
getSocketStatus,
|
getSocketStatus,
|
||||||
|
@ -2099,6 +2159,44 @@ export function initialize({
|
||||||
languages: Record<string, Array<string>>;
|
languages: Record<string, Array<string>>;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
async function getReleaseNote({
|
||||||
|
uuid,
|
||||||
|
}: GetReleaseNoteOptionsType): Promise<ReleaseNoteResponseType> {
|
||||||
|
const rawRes = await _ajax({
|
||||||
|
call: 'releaseNotes',
|
||||||
|
host: resourcesUrl,
|
||||||
|
httpType: 'GET',
|
||||||
|
responseType: 'json',
|
||||||
|
urlParameters: `/${uuid}/en.json`,
|
||||||
|
});
|
||||||
|
return parseUnknown(releaseNoteSchema, rawRes);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getReleaseNotesManifest(): Promise<ReleaseNotesManifestResponseType> {
|
||||||
|
const rawRes = await _ajax({
|
||||||
|
call: 'releaseNotesManifest',
|
||||||
|
host: resourcesUrl,
|
||||||
|
httpType: 'GET',
|
||||||
|
responseType: 'json',
|
||||||
|
});
|
||||||
|
return parseUnknown(releaseNotesManifestSchema, rawRes);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getReleaseNotesManifestHash(): Promise<string | undefined> {
|
||||||
|
const { response } = await _ajax({
|
||||||
|
call: 'releaseNotesManifest',
|
||||||
|
host: resourcesUrl,
|
||||||
|
httpType: 'HEAD',
|
||||||
|
responseType: 'byteswithdetails',
|
||||||
|
});
|
||||||
|
|
||||||
|
const etag = response.headers.get('etag');
|
||||||
|
if (etag == null) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return etag;
|
||||||
|
}
|
||||||
|
|
||||||
async function getStorageManifest(
|
async function getStorageManifest(
|
||||||
options: StorageServiceCallOptionsType = {}
|
options: StorageServiceCallOptionsType = {}
|
||||||
|
|
3
ts/types/Storage.d.ts
vendored
3
ts/types/Storage.d.ts
vendored
|
@ -196,6 +196,9 @@ export type StorageAccessType = {
|
||||||
// Note: Upon capability deprecation - change the value type to `never` and
|
// Note: Upon capability deprecation - change the value type to `never` and
|
||||||
// remove it in `ts/background.ts`
|
// remove it in `ts/background.ts`
|
||||||
};
|
};
|
||||||
|
releaseNotesNextFetchTime: number;
|
||||||
|
releaseNotesVersionWatermark: string;
|
||||||
|
releaseNotesPreviousManifestHash: string;
|
||||||
|
|
||||||
// If present - we are downloading backup
|
// If present - we are downloading backup
|
||||||
backupDownloadPath: string;
|
backupDownloadPath: string;
|
||||||
|
|
|
@ -16,3 +16,7 @@ export function isSignalConversation(conversation: {
|
||||||
|
|
||||||
return window.ConversationController.isSignalConversationId(id);
|
return window.ConversationController.isSignalConversationId(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isSignalServiceId(serviceId: ServiceIdString): boolean {
|
||||||
|
return serviceId === SIGNAL_ACI;
|
||||||
|
}
|
||||||
|
|
|
@ -12,6 +12,7 @@ import { isConversationUnregistered } from './isConversationUnregistered';
|
||||||
import { missingCaseError } from './missingCaseError';
|
import { missingCaseError } from './missingCaseError';
|
||||||
import type { ConversationModel } from '../models/conversations';
|
import type { ConversationModel } from '../models/conversations';
|
||||||
import { mapEmplace } from './mapEmplace';
|
import { mapEmplace } from './mapEmplace';
|
||||||
|
import { isSignalConversation } from './isSignalConversation';
|
||||||
|
|
||||||
const CHUNK_SIZE = 100;
|
const CHUNK_SIZE = 100;
|
||||||
|
|
||||||
|
@ -124,7 +125,12 @@ export async function sendReceipts({
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (isSignalConversation(sender.attributes)) {
|
||||||
|
log.info(
|
||||||
|
`conversation ${sender.idForLogging()} is Signal conversation; refusing to send`
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
log.info(`Sending receipt of type ${type} to ${sender.idForLogging()}`);
|
log.info(`Sending receipt of type ${type} to ${sender.idForLogging()}`);
|
||||||
|
|
||||||
const conversation = window.ConversationController.get(conversationId);
|
const conversation = window.ConversationController.get(conversationId);
|
||||||
|
|
Loading…
Add table
Reference in a new issue