From c59d468c73b50fbd91a34e838aafba9f10e1be61 Mon Sep 17 00:00:00 2001 From: Alexander Zinchuk Date: Fri, 8 Sep 2023 18:39:23 +0200 Subject: [PATCH] Web Apps: Support new event types (#3818) --- src/api/gramjs/apiBuilders/messages.ts | 2 + src/api/gramjs/methods/bots.ts | 49 ++ src/api/gramjs/methods/index.ts | 1 + src/bundles/extra.ts | 8 +- src/components/main/Main.tsx | 8 +- .../AttachBotInstallModal.async.tsx | 8 +- .../AttachBotInstallModal.tsx | 18 +- .../{mapModal => map}/MapModal.async.tsx | 0 .../{mapModal => map}/MapModal.module.scss | 0 .../modals/{mapModal => map}/MapModal.tsx | 0 .../urlAuth}/UrlAuthModal.async.tsx | 8 +- .../urlAuth}/UrlAuthModal.module.scss | 0 .../{main => modals/urlAuth}/UrlAuthModal.tsx | 22 +- .../webApp}/WebAppModal.async.tsx | 8 +- .../webApp}/WebAppModal.module.scss | 0 .../{main => modals/webApp}/WebAppModal.tsx | 505 +++++++++++------- .../modals/webApp/hooks/usePopupLimit.ts | 34 ++ .../webApp}/hooks/useWebAppFrame.ts | 198 ++----- src/config.ts | 2 +- src/global/actions/api/bots.ts | 34 ++ src/global/types.ts | 4 + src/lib/gramjs/tl/AllTLObjects.js | 2 +- src/lib/gramjs/tl/api.d.ts | 34 +- src/lib/gramjs/tl/apiTl.js | 9 +- src/lib/gramjs/tl/static/api.json | 3 + src/lib/gramjs/tl/static/api.tl | 12 +- src/types/webapp.ts | 174 ++++++ 27 files changed, 741 insertions(+), 402 deletions(-) rename src/components/{main => modals/attachBotInstall}/AttachBotInstallModal.async.tsx (66%) rename src/components/{main => modals/attachBotInstall}/AttachBotInstallModal.tsx (76%) rename src/components/modals/{mapModal => map}/MapModal.async.tsx (100%) rename src/components/modals/{mapModal => map}/MapModal.module.scss (100%) rename src/components/modals/{mapModal => map}/MapModal.tsx (100%) rename src/components/{main => modals/urlAuth}/UrlAuthModal.async.tsx (63%) rename src/components/{main => modals/urlAuth}/UrlAuthModal.module.scss (100%) rename src/components/{main => modals/urlAuth}/UrlAuthModal.tsx (83%) rename src/components/{main => modals/webApp}/WebAppModal.async.tsx (63%) rename src/components/{main => modals/webApp}/WebAppModal.module.scss (100%) rename src/components/{main => modals/webApp}/WebAppModal.tsx (63%) create mode 100644 src/components/modals/webApp/hooks/usePopupLimit.ts rename src/components/{main => modals/webApp}/hooks/useWebAppFrame.ts (53%) create mode 100644 src/types/webapp.ts diff --git a/src/api/gramjs/apiBuilders/messages.ts b/src/api/gramjs/apiBuilders/messages.ts index cf6c4c997..4c13b36f7 100644 --- a/src/api/gramjs/apiBuilders/messages.ts +++ b/src/api/gramjs/apiBuilders/messages.ts @@ -426,6 +426,8 @@ function buildAction( if (action.domain) { text = 'ActionBotAllowed'; translationValues.push(action.domain); + } else if (action.fromRequest) { + text = 'lng_action_webapp_bot_allowed'; } else { text = 'ActionAttachMenuBotAllowed'; } diff --git a/src/api/gramjs/methods/bots.ts b/src/api/gramjs/methods/bots.ts index 545e60c17..49314041c 100644 --- a/src/api/gramjs/methods/bots.ts +++ b/src/api/gramjs/methods/bots.ts @@ -468,6 +468,55 @@ export async function acceptLinkUrlAuth({ url, isWriteAllowed }: { url: string; return authResult; } +export function fetchBotCanSendMessage({ bot } : { bot: ApiUser }) { + return invokeRequest(new GramJs.bots.CanSendMessage({ + bot: buildInputEntity(bot.id, bot.accessHash) as GramJs.InputUser, + })); +} + +export function allowBotSendMessages({ bot } : { bot: ApiUser }) { + return invokeRequest(new GramJs.bots.AllowSendMessage({ + bot: buildInputEntity(bot.id, bot.accessHash) as GramJs.InputUser, + }), { + shouldReturnTrue: true, + }); +} + +export async function invokeWebViewCustomMethod({ + bot, + customMethod, + parameters, +}: { + bot: ApiUser; + customMethod: string; + parameters: string; +}): Promise<{ + result: object; + } | { + error: string; + }> { + try { + const result = await invokeRequest(new GramJs.bots.InvokeWebViewCustomMethod({ + bot: buildInputPeer(bot.id, bot.accessHash), + params: new GramJs.DataJSON({ + data: parameters, + }), + customMethod, + }), { + shouldThrow: true, + }); + + return { + result: JSON.parse(result!.data), + }; + } catch (e) { + const error = e as Error; + return { + error: error.message, + }; + } +} + function processInlineBotResult(queryId: string, results: GramJs.TypeBotInlineResult[]) { return results.map((result) => { if (result instanceof GramJs.BotInlineMediaResult) { diff --git a/src/api/gramjs/methods/index.ts b/src/api/gramjs/methods/index.ts index fed8fb08c..987855696 100644 --- a/src/api/gramjs/methods/index.ts +++ b/src/api/gramjs/methods/index.ts @@ -77,6 +77,7 @@ export { answerCallbackButton, fetchTopInlineBots, fetchInlineBot, fetchInlineBotResults, sendInlineBotResult, startBot, requestWebView, requestSimpleWebView, sendWebViewData, prolongWebView, loadAttachBots, toggleAttachBot, fetchBotApp, requestBotUrlAuth, requestLinkUrlAuth, acceptBotUrlAuth, acceptLinkUrlAuth, loadAttachBot, requestAppWebView, + allowBotSendMessages, fetchBotCanSendMessage, invokeWebViewCustomMethod, } from './bots'; export { diff --git a/src/bundles/extra.ts b/src/bundles/extra.ts index a6ef5fecf..e6dffc9a5 100644 --- a/src/bundles/extra.ts +++ b/src/bundles/extra.ts @@ -7,13 +7,13 @@ export { default as AttachBotRecipientPicker } from '../components/main/AttachBo export { default as Dialogs } from '../components/main/Dialogs'; export { default as Notifications } from '../components/main/Notifications'; export { default as SafeLinkModal } from '../components/main/SafeLinkModal'; -export { default as MapModal } from '../components/modals/mapModal/MapModal'; -export { default as UrlAuthModal } from '../components/main/UrlAuthModal'; +export { default as MapModal } from '../components/modals/map/MapModal'; +export { default as UrlAuthModal } from '../components/modals/urlAuth/UrlAuthModal'; export { default as HistoryCalendar } from '../components/main/HistoryCalendar'; export { default as NewContactModal } from '../components/main/NewContactModal'; -export { default as WebAppModal } from '../components/main/WebAppModal'; +export { default as WebAppModal } from '../components/modals/webApp/WebAppModal'; export { default as BotTrustModal } from '../components/main/BotTrustModal'; -export { default as AttachBotInstallModal } from '../components/main/AttachBotInstallModal'; +export { default as AttachBotInstallModal } from '../components/modals/attachBotInstall/AttachBotInstallModal'; export { default as DeleteFolderDialog } from '../components/main/DeleteFolderDialog'; export { default as PremiumMainModal } from '../components/main/premium/PremiumMainModal'; export { default as GiftPremiumModal } from '../components/main/premium/GiftPremiumModal'; diff --git a/src/components/main/Main.tsx b/src/components/main/Main.tsx index b5ce2245e..2682cfa73 100644 --- a/src/components/main/Main.tsx +++ b/src/components/main/Main.tsx @@ -80,11 +80,11 @@ import PhoneCall from '../calls/phone/PhoneCall.async'; import MessageListHistoryHandler from '../middle/MessageListHistoryHandler'; import NewContactModal from './NewContactModal.async'; import RatePhoneCallModal from '../calls/phone/RatePhoneCallModal.async'; -import WebAppModal from './WebAppModal.async'; +import WebAppModal from '../modals/webApp/WebAppModal.async'; import BotTrustModal from './BotTrustModal.async'; -import AttachBotInstallModal from './AttachBotInstallModal.async'; +import AttachBotInstallModal from '../modals/attachBotInstall/AttachBotInstallModal.async'; import ConfettiContainer from './ConfettiContainer'; -import UrlAuthModal from './UrlAuthModal.async'; +import UrlAuthModal from '../modals/urlAuth/UrlAuthModal.async'; import PremiumMainModal from './premium/PremiumMainModal.async'; import PaymentModal from '../payment/PaymentModal.async'; import ReceiptModal from '../payment/ReceiptModal.async'; @@ -96,7 +96,7 @@ import AttachBotRecipientPicker from './AttachBotRecipientPicker.async'; import ReactionPicker from '../middle/message/ReactionPicker.async'; import ChatlistModal from '../modals/chatlist/ChatlistModal.async'; import StoryViewer from '../story/StoryViewer.async'; -import MapModal from '../modals/mapModal/MapModal.async'; +import MapModal from '../modals/map/MapModal.async'; import './Main.scss'; diff --git a/src/components/main/AttachBotInstallModal.async.tsx b/src/components/modals/attachBotInstall/AttachBotInstallModal.async.tsx similarity index 66% rename from src/components/main/AttachBotInstallModal.async.tsx rename to src/components/modals/attachBotInstall/AttachBotInstallModal.async.tsx index 47d9db465..05ed8bb99 100644 --- a/src/components/main/AttachBotInstallModal.async.tsx +++ b/src/components/modals/attachBotInstall/AttachBotInstallModal.async.tsx @@ -1,10 +1,10 @@ -import type { FC } from '../../lib/teact/teact'; -import React from '../../lib/teact/teact'; -import { Bundles } from '../../util/moduleLoader'; +import type { FC } from '../../../lib/teact/teact'; +import React from '../../../lib/teact/teact'; +import { Bundles } from '../../../util/moduleLoader'; import type { OwnProps } from './AttachBotInstallModal'; -import useModuleLoader from '../../hooks/useModuleLoader'; +import useModuleLoader from '../../../hooks/useModuleLoader'; const AttachBotInstallModalAsync: FC = (props) => { const { bot } = props; diff --git a/src/components/main/AttachBotInstallModal.tsx b/src/components/modals/attachBotInstall/AttachBotInstallModal.tsx similarity index 76% rename from src/components/main/AttachBotInstallModal.tsx rename to src/components/modals/attachBotInstall/AttachBotInstallModal.tsx index 3e9fa6431..8074210ec 100644 --- a/src/components/main/AttachBotInstallModal.tsx +++ b/src/components/modals/attachBotInstall/AttachBotInstallModal.tsx @@ -1,18 +1,18 @@ import React, { memo, useCallback, useEffect, useState, -} from '../../lib/teact/teact'; -import { getActions } from '../../global'; +} from '../../../lib/teact/teact'; +import { getActions } from '../../../global'; -import type { FC } from '../../lib/teact/teact'; -import type { ApiAttachBot } from '../../api/types'; +import type { FC } from '../../../lib/teact/teact'; +import type { ApiAttachBot } from '../../../api/types'; -import renderText from '../common/helpers/renderText'; +import renderText from '../../common/helpers/renderText'; -import useLang from '../../hooks/useLang'; -import usePrevious from '../../hooks/usePrevious'; +import useLang from '../../../hooks/useLang'; +import usePrevious from '../../../hooks/usePrevious'; -import ConfirmDialog from '../ui/ConfirmDialog'; -import Checkbox from '../ui/Checkbox'; +import ConfirmDialog from '../../ui/ConfirmDialog'; +import Checkbox from '../../ui/Checkbox'; export type OwnProps = { bot?: ApiAttachBot; diff --git a/src/components/modals/mapModal/MapModal.async.tsx b/src/components/modals/map/MapModal.async.tsx similarity index 100% rename from src/components/modals/mapModal/MapModal.async.tsx rename to src/components/modals/map/MapModal.async.tsx diff --git a/src/components/modals/mapModal/MapModal.module.scss b/src/components/modals/map/MapModal.module.scss similarity index 100% rename from src/components/modals/mapModal/MapModal.module.scss rename to src/components/modals/map/MapModal.module.scss diff --git a/src/components/modals/mapModal/MapModal.tsx b/src/components/modals/map/MapModal.tsx similarity index 100% rename from src/components/modals/mapModal/MapModal.tsx rename to src/components/modals/map/MapModal.tsx diff --git a/src/components/main/UrlAuthModal.async.tsx b/src/components/modals/urlAuth/UrlAuthModal.async.tsx similarity index 63% rename from src/components/main/UrlAuthModal.async.tsx rename to src/components/modals/urlAuth/UrlAuthModal.async.tsx index 0886d33ba..b1fb6857b 100644 --- a/src/components/main/UrlAuthModal.async.tsx +++ b/src/components/modals/urlAuth/UrlAuthModal.async.tsx @@ -1,9 +1,9 @@ -import type { FC } from '../../lib/teact/teact'; -import React from '../../lib/teact/teact'; -import { Bundles } from '../../util/moduleLoader'; +import type { FC } from '../../../lib/teact/teact'; +import React from '../../../lib/teact/teact'; +import { Bundles } from '../../../util/moduleLoader'; import type { OwnProps } from './UrlAuthModal'; -import useModuleLoader from '../../hooks/useModuleLoader'; +import useModuleLoader from '../../../hooks/useModuleLoader'; const UrlAuthModalAsync: FC = (props) => { const { urlAuth } = props; diff --git a/src/components/main/UrlAuthModal.module.scss b/src/components/modals/urlAuth/UrlAuthModal.module.scss similarity index 100% rename from src/components/main/UrlAuthModal.module.scss rename to src/components/modals/urlAuth/UrlAuthModal.module.scss diff --git a/src/components/main/UrlAuthModal.tsx b/src/components/modals/urlAuth/UrlAuthModal.tsx similarity index 83% rename from src/components/main/UrlAuthModal.tsx rename to src/components/modals/urlAuth/UrlAuthModal.tsx index 201721e0b..ea4e9244e 100644 --- a/src/components/main/UrlAuthModal.tsx +++ b/src/components/modals/urlAuth/UrlAuthModal.tsx @@ -1,20 +1,20 @@ import React, { memo, useCallback, useEffect, useState, -} from '../../lib/teact/teact'; -import { getActions, getGlobal } from '../../global'; +} from '../../../lib/teact/teact'; +import { getActions, getGlobal } from '../../../global'; -import type { FC } from '../../lib/teact/teact'; -import type { TabState } from '../../global/types'; +import type { FC } from '../../../lib/teact/teact'; +import type { TabState } from '../../../global/types'; -import { ensureProtocol } from '../../util/ensureProtocol'; -import renderText from '../common/helpers/renderText'; -import { getUserFullName } from '../../global/helpers'; +import { ensureProtocol } from '../../../util/ensureProtocol'; +import renderText from '../../common/helpers/renderText'; +import { getUserFullName } from '../../../global/helpers'; -import useLang from '../../hooks/useLang'; -import useCurrentOrPrev from '../../hooks/useCurrentOrPrev'; +import useLang from '../../../hooks/useLang'; +import useCurrentOrPrev from '../../../hooks/useCurrentOrPrev'; -import ConfirmDialog from '../ui/ConfirmDialog'; -import Checkbox from '../ui/Checkbox'; +import ConfirmDialog from '../../ui/ConfirmDialog'; +import Checkbox from '../../ui/Checkbox'; import styles from './UrlAuthModal.module.scss'; diff --git a/src/components/main/WebAppModal.async.tsx b/src/components/modals/webApp/WebAppModal.async.tsx similarity index 63% rename from src/components/main/WebAppModal.async.tsx rename to src/components/modals/webApp/WebAppModal.async.tsx index 7f87d4b97..82583bc1a 100644 --- a/src/components/main/WebAppModal.async.tsx +++ b/src/components/modals/webApp/WebAppModal.async.tsx @@ -1,10 +1,10 @@ -import type { FC } from '../../lib/teact/teact'; -import React from '../../lib/teact/teact'; -import { Bundles } from '../../util/moduleLoader'; +import type { FC } from '../../../lib/teact/teact'; +import React from '../../../lib/teact/teact'; +import { Bundles } from '../../../util/moduleLoader'; import type { OwnProps } from './WebAppModal'; -import useModuleLoader from '../../hooks/useModuleLoader'; +import useModuleLoader from '../../../hooks/useModuleLoader'; const WebAppModalAsync: FC = (props) => { const { webApp } = props; diff --git a/src/components/main/WebAppModal.module.scss b/src/components/modals/webApp/WebAppModal.module.scss similarity index 100% rename from src/components/main/WebAppModal.module.scss rename to src/components/modals/webApp/WebAppModal.module.scss diff --git a/src/components/main/WebAppModal.tsx b/src/components/modals/webApp/WebAppModal.tsx similarity index 63% rename from src/components/main/WebAppModal.tsx rename to src/components/modals/webApp/WebAppModal.tsx index 69ed49c84..ec10da7f3 100644 --- a/src/components/main/WebAppModal.tsx +++ b/src/components/modals/webApp/WebAppModal.tsx @@ -1,36 +1,39 @@ import React, { - memo, useCallback, useEffect, useMemo, useRef, useState, -} from '../../lib/teact/teact'; -import { getActions, withGlobal } from '../../global'; + memo, useEffect, useMemo, useRef, useState, +} from '../../../lib/teact/teact'; +import { getActions, withGlobal } from '../../../global'; -import type { FC } from '../../lib/teact/teact'; -import type { ApiAttachBot, ApiChat, ApiUser } from '../../api/types'; -import type { TabState } from '../../global/types'; -import type { ThemeKey } from '../../types'; -import type { PopupOptions, WebAppInboundEvent } from './hooks/useWebAppFrame'; +import type { FC } from '../../../lib/teact/teact'; +import type { ApiAttachBot, ApiChat, ApiUser } from '../../../api/types'; +import type { TabState } from '../../../global/types'; +import type { ThemeKey } from '../../../types'; +import type { PopupOptions, WebAppInboundEvent } from '../../../types/webapp'; -import { TME_LINK_PREFIX } from '../../config'; +import { callApi } from '../../../api/gramjs'; +import { TME_LINK_PREFIX } from '../../../config'; import { selectCurrentChat, selectTabState, selectTheme, selectUser, -} from '../../global/selectors'; -import buildClassName from '../../util/buildClassName'; -import { extractCurrentThemeParams, validateHexColor } from '../../util/themeStyle'; -import { convertToApiChatType } from '../../global/helpers'; +} from '../../../global/selectors'; +import buildClassName from '../../../util/buildClassName'; +import { extractCurrentThemeParams, validateHexColor } from '../../../util/themeStyle'; +import { convertToApiChatType } from '../../../global/helpers'; -import useInterval from '../../hooks/useInterval'; -import useLang from '../../hooks/useLang'; -import useSyncEffect from '../../hooks/useSyncEffect'; +import useInterval from '../../../hooks/useInterval'; +import useLang from '../../../hooks/useLang'; +import useSyncEffect from '../../../hooks/useSyncEffect'; import useWebAppFrame from './hooks/useWebAppFrame'; -import usePrevious from '../../hooks/usePrevious'; -import useFlag from '../../hooks/useFlag'; -import useAppLayout from '../../hooks/useAppLayout'; +import usePrevious from '../../../hooks/usePrevious'; +import useFlag from '../../../hooks/useFlag'; +import useAppLayout from '../../../hooks/useAppLayout'; +import useLastCallback from '../../../hooks/useLastCallback'; +import usePopupLimit from './hooks/usePopupLimit'; -import Modal from '../ui/Modal'; -import Button from '../ui/Button'; -import DropdownMenu from '../ui/DropdownMenu'; -import MenuItem from '../ui/MenuItem'; -import Spinner from '../ui/Spinner'; -import ConfirmDialog from '../ui/ConfirmDialog'; +import Modal from '../../ui/Modal'; +import Button from '../../ui/Button'; +import DropdownMenu from '../../ui/DropdownMenu'; +import MenuItem from '../../ui/MenuItem'; +import Spinner from '../../ui/Spinner'; +import ConfirmDialog from '../../ui/ConfirmDialog'; import styles from './WebAppModal.module.scss'; @@ -61,6 +64,8 @@ const NBSP = '\u00A0'; const MAIN_BUTTON_ANIMATION_TIME = 250; const PROLONG_INTERVAL = 45000; // 45s const ANIMATION_WAIT = 400; +const POPUP_SEQUENTIAL_LIMIT = 3; +const POPUP_RESET_DELAY = 2000; // 2s const SANDBOX_ATTRIBUTES = [ 'allow-scripts', 'allow-same-origin', @@ -92,22 +97,29 @@ const WebAppModal: FC = ({ toggleAttachBot, openTelegramLink, openChat, - openInvoice, setWebAppPaymentSlug, - showNotification, switchBotInline, + sharePhoneWithBot, } = getActions(); const [mainButton, setMainButton] = useState(); const [isBackButtonVisible, setIsBackButtonVisible] = useState(false); + const [backgroundColor, setBackgroundColor] = useState(); const [headerColor, setHeaderColor] = useState(); - const [confirmClose, setConfirmClose] = useState(false); - const [isCloseModalOpen, openCloseModal, closeModal] = useFlag(false); + + const [shouldConfirmClosing, setShouldConfirmClosing] = useState(false); + const [isCloseModalOpen, openCloseModal, hideCloseModal] = useFlag(false); + const [isLoaded, markLoaded, markUnloaded] = useFlag(false); - const [popupParams, setPopupParams] = useState(); + + const [popupParameters, setPopupParameters] = useState(); + const [isRequestingPhone, setIsRequestingPhone] = useState(false); + const [isRequesingWriteAccess, setIsRequestingWriteAccess] = useState(false); + const { + unlockPopupsAt, handlePopupOpened, handlePopupClosed, + } = usePopupLimit(POPUP_SEQUENTIAL_LIMIT, POPUP_RESET_DELAY); + const { isMobile } = useAppLayout(); - const prevPopupParams = usePrevious(popupParams); - const renderingPopupParams = popupParams || prevPopupParams; useEffect(() => { const themeParams = extractCurrentThemeParams(); @@ -125,32 +137,224 @@ const WebAppModal: FC = ({ const isOpen = Boolean(url); const isSimple = Boolean(buttonText); - const handleEvent = useCallback((event: WebAppInboundEvent) => { - const { eventType, eventData } = event; - if (eventType === 'web_app_close') { + const { + reloadFrame, sendEvent, sendViewport, sendTheme, + } = useWebAppFrame(frameRef, isOpen, isSimple, handleEvent, markLoaded); + + const shouldShowMainButton = mainButton?.isVisible && mainButton.text.trim().length > 0; + + useInterval(() => { + prolongWebView({ + botId: bot!.id, + queryId: queryId!, + peerId: chat!.id, + replyToMessageId, + threadId, + }); + }, queryId ? PROLONG_INTERVAL : undefined, true); + + const handleMainButtonClick = useLastCallback(() => { + sendEvent({ + eventType: 'main_button_pressed', + }); + }); + + const handleSettingsButtonClick = useLastCallback(() => { + sendEvent({ + eventType: 'settings_button_pressed', + }); + }); + + const handleRefreshClick = useLastCallback(() => { + reloadFrame(webApp!.url); + }); + + const handleClose = useLastCallback(() => { + if (shouldConfirmClosing) { + openCloseModal(); + } else { closeWebApp(); } + }); - if (eventType === 'web_app_open_invoice') { - setWebAppPaymentSlug({ - slug: eventData.slug, + const handleAppPopupClose = useLastCallback((buttonId?: string) => { + setPopupParameters(undefined); + handlePopupClosed(); + sendEvent({ + eventType: 'popup_closed', + eventData: { + button_id: buttonId, + }, + }); + }); + + const handleAppPopupModalClose = useLastCallback(() => { + handleAppPopupClose(); + }); + + // Notify view that height changed + useSyncEffect(() => { + setTimeout(() => { + sendViewport(); + }, ANIMATION_WAIT); + }, [mainButton?.isVisible, sendViewport]); + + // Notify view that theme changed + useSyncEffect(() => { + setTimeout(() => { + sendTheme(); + }, ANIMATION_WAIT); + }, [theme, sendTheme]); + + useSyncEffect(([prevIsPaymentModalOpen]) => { + if (isPaymentModalOpen === prevIsPaymentModalOpen) return; + if (webApp?.slug && !isPaymentModalOpen && paymentStatus) { + sendEvent({ + eventType: 'invoice_closed', + eventData: { + slug: webApp.slug, + status: paymentStatus, + }, }); - openInvoice({ - slug: eventData.slug, + setWebAppPaymentSlug({ + slug: undefined, + }); + } + }, [isPaymentModalOpen, paymentStatus, sendEvent, setWebAppPaymentSlug, webApp]); + + const handleToggleClick = useLastCallback(() => { + toggleAttachBot({ + botId: bot!.id, + isEnabled: !attachBot, + }); + }); + + const handleBackClick = useLastCallback(() => { + if (isBackButtonVisible) { + sendEvent({ + eventType: 'back_button_pressed', + }); + } else { + handleClose(); + } + }); + + const handleRejectPhone = useLastCallback(() => { + setIsRequestingPhone(false); + handlePopupClosed(); + sendEvent({ + eventType: 'phone_requested', + eventData: { + status: 'cancelled', + }, + }); + }); + + const handleAcceptPhone = useLastCallback(() => { + sharePhoneWithBot({ botId: bot!.id }); + setIsRequestingPhone(false); + handlePopupClosed(); + sendEvent({ + eventType: 'phone_requested', + eventData: { + status: 'sent', + }, + }); + }); + + const handleRejectWriteAccess = useLastCallback(() => { + sendEvent({ + eventType: 'write_access_requested', + eventData: { + status: 'cancelled', + }, + }); + setIsRequestingWriteAccess(false); + handlePopupClosed(); + }); + + const handleAcceptWriteAccess = useLastCallback(async () => { + const result = await callApi('allowBotSendMessages', { bot: bot! }); + if (!result) { + handleRejectWriteAccess(); + return; + } + + sendEvent({ + eventType: 'write_access_requested', + eventData: { + status: 'allowed', + }, + }); + setIsRequestingWriteAccess(false); + handlePopupClosed(); + }); + + async function handleRequestWriteAccess() { + const canWrite = await callApi('fetchBotCanSendMessage', { + bot: bot!, + }); + + if (canWrite) { + sendEvent({ + eventType: 'write_access_requested', + eventData: { + status: 'allowed', + }, }); } + setIsRequestingWriteAccess(!canWrite); + } + + async function handleInvokeCustomMethod(requestId: string, method: string, parameters: string) { + const result = await callApi('invokeWebViewCustomMethod', { + bot: bot!, + customMethod: method, + parameters, + }); + + sendEvent({ + eventType: 'custom_method_invoked', + eventData: { + req_id: requestId, + ...result, + }, + }); + } + + const openBotChat = useLastCallback(() => { + openChat({ + id: bot!.id, + }); + closeWebApp(); + }); + + useEffect(() => { + if (!isOpen) { + const themeParams = extractCurrentThemeParams(); + + setShouldConfirmClosing(false); + hideCloseModal(); + setPopupParameters(undefined); + setIsRequestingPhone(false); + setIsRequestingWriteAccess(false); + setMainButton(undefined); + setIsBackButtonVisible(false); + setBackgroundColor(themeParams.bg_color); + setHeaderColor(themeParams.bg_color); + markUnloaded(); + } + }, [hideCloseModal, isOpen, markUnloaded]); + + function handleEvent(event: WebAppInboundEvent) { + const { eventType, eventData } = event; if (eventType === 'web_app_open_tg_link' && !isPaymentModalOpen) { const linkUrl = TME_LINK_PREFIX + eventData.path_full; openTelegramLink({ url: linkUrl }); closeWebApp(); } - if (eventType === 'web_app_open_link') { - const linkUrl = eventData.url; - window.open(linkUrl, '_blank', 'noreferrer'); - } - if (eventType === 'web_app_setup_back_button') { setIsBackButtonVisible(eventData.is_visible); } @@ -193,18 +397,19 @@ const WebAppModal: FC = ({ } if (eventType === 'web_app_setup_closing_behavior') { - setConfirmClose(eventData.need_confirmation); + setShouldConfirmClosing(eventData.need_confirmation); } if (eventType === 'web_app_open_popup') { - if (!eventData.message.trim().length || !eventData.buttons?.length || eventData.buttons.length > 3) return; - setPopupParams(eventData); - } + if (popupParameters || !eventData.message.trim().length || !eventData.buttons?.length + || eventData.buttons.length > 3 || isRequestingPhone || isRequesingWriteAccess + || unlockPopupsAt > Date.now()) { + handleAppPopupClose(undefined); + return; + } - if (eventType === 'web_app_open_scan_qr_popup') { - showNotification({ - message: 'Scan QR code is not supported in this client yet', - }); + setPopupParameters(eventData); + handlePopupOpened(); } if (eventType === 'web_app_switch_inline_query') { @@ -220,127 +425,32 @@ const WebAppModal: FC = ({ closeWebApp(); } - }, [ - bot, buttonText, closeWebApp, openInvoice, openTelegramLink, sendWebViewData, setWebAppPaymentSlug, - isPaymentModalOpen, showNotification, - ]); - const { - reloadFrame, sendEvent, sendViewport, sendTheme, - } = useWebAppFrame(frameRef, isOpen, isSimple, handleEvent, markLoaded); + if (eventType === 'web_app_request_phone') { + if (popupParameters || isRequesingWriteAccess || unlockPopupsAt > Date.now()) { + handleRejectPhone(); + return; + } - const shouldShowMainButton = mainButton?.isVisible && mainButton.text.trim().length > 0; - - useInterval(() => { - prolongWebView({ - botId: bot!.id, - queryId: queryId!, - peerId: chat!.id, - replyToMessageId, - threadId, - }); - }, queryId ? PROLONG_INTERVAL : undefined, true); - - const handleMainButtonClick = useCallback(() => { - sendEvent({ - eventType: 'main_button_pressed', - }); - }, [sendEvent]); - - const handleSettingsButtonClick = useCallback(() => { - sendEvent({ - eventType: 'settings_button_pressed', - }); - }, [sendEvent]); - - const handleRefreshClick = useCallback(() => { - reloadFrame(webApp!.url); - }, [reloadFrame, webApp]); - - const handleClose = useCallback(() => { - if (confirmClose) { - openCloseModal(); - } else { - closeWebApp(); + setIsRequestingPhone(true); + handlePopupOpened(); } - }, [confirmClose, openCloseModal, closeWebApp]); - const handlePopupClose = useCallback((buttonId?: string) => { - setPopupParams(undefined); - sendEvent({ - eventType: 'popup_closed', - eventData: { - button_id: buttonId, - }, - }); - }, [sendEvent]); + if (eventType === 'web_app_request_write_access') { + if (popupParameters || isRequestingPhone || unlockPopupsAt > Date.now()) { + handleRejectWriteAccess(); + return; + } - const handlePopupModalClose = useCallback(() => { - handlePopupClose(); - }, [handlePopupClose]); - - // Notify view that height changed - useSyncEffect(() => { - setTimeout(() => { - sendViewport(); - }, ANIMATION_WAIT); - }, [mainButton?.isVisible, sendViewport]); - - // Notify view that theme changed - useSyncEffect(() => { - setTimeout(() => { - sendTheme(); - }, ANIMATION_WAIT); - }, [theme, sendTheme]); - - useSyncEffect(([prevIsPaymentModalOpen]) => { - if (isPaymentModalOpen === prevIsPaymentModalOpen) return; - if (webApp?.slug && !isPaymentModalOpen && paymentStatus) { - sendEvent({ - eventType: 'invoice_closed', - eventData: { - slug: webApp.slug, - status: paymentStatus, - }, - }); - setWebAppPaymentSlug({ - slug: undefined, - }); + handleRequestWriteAccess(); + handlePopupOpened(); } - }, [isPaymentModalOpen, paymentStatus, sendEvent, setWebAppPaymentSlug, webApp]); - const handleToggleClick = useCallback(() => { - toggleAttachBot({ - botId: bot!.id, - isEnabled: !attachBot, - }); - }, [bot, attachBot, toggleAttachBot]); - - const handleBackClick = useCallback(() => { - if (isBackButtonVisible) { - sendEvent({ - eventType: 'back_button_pressed', - }); - } else { - handleClose(); + if (eventType === 'web_app_invoke_custom_method') { + const { method, params, req_id: requestId } = eventData; + handleInvokeCustomMethod(requestId, method, JSON.stringify(params)); } - }, [handleClose, isBackButtonVisible, sendEvent]); - - const openBotChat = useCallback(() => { - openChat({ - id: bot!.id, - }); - closeWebApp(); - }, [bot, closeWebApp, openChat]); - - useEffect(() => { - if (!isOpen) { - setConfirmClose(false); - closeModal(); - setPopupParams(undefined); - markUnloaded(); - } - }, [closeModal, isOpen, markUnloaded]); + } const MoreMenuButton: FC<{ onTrigger: () => void; isOpen?: boolean }> = useMemo(() => { return ({ onTrigger, isOpen: isMenuOpen }) => ( @@ -414,16 +524,6 @@ const WebAppModal: FC = ({ const mainButtonCurrentIsActive = mainButton?.isActive !== undefined ? mainButton.isActive : prevMainButtonIsActive; const mainButtonCurrentText = mainButton?.text || prevMainButtonText; - useEffect(() => { - if (!isOpen) { - const themeParams = extractCurrentThemeParams(); - setMainButton(undefined); - setIsBackButtonVisible(false); - setBackgroundColor(themeParams.bg_color); - setHeaderColor(themeParams.bg_color); - } - }, [isOpen]); - const [shouldDecreaseWebFrameSize, setShouldDecreaseWebFrameSize] = useState(false); const [shouldHideButton, setShouldHideButton] = useState(true); @@ -479,30 +579,35 @@ const WebAppModal: FC = ({ )} - {confirmClose && ( - - )} - {renderingPopupParams && ( + + + {popupParameters && ( - {renderingPopupParams.message} + {popupParameters.message}
- {renderingPopupParams.buttons.map((button) => ( + {popupParameters.buttons.map((button) => ( @@ -518,6 +623,16 @@ const WebAppModal: FC = ({
)} + + ); }; diff --git a/src/components/modals/webApp/hooks/usePopupLimit.ts b/src/components/modals/webApp/hooks/usePopupLimit.ts new file mode 100644 index 000000000..ba22d5b8a --- /dev/null +++ b/src/components/modals/webApp/hooks/usePopupLimit.ts @@ -0,0 +1,34 @@ +import { useRef, useState } from '../../../../lib/teact/teact'; +import useLastCallback from '../../../../hooks/useLastCallback'; + +export default function usePopupLimit(sequentialLimit: number, resetAfter: number) { + const [unlockPopupsAt, setUnlockPopupsAt] = useState(0); + const sequentialCalls = useRef(0); + const lastClosedDate = useRef(0); + + const handlePopupOpened = useLastCallback(() => { + const now = Date.now(); + + if (now - lastClosedDate.current > resetAfter) { + sequentialCalls.current = 0; + } + + sequentialCalls.current += 1; + + if (sequentialCalls.current >= sequentialLimit) { + setUnlockPopupsAt(now + resetAfter); + } + }); + + const handlePopupClosed = useLastCallback(() => { + if (unlockPopupsAt < Date.now()) { // Prevent confused user from extending lock time + lastClosedDate.current = Date.now(); + } + }); + + return { + unlockPopupsAt, + handlePopupOpened, + handlePopupClosed, + }; +} diff --git a/src/components/main/hooks/useWebAppFrame.ts b/src/components/modals/webApp/hooks/useWebAppFrame.ts similarity index 53% rename from src/components/main/hooks/useWebAppFrame.ts rename to src/components/modals/webApp/hooks/useWebAppFrame.ts index cb4c645f8..e74ad952d 100644 --- a/src/components/main/hooks/useWebAppFrame.ts +++ b/src/components/modals/webApp/hooks/useWebAppFrame.ts @@ -1,155 +1,11 @@ -import { useCallback, useEffect, useRef } from '../../../lib/teact/teact'; -import { getActions } from '../../../lib/teact/teactn'; -import { extractCurrentThemeParams } from '../../../util/themeStyle'; -import useWindowSize from '../../../hooks/useWindowSize'; +import { useCallback, useEffect, useRef } from '../../../../lib/teact/teact'; +import { getActions } from '../../../../global'; -export type PopupOptions = { - title: string; - message: string; - buttons: { - id: string; - type: 'default' | 'ok' | 'close' | 'cancel' | 'destructive'; - text: string; - }[]; -}; +import type { WebAppInboundEvent, WebAppOutboundEvent } from '../../../../types/webapp'; -export type WebAppInboundEvent = { - eventType: 'web_app_data_send'; - eventData: { - data: string; - }; -} | { - eventType: 'web_app_setup_main_button'; - eventData: { - is_visible: boolean; - is_active: boolean; - text: string; - color: string; - text_color: string; - is_progress_visible: boolean; - }; -} | { - eventType: 'web_app_setup_back_button'; - eventData: { - is_visible: boolean; - }; -} | { - eventType: 'web_app_open_link'; - eventData: { - url: string; - try_instant_view?: boolean; - }; -} | { - eventType: 'web_app_open_tg_link'; - eventData: { - path_full: string; - }; -} | { - eventType: 'web_app_open_invoice'; - eventData: { - slug: string; - }; -} | { - eventType: 'web_app_trigger_haptic_feedback'; - eventData: { - type: 'impact' | 'notification' | 'selection_change'; - impact_style?: 'light' | 'medium' | 'heavy'; - notification_type?: 'error' | 'success' | 'warning'; - }; -} | { - eventType: 'web_app_set_background_color'; - eventData: { - color: string; - }; -} | { - eventType: 'web_app_set_header_color'; - eventData: { - color_key: 'bg_color' | 'secondary_bg_color'; - }; -} | { - eventType: 'web_app_open_popup'; - eventData: PopupOptions; -} | { - eventType: 'web_app_setup_closing_behavior'; - eventData: { - need_confirmation: boolean; - }; -} | { - eventType: 'web_app_open_scan_qr_popup'; - eventData: { - text?: string; - }; -} | { - eventType: 'web_app_read_text_from_clipboard'; - eventData: { - req_id: string; - }; -} | { - eventType: 'web_app_switch_inline_query'; - eventData: { - query: string; - chat_types: ('users' | 'bots' | 'groups' | 'channels')[]; - }; -} | { - eventType: 'web_app_request_viewport' | 'web_app_request_theme' | 'web_app_ready' | 'web_app_expand' - | 'web_app_request_phone' | 'web_app_close' | 'iframe_ready' | 'web_app_close_scan_qr_popup'; - eventData: null; -}; +import { extractCurrentThemeParams } from '../../../../util/themeStyle'; -type WebAppOutboundEvent = { - eventType: 'viewport_changed'; - eventData: { - height: number; - width?: number; - is_expanded?: boolean; - is_state_stable?: boolean; - }; -} | { - eventType: 'theme_changed'; - eventData: { - theme_params: { - bg_color: string; - text_color: string; - hint_color: string; - link_color: string; - button_color: string; - button_text_color: string; - secondary_bg_color: string; - }; - }; -} | { - eventType: 'set_custom_style'; - eventData: string; -} | { - eventType: 'invoice_closed'; - eventData: { - slug: string; - status: 'paid' | 'cancelled' | 'pending' | 'failed'; - }; -} | { - eventType: 'phone_requested'; - eventData: { - phone_number: string; - }; -} | { - eventType: 'popup_closed'; - eventData: { - button_id?: string; - }; -} | { - eventType: 'qr_text_received'; - eventData: { - data: string; - }; -} | { - eventType: 'clipboard_text_received'; - eventData: { - req_id: string; - data: string | null; - }; -} | { - eventType: 'main_button_pressed' | 'back_button_pressed' | 'settings_button_pressed' | 'scan_qr_popup_closed'; -}; +import useWindowSize from '../../../../hooks/useWindowSize'; const SCROLLBAR_STYLE = `* { scrollbar-width: thin; @@ -180,6 +36,9 @@ const useWebAppFrame = ( ) => { const { showNotification, + setWebAppPaymentSlug, + openInvoice, + closeWebApp, } = getActions(); const ignoreEventsRef = useRef(false); @@ -253,34 +112,40 @@ const useWebAppFrame = ( try { const data = JSON.parse(event.data) as WebAppInboundEvent; + const { eventType, eventData } = data; // Handle some app requests here to simplify hook usage - if (data.eventType === 'web_app_ready') { + if (eventType === 'web_app_ready') { onLoad?.(); } - if (data.eventType === 'web_app_request_viewport') { + if (eventType === 'web_app_close') { + closeWebApp(); + } + + if (eventType === 'web_app_request_viewport') { sendViewport(windowSize.isResizing); } - if (data.eventType === 'web_app_request_theme') { + if (eventType === 'web_app_request_theme') { sendTheme(); } - if (data.eventType === 'iframe_ready') { + if (eventType === 'iframe_ready') { const scrollbarColor = getComputedStyle(document.body).getPropertyValue('--color-scrollbar'); sendCustomStyle(SCROLLBAR_STYLE.replace(/%SCROLLBAR_COLOR%/g, scrollbarColor)); } - if (data.eventType === 'web_app_data_send') { + if (eventType === 'web_app_data_send') { if (!isSimpleView) return; // Allowed only in simple view ignoreEventsRef.current = true; } - if (data.eventType === 'web_app_read_text_from_clipboard') { + // Clipboard access temporarily disabled to address security concerns + if (eventType === 'web_app_read_text_from_clipboard') { sendEvent({ eventType: 'clipboard_text_received', eventData: { - req_id: data.eventData.req_id, + req_id: eventData.req_id, // eslint-disable-next-line no-null/no-null data: null, }, @@ -290,6 +155,27 @@ const useWebAppFrame = ( message: 'Clipboard access is not supported in this client yet', }); } + + if (eventType === 'web_app_open_scan_qr_popup') { + showNotification({ + message: 'Scanning QR code is not supported in this client yet', + }); + } + + if (eventType === 'web_app_open_invoice') { + setWebAppPaymentSlug({ + slug: eventData.slug, + }); + openInvoice({ + slug: eventData.slug, + }); + } + + if (eventType === 'web_app_open_link') { + const linkUrl = eventData.url; + window.open(linkUrl, '_blank', 'noreferrer'); + } + onEvent(data); } catch (err) { // Ignore other messages diff --git a/src/config.ts b/src/config.ts index b29cf4037..96d2e12db 100644 --- a/src/config.ts +++ b/src/config.ts @@ -49,7 +49,7 @@ export const CUSTOM_EMOJI_PREVIEW_CACHE_DISABLED = false; export const CUSTOM_EMOJI_PREVIEW_CACHE_NAME = 'tt-custom-emoji-preview'; export const MEDIA_CACHE_MAX_BYTES = 512 * 1024; // 512 KB export const CUSTOM_BG_CACHE_NAME = 'tt-custom-bg'; -export const LANG_CACHE_NAME = 'tt-lang-packs-v21'; +export const LANG_CACHE_NAME = 'tt-lang-packs-v22'; export const ASSET_CACHE_NAME = 'tt-assets'; export const AUTODOWNLOAD_FILESIZE_MB_LIMITS = [1, 5, 10, 50, 100, 500]; export const DATA_BROADCAST_CHANNEL_NAME = 'tt-global'; diff --git a/src/global/actions/api/bots.ts b/src/global/actions/api/bots.ts index 89e7895e2..8896e7a60 100644 --- a/src/global/actions/api/bots.ts +++ b/src/global/actions/api/bots.ts @@ -417,6 +417,40 @@ addActionHandler('startBot', async (global, actions, payload): Promise => }); }); +addActionHandler('sharePhoneWithBot', async (global, actions, payload): Promise => { + const { botId } = payload; + const bot = selectUser(global, botId); + if (!bot) { + return; + } + + let fullInfo = selectUserFullInfo(global, botId); + if (!fullInfo) { + const result = await callApi('fetchFullUser', { id: bot.id, accessHash: bot.accessHash }); + fullInfo = result?.fullInfo; + } + + if (fullInfo?.isBlocked) { + await callApi('unblockUser', { user: bot }); + } + + global = getGlobal(); + const chat = selectChat(global, botId); + const currentUser = selectUser(global, global.currentUserId!)!; + + if (!chat) return; + + await callApi('sendMessage', { + chat, + contact: { + firstName: currentUser.firstName || '', + lastName: currentUser.lastName || '', + phoneNumber: currentUser.phoneNumber || '', + userId: currentUser.id, + }, + }); +}); + addActionHandler('requestSimpleWebView', async (global, actions, payload): Promise => { const { url, botId, theme, buttonText, diff --git a/src/global/types.ts b/src/global/types.ts index 0a05f7aaa..2d1fa1915 100644 --- a/src/global/types.ts +++ b/src/global/types.ts @@ -529,6 +529,7 @@ export type TabState = { slug?: string; replyToMessageId?: number; threadId?: number; + canSendMessages?: boolean; }; botTrustRequest?: { @@ -2304,6 +2305,9 @@ export interface ActionPayloads { restartBot: { chatId: string; } & WithTabId; + sharePhoneWithBot: { + botId: string; + }; clickBotInlineButton: { messageId: number; diff --git a/src/lib/gramjs/tl/AllTLObjects.js b/src/lib/gramjs/tl/AllTLObjects.js index 6f03545eb..999732e0b 100644 --- a/src/lib/gramjs/tl/AllTLObjects.js +++ b/src/lib/gramjs/tl/AllTLObjects.js @@ -1,6 +1,6 @@ const api = require('./api'); -const LAYER = 161; +const LAYER = 162; const tlobjects = {}; for (const tl of Object.values(api)) { diff --git a/src/lib/gramjs/tl/api.d.ts b/src/lib/gramjs/tl/api.d.ts index 96fabc956..50fa2b827 100644 --- a/src/lib/gramjs/tl/api.d.ts +++ b/src/lib/gramjs/tl/api.d.ts @@ -1775,11 +1775,13 @@ namespace Api { export class MessageActionBotAllowed extends VirtualClass<{ // flags: undefined; attachMenu?: true; + fromRequest?: true; domain?: string; app?: Api.TypeBotApp; } | void> { // flags: undefined; attachMenu?: true; + fromRequest?: true; domain?: string; app?: Api.TypeBotApp; }; @@ -4092,6 +4094,9 @@ namespace Api { public?: true; megagroup?: true; requestNeeded?: true; + verified?: true; + scam?: true; + fake?: true; title: string; about?: string; photo: Api.TypePhoto; @@ -4104,6 +4109,9 @@ namespace Api { public?: true; megagroup?: true; requestNeeded?: true; + verified?: true; + scam?: true; + fake?: true; title: string; about?: string; photo: Api.TypePhoto; @@ -8311,11 +8319,13 @@ namespace Api { }; export class StoryViews extends VirtualClass<{ // flags: undefined; + hasViewers?: true; viewsCount: int; reactionsCount: int; recentViewers?: long[]; }> { // flags: undefined; + hasViewers?: true; viewsCount: int; reactionsCount: int; recentViewers?: long[]; @@ -14410,6 +14420,25 @@ namespace Api { username: string; active: Bool; }; + export class CanSendMessage extends Request, Bool> { + bot: Api.TypeInputUser; + }; + export class AllowSendMessage extends Request, Api.TypeUpdates> { + bot: Api.TypeInputUser; + }; + export class InvokeWebViewCustomMethod extends Request, Api.TypeDataJSON> { + bot: Api.TypeInputUser; + customMethod: string; + params: Api.TypeDataJSON; + }; } export namespace payments { @@ -15058,6 +15087,7 @@ namespace Api { } export namespace stories { + export class CanSendStory extends Request {}; export class SendStory extends Request credentials:SecureCredentialsEncrypted = MessageAction; messageActionSecureValuesSent#d95c6154 types:Vector = MessageAction; messageActionContactSignUp#f3f25f76 = MessageAction; @@ -472,7 +472,7 @@ receivedNotifyMessage#a384b779 id:int flags:int = ReceivedNotifyMessage; chatInviteExported#ab4a819 flags:# revoked:flags.0?true permanent:flags.5?true request_needed:flags.6?true link:string admin_id:long date:int start_date:flags.4?int expire_date:flags.1?int usage_limit:flags.2?int usage:flags.3?int requested:flags.7?int title:flags.8?string = ExportedChatInvite; chatInvitePublicJoinRequests#ed107ab7 = ExportedChatInvite; chatInviteAlready#5a686d7c chat:Chat = ChatInvite; -chatInvite#300c44c1 flags:# channel:flags.0?true broadcast:flags.1?true public:flags.2?true megagroup:flags.3?true request_needed:flags.6?true title:string about:flags.5?string photo:Photo participants_count:int participants:flags.4?Vector = ChatInvite; +chatInvite#300c44c1 flags:# channel:flags.0?true broadcast:flags.1?true public:flags.2?true megagroup:flags.3?true request_needed:flags.6?true verified:flags.7?true scam:flags.8?true fake:flags.9?true title:string about:flags.5?string photo:Photo participants_count:int participants:flags.4?Vector = ChatInvite; chatInvitePeek#61695cb0 chat:Chat expires:int = ChatInvite; inputStickerSetEmpty#ffb62b95 = InputStickerSet; inputStickerSetID#9de7a269 id:long access_hash:long = InputStickerSet; @@ -1126,7 +1126,7 @@ messagePeerVote#b6cc2d5c peer:Peer option:bytes date:int = MessagePeerVote; messagePeerVoteInputOption#74cda504 peer:Peer date:int = MessagePeerVote; messagePeerVoteMultiple#4628f6e6 peer:Peer options:Vector date:int = MessagePeerVote; sponsoredWebPage#3db8ec63 flags:# url:string site_name:string photo:flags.0?Photo = SponsoredWebPage; -storyViews#c64c0b97 flags:# views_count:int reactions_count:int recent_viewers:flags.0?Vector = StoryViews; +storyViews#c64c0b97 flags:# has_viewers:flags.1?true views_count:int reactions_count:int recent_viewers:flags.0?Vector = StoryViews; storyItemDeleted#51e6ee4f id:int = StoryItem; storyItemSkipped#ffadc913 flags:# close_friends:flags.8?true id:int date:int expire_date:int = StoryItem; storyItem#44c457ce flags:# pinned:flags.5?true public:flags.7?true close_friends:flags.8?true min:flags.9?true noforwards:flags.10?true edited:flags.11?true contacts:flags.12?true selected_contacts:flags.13?true id:int date:int expire_date:int caption:flags.0?string entities:flags.1?Vector media:MessageMedia media_areas:flags.14?Vector privacy:flags.2?Vector views:flags.3?StoryViews sent_reaction:flags.15?Reaction = StoryItem; @@ -1403,6 +1403,9 @@ channels.editForumTopic#f4dfa185 flags:# channel:InputChannel topic_id:int title channels.updatePinnedForumTopic#6c2d9026 channel:InputChannel topic_id:int pinned:Bool = Updates; channels.deleteTopicHistory#34435f2d channel:InputChannel top_msg_id:int = messages.AffectedHistory; channels.toggleParticipantsHidden#6a6e7854 channel:InputChannel enabled:Bool = Updates; +bots.canSendMessage#1359f4e6 bot:InputUser = Bool; +bots.allowSendMessage#f132e3ef bot:InputUser = Updates; +bots.invokeWebViewCustomMethod#87fc5e7 bot:InputUser custom_method:string params:DataJSON = DataJSON; payments.getPaymentForm#37148dbb flags:# invoice:InputInvoice theme_params:flags.0?DataJSON = payments.PaymentForm; payments.getPaymentReceipt#2478d1cc peer:InputPeer msg_id:int = payments.PaymentReceipt; payments.validateRequestedInfo#b6c8f12b flags:# save:flags.0?true invoice:InputInvoice info:PaymentRequestedInfo = payments.ValidatedRequestedInfo; diff --git a/src/lib/gramjs/tl/static/api.json b/src/lib/gramjs/tl/static/api.json index 76ed2d5d7..1c33b7b70 100644 --- a/src/lib/gramjs/tl/static/api.json +++ b/src/lib/gramjs/tl/static/api.json @@ -216,6 +216,9 @@ "channels.toggleUsername", "channels.viewSponsoredMessage", "channels.getSponsoredMessages", + "bots.canSendMessage", + "bots.allowSendMessage", + "bots.invokeWebViewCustomMethod", "payments.getPaymentForm", "payments.getPaymentReceipt", "payments.validateRequestedInfo", diff --git a/src/lib/gramjs/tl/static/api.tl b/src/lib/gramjs/tl/static/api.tl index bced7ee92..3eb6f4bc2 100644 --- a/src/lib/gramjs/tl/static/api.tl +++ b/src/lib/gramjs/tl/static/api.tl @@ -150,7 +150,7 @@ messageActionPaymentSent#96163f56 flags:# recurring_init:flags.2?true recurring_ messageActionPhoneCall#80e11a7f flags:# video:flags.2?true call_id:long reason:flags.0?PhoneCallDiscardReason duration:flags.1?int = MessageAction; messageActionScreenshotTaken#4792929b = MessageAction; messageActionCustomAction#fae69f56 message:string = MessageAction; -messageActionBotAllowed#c516d679 flags:# attach_menu:flags.1?true domain:flags.0?string app:flags.2?BotApp = MessageAction; +messageActionBotAllowed#c516d679 flags:# attach_menu:flags.1?true from_request:flags.3?true domain:flags.0?string app:flags.2?BotApp = MessageAction; messageActionSecureValuesSentMe#1b287353 values:Vector credentials:SecureCredentialsEncrypted = MessageAction; messageActionSecureValuesSent#d95c6154 types:Vector = MessageAction; messageActionContactSignUp#f3f25f76 = MessageAction; @@ -570,7 +570,7 @@ chatInviteExported#ab4a819 flags:# revoked:flags.0?true permanent:flags.5?true r chatInvitePublicJoinRequests#ed107ab7 = ExportedChatInvite; chatInviteAlready#5a686d7c chat:Chat = ChatInvite; -chatInvite#300c44c1 flags:# channel:flags.0?true broadcast:flags.1?true public:flags.2?true megagroup:flags.3?true request_needed:flags.6?true title:string about:flags.5?string photo:Photo participants_count:int participants:flags.4?Vector = ChatInvite; +chatInvite#300c44c1 flags:# channel:flags.0?true broadcast:flags.1?true public:flags.2?true megagroup:flags.3?true request_needed:flags.6?true verified:flags.7?true scam:flags.8?true fake:flags.9?true title:string about:flags.5?string photo:Photo participants_count:int participants:flags.4?Vector = ChatInvite; chatInvitePeek#61695cb0 chat:Chat expires:int = ChatInvite; inputStickerSetEmpty#ffb62b95 = InputStickerSet; @@ -1528,7 +1528,7 @@ messagePeerVoteMultiple#4628f6e6 peer:Peer options:Vector date:int = Mess sponsoredWebPage#3db8ec63 flags:# url:string site_name:string photo:flags.0?Photo = SponsoredWebPage; -storyViews#c64c0b97 flags:# views_count:int reactions_count:int recent_viewers:flags.0?Vector = StoryViews; +storyViews#c64c0b97 flags:# has_viewers:flags.1?true views_count:int reactions_count:int recent_viewers:flags.0?Vector = StoryViews; storyItemDeleted#51e6ee4f id:int = StoryItem; storyItemSkipped#ffadc913 flags:# close_friends:flags.8?true id:int date:int expire_date:int = StoryItem; @@ -2017,6 +2017,9 @@ bots.setBotInfo#10cf3123 flags:# bot:flags.2?InputUser lang_code:string name:fla bots.getBotInfo#dcd914fd flags:# bot:flags.0?InputUser lang_code:string = bots.BotInfo; bots.reorderUsernames#9709b1c2 bot:InputUser order:Vector = Bool; bots.toggleUsername#53ca973 bot:InputUser username:string active:Bool = Bool; +bots.canSendMessage#1359f4e6 bot:InputUser = Bool; +bots.allowSendMessage#f132e3ef bot:InputUser = Updates; +bots.invokeWebViewCustomMethod#87fc5e7 bot:InputUser custom_method:string params:DataJSON = DataJSON; payments.getPaymentForm#37148dbb flags:# invoice:InputInvoice theme_params:flags.0?DataJSON = payments.PaymentForm; payments.getPaymentReceipt#2478d1cc peer:InputPeer msg_id:int = payments.PaymentReceipt; @@ -2099,6 +2102,7 @@ chatlists.hideChatlistUpdates#66e486fb chatlist:InputChatlist = Bool; chatlists.getLeaveChatlistSuggestions#fdbcd714 chatlist:InputChatlist = Vector; chatlists.leaveChatlist#74fae13a chatlist:InputChatlist peers:Vector = Updates; +stories.canSendStory#b100d45d = Bool; stories.sendStory#d455fcec flags:# pinned:flags.2?true noforwards:flags.4?true media:InputMedia media_areas:flags.5?Vector caption:flags.0?string entities:flags.1?Vector privacy_rules:Vector random_id:long period:flags.3?int = Updates; stories.editStory#a9b91ae4 flags:# id:int media:flags.0?InputMedia media_areas:flags.3?Vector caption:flags.1?string entities:flags.1?Vector privacy_rules:flags.2?Vector = Updates; stories.deleteStories#b5d501d7 id:Vector = Vector; @@ -2117,4 +2121,4 @@ stories.getStoriesViews#9a75d6a6 id:Vector = stories.StoryViews; stories.exportStoryLink#16e443ce user_id:InputUser id:int = ExportedStoryLink; stories.report#c95be06a user_id:InputUser id:Vector reason:ReportReason message:string = Bool; stories.activateStealthMode#57bbd166 flags:# past:flags.0?true future:flags.1?true = Updates; -stories.sendReaction#49aaa9b3 flags:# add_to_recent:flags.0?true user_id:InputUser story_id:int reaction:Reaction = Updates; +stories.sendReaction#49aaa9b3 flags:# add_to_recent:flags.0?true user_id:InputUser story_id:int reaction:Reaction = Updates; \ No newline at end of file diff --git a/src/types/webapp.ts b/src/types/webapp.ts new file mode 100644 index 000000000..84b897baa --- /dev/null +++ b/src/types/webapp.ts @@ -0,0 +1,174 @@ +export type PopupOptions = { + title: string; + message: string; + buttons: { + id: string; + type: 'default' | 'ok' | 'close' | 'cancel' | 'destructive'; + text: string; + }[]; +}; + +export type WebAppInboundEvent = { + eventType: 'web_app_data_send'; + eventData: { + data: string; + }; +} | { + eventType: 'web_app_setup_main_button'; + eventData: { + is_visible: boolean; + is_active: boolean; + text: string; + color: string; + text_color: string; + is_progress_visible: boolean; + }; +} | { + eventType: 'web_app_setup_back_button'; + eventData: { + is_visible: boolean; + }; +} | { + eventType: 'web_app_open_link'; + eventData: { + url: string; + try_instant_view?: boolean; + }; +} | { + eventType: 'web_app_open_tg_link'; + eventData: { + path_full: string; + }; +} | { + eventType: 'web_app_open_invoice'; + eventData: { + slug: string; + }; +} | { + eventType: 'web_app_trigger_haptic_feedback'; + eventData: { + type: 'impact' | 'notification' | 'selection_change'; + impact_style?: 'light' | 'medium' | 'heavy'; + notification_type?: 'error' | 'success' | 'warning'; + }; +} | { + eventType: 'web_app_set_background_color'; + eventData: { + color: string; + }; +} | { + eventType: 'web_app_set_header_color'; + eventData: { + color_key: 'bg_color' | 'secondary_bg_color'; + }; +} | { + eventType: 'web_app_open_popup'; + eventData: PopupOptions; +} | { + eventType: 'web_app_setup_closing_behavior'; + eventData: { + need_confirmation: boolean; + }; +} | { + eventType: 'web_app_open_scan_qr_popup'; + eventData: { + text?: string; + }; +} | { + eventType: 'web_app_read_text_from_clipboard'; + eventData: { + req_id: string; + }; +} | { + eventType: 'web_app_switch_inline_query'; + eventData: { + query: string; + chat_types: ('users' | 'bots' | 'groups' | 'channels')[]; + }; +} | { + eventType: 'web_app_invoke_custom_method'; + eventData: { + req_id: string; + method: string; + params: object; + }; +} | { + eventType: 'web_app_request_viewport' | 'web_app_request_theme' | 'web_app_ready' | 'web_app_expand' + | 'web_app_request_phone' | 'web_app_close' | 'iframe_ready' | 'web_app_close_scan_qr_popup' + | 'web_app_request_write_access' | 'web_app_request_phone'; + eventData: null; +}; + +export type WebAppOutboundEvent = { + eventType: 'viewport_changed'; + eventData: { + height: number; + width?: number; + is_expanded?: boolean; + is_state_stable?: boolean; + }; +} | { + eventType: 'theme_changed'; + eventData: { + theme_params: { + bg_color: string; + text_color: string; + hint_color: string; + link_color: string; + button_color: string; + button_text_color: string; + secondary_bg_color: string; + }; + }; +} | { + eventType: 'set_custom_style'; + eventData: string; +} | { + eventType: 'invoice_closed'; + eventData: { + slug: string; + status: 'paid' | 'cancelled' | 'pending' | 'failed'; + }; +} | { + eventType: 'phone_requested'; + eventData: { + phone_number: string; + }; +} | { + eventType: 'popup_closed'; + eventData: { + button_id?: string; + }; +} | { + eventType: 'qr_text_received'; + eventData: { + data: string; + }; +} | { + eventType: 'clipboard_text_received'; + eventData: { + req_id: string; + data: string | null; + }; +} | { + eventType: 'write_access_requested'; + eventData: { + status: 'allowed' | 'cancelled'; + }; +} | { + eventType: 'phone_requested'; + eventData: { + status: 'sent' | 'cancelled'; + }; +} | { + eventType: 'custom_method_invoked'; + eventData: { + req_id: string; + } & ({ + result: object; + } | { + error: string; + }); +} | { + eventType: 'main_button_pressed' | 'back_button_pressed' | 'settings_button_pressed' | 'scan_qr_popup_closed'; +};