From ae0ad364783c4b41b4a9dd2ab0f1bee93e382a4d Mon Sep 17 00:00:00 2001 From: Alexander Zinchuk Date: Thu, 12 Aug 2021 03:34:56 +0300 Subject: [PATCH] Support redirects from t.me (websync) (#1383) --- src/api/gramjs/apiBuilders/symbols.ts | 2 + src/api/gramjs/gramjsBuilders/index.ts | 6 ++ src/api/gramjs/methods/symbols.ts | 9 ++- src/api/types/messages.ts | 1 + src/components/common/StickerSetModal.tsx | 37 +++++++--- src/components/main/Main.tsx | 22 +++++- src/global/types.ts | 2 + src/hooks/useHistoryBack.ts | 3 +- src/modules/actions/api/initial.ts | 2 + src/modules/actions/api/symbols.ts | 19 +++-- src/modules/actions/apiUpdaters/initial.ts | 5 ++ src/modules/actions/ui/initial.ts | 2 + src/modules/selectors/symbols.ts | 4 ++ src/serviceWorker.ts | 10 ++- src/util/deeplink.ts | 54 ++++++++++++++ src/util/websync.ts | 82 ++++++++++++++++++++++ 16 files changed, 237 insertions(+), 23 deletions(-) create mode 100644 src/util/deeplink.ts create mode 100644 src/util/websync.ts diff --git a/src/api/gramjs/apiBuilders/symbols.ts b/src/api/gramjs/apiBuilders/symbols.ts index e769290a0..8c7cccb56 100644 --- a/src/api/gramjs/apiBuilders/symbols.ts +++ b/src/api/gramjs/apiBuilders/symbols.ts @@ -73,6 +73,7 @@ export function buildStickerSet(set: GramJs.StickerSet): ApiStickerSet { thumbs, count, hash, + shortName, } = set; return { @@ -85,6 +86,7 @@ export function buildStickerSet(set: GramJs.StickerSet): ApiStickerSet { hasThumbnail: Boolean(thumbs && thumbs.length), count, hash, + shortName, }; } diff --git a/src/api/gramjs/gramjsBuilders/index.ts b/src/api/gramjs/gramjsBuilders/index.ts index c5d817897..b13bf2091 100644 --- a/src/api/gramjs/gramjsBuilders/index.ts +++ b/src/api/gramjs/gramjsBuilders/index.ts @@ -113,6 +113,12 @@ export function buildInputStickerSet(id: string, accessHash: string) { }); } +export function buildInputStickerSetShortName(shortName: string) { + return new GramJs.InputStickerSetShortName({ + shortName, + }); +} + export function buildInputDocument(media: ApiSticker | ApiVideo) { const document = localDb.documents[media.id]; diff --git a/src/api/gramjs/methods/symbols.ts b/src/api/gramjs/methods/symbols.ts index ef2ac640a..8f5fd2ae6 100644 --- a/src/api/gramjs/methods/symbols.ts +++ b/src/api/gramjs/methods/symbols.ts @@ -3,7 +3,7 @@ import { ApiSticker, ApiVideo, OnApiUpdate } from '../../types'; import { invokeRequest } from './client'; import { buildStickerFromDocument, buildStickerSet, buildStickerSetCovered } from '../apiBuilders/symbols'; -import { buildInputStickerSet, buildInputDocument } from '../gramjsBuilders'; +import { buildInputStickerSet, buildInputDocument, buildInputStickerSetShortName } from '../gramjsBuilders'; import { buildVideoFromDocument } from '../apiBuilders/messages'; import { RECENT_STICKERS_LIMIT } from '../../../config'; @@ -93,9 +93,12 @@ export async function faveSticker({ } } -export async function fetchStickers({ stickerSetId, accessHash }: { stickerSetId: string; accessHash: string }) { +export async function fetchStickers({ stickerSetShortName, stickerSetId, accessHash }: +{ stickerSetShortName?: string; stickerSetId?: string; accessHash: string }) { const result = await invokeRequest(new GramJs.messages.GetStickerSet({ - stickerset: buildInputStickerSet(stickerSetId, accessHash), + stickerset: stickerSetId + ? buildInputStickerSet(stickerSetId, accessHash) + : buildInputStickerSetShortName(stickerSetShortName!), })); if (!result) { diff --git a/src/api/types/messages.ts b/src/api/types/messages.ts index 0e3b08a3c..6882a4cfc 100644 --- a/src/api/types/messages.ts +++ b/src/api/types/messages.ts @@ -43,6 +43,7 @@ export interface ApiStickerSet { stickers?: ApiSticker[]; packs?: Record; covers?: ApiSticker[]; + shortName: string; } export interface ApiVideo { diff --git a/src/components/common/StickerSetModal.tsx b/src/components/common/StickerSetModal.tsx index 27c541078..f21f42d85 100644 --- a/src/components/common/StickerSetModal.tsx +++ b/src/components/common/StickerSetModal.tsx @@ -8,7 +8,7 @@ import { GlobalActions } from '../../global/types'; import { STICKER_SIZE_MODAL } from '../../config'; import { pick } from '../../util/iteratees'; -import { selectStickerSet } from '../../modules/selectors'; +import { selectStickerSet, selectStickerSetByShortName } from '../../modules/selectors'; import { useIntersectionObserver } from '../../hooks/useIntersectionObserver'; import useLang from '../../hooks/useLang'; import renderText from './helpers/renderText'; @@ -22,7 +22,8 @@ import './StickerSetModal.scss'; export type OwnProps = { isOpen: boolean; - fromSticker: ApiSticker; + fromSticker?: ApiSticker; + stickerSetShortName?: string; onClose: () => void; }; @@ -37,6 +38,7 @@ const INTERSECTION_THROTTLE = 200; const StickerSetModal: FC = ({ isOpen, fromSticker, + stickerSetShortName, stickerSet, onClose, loadStickers, @@ -53,10 +55,19 @@ const StickerSetModal: FC = ({ useEffect(() => { if (isOpen) { - const { stickerSetId, stickerSetAccessHash } = fromSticker; - loadStickers({ stickerSetId, stickerSetAccessHash }); + if (fromSticker) { + const { stickerSetId, stickerSetAccessHash } = fromSticker; + loadStickers({ + stickerSetId, + stickerSetAccessHash, + }); + } else { + loadStickers({ + stickerSetShortName, + }); + } } - }, [isOpen, fromSticker, loadStickers]); + }, [isOpen, fromSticker, loadStickers, stickerSetShortName]); const handleSelect = useCallback((sticker: ApiSticker) => { sticker = { @@ -69,9 +80,11 @@ const StickerSetModal: FC = ({ }, [onClose, sendMessage]); const handleButtonClick = useCallback(() => { - toggleStickerSet({ stickerSetId: fromSticker.stickerSetId }); - onClose(); - }, [fromSticker.stickerSetId, onClose, toggleStickerSet]); + if (stickerSet) { + toggleStickerSet({ stickerSetId: stickerSet.id }); + onClose(); + } + }, [onClose, stickerSet, toggleStickerSet]); return ( = ({ }; export default memo(withGlobal( - (global, { fromSticker }: OwnProps) => { - return { stickerSet: selectStickerSet(global, fromSticker.stickerSetId) }; + (global, { fromSticker, stickerSetShortName }: OwnProps) => { + return { + stickerSet: fromSticker + ? selectStickerSet(global, fromSticker.stickerSetId) + : selectStickerSetByShortName(global, stickerSetShortName!), + }; }, (setGlobal, actions): DispatchProps => pick(actions, [ 'loadStickers', diff --git a/src/components/main/Main.tsx b/src/components/main/Main.tsx index a3a87cf37..212bde0d0 100644 --- a/src/components/main/Main.tsx +++ b/src/components/main/Main.tsx @@ -27,6 +27,8 @@ import useShowTransition from '../../hooks/useShowTransition'; import useBackgroundMode from '../../hooks/useBackgroundMode'; import useBeforeUnload from '../../hooks/useBeforeUnload'; import useOnChange from '../../hooks/useOnChange'; +import { processDeepLink } from '../../util/deeplink'; +import { LOCATION_HASH } from '../../hooks/useHistoryBack'; import LeftColumn from '../left/LeftColumn'; import MiddleColumn from '../middle/MiddleColumn'; @@ -38,6 +40,7 @@ import Dialogs from './Dialogs.async'; import ForwardPicker from './ForwardPicker.async'; import SafeLinkModal from './SafeLinkModal.async'; import HistoryCalendar from './HistoryCalendar.async'; +import StickerSetModal from '../common/StickerSetModal.async'; import './Main.scss'; @@ -55,11 +58,12 @@ type StateProps = { isHistoryCalendarOpen: boolean; shouldSkipHistoryAnimations?: boolean; language?: LangCode; + openedStickerSetShortName?: string; }; type DispatchProps = Pick; const NOTIFICATION_INTERVAL = 1000; @@ -82,12 +86,14 @@ const Main: FC = ({ isHistoryCalendarOpen, shouldSkipHistoryAnimations, language, + openedStickerSetShortName, loadAnimatedEmojis, loadNotificationSettings, loadNotificationExceptions, updateIsOnline, loadTopInlineBots, loadEmojiKeywords, + openStickerSetShortName, }) => { if (DEBUG && !DEBUG_isLogged) { DEBUG_isLogged = true; @@ -114,6 +120,12 @@ const Main: FC = ({ loadTopInlineBots, loadEmojiKeywords, language, ]); + useEffect(() => { + if (lastSyncTime && LOCATION_HASH.startsWith('#?tgaddr=')) { + processDeepLink(decodeURIComponent(LOCATION_HASH.substr('#?tgaddr='.length))); + } + }, [lastSyncTime]); + const { transitionClassNames: middleColumnTransitionClassNames, } = useShowTransition(!isLeftColumnShown, undefined, true, undefined, shouldSkipHistoryAnimations); @@ -223,6 +235,11 @@ const Main: FC = ({ {audioMessage && } + openStickerSetShortName({ stickerSetShortName: undefined })} + stickerSetShortName={openedStickerSetShortName} + /> ); }; @@ -269,10 +286,11 @@ export default memo(withGlobal( isHistoryCalendarOpen: Boolean(global.historyCalendarSelectedAt), shouldSkipHistoryAnimations: global.shouldSkipHistoryAnimations, language: global.settings.byKey.language, + openedStickerSetShortName: global.openedStickerSetShortName, }; }, (setGlobal, actions): DispatchProps => pick(actions, [ 'loadAnimatedEmojis', 'loadNotificationSettings', 'loadNotificationExceptions', 'updateIsOnline', - 'loadTopInlineBots', 'loadEmojiKeywords', + 'loadTopInlineBots', 'loadEmojiKeywords', 'openStickerSetShortName', ]), )(Main)); diff --git a/src/global/types.ts b/src/global/types.ts index bc48da9a7..c2994b1db 100644 --- a/src/global/types.ts +++ b/src/global/types.ts @@ -421,6 +421,7 @@ export type GlobalState = { safeLinkModalUrl?: string; historyCalendarSelectedAt?: number; + openedStickerSetShortName?: string; // TODO To be removed in August 2021 shouldShowContextMenuHint?: boolean; @@ -491,6 +492,7 @@ export type ActionTypes = ( 'loadStickers' | 'setStickerSearchQuery' | 'loadSavedGifs' | 'setGifSearchQuery' | 'searchMoreGifs' | 'faveSticker' | 'unfaveSticker' | 'toggleStickerSet' | 'loadAnimatedEmojis' | 'loadStickersForEmoji' | 'clearStickersForEmoji' | 'loadEmojiKeywords' | 'loadGreetingStickers' | + 'openStickerSetShortName' | // bots 'clickInlineButton' | 'sendBotCommand' | 'loadTopInlineBots' | 'queryInlineBot' | 'sendInlineBotResult' | 'resetInlineBot' | 'restartBot' | diff --git a/src/hooks/useHistoryBack.ts b/src/hooks/useHistoryBack.ts index f18358985..836a94666 100644 --- a/src/hooks/useHistoryBack.ts +++ b/src/hooks/useHistoryBack.ts @@ -9,6 +9,7 @@ import { areSortedArraysEqual } from '../util/iteratees'; // TODO: may be different on other devices such as iPad, maybe take dpi into account? const SAFARI_EDGE_BACK_GESTURE_LIMIT = 300; const SAFARI_EDGE_BACK_GESTURE_DURATION = 350; +export const LOCATION_HASH = window.location.hash; type HistoryState = { currentIndex: number; @@ -54,7 +55,7 @@ if (IS_IOS) { window.addEventListener('popstate', handleTouchEnd); } -window.history.replaceState({ index: historyState.currentIndex }, ''); +window.history.replaceState({ index: historyState.currentIndex }, '', window.location.pathname); export default function useHistoryBack( isActive: boolean | undefined, diff --git a/src/modules/actions/api/initial.ts b/src/modules/actions/api/initial.ts index 2082cb007..567b8b3e4 100644 --- a/src/modules/actions/api/initial.ts +++ b/src/modules/actions/api/initial.ts @@ -24,6 +24,7 @@ import { importLegacySession, clearLegacySessions, } from '../../../util/sessions'; +import { forceWebsync } from '../../../util/websync'; addReducer('initApi', (global: GlobalState, actions) => { (async () => { @@ -128,6 +129,7 @@ addReducer('signOut', () => { try { await unsubscribe(); await callApi('destroy'); + await forceWebsync(false); } catch (err) { // Do nothing } diff --git a/src/modules/actions/api/symbols.ts b/src/modules/actions/api/symbols.ts index c7477e772..1c00c6699 100644 --- a/src/modules/actions/api/symbols.ts +++ b/src/modules/actions/api/symbols.ts @@ -83,10 +83,10 @@ addReducer('loadFeaturedStickers', (global) => { }); addReducer('loadStickers', (global, actions, payload) => { - const { stickerSetId } = payload!; + const { stickerSetId, stickerSetShortName } = payload!; let { stickerSetAccessHash } = payload!; - if (!stickerSetAccessHash) { + if (!stickerSetAccessHash && !stickerSetShortName) { const stickerSet = selectStickerSet(global, stickerSetId); if (!stickerSet) { return; @@ -95,7 +95,7 @@ addReducer('loadStickers', (global, actions, payload) => { stickerSetAccessHash = stickerSet.accessHash; } - void loadStickers(stickerSetId, stickerSetAccessHash); + void loadStickers(stickerSetId, stickerSetAccessHash, stickerSetShortName); }); addReducer('loadAnimatedEmojis', () => { @@ -257,8 +257,9 @@ async function loadFeaturedStickers(hash = 0) { )); } -async function loadStickers(stickerSetId: string, accessHash: string) { - const stickerSet = await callApi('fetchStickers', { stickerSetId, accessHash }); +async function loadStickers(stickerSetId: string, accessHash: string, stickerSetShortName?: string) { + const stickerSet = await callApi('fetchStickers', + { stickerSetShortName, stickerSetId, accessHash }); if (!stickerSet) { return; } @@ -356,6 +357,14 @@ addReducer('clearStickersForEmoji', (global) => { }; }); +addReducer('openStickerSetShortName', (global, actions, payload) => { + const { stickerSetShortName } = payload!; + return { + ...global, + openedStickerSetShortName: stickerSetShortName, + }; +}); + async function searchStickers(query: string, hash = 0) { const result = await callApi('searchStickers', { query, hash }); diff --git a/src/modules/actions/apiUpdaters/initial.ts b/src/modules/actions/apiUpdaters/initial.ts index 6c83307ca..8b40cd6b1 100644 --- a/src/modules/actions/apiUpdaters/initial.ts +++ b/src/modules/actions/apiUpdaters/initial.ts @@ -17,6 +17,7 @@ import { subscribe } from '../../../util/notifications'; import { updateUser } from '../../reducers'; import { setLanguage } from '../../../util/langProvider'; import { selectNotifySettings } from '../../selectors'; +import { forceWebsync } from '../../../util/websync'; addReducer('apiUpdate', (global, actions, update: ApiUpdate) => { if (DEBUG) { @@ -90,6 +91,8 @@ function onUpdateAuthorizationState(update: ApiUpdateAuthorizationState) { switch (authState) { case 'authorizationStateLoggingOut': + void forceWebsync(false); + setGlobal({ ...global, isLoggingOut: true, @@ -119,6 +122,8 @@ function onUpdateAuthorizationState(update: ApiUpdateAuthorizationState) { break; } + void forceWebsync(true); + setGlobal({ ...global, isLoggingOut: false, diff --git a/src/modules/actions/ui/initial.ts b/src/modules/actions/ui/initial.ts index 30946facd..9ac170912 100644 --- a/src/modules/actions/ui/initial.ts +++ b/src/modules/actions/ui/initial.ts @@ -7,6 +7,7 @@ import { import { setLanguage } from '../../../util/langProvider'; import switchTheme from '../../../util/switchTheme'; import { selectTheme } from '../../selectors'; +import { startWebsync } from '../../../util/websync'; const HISTORY_ANIMATION_DURATION = 450; @@ -28,6 +29,7 @@ addReducer('init', (global) => { document.body.classList.add(`animation-level-${animationLevel}`); document.body.classList.add(IS_TOUCH_ENV ? 'is-touch-env' : 'is-pointer-env'); switchTheme(theme, animationLevel === ANIMATION_LEVEL_MAX); + startWebsync(); if (IS_SAFARI) { document.body.classList.add('is-safari'); diff --git a/src/modules/selectors/symbols.ts b/src/modules/selectors/symbols.ts index 59b32fcf4..8b8e421f5 100644 --- a/src/modules/selectors/symbols.ts +++ b/src/modules/selectors/symbols.ts @@ -18,6 +18,10 @@ export function selectStickerSet(global: GlobalState, id: string) { return global.stickers.setsById[id]; } +export function selectStickerSetByShortName(global: GlobalState, shortName: string) { + return Object.values(global.stickers.setsById).find((l) => l.shortName.toLowerCase() === shortName.toLowerCase()); +} + export function selectStickersForEmoji(global: GlobalState, emoji: string) { const stickerSets = Object.values(global.stickers.setsById); let stickersForEmoji: ApiSticker[] = []; diff --git a/src/serviceWorker.ts b/src/serviceWorker.ts index 3e2dcd6a3..0ab5b9bc6 100644 --- a/src/serviceWorker.ts +++ b/src/serviceWorker.ts @@ -30,9 +30,13 @@ self.addEventListener('activate', (e) => { // eslint-disable-next-line no-restricted-globals self.addEventListener('fetch', (e: FetchEvent) => { - e.respondWith((() => { - const { url } = e.request; + const { url } = e.request; + if (url.includes('_websync_')) { + return false; + } + + e.respondWith((() => { if (url.includes('/progressive/')) { return respondForProgressive(e); } @@ -43,6 +47,8 @@ self.addEventListener('fetch', (e: FetchEvent) => { return fetch(e.request); })()); + + return true; }); self.addEventListener('push', handlePush); diff --git a/src/util/deeplink.ts b/src/util/deeplink.ts new file mode 100644 index 000000000..51d422c7c --- /dev/null +++ b/src/util/deeplink.ts @@ -0,0 +1,54 @@ +import { getDispatch } from '../lib/teact/teactn'; + +export const processDeepLink = (url: string) => { + const { protocol, searchParams, pathname } = new URL(url); + + if (protocol !== 'tg:') return; + + const { openChatByUsername, openStickerSetShortName } = getDispatch(); + + const method = pathname.replace(/^\/\//, ''); + const params: Record = {}; + searchParams.forEach((value, key) => { + params[key] = value; + }); + + switch (method) { + case 'resolve': { + const { + domain, + } = params; + + if (domain !== 'telegrampassport') { + openChatByUsername({ + username: domain, + }); + } + break; + } + case 'privatepost': + + break; + case 'bg': + + break; + case 'join': + + break; + case 'addstickers': { + const { set } = params; + + openStickerSetShortName({ + stickerSetShortName: set, + }); + break; + } + case 'msg': + + break; + default: + // Unsupported deeplink + + break; + } +}; diff --git a/src/util/websync.ts b/src/util/websync.ts new file mode 100644 index 000000000..636afffae --- /dev/null +++ b/src/util/websync.ts @@ -0,0 +1,82 @@ +import { APP_VERSION } from '../config'; +import { getGlobal } from '../lib/teact/teactn'; +import { hasStoredSession } from './sessions'; + +const WEBSYNC_URLS = [ + 't.me', + 'telegram.me', +].map((domain) => `//${domain}/_websync_?`); +const WEBSYNC_VERSION = `${APP_VERSION} Z`; +const WEBSYNC_KEY = 'tgme_sync'; +const WEBSYNC_TIMEOUT = 86400; + +const getTs = () => { + return Math.floor(Number(new Date()) / 1000); +}; + +const saveSync = (authed: boolean) => { + const ts = getTs(); + localStorage.setItem(WEBSYNC_KEY, JSON.stringify({ + canRedirect: authed, + ts, + })); +}; + +let lastTimeout: NodeJS.Timeout | undefined; + +export const forceWebsync = (authed: boolean) => { + const currentTs = getTs(); + + const { canRedirect, ts } = JSON.parse(localStorage.getItem(WEBSYNC_KEY) || '{}'); + + if (canRedirect !== authed || ts + WEBSYNC_TIMEOUT <= currentTs) { + return Promise.all(WEBSYNC_URLS.map((url) => { + return new Promise((resolve, reject) => { + const script = document.createElement('script'); + + const removeElement = () => !!document.body.removeChild(script); + + script.src = url + new URLSearchParams({ + authed: Number(authed).toString(), + version: WEBSYNC_VERSION, + }); + + document.body.appendChild(script); + + script.onload = () => { + saveSync(authed); + removeElement(); + if (lastTimeout) { + clearTimeout(lastTimeout); + lastTimeout = undefined; + } + startWebsync(); + resolve(); + }; + + script.onerror = () => { + removeElement(); + reject(); + }; + }); + })); + } else { + return Promise.resolve(); + } +}; + +export function startWebsync() { + if (lastTimeout !== undefined) return; + const currentTs = getTs(); + + const { ts } = JSON.parse(localStorage.getItem(WEBSYNC_KEY) || '{}'); + + const timeout = WEBSYNC_TIMEOUT - (currentTs - ts); + + lastTimeout = setTimeout(() => { + const { authState } = getGlobal(); + + const authed = authState === 'authorizationStateReady' || hasStoredSession(true); + forceWebsync(authed); + }, Math.max(0, timeout * 1000)); +}