diff --git a/src/components/common/UsernameInput.tsx b/src/components/common/UsernameInput.tsx index a8db376ff..f44492f3a 100644 --- a/src/components/common/UsernameInput.tsx +++ b/src/components/common/UsernameInput.tsx @@ -92,7 +92,7 @@ const UsernameInput: FC = ({ setUsername(newUsername); - const isValid = isUsernameValid(newUsername); + const isValid = newUsername === '' ? true : isUsernameValid(newUsername); if (!isValid) return; onChange?.(newUsername); diff --git a/src/util/deepLinkParser.ts b/src/util/deepLinkParser.ts index c41b357cf..a59e4c988 100644 --- a/src/util/deepLinkParser.ts +++ b/src/util/deepLinkParser.ts @@ -1,7 +1,7 @@ import type { ThreadId } from '../types'; 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' | 'setlanguage' | 'addtheme' | 'confirmphone' | 'socks' | 'proxy' | 'privatepost' | 'bg' | 'share' | 'msg' | 'msg_url' | @@ -55,26 +55,33 @@ interface TelegramPassportLink { payload?: string; } +interface PublicUsernameOrBotLink { + type: 'publicUsernameOrBotLink'; + username: string; + parameter?: string; +} + type DeepLink = TelegramPassportLink | LoginCodeLink | PublicMessageLink | PrivateMessageLink | ShareLink | - ChatFolderLink; + ChatFolderLink | + PublicUsernameOrBotLink; -type BuilderParams = Record, string>; +type BuilderParams = Record, string | undefined>; type BuilderReturnType = T | undefined; type DeepLinkType = DeepLink['type'] | 'unknown'; type PrivateMessageLinkBuilderParams = Omit, 'isSingle' | 'isBoost'> & { - single: string; - boost: string; + single: string | undefined; + boost: string | undefined; }; type PublicMessageLinkBuilderParams = Omit, 'isSingle' | 'isBoost'> & { - single: string; - boost: string; + single: string | undefined; + boost: string | undefined; }; 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 { + if (!isDeepLink(link)) { + return undefined; + } try { return parseDeepLink(link); } catch (err) { @@ -92,19 +102,23 @@ export function tryParseDeepLink(link: string): DeepLink | undefined { } function parseDeepLink(url: string) { - if (url.startsWith('https:')) { - const urlParsed = new URL(url); - return handleHttpLink(urlParsed); + const correctUrl = ensureProtocol(url); + if (!correctUrl) { + 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 - const urlParsed = new URL(url.replace(/^tg:/, 'http:')); - return handleTgLink(urlParsed); + const urlParsed = new URL(correctUrl.replace(/^tg:/, 'http:')); + return parseTgLink(urlParsed); } return undefined; } -function handleTgLink(url: URL) { +function parseTgLink(url: URL) { const { hostname } = url; const queryParams = getQueryParams(url); const pathParams = getPathParams(url); @@ -155,13 +169,18 @@ function handleTgLink(url: URL) { callbackUrl: queryParams.callback_url, payload: queryParams.payload, }); + case 'publicUsernameOrBotLink': + return buildPublicUsernameOrBotLink({ + username: queryParams.domain, + parameter: queryParams.start, + }); default: break; } return undefined; } -function handleHttpLink(url: URL) { +function parseHttpLink(url: URL) { if (!ELIGIBLE_HOSTNAMES.has(url.hostname)) { return undefined; } @@ -231,6 +250,11 @@ function handleHttpLink(url: URL) { return buildChatFolderLink({ slug: pathParams[1] }); case 'loginCodeLink': return buildLoginCodeLink({ code: pathParams[1] }); + case 'publicUsernameOrBotLink': + return buildPublicUsernameOrBotLink({ + username: pathParams[0], + parameter: queryParams.start, + }); default: break; } @@ -247,6 +271,9 @@ function getHttpDeepLinkType( if (method === 'share') { return 'shareLink'; } + if (isUsernameValid(method)) { + return 'publicUsernameOrBotLink'; + } } else if (len === 2) { if (method === 'addlist') { return 'chatFolderLink'; @@ -289,6 +316,9 @@ function getTgDeepLinkType( if (domain && post) { return 'publicMessageLink'; } + if (isUsernameValid(domain)) { + return 'publicUsernameOrBotLink'; + } break; } case 'privatepost': { @@ -431,6 +461,26 @@ function buildTelegramPassportLink( }; } +function buildPublicUsernameOrBotLink( + params: BuilderParams, +): BuilderReturnType { + const { + username, + parameter, + } = params; + if (!username) { + return undefined; + } + if (!isUsernameValid(username)) { + return undefined; + } + return { + type: 'publicUsernameOrBotLink', + username, + parameter, + }; +} + function isNumber(s: string) { return /^-?\d+$/.test(s); } @@ -442,3 +492,7 @@ function getPathParams(url: URL) { function getQueryParams(url: URL) { return Object.fromEntries(url.searchParams); } + +function isUsernameValid(username: string) { + return /^\D([a-zA-Z0-9_]){1,64}$/.test(username); +} diff --git a/src/util/deeplink.ts b/src/util/deeplink.ts index 2020560a1..dccf11317 100644 --- a/src/util/deeplink.ts +++ b/src/util/deeplink.ts @@ -3,7 +3,7 @@ import { getActions } from '../global'; import type { ApiChatType } from '../api/types'; 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 { tryParseDeepLink } from './deepLinkParser'; import { IS_SAFARI } from './windowEnvironment'; @@ -17,11 +17,21 @@ export const processDeepLink = (url: string): boolean => { case 'privateMessageLink': handlePrivateMessageLink(parsedLink, actions); return true; + case 'publicUsernameOrBotLink': + actions.openChatByUsername({ + username: parsedLink.username, + startParam: parsedLink.parameter, + }); + return true; default: break; } } + if (!url.match(RE_TG_LINK)) { + return false; + } + const { protocol, searchParams, pathname, hostname, } = new URL(url); diff --git a/src/util/username.ts b/src/util/username.ts index dbc3ca08d..e4ff590a8 100644 --- a/src/util/username.ts +++ b/src/util/username.ts @@ -3,9 +3,7 @@ export const MAX_USERNAME_LENGTH = 32; export const USERNAME_REGEX = /^\D([a-zA-Z0-9_]+)$/; export function isUsernameValid(username: string) { - return username.length === 0 || ( - username.length >= MIN_USERNAME_LENGTH + return username.length >= MIN_USERNAME_LENGTH && username.length <= MAX_USERNAME_LENGTH - && USERNAME_REGEX.test(username) - ); + && USERNAME_REGEX.test(username); }