Deep Link: Fix bot link (#4266)

This commit is contained in:
Alexander Zinchuk 2024-02-23 14:06:33 +01:00
parent deede8cb6f
commit 3d4ba7c47b
4 changed files with 83 additions and 21 deletions

View File

@ -92,7 +92,7 @@ const UsernameInput: FC<OwnProps> = ({
setUsername(newUsername); setUsername(newUsername);
const isValid = isUsernameValid(newUsername); const isValid = newUsername === '' ? true : isUsernameValid(newUsername);
if (!isValid) return; if (!isValid) return;
onChange?.(newUsername); onChange?.(newUsername);

View File

@ -1,7 +1,7 @@
import type { ThreadId } from '../types'; import type { ThreadId } from '../types';
import { RE_TG_LINK, RE_TME_LINK } from '../config'; import { RE_TG_LINK, RE_TME_LINK } from '../config';
import { isUsernameValid } from './username'; import { ensureProtocol } from './ensureProtocol';
export type DeepLinkMethod = 'resolve' | 'login' | 'passport' | 'settings' | 'join' | 'addstickers' | 'addemoji' | export type DeepLinkMethod = 'resolve' | 'login' | 'passport' | 'settings' | 'join' | 'addstickers' | 'addemoji' |
'setlanguage' | 'addtheme' | 'confirmphone' | 'socks' | 'proxy' | 'privatepost' | 'bg' | 'share' | 'msg' | 'msg_url' | 'setlanguage' | 'addtheme' | 'confirmphone' | 'socks' | 'proxy' | 'privatepost' | 'bg' | 'share' | 'msg' | 'msg_url' |
@ -55,26 +55,33 @@ interface TelegramPassportLink {
payload?: string; payload?: string;
} }
interface PublicUsernameOrBotLink {
type: 'publicUsernameOrBotLink';
username: string;
parameter?: string;
}
type DeepLink = type DeepLink =
TelegramPassportLink | TelegramPassportLink |
LoginCodeLink | LoginCodeLink |
PublicMessageLink | PublicMessageLink |
PrivateMessageLink | PrivateMessageLink |
ShareLink | ShareLink |
ChatFolderLink; ChatFolderLink |
PublicUsernameOrBotLink;
type BuilderParams<T extends DeepLink> = Record<keyof Omit<T, 'type'>, string>; type BuilderParams<T extends DeepLink> = Record<keyof Omit<T, 'type'>, string | undefined>;
type BuilderReturnType<T extends DeepLink> = T | undefined; type BuilderReturnType<T extends DeepLink> = T | undefined;
type DeepLinkType = DeepLink['type'] | 'unknown'; type DeepLinkType = DeepLink['type'] | 'unknown';
type PrivateMessageLinkBuilderParams = Omit<BuilderParams<PrivateMessageLink>, 'isSingle' | 'isBoost'> & { type PrivateMessageLinkBuilderParams = Omit<BuilderParams<PrivateMessageLink>, 'isSingle' | 'isBoost'> & {
single: string; single: string | undefined;
boost: string; boost: string | undefined;
}; };
type PublicMessageLinkBuilderParams = Omit<BuilderParams<PublicMessageLink>, 'isSingle' | 'isBoost'> & { type PublicMessageLinkBuilderParams = Omit<BuilderParams<PublicMessageLink>, 'isSingle' | 'isBoost'> & {
single: string; single: string | undefined;
boost: string; boost: string | undefined;
}; };
const ELIGIBLE_HOSTNAMES = new Set(['t.me', 'telegram.me', 'telegram.dog']); const ELIGIBLE_HOSTNAMES = new Set(['t.me', 'telegram.me', 'telegram.dog']);
@ -84,6 +91,9 @@ export function isDeepLink(link: string): boolean {
} }
export function tryParseDeepLink(link: string): DeepLink | undefined { export function tryParseDeepLink(link: string): DeepLink | undefined {
if (!isDeepLink(link)) {
return undefined;
}
try { try {
return parseDeepLink(link); return parseDeepLink(link);
} catch (err) { } catch (err) {
@ -92,19 +102,23 @@ export function tryParseDeepLink(link: string): DeepLink | undefined {
} }
function parseDeepLink(url: string) { function parseDeepLink(url: string) {
if (url.startsWith('https:')) { const correctUrl = ensureProtocol(url);
const urlParsed = new URL(url); if (!correctUrl) {
return handleHttpLink(urlParsed); return undefined;
} }
if (url.startsWith('tg:')) { if (correctUrl.startsWith('https:')) {
const urlParsed = new URL(correctUrl);
return parseHttpLink(urlParsed);
}
if (correctUrl.startsWith('tg:')) {
// Chrome parse url with tg: protocol incorrectly // Chrome parse url with tg: protocol incorrectly
const urlParsed = new URL(url.replace(/^tg:/, 'http:')); const urlParsed = new URL(correctUrl.replace(/^tg:/, 'http:'));
return handleTgLink(urlParsed); return parseTgLink(urlParsed);
} }
return undefined; return undefined;
} }
function handleTgLink(url: URL) { function parseTgLink(url: URL) {
const { hostname } = url; const { hostname } = url;
const queryParams = getQueryParams(url); const queryParams = getQueryParams(url);
const pathParams = getPathParams(url); const pathParams = getPathParams(url);
@ -155,13 +169,18 @@ function handleTgLink(url: URL) {
callbackUrl: queryParams.callback_url, callbackUrl: queryParams.callback_url,
payload: queryParams.payload, payload: queryParams.payload,
}); });
case 'publicUsernameOrBotLink':
return buildPublicUsernameOrBotLink({
username: queryParams.domain,
parameter: queryParams.start,
});
default: default:
break; break;
} }
return undefined; return undefined;
} }
function handleHttpLink(url: URL) { function parseHttpLink(url: URL) {
if (!ELIGIBLE_HOSTNAMES.has(url.hostname)) { if (!ELIGIBLE_HOSTNAMES.has(url.hostname)) {
return undefined; return undefined;
} }
@ -231,6 +250,11 @@ function handleHttpLink(url: URL) {
return buildChatFolderLink({ slug: pathParams[1] }); return buildChatFolderLink({ slug: pathParams[1] });
case 'loginCodeLink': case 'loginCodeLink':
return buildLoginCodeLink({ code: pathParams[1] }); return buildLoginCodeLink({ code: pathParams[1] });
case 'publicUsernameOrBotLink':
return buildPublicUsernameOrBotLink({
username: pathParams[0],
parameter: queryParams.start,
});
default: default:
break; break;
} }
@ -247,6 +271,9 @@ function getHttpDeepLinkType(
if (method === 'share') { if (method === 'share') {
return 'shareLink'; return 'shareLink';
} }
if (isUsernameValid(method)) {
return 'publicUsernameOrBotLink';
}
} else if (len === 2) { } else if (len === 2) {
if (method === 'addlist') { if (method === 'addlist') {
return 'chatFolderLink'; return 'chatFolderLink';
@ -289,6 +316,9 @@ function getTgDeepLinkType(
if (domain && post) { if (domain && post) {
return 'publicMessageLink'; return 'publicMessageLink';
} }
if (isUsernameValid(domain)) {
return 'publicUsernameOrBotLink';
}
break; break;
} }
case 'privatepost': { case 'privatepost': {
@ -431,6 +461,26 @@ function buildTelegramPassportLink(
}; };
} }
function buildPublicUsernameOrBotLink(
params: BuilderParams<PublicUsernameOrBotLink>,
): BuilderReturnType<PublicUsernameOrBotLink> {
const {
username,
parameter,
} = params;
if (!username) {
return undefined;
}
if (!isUsernameValid(username)) {
return undefined;
}
return {
type: 'publicUsernameOrBotLink',
username,
parameter,
};
}
function isNumber(s: string) { function isNumber(s: string) {
return /^-?\d+$/.test(s); return /^-?\d+$/.test(s);
} }
@ -442,3 +492,7 @@ function getPathParams(url: URL) {
function getQueryParams(url: URL) { function getQueryParams(url: URL) {
return Object.fromEntries(url.searchParams); return Object.fromEntries(url.searchParams);
} }
function isUsernameValid(username: string) {
return /^\D([a-zA-Z0-9_]){1,64}$/.test(username);
}

View File

@ -3,7 +3,7 @@ import { getActions } from '../global';
import type { ApiChatType } from '../api/types'; import type { ApiChatType } from '../api/types';
import type { DeepLinkMethod, PrivateMessageLink } from './deepLinkParser'; import type { DeepLinkMethod, PrivateMessageLink } from './deepLinkParser';
import { API_CHAT_TYPES } from '../config'; import { API_CHAT_TYPES, RE_TG_LINK } from '../config';
import { toChannelId } from '../global/helpers'; import { toChannelId } from '../global/helpers';
import { tryParseDeepLink } from './deepLinkParser'; import { tryParseDeepLink } from './deepLinkParser';
import { IS_SAFARI } from './windowEnvironment'; import { IS_SAFARI } from './windowEnvironment';
@ -17,11 +17,21 @@ export const processDeepLink = (url: string): boolean => {
case 'privateMessageLink': case 'privateMessageLink':
handlePrivateMessageLink(parsedLink, actions); handlePrivateMessageLink(parsedLink, actions);
return true; return true;
case 'publicUsernameOrBotLink':
actions.openChatByUsername({
username: parsedLink.username,
startParam: parsedLink.parameter,
});
return true;
default: default:
break; break;
} }
} }
if (!url.match(RE_TG_LINK)) {
return false;
}
const { const {
protocol, searchParams, pathname, hostname, protocol, searchParams, pathname, hostname,
} = new URL(url); } = new URL(url);

View File

@ -3,9 +3,7 @@ export const MAX_USERNAME_LENGTH = 32;
export const USERNAME_REGEX = /^\D([a-zA-Z0-9_]+)$/; export const USERNAME_REGEX = /^\D([a-zA-Z0-9_]+)$/;
export function isUsernameValid(username: string) { export function isUsernameValid(username: string) {
return username.length === 0 || ( return username.length >= MIN_USERNAME_LENGTH
username.length >= MIN_USERNAME_LENGTH
&& username.length <= MAX_USERNAME_LENGTH && username.length <= MAX_USERNAME_LENGTH
&& USERNAME_REGEX.test(username) && USERNAME_REGEX.test(username);
);
} }