Handle signal.me links
This commit is contained in:
parent
4273ddb6d0
commit
6f242eca57
9 changed files with 195 additions and 36 deletions
16
main.js
16
main.js
|
@ -1744,7 +1744,8 @@ function handleSgnlHref(incomingHref) {
|
||||||
({ command, args, hash } = parseSignalHttpsLink(incomingHref, logger));
|
({ command, args, hash } = parseSignalHttpsLink(incomingHref, logger));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (command === 'addstickers' && mainWindow && mainWindow.webContents) {
|
if (mainWindow && mainWindow.webContents) {
|
||||||
|
if (command === 'addstickers') {
|
||||||
console.log('Opening sticker pack from sgnl protocol link');
|
console.log('Opening sticker pack from sgnl protocol link');
|
||||||
const packId = args.get('pack_id');
|
const packId = args.get('pack_id');
|
||||||
const packKeyHex = args.get('pack_key');
|
const packKeyHex = args.get('pack_key');
|
||||||
|
@ -1752,17 +1753,16 @@ function handleSgnlHref(incomingHref) {
|
||||||
? Buffer.from(packKeyHex, 'hex').toString('base64')
|
? Buffer.from(packKeyHex, 'hex').toString('base64')
|
||||||
: '';
|
: '';
|
||||||
mainWindow.webContents.send('show-sticker-pack', { packId, packKey });
|
mainWindow.webContents.send('show-sticker-pack', { packId, packKey });
|
||||||
} else if (
|
} else if (command === 'signal.group' && hash) {
|
||||||
command === 'signal.group' &&
|
|
||||||
hash &&
|
|
||||||
mainWindow &&
|
|
||||||
mainWindow.webContents
|
|
||||||
) {
|
|
||||||
console.log('Showing group from sgnl protocol link');
|
console.log('Showing group from sgnl protocol link');
|
||||||
mainWindow.webContents.send('show-group-via-link', { hash });
|
mainWindow.webContents.send('show-group-via-link', { hash });
|
||||||
} else if (mainWindow && mainWindow.webContents) {
|
} else if (command === 'signal.me' && hash) {
|
||||||
|
console.log('Showing conversation from sgnl protocol link');
|
||||||
|
mainWindow.webContents.send('show-conversation-via-signal.me', { hash });
|
||||||
|
} else {
|
||||||
console.log('Showing warning that we cannot process link');
|
console.log('Showing warning that we cannot process link');
|
||||||
mainWindow.webContents.send('unknown-sgnl-link');
|
mainWindow.webContents.send('unknown-sgnl-link');
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
console.error('Unhandled sgnl link');
|
console.error('Unhandled sgnl link');
|
||||||
}
|
}
|
||||||
|
|
13
preload.js
13
preload.js
|
@ -12,6 +12,7 @@ try {
|
||||||
const electron = require('electron');
|
const electron = require('electron');
|
||||||
const semver = require('semver');
|
const semver = require('semver');
|
||||||
const _ = require('lodash');
|
const _ = require('lodash');
|
||||||
|
const { strictAssert } = require('./ts/util/assert');
|
||||||
|
|
||||||
// It is important to call this as early as possible
|
// It is important to call this as early as possible
|
||||||
require('./ts/windows/context');
|
require('./ts/windows/context');
|
||||||
|
@ -301,6 +302,16 @@ try {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
ipc.on('show-conversation-via-signal.me', (_event, info) => {
|
||||||
|
const { hash } = info;
|
||||||
|
strictAssert(typeof hash === 'string', 'Got an invalid hash over IPC');
|
||||||
|
|
||||||
|
const { showConversationViaSignalDotMe } = window.Events;
|
||||||
|
if (showConversationViaSignalDotMe) {
|
||||||
|
showConversationViaSignalDotMe(hash);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
ipc.on('unknown-sgnl-link', () => {
|
ipc.on('unknown-sgnl-link', () => {
|
||||||
const { unknownSignalLink } = window.Events;
|
const { unknownSignalLink } = window.Events;
|
||||||
if (unknownSignalLink) {
|
if (unknownSignalLink) {
|
||||||
|
@ -398,8 +409,6 @@ try {
|
||||||
};
|
};
|
||||||
|
|
||||||
window.isValidGuid = isValidGuid;
|
window.isValidGuid = isValidGuid;
|
||||||
// https://stackoverflow.com/a/23299989
|
|
||||||
window.isValidE164 = maybeE164 => /^\+?[1-9]\d{1,14}$/.test(maybeE164);
|
|
||||||
|
|
||||||
window.React = require('react');
|
window.React = require('react');
|
||||||
window.ReactDOM = require('react-dom');
|
window.ReactDOM = require('react-dom');
|
||||||
|
|
|
@ -33,6 +33,7 @@ import { isConversationSMSOnly } from '../util/isConversationSMSOnly';
|
||||||
import { isConversationUnregistered } from '../util/isConversationUnregistered';
|
import { isConversationUnregistered } from '../util/isConversationUnregistered';
|
||||||
import { missingCaseError } from '../util/missingCaseError';
|
import { missingCaseError } from '../util/missingCaseError';
|
||||||
import { sniffImageMimeType } from '../util/sniffImageMimeType';
|
import { sniffImageMimeType } from '../util/sniffImageMimeType';
|
||||||
|
import { isValidE164 } from '../util/isValidE164';
|
||||||
import { MIMEType, IMAGE_WEBP } from '../types/MIME';
|
import { MIMEType, IMAGE_WEBP } from '../types/MIME';
|
||||||
import {
|
import {
|
||||||
arrayBufferToBase64,
|
arrayBufferToBase64,
|
||||||
|
@ -232,7 +233,7 @@ export class ConversationModel extends window.Backbone
|
||||||
}
|
}
|
||||||
|
|
||||||
initialize(attributes: Partial<ConversationAttributesType> = {}): void {
|
initialize(attributes: Partial<ConversationAttributesType> = {}): void {
|
||||||
if (window.isValidE164(attributes.id)) {
|
if (isValidE164(attributes.id, false)) {
|
||||||
this.set({ id: window.getGuid(), e164: attributes.id });
|
this.set({ id: window.getGuid(), e164: attributes.id });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
35
ts/test-both/util/isValidE164_test.ts
Normal file
35
ts/test-both/util/isValidE164_test.ts
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
// Copyright 2021 Signal Messenger, LLC
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
import { assert } from 'chai';
|
||||||
|
|
||||||
|
import { isValidE164 } from '../../util/isValidE164';
|
||||||
|
|
||||||
|
describe('isValidE164', () => {
|
||||||
|
it('returns false for non-strings', () => {
|
||||||
|
assert.isFalse(isValidE164(undefined, false));
|
||||||
|
assert.isFalse(isValidE164(18885551234, false));
|
||||||
|
assert.isFalse(isValidE164(['+18885551234'], false));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns false for invalid E164s', () => {
|
||||||
|
assert.isFalse(isValidE164('', false));
|
||||||
|
assert.isFalse(isValidE164('+05551234', false));
|
||||||
|
assert.isFalse(isValidE164('+1800ENCRYPT', false));
|
||||||
|
assert.isFalse(isValidE164('+1-888-555-1234', false));
|
||||||
|
assert.isFalse(isValidE164('+1 (888) 555-1234', false));
|
||||||
|
assert.isFalse(isValidE164('+1012345678901234', false));
|
||||||
|
assert.isFalse(isValidE164('+18885551234extra', false));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns true for E164s that look valid', () => {
|
||||||
|
assert.isTrue(isValidE164('+18885551234', false));
|
||||||
|
assert.isTrue(isValidE164('+123456789012', false));
|
||||||
|
assert.isTrue(isValidE164('+12', false));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can make the leading + optional or required', () => {
|
||||||
|
assert.isTrue(isValidE164('18885551234', false));
|
||||||
|
assert.isFalse(isValidE164('18885551234', true));
|
||||||
|
});
|
||||||
|
});
|
|
@ -1,4 +1,4 @@
|
||||||
// Copyright 2020 Signal Messenger, LLC
|
// Copyright 2020-2021 Signal Messenger, LLC
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
import { assert } from 'chai';
|
import { assert } from 'chai';
|
||||||
|
@ -11,6 +11,7 @@ import {
|
||||||
isSignalHttpsLink,
|
isSignalHttpsLink,
|
||||||
parseSgnlHref,
|
parseSgnlHref,
|
||||||
parseCaptchaHref,
|
parseCaptchaHref,
|
||||||
|
parseE164FromSignalDotMeHash,
|
||||||
parseSignalHttpsLink,
|
parseSignalHttpsLink,
|
||||||
} from '../../util/sgnlHref';
|
} from '../../util/sgnlHref';
|
||||||
|
|
||||||
|
@ -131,10 +132,16 @@ describe('sgnlHref', () => {
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('returns false if the URL is not a valid Signal URL', () => {
|
||||||
|
assert.isFalse(isSignalHttpsLink('https://signal.org', explodingLogger));
|
||||||
|
assert.isFalse(isSignalHttpsLink('https://example.com', explodingLogger));
|
||||||
|
});
|
||||||
|
|
||||||
it('returns true if the protocol is "https:"', () => {
|
it('returns true if the protocol is "https:"', () => {
|
||||||
assert.isTrue(isSignalHttpsLink('https://signal.group', explodingLogger));
|
assert.isTrue(isSignalHttpsLink('https://signal.group', explodingLogger));
|
||||||
assert.isTrue(isSignalHttpsLink('https://signal.art', explodingLogger));
|
assert.isTrue(isSignalHttpsLink('https://signal.art', explodingLogger));
|
||||||
assert.isTrue(isSignalHttpsLink('HTTPS://signal.art', explodingLogger));
|
assert.isTrue(isSignalHttpsLink('HTTPS://signal.art', explodingLogger));
|
||||||
|
assert.isTrue(isSignalHttpsLink('https://signal.me', explodingLogger));
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns false if username or password are set', () => {
|
it('returns false if username or password are set', () => {
|
||||||
|
@ -288,6 +295,34 @@ describe('sgnlHref', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('parseE164FromSignalDotMeHash', () => {
|
||||||
|
it('returns undefined for invalid inputs', () => {
|
||||||
|
[
|
||||||
|
'',
|
||||||
|
' p/+18885551234',
|
||||||
|
'p/+18885551234 ',
|
||||||
|
'x/+18885551234',
|
||||||
|
'p/+notanumber',
|
||||||
|
'p/7c7e87a0-3b74-4efd-9a00-6eb8b1dd5be8',
|
||||||
|
'p/+08885551234',
|
||||||
|
'p/18885551234',
|
||||||
|
].forEach(hash => {
|
||||||
|
assert.isUndefined(parseE164FromSignalDotMeHash(hash));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns the E164 for valid inputs', () => {
|
||||||
|
assert.strictEqual(
|
||||||
|
parseE164FromSignalDotMeHash('p/+18885551234'),
|
||||||
|
'+18885551234'
|
||||||
|
);
|
||||||
|
assert.strictEqual(
|
||||||
|
parseE164FromSignalDotMeHash('p/+441632960104'),
|
||||||
|
'+441632960104'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('parseSignalHttpsLink', () => {
|
describe('parseSignalHttpsLink', () => {
|
||||||
it('returns a null command for invalid URLs', () => {
|
it('returns a null command for invalid URLs', () => {
|
||||||
['', 'https', 'https://example/?foo=bar'].forEach(href => {
|
['', 'https', 'https://example/?foo=bar'].forEach(href => {
|
||||||
|
@ -329,5 +364,19 @@ describe('sgnlHref', () => {
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('handles signal.me links', () => {
|
||||||
|
assert.deepEqual(
|
||||||
|
parseSignalHttpsLink(
|
||||||
|
'https://signal.me/#p/+18885551234',
|
||||||
|
explodingLogger
|
||||||
|
),
|
||||||
|
{
|
||||||
|
command: 'signal.me',
|
||||||
|
args: new Map<never, never>(),
|
||||||
|
hash: 'p/+18885551234',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -21,6 +21,7 @@ import { ConversationType } from '../state/ducks/conversations';
|
||||||
import { calling } from '../services/calling';
|
import { calling } from '../services/calling';
|
||||||
import { getConversationsWithCustomColorSelector } from '../state/selectors/conversations';
|
import { getConversationsWithCustomColorSelector } from '../state/selectors/conversations';
|
||||||
import { getCustomColors } from '../state/selectors/items';
|
import { getCustomColors } from '../state/selectors/items';
|
||||||
|
import { trigger } from '../shims/events';
|
||||||
import { themeChanged } from '../shims/themeChanged';
|
import { themeChanged } from '../shims/themeChanged';
|
||||||
import { renderClearingDataView } from '../shims/renderClearingDataView';
|
import { renderClearingDataView } from '../shims/renderClearingDataView';
|
||||||
|
|
||||||
|
@ -30,6 +31,7 @@ import { PhoneNumberSharingMode } from './phoneNumberSharingMode';
|
||||||
import { assert } from './assert';
|
import { assert } from './assert';
|
||||||
import * as durations from './durations';
|
import * as durations from './durations';
|
||||||
import { isPhoneNumberSharingEnabled } from './isPhoneNumberSharingEnabled';
|
import { isPhoneNumberSharingEnabled } from './isPhoneNumberSharingEnabled';
|
||||||
|
import { parseE164FromSignalDotMeHash } from './sgnlHref';
|
||||||
|
|
||||||
type ThemeType = 'light' | 'dark' | 'system';
|
type ThemeType = 'light' | 'dark' | 'system';
|
||||||
type NotificationSettingType = 'message' | 'name' | 'count' | 'off';
|
type NotificationSettingType = 'message' | 'name' | 'count' | 'off';
|
||||||
|
@ -92,6 +94,7 @@ export type IPCEventsCallbacksType = {
|
||||||
removeDarkOverlay: () => void;
|
removeDarkOverlay: () => void;
|
||||||
resetAllChatColors: () => void;
|
resetAllChatColors: () => void;
|
||||||
resetDefaultChatColor: () => void;
|
resetDefaultChatColor: () => void;
|
||||||
|
showConversationViaSignalDotMe: (hash: string) => void;
|
||||||
showKeyboardShortcuts: () => void;
|
showKeyboardShortcuts: () => void;
|
||||||
showGroupViaLink: (x: string) => Promise<void>;
|
showGroupViaLink: (x: string) => Promise<void>;
|
||||||
showStickerPack: (packId: string, key: string) => void;
|
showStickerPack: (packId: string, key: string) => void;
|
||||||
|
@ -465,19 +468,33 @@ export function createIPCEvents(
|
||||||
}
|
}
|
||||||
window.isShowingModal = false;
|
window.isShowingModal = false;
|
||||||
},
|
},
|
||||||
|
showConversationViaSignalDotMe(hash: string) {
|
||||||
|
if (!window.Signal.Util.Registration.everDone()) {
|
||||||
|
window.log.info(
|
||||||
|
'showConversationViaSignalDotMe: Not registered, returning early'
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const maybeE164 = parseE164FromSignalDotMeHash(hash);
|
||||||
|
if (maybeE164) {
|
||||||
|
trigger('showConversation', maybeE164);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
window.log.info('showConversationViaSignalDotMe: invalid E164');
|
||||||
|
if (window.isShowingModal) {
|
||||||
|
window.log.info(
|
||||||
|
'showConversationViaSignalDotMe: a modal is already showing. Doing nothing'
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
showUnknownSgnlLinkModal();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
unknownSignalLink: () => {
|
unknownSignalLink: () => {
|
||||||
window.log.warn('unknownSignalLink: Showing error dialog');
|
window.log.warn('unknownSignalLink: Showing error dialog');
|
||||||
const errorView = new window.Whisper.ReactWrapperView({
|
showUnknownSgnlLinkModal();
|
||||||
className: 'error-modal-wrapper',
|
|
||||||
Component: window.Signal.Components.ErrorModal,
|
|
||||||
props: {
|
|
||||||
description: window.i18n('unknown-sgnl-link'),
|
|
||||||
onClose: () => {
|
|
||||||
errorView.remove();
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
|
|
||||||
installStickerPack: async (packId, key) => {
|
installStickerPack: async (packId, key) => {
|
||||||
|
@ -494,3 +511,16 @@ export function createIPCEvents(
|
||||||
...overrideEvents,
|
...overrideEvents,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function showUnknownSgnlLinkModal(): void {
|
||||||
|
const errorView = new window.Whisper.ReactWrapperView({
|
||||||
|
className: 'error-modal-wrapper',
|
||||||
|
Component: window.Signal.Components.ErrorModal,
|
||||||
|
props: {
|
||||||
|
description: window.i18n('unknown-sgnl-link'),
|
||||||
|
onClose: () => {
|
||||||
|
errorView.remove();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
22
ts/util/isValidE164.ts
Normal file
22
ts/util/isValidE164.ts
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
// Copyright 2021 Signal Messenger, LLC
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns `true` if the input looks like a valid E164, and `false` otherwise. Note that
|
||||||
|
* this may return false positives, as it is a fairly naïve check.
|
||||||
|
*
|
||||||
|
* See <https://www.twilio.com/docs/glossary/what-e164#regex-matching-for-e164> and
|
||||||
|
* <https://stackoverflow.com/a/23299989>.
|
||||||
|
*/
|
||||||
|
export function isValidE164(
|
||||||
|
value: unknown,
|
||||||
|
mustStartWithPlus: boolean
|
||||||
|
): value is string {
|
||||||
|
if (typeof value !== 'string') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const regex = mustStartWithPlus ? /^\+[1-9]\d{1,14}$/ : /^\+?[1-9]\d{1,14}$/;
|
||||||
|
|
||||||
|
return regex.test(value);
|
||||||
|
}
|
|
@ -3,6 +3,9 @@
|
||||||
|
|
||||||
import { LoggerType } from '../types/Logging';
|
import { LoggerType } from '../types/Logging';
|
||||||
import { maybeParseUrl } from './url';
|
import { maybeParseUrl } from './url';
|
||||||
|
import { isValidE164 } from './isValidE164';
|
||||||
|
|
||||||
|
const SIGNAL_DOT_ME_HASH_PREFIX = 'p/';
|
||||||
|
|
||||||
function parseUrl(value: string | URL, logger: LoggerType): undefined | URL {
|
function parseUrl(value: string | URL, logger: LoggerType): undefined | URL {
|
||||||
if (value instanceof URL) {
|
if (value instanceof URL) {
|
||||||
|
@ -41,7 +44,9 @@ export function isSignalHttpsLink(
|
||||||
!url.password &&
|
!url.password &&
|
||||||
!url.port &&
|
!url.port &&
|
||||||
url.protocol === 'https:' &&
|
url.protocol === 'https:' &&
|
||||||
(url.host === 'signal.group' || url.host === 'signal.art')
|
(url.host === 'signal.group' ||
|
||||||
|
url.host === 'signal.art' ||
|
||||||
|
url.host === 'signal.me')
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -119,7 +124,7 @@ export function parseSignalHttpsLink(
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (url.host === 'signal.group') {
|
if (url.host === 'signal.group' || url.host === 'signal.me') {
|
||||||
return {
|
return {
|
||||||
command: url.host,
|
command: url.host,
|
||||||
args: new Map<string, string>(),
|
args: new Map<string, string>(),
|
||||||
|
@ -129,3 +134,12 @@ export function parseSignalHttpsLink(
|
||||||
|
|
||||||
return { command: null, args: new Map<never, never>() };
|
return { command: null, args: new Map<never, never>() };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function parseE164FromSignalDotMeHash(hash: string): undefined | string {
|
||||||
|
if (!hash.startsWith(SIGNAL_DOT_ME_HASH_PREFIX)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const maybeE164 = hash.slice(SIGNAL_DOT_ME_HASH_PREFIX.length);
|
||||||
|
return isValidE164(maybeE164, true) ? maybeE164 : undefined;
|
||||||
|
}
|
||||||
|
|
1
ts/window.d.ts
vendored
1
ts/window.d.ts
vendored
|
@ -229,7 +229,6 @@ declare global {
|
||||||
isBeforeVersion: (version: string, anotherVersion: string) => boolean;
|
isBeforeVersion: (version: string, anotherVersion: string) => boolean;
|
||||||
isFullScreen: () => boolean;
|
isFullScreen: () => boolean;
|
||||||
isValidGuid: typeof isValidGuid;
|
isValidGuid: typeof isValidGuid;
|
||||||
isValidE164: (maybeE164: unknown) => boolean;
|
|
||||||
libphonenumber: {
|
libphonenumber: {
|
||||||
util: {
|
util: {
|
||||||
getRegionCodeForNumber: (number: string) => string;
|
getRegionCodeForNumber: (number: string) => string;
|
||||||
|
|
Loading…
Reference in a new issue