Use libsignal-client for username validation

This commit is contained in:
Fedor Indutny 2023-05-24 02:07:59 +02:00 committed by GitHub
parent 3ff390e1c4
commit c0663ed57c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 35 additions and 68 deletions

View file

@ -98,11 +98,12 @@ export function ChooseGroupMembersModal({
}: PropsType): JSX.Element { }: PropsType): JSX.Element {
const [focusRef] = useRestoreFocus(); const [focusRef] = useRestoreFocus();
const parsedUsername = getUsernameFromSearch(searchTerm);
let username: string | undefined; let username: string | undefined;
let isUsernameChecked = false; let isUsernameChecked = false;
let isUsernameVisible = false; let isUsernameVisible = false;
if (isUsernamesEnabled) { if (isUsernamesEnabled) {
username = getUsernameFromSearch(searchTerm); username = parsedUsername;
isUsernameChecked = selectedContacts.some( isUsernameChecked = selectedContacts.some(
contact => contact.username === username contact => contact.username === username
@ -114,7 +115,7 @@ export function ChooseGroupMembersModal({
} }
let phoneNumber: ParsedE164Type | undefined; let phoneNumber: ParsedE164Type | undefined;
if (!username) { if (!parsedUsername) {
phoneNumber = parseAndFormatPhoneNumber(searchTerm, regionCode); phoneNumber = parseAndFormatPhoneNumber(searchTerm, regionCode);
} }

View file

@ -91,13 +91,13 @@ export class LeftPaneChooseGroupMembersHelper extends LeftPaneHelper<LeftPaneCho
isShowingRecommendedGroupSizeModal; isShowingRecommendedGroupSizeModal;
this.searchTerm = searchTerm; this.searchTerm = searchTerm;
if (isUsernamesEnabled) { const username = getUsernameFromSearch(searchTerm);
const username = getUsernameFromSearch(searchTerm); const isUsernameVisible =
const isVisible = this.candidateContacts.every( username !== undefined &&
contact => contact.username !== username this.candidateContacts.every(contact => contact.username !== username);
);
if (isVisible) { if (isUsernamesEnabled) {
if (isUsernameVisible) {
this.username = username; this.username = username;
} }
@ -109,7 +109,7 @@ export class LeftPaneChooseGroupMembersHelper extends LeftPaneHelper<LeftPaneCho
} }
const phoneNumber = parseAndFormatPhoneNumber(searchTerm, regionCode); const phoneNumber = parseAndFormatPhoneNumber(searchTerm, regionCode);
if (!this.username && phoneNumber) { if (!isUsernameVisible && phoneNumber) {
this.isPhoneNumberChecked = this.isPhoneNumberChecked =
phoneNumber.isValid && phoneNumber.isValid &&
selectedContacts.some(contact => contact.e164 === phoneNumber.e164); selectedContacts.some(contact => contact.e164 === phoneNumber.e164);

View file

@ -68,20 +68,20 @@ export class LeftPaneComposeHelper extends LeftPaneHelper<LeftPaneComposePropsTy
this.searchTerm = searchTerm; this.searchTerm = searchTerm;
this.uuidFetchState = uuidFetchState; this.uuidFetchState = uuidFetchState;
const username = getUsernameFromSearch(this.searchTerm);
if (isUsernamesEnabled) { if (isUsernamesEnabled) {
this.username = getUsernameFromSearch(this.searchTerm); this.username = username;
this.isUsernameVisible = this.isUsernameVisible =
isUsernamesEnabled && isUsernamesEnabled &&
Boolean(this.username) && Boolean(username) &&
this.composeContacts.every( this.composeContacts.every(contact => contact.username !== username);
contact => contact.username !== this.username
);
} else { } else {
this.isUsernameVisible = false; this.isUsernameVisible = false;
} }
const phoneNumber = parseAndFormatPhoneNumber(searchTerm, regionCode); const phoneNumber = parseAndFormatPhoneNumber(searchTerm, regionCode);
if (!this.username && phoneNumber) { if (!username && phoneNumber) {
this.phoneNumber = phoneNumber; this.phoneNumber = phoneNumber;
this.isPhoneNumberVisible = this.composeContacts.every( this.isPhoneNumberVisible = this.composeContacts.every(
contact => contact.e164 !== phoneNumber.e164 contact => contact.e164 !== phoneNumber.e164

View file

@ -258,7 +258,7 @@ describe('pnp/username', function needsName() {
await window.locator('button[aria-label="New chat"]').click(); await window.locator('button[aria-label="New chat"]').click();
const searchInput = window.locator('.module-SearchInput__container input'); const searchInput = window.locator('.module-SearchInput__container input');
await searchInput.type(`@${CARL_USERNAME}`); await searchInput.type(CARL_USERNAME);
debug('starting lookup'); debug('starting lookup');
await window.locator(`div.ListTile >> "${CARL_USERNAME}"`).click(); await window.locator(`div.ListTile >> "${CARL_USERNAME}"`).click();

View file

@ -185,7 +185,7 @@ describe('LeftPaneChooseGroupMembersHelper', () => {
const helper = new LeftPaneChooseGroupMembersHelper({ const helper = new LeftPaneChooseGroupMembersHelper({
...defaults, ...defaults,
candidateContacts: [], candidateContacts: [],
searchTerm: 'signal', searchTerm: 'signal.01',
selectedContacts: [], selectedContacts: [],
}); });
@ -195,7 +195,7 @@ describe('LeftPaneChooseGroupMembersHelper', () => {
); );
assert.deepEqual(helper.getRow(1), { assert.deepEqual(helper.getRow(1), {
type: RowType.UsernameCheckbox, type: RowType.UsernameCheckbox,
username: 'signal', username: 'signal.01',
isChecked: false, isChecked: false,
isFetching: false, isFetching: false,
}); });

View file

@ -88,7 +88,7 @@ describe('LeftPaneComposeHelper', () => {
composeContacts: [getDefaultConversation(), getDefaultConversation()], composeContacts: [getDefaultConversation(), getDefaultConversation()],
composeGroups: [getDefaultGroupListItem(), getDefaultGroupListItem()], composeGroups: [getDefaultGroupListItem(), getDefaultGroupListItem()],
regionCode: 'US', regionCode: 'US',
searchTerm: 'someone', searchTerm: 'someone.01',
isUsernamesEnabled: true, isUsernamesEnabled: true,
uuidFetchState: {}, uuidFetchState: {},
}).getRowCount(), }).getRowCount(),
@ -102,7 +102,7 @@ describe('LeftPaneComposeHelper', () => {
composeContacts: [getDefaultConversation(), getDefaultConversation()], composeContacts: [getDefaultConversation(), getDefaultConversation()],
composeGroups: [getDefaultGroupListItem(), getDefaultGroupListItem()], composeGroups: [getDefaultGroupListItem(), getDefaultGroupListItem()],
regionCode: 'US', regionCode: 'US',
searchTerm: 'someone', searchTerm: 'someone.54321',
isUsernamesEnabled: false, isUsernamesEnabled: false,
uuidFetchState: {}, uuidFetchState: {},
}).getRowCount(), }).getRowCount(),
@ -116,7 +116,7 @@ describe('LeftPaneComposeHelper', () => {
composeContacts: [], composeContacts: [],
composeGroups: [], composeGroups: [],
regionCode: 'US', regionCode: 'US',
searchTerm: 'foobar', searchTerm: 'foobar.01',
isUsernamesEnabled: true, isUsernamesEnabled: true,
uuidFetchState: {}, uuidFetchState: {},
}).getRowCount(), }).getRowCount(),
@ -127,7 +127,7 @@ describe('LeftPaneComposeHelper', () => {
composeContacts: [getDefaultConversation(), getDefaultConversation()], composeContacts: [getDefaultConversation(), getDefaultConversation()],
composeGroups: [], composeGroups: [],
regionCode: 'US', regionCode: 'US',
searchTerm: 'foobar', searchTerm: 'foobar.01',
isUsernamesEnabled: true, isUsernamesEnabled: true,
uuidFetchState: {}, uuidFetchState: {},
}).getRowCount(), }).getRowCount(),
@ -138,7 +138,7 @@ describe('LeftPaneComposeHelper', () => {
composeContacts: [getDefaultConversation(), getDefaultConversation()], composeContacts: [getDefaultConversation(), getDefaultConversation()],
composeGroups: [getDefaultGroupListItem()], composeGroups: [getDefaultGroupListItem()],
regionCode: 'US', regionCode: 'US',
searchTerm: 'foobar', searchTerm: 'foobar.01',
isUsernamesEnabled: true, isUsernamesEnabled: true,
uuidFetchState: {}, uuidFetchState: {},
}).getRowCount(), }).getRowCount(),
@ -166,7 +166,7 @@ describe('LeftPaneComposeHelper', () => {
composeContacts: [], composeContacts: [],
composeGroups: [], composeGroups: [],
regionCode: 'US', regionCode: 'US',
searchTerm: 'someone', searchTerm: 'someone.02',
isUsernamesEnabled: true, isUsernamesEnabled: true,
uuidFetchState: {}, uuidFetchState: {},
}).getRowCount(), }).getRowCount(),
@ -346,7 +346,7 @@ describe('LeftPaneComposeHelper', () => {
}); });
it('returns just a "find by username" header if no results', () => { it('returns just a "find by username" header if no results', () => {
const username = 'someone'; const username = 'someone.02';
const helper = new LeftPaneComposeHelper({ const helper = new LeftPaneComposeHelper({
composeContacts: [], composeContacts: [],

View file

@ -10,38 +10,13 @@ describe('Username', () => {
const { getUsernameFromSearch } = Username; const { getUsernameFromSearch } = Username;
it('matches invalid username searches', () => { it('matches invalid username searches', () => {
assert.strictEqual(getUsernameFromSearch('use'), 'use'); assert.isUndefined(getUsernameFromSearch('use'));
assert.strictEqual( assert.isUndefined(getUsernameFromSearch('username9012345678901234567'));
getUsernameFromSearch('username9012345678901234567'),
'username9012345678901234567'
);
}); });
it('matches valid username searches', () => { it('matches valid username searches', () => {
assert.strictEqual(getUsernameFromSearch('username_34'), 'username_34');
assert.strictEqual(getUsernameFromSearch('u5ername'), 'u5ername');
assert.strictEqual(getUsernameFromSearch('username.12'), 'username.12'); assert.strictEqual(getUsernameFromSearch('username.12'), 'username.12');
assert.strictEqual(getUsernameFromSearch('user'), 'user'); assert.strictEqual(getUsernameFromSearch('xyz.568'), 'xyz.568');
assert.strictEqual(
getUsernameFromSearch('username901234567890123456'),
'username901234567890123456'
);
});
it('matches valid and invalid usernames with @ prefix', () => {
assert.strictEqual(getUsernameFromSearch('@username!'), 'username!');
assert.strictEqual(getUsernameFromSearch('@1username'), '1username');
assert.strictEqual(getUsernameFromSearch('@username_34'), 'username_34');
assert.strictEqual(getUsernameFromSearch('@username.34'), 'username.34');
assert.strictEqual(getUsernameFromSearch('@u5ername'), 'u5ername');
});
it('matches valid and invalid usernames with @ suffix', () => {
assert.strictEqual(getUsernameFromSearch('username!@'), 'username!');
assert.strictEqual(getUsernameFromSearch('1username@'), '1username');
assert.strictEqual(getUsernameFromSearch('username_34@'), 'username_34');
assert.strictEqual(getUsernameFromSearch('username.34@'), 'username.34');
assert.strictEqual(getUsernameFromSearch('u5ername@'), 'u5ername');
}); });
it('does not match something that looks like a phone number', () => { it('does not match something that looks like a phone number', () => {

View file

@ -1,6 +1,8 @@
// Copyright 2022 Signal Messenger, LLC // Copyright 2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
import { usernames } from '@signalapp/libsignal-client';
export type UsernameReservationType = Readonly<{ export type UsernameReservationType = Readonly<{
username: string; username: string;
previousUsername: string | undefined; previousUsername: string | undefined;
@ -24,23 +26,12 @@ export enum ConfirmUsernameResult {
} }
export function getUsernameFromSearch(searchTerm: string): string | undefined { export function getUsernameFromSearch(searchTerm: string): string | undefined {
// Search term contains username if it: try {
// - Is a valid username with or without a discriminator usernames.hash(searchTerm);
// - Starts with @ return searchTerm;
// - Ends with @ } catch {
const match = searchTerm.match(
/^(?:(?<valid>[a-z_][0-9a-z_]*(?:\.\d*)?)|@(?<start>.*?)@?|@?(?<end>.*?)?@)$/
);
if (!match) {
return undefined; return undefined;
} }
const { groups } = match;
if (!groups) {
return undefined;
}
return (groups.valid || groups.start || groups.end) ?? undefined;
} }
export function getNickname(username: string): string | undefined { export function getNickname(username: string): string | undefined {