diff --git a/src/api/gramjs/apiBuilders/bots.ts b/src/api/gramjs/apiBuilders/bots.ts index b43e48a57..128ea148a 100644 --- a/src/api/gramjs/apiBuilders/bots.ts +++ b/src/api/gramjs/apiBuilders/bots.ts @@ -3,11 +3,13 @@ import type { ApiAttachBot, ApiAttachBotIcon, ApiAttachMenuPeerType, + ApiBotApp, ApiBotCommand, ApiBotInfo, ApiBotInlineMediaResult, ApiBotInlineResult, ApiBotInlineSwitchPm, + ApiBotInlineSwitchWebview, ApiBotMenuButton, ApiInlineResultType, } from '../../types'; @@ -62,6 +64,10 @@ export function buildBotSwitchPm(switchPm?: GramJs.InlineBotSwitchPM) { return switchPm ? pick(switchPm, ['text', 'startParam']) as ApiBotInlineSwitchPm : undefined; } +export function buildBotSwitchWebview(switchWebview?: GramJs.InlineBotWebView) { + return switchWebview ? pick(switchWebview, ['text', 'url']) as ApiBotInlineSwitchWebview : undefined; +} + export function buildApiAttachBot(bot: GramJs.AttachMenuBot): ApiAttachBot { return { id: bot.botId.toString(), @@ -138,3 +144,26 @@ export function buildApiBotMenuButton(menuButton?: GramJs.TypeBotMenuButton): Ap type: 'commands', }; } + +export function buildApiBotApp(botApp: GramJs.messages.BotApp): ApiBotApp | undefined { + const { app, inactive, requestWriteAccess } = botApp; + if (app instanceof GramJs.BotAppNotModified) return undefined; + const { + id, accessHash, title, description, shortName, photo, document, + } = app; + + const apiPhoto = photo instanceof GramJs.Photo ? buildApiPhoto(photo) : undefined; + const apiDocument = document instanceof GramJs.Document ? buildApiDocument(document) : undefined; + + return { + id: id.toString(), + accessHash: accessHash.toString(), + title, + description, + shortName, + photo: apiPhoto, + document: apiDocument, + isInactive: inactive, + shouldRequestWriteAccess: requestWriteAccess, + }; +} diff --git a/src/api/gramjs/apiBuilders/messages.ts b/src/api/gramjs/apiBuilders/messages.ts index 91ea734d0..eee1ce84f 100644 --- a/src/api/gramjs/apiBuilders/messages.ts +++ b/src/api/gramjs/apiBuilders/messages.ts @@ -1145,6 +1145,15 @@ function buildReplyButtons(message: UniversalMessage, shouldSkipBuyButton?: bool }]], }; } + if (media.webpage.type === 'telegram_botapp') { + return { + inlineButtons: [[{ + type: 'url', + text: 'Open App', + url: media.webpage.url, + }]], + }; + } } return undefined; diff --git a/src/api/gramjs/gramjsBuilders/index.ts b/src/api/gramjs/gramjsBuilders/index.ts index c7044d8c4..44e28230d 100644 --- a/src/api/gramjs/gramjsBuilders/index.ts +++ b/src/api/gramjs/gramjsBuilders/index.ts @@ -23,6 +23,7 @@ import type { ApiChatReactions, ApiReaction, ApiFormattedText, + ApiBotApp, } from '../../types'; import { ApiMessageEntityTypes, @@ -606,3 +607,10 @@ export function buildInputTextWithEntities(formatted: ApiFormattedText) { entities: formatted.entities?.map(buildMtpMessageEntity) || [], }); } + +export function buildInputBotApp(app: ApiBotApp) { + return new GramJs.InputBotAppID({ + id: BigInt(app.id), + accessHash: BigInt(app.accessHash), + }); +} diff --git a/src/api/gramjs/methods/bots.ts b/src/api/gramjs/methods/bots.ts index 7b43af4bf..4a8116e54 100644 --- a/src/api/gramjs/methods/bots.ts +++ b/src/api/gramjs/methods/bots.ts @@ -2,15 +2,24 @@ import BigInt from 'big-integer'; import { Api as GramJs } from '../../../lib/gramjs'; import type { + ApiBotApp, ApiChat, ApiThemeParameters, ApiUser, OnApiUpdate, } from '../../types'; import localDb from '../localDb'; +import { WEB_APP_PLATFORM } from '../../../config'; import { invokeRequest } from './client'; -import { buildInputPeer, buildInputThemeParams, generateRandomBigInt } from '../gramjsBuilders'; +import { + buildInputBotApp, buildInputEntity, buildInputPeer, buildInputThemeParams, generateRandomBigInt, +} from '../gramjsBuilders'; import { buildApiUser } from '../apiBuilders/users'; import { - buildApiAttachBot, buildApiBotInlineMediaResult, buildApiBotInlineResult, buildBotSwitchPm, + buildApiAttachBot, + buildApiBotApp, + buildApiBotInlineMediaResult, + buildApiBotInlineResult, + buildBotSwitchPm, + buildBotSwitchWebview, } from '../apiBuilders/bots'; import { buildApiChatFromPreview } from '../apiBuilders/chats'; import { addEntitiesWithPhotosToLocalDb, addUserToLocalDb, deserializeBytes } from '../helpers'; @@ -102,6 +111,7 @@ export async function fetchInlineBotResults({ help: bot.botPlaceholder, nextOffset: getInlineBotResultsNextOffset(bot.usernames![0].username, result.nextOffset), switchPm: buildBotSwitchPm(result.switchPm), + switchWebview: buildBotSwitchWebview(result.switchWebview), users: result.users.map(buildApiUser).filter(Boolean), results: processInlineBotResult(String(result.queryId), result.results), cacheTime: result.cacheTime, @@ -184,7 +194,7 @@ export async function requestWebView({ startParam, themeParams: theme ? buildInputThemeParams(theme) : undefined, fromBotMenu: isFromBotMenu || undefined, - platform: 'weba', + platform: WEB_APP_PLATFORM, ...(threadId && { topMsgId: threadId }), ...(sendAs && { sendAs: buildInputPeer(sendAs.id, sendAs.accessHash) }), })); @@ -210,7 +220,53 @@ export async function requestSimpleWebView({ url, bot: buildInputPeer(bot.id, bot.accessHash), themeParams: theme ? buildInputThemeParams(theme) : undefined, - platform: 'weba', + platform: WEB_APP_PLATFORM, + })); + + return result?.url; +} + +export async function fetchBotApp({ + bot, + appName, +}: { + bot: ApiUser; + appName: string; +}) { + const result = await invokeRequest(new GramJs.messages.GetBotApp({ + app: new GramJs.InputBotAppShortName({ + botId: buildInputEntity(bot.id, bot.accessHash) as GramJs.InputUser, + shortName: appName, + }), + })); + + if (!result || result instanceof GramJs.BotAppNotModified) { + return undefined; + } + + return buildApiBotApp(result); +} + +export async function requestAppWebView({ + peer, + app, + startParam, + theme, + isWriteAllowed, +}: { + peer: ApiChat | ApiUser; + app: ApiBotApp; + startParam?: string; + theme?: ApiThemeParameters; + isWriteAllowed?: boolean; +}) { + const result = await invokeRequest(new GramJs.messages.RequestAppWebView({ + peer: buildInputPeer(peer.id, peer.accessHash), + app: buildInputBotApp(app), + startParam, + themeParams: theme ? buildInputThemeParams(theme) : undefined, + platform: WEB_APP_PLATFORM, + writeAllowed: isWriteAllowed || undefined, })); return result?.url; diff --git a/src/api/gramjs/methods/index.ts b/src/api/gramjs/methods/index.ts index 7b127454b..483bf5be6 100644 --- a/src/api/gramjs/methods/index.ts +++ b/src/api/gramjs/methods/index.ts @@ -71,8 +71,8 @@ export { export { answerCallbackButton, fetchTopInlineBots, fetchInlineBot, fetchInlineBotResults, sendInlineBotResult, startBot, - requestWebView, requestSimpleWebView, sendWebViewData, prolongWebView, loadAttachBots, toggleAttachBot, - requestBotUrlAuth, requestLinkUrlAuth, acceptBotUrlAuth, acceptLinkUrlAuth, loadAttachBot, + requestWebView, requestSimpleWebView, sendWebViewData, prolongWebView, loadAttachBots, toggleAttachBot, fetchBotApp, + requestBotUrlAuth, requestLinkUrlAuth, acceptBotUrlAuth, acceptLinkUrlAuth, loadAttachBot, requestAppWebView, } from './bots'; export { diff --git a/src/api/types/bots.ts b/src/api/types/bots.ts index 364188e2d..226c69a3b 100644 --- a/src/api/types/bots.ts +++ b/src/api/types/bots.ts @@ -43,6 +43,11 @@ export interface ApiBotInlineSwitchPm { startParam: string; } +export interface ApiBotInlineSwitchWebview { + text: string; + url: string; +} + export interface ApiBotCommand { botId: string; command: string; diff --git a/src/api/types/messages.ts b/src/api/types/messages.ts index 07778a766..0ce9b3d8b 100644 --- a/src/api/types/messages.ts +++ b/src/api/types/messages.ts @@ -637,6 +637,18 @@ export type ApiThemeParameters = { secondary_bg_color: string; }; +export type ApiBotApp = { + id: string; + accessHash: string; + title: string; + shortName: string; + description: string; + photo?: ApiPhoto; + document?: ApiDocument; + isInactive?: boolean; + shouldRequestWriteAccess?: boolean; +}; + export const MAIN_THREAD_ID = -1; // `Symbol` can not be transferred from worker diff --git a/src/components/main/BotTrustModal.tsx b/src/components/main/BotTrustModal.tsx index 951a8dfe4..e7909d5be 100644 --- a/src/components/main/BotTrustModal.tsx +++ b/src/components/main/BotTrustModal.tsx @@ -1,7 +1,9 @@ -import type { FC } from '../../lib/teact/teact'; -import React, { memo, useCallback } from '../../lib/teact/teact'; +import React, { + memo, useCallback, useMemo, useState, +} from '../../lib/teact/teact'; import { getActions } from '../../global'; +import type { FC } from '../../lib/teact/teact'; import type { ApiUser } from '../../api/types'; import { getUserFullName } from '../../global/helpers'; @@ -10,15 +12,20 @@ import renderText from '../common/helpers/renderText'; import useLang from '../../hooks/useLang'; import usePrevious from '../../hooks/usePrevious'; +import Checkbox from '../ui/Checkbox'; import ConfirmDialog from '../ui/ConfirmDialog'; export type OwnProps = { bot?: ApiUser; - type?: 'game' | 'webApp'; + type?: 'game' | 'webApp' | 'botApp'; + shouldRequestWriteAccess?: boolean; }; -const BotTrustModal: FC = ({ bot, type }) => { +const BotTrustModal: FC = ({ bot, type, shouldRequestWriteAccess }) => { const { cancelBotTrustRequest, markBotTrusted } = getActions(); + + const [isWriteAllowed, setIsWriteAllowed] = useState(shouldRequestWriteAccess || false); + const lang = useLang(); // Keep props a little bit longer, to show correct text on closing animation const previousBot = usePrevious(bot, false); @@ -27,21 +34,46 @@ const BotTrustModal: FC = ({ bot, type }) => { const currentType = type || previousType; const handleBotTrustAccept = useCallback(() => { - markBotTrusted({ botId: bot!.id }); - }, [markBotTrusted, bot]); + markBotTrusted({ botId: bot!.id, isWriteAllowed }); + }, [markBotTrusted, isWriteAllowed, bot]); + + const handleBotTrustDecline = useCallback(() => { + cancelBotTrustRequest(); + }, []); const title = currentType === 'game' ? lang('AppName') : lang('BotOpenPageTitle'); - const text = currentType === 'game' ? lang('BotPermissionGameAlert', getUserFullName(currentBot)) - : lang('BotOpenPageMessage', getUserFullName(currentBot)); + const text = useMemo(() => { + switch (currentType) { + case 'game': + return lang('BotPermissionGameAlert', getUserFullName(currentBot)); + case 'webApp': + return lang('BotOpenPageMessage', getUserFullName(currentBot)); + case 'botApp': + default: + return lang('BotWebViewStartPermission'); + } + }, [currentBot, currentType, lang]); return ( + confirmHandler={handleBotTrustAccept} + > + {text} + {shouldRequestWriteAccess && ( + + )} + ); }; diff --git a/src/components/main/DraftRecipientPicker.tsx b/src/components/main/DraftRecipientPicker.tsx index af8993bb4..c167c4e10 100644 --- a/src/components/main/DraftRecipientPicker.tsx +++ b/src/components/main/DraftRecipientPicker.tsx @@ -54,6 +54,7 @@ const DraftRecipientPicker: FC = ({ = ({ - + diff --git a/src/components/main/WebAppModal.tsx b/src/components/main/WebAppModal.tsx index d614b3dcd..69ed49c84 100644 --- a/src/components/main/WebAppModal.tsx +++ b/src/components/main/WebAppModal.tsx @@ -15,6 +15,7 @@ import { } 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'; @@ -94,19 +95,26 @@ const WebAppModal: FC = ({ openInvoice, setWebAppPaymentSlug, showNotification, + switchBotInline, } = getActions(); const [mainButton, setMainButton] = useState(); const [isBackButtonVisible, setIsBackButtonVisible] = useState(false); - const [backgroundColor, setBackgroundColor] = useState(extractCurrentThemeParams().bg_color); - const [headerColor, setHeaderColor] = useState(extractCurrentThemeParams().bg_color); + const [backgroundColor, setBackgroundColor] = useState(); + const [headerColor, setHeaderColor] = useState(); const [confirmClose, setConfirmClose] = useState(false); - const [isCloseModalOpen, openCloseModal, closeCloseModal] = useFlag(false); + const [isCloseModalOpen, openCloseModal, closeModal] = useFlag(false); const [isLoaded, markLoaded, markUnloaded] = useFlag(false); const [popupParams, setPopupParams] = useState(); const { isMobile } = useAppLayout(); const prevPopupParams = usePrevious(popupParams); const renderingPopupParams = popupParams || prevPopupParams; + useEffect(() => { + const themeParams = extractCurrentThemeParams(); + setBackgroundColor(themeParams.bg_color); + setHeaderColor(themeParams.bg_color); + }, []); + // eslint-disable-next-line no-null/no-null const frameRef = useRef(null); @@ -115,7 +123,7 @@ const WebAppModal: FC = ({ url, buttonText, queryId, replyToMessageId, threadId, } = webApp || {}; const isOpen = Boolean(url); - const isSimple = !queryId; + const isSimple = Boolean(buttonText); const handleEvent = useCallback((event: WebAppInboundEvent) => { const { eventType, eventData } = event; @@ -198,6 +206,20 @@ const WebAppModal: FC = ({ message: 'Scan QR code is not supported in this client yet', }); } + + if (eventType === 'web_app_switch_inline_query') { + const filter = eventData.chat_types?.map(convertToApiChatType).filter(Boolean); + const isSamePeer = !filter?.length; + + switchBotInline({ + botId: bot!.id, + query: eventData.query, + filter, + isSamePeer, + }); + + closeWebApp(); + } }, [ bot, buttonText, closeWebApp, openInvoice, openTelegramLink, sendWebViewData, setWebAppPaymentSlug, isPaymentModalOpen, showNotification, @@ -314,11 +336,11 @@ const WebAppModal: FC = ({ useEffect(() => { if (!isOpen) { setConfirmClose(false); - closeCloseModal(); + closeModal(); setPopupParams(undefined); markUnloaded(); } - }, [closeCloseModal, isOpen, markUnloaded]); + }, [closeModal, isOpen, markUnloaded]); const MoreMenuButton: FC<{ onTrigger: () => void; isOpen?: boolean }> = useMemo(() => { return ({ onTrigger, isOpen: isMenuOpen }) => ( @@ -460,7 +482,7 @@ const WebAppModal: FC = ({ {confirmClose && ( void, onLoad?: () => void, ) => { + const { + showNotification, + } = getActions(); + const ignoreEventsRef = useRef(false); const lastFrameSizeRef = useRef<{ width: number; height: number; isResizing?: boolean }>(); const windowSize = useWindowSize(); @@ -266,23 +277,24 @@ const useWebAppFrame = ( } if (data.eventType === 'web_app_read_text_from_clipboard') { - const { req_id: requestId } = data.eventData; - // eslint-disable-next-line no-null/no-null -- Required by spec - window.navigator.clipboard.readText().catch(() => null).then((text) => { - sendEvent({ - eventType: 'clipboard_text_received', - eventData: { - req_id: requestId, - data: text, - }, - }); + sendEvent({ + eventType: 'clipboard_text_received', + eventData: { + req_id: data.eventData.req_id, + // eslint-disable-next-line no-null/no-null + data: null, + }, + }); + + showNotification({ + message: 'Clipboard access is not supported in this client yet', }); } onEvent(data); } catch (err) { // Ignore other messages } - }, [isSimpleView, onEvent, sendCustomStyle, sendEvent, sendTheme, sendViewport, onLoad, windowSize.isResizing]); + }, [isSimpleView, sendEvent, onEvent, sendCustomStyle, sendTheme, sendViewport, onLoad, windowSize.isResizing]); useEffect(() => { const { width, height, isResizing } = windowSize; diff --git a/src/components/middle/composer/Composer.tsx b/src/components/middle/composer/Composer.tsx index c0d4aafdf..67c2a9312 100644 --- a/src/components/middle/composer/Composer.tsx +++ b/src/components/middle/composer/Composer.tsx @@ -496,6 +496,7 @@ const Composer: FC = ({ botId: inlineBotId, isGallery: isInlineBotTooltipGallery, switchPm: inlineBotSwitchPm, + switchWebview: inlineBotSwitchWebview, results: inlineBotResults, closeTooltip: closeInlineBotTooltip, help: inlineBotHelp, @@ -1322,6 +1323,7 @@ const Composer: FC = ({ isGallery={isInlineBotTooltipGallery} inlineBotResults={inlineBotResults} switchPm={inlineBotSwitchPm} + switchWebview={inlineBotSwitchWebview} loadMore={loadMoreForInlineBot} isSavedMessages={isChatWithSelf} canSendGifs={canSendGifs} diff --git a/src/components/middle/composer/InlineBotTooltip.tsx b/src/components/middle/composer/InlineBotTooltip.tsx index fdb0b7a5a..3eadf87a8 100644 --- a/src/components/middle/composer/InlineBotTooltip.tsx +++ b/src/components/middle/composer/InlineBotTooltip.tsx @@ -1,17 +1,21 @@ -import type { FC } from '../../../lib/teact/teact'; import React, { memo, useCallback, useEffect, useRef, } from '../../../lib/teact/teact'; import { getActions } from '../../../global'; -import type { ApiBotInlineMediaResult, ApiBotInlineResult, ApiBotInlineSwitchPm } from '../../../api/types'; +import type { FC } from '../../../lib/teact/teact'; +import type { + ApiBotInlineMediaResult, ApiBotInlineResult, ApiBotInlineSwitchPm, ApiBotInlineSwitchWebview, +} from '../../../api/types'; import { LoadMoreDirection } from '../../../types'; import { IS_TOUCH_ENV } from '../../../util/windowEnvironment'; import setTooltipItemVisible from '../../../util/setTooltipItemVisible'; import buildClassName from '../../../util/buildClassName'; -import useShowTransition from '../../../hooks/useShowTransition'; +import { extractCurrentThemeParams } from '../../../util/themeStyle'; import { throttle } from '../../../util/schedulers'; + +import useShowTransition from '../../../hooks/useShowTransition'; import { useIntersectionObserver } from '../../../hooks/useIntersectionObserver'; import usePrevious from '../../../hooks/usePrevious'; import useCurrentOrPrev from '../../../hooks/useCurrentOrPrev'; @@ -35,6 +39,7 @@ export type OwnProps = { isGallery?: boolean; inlineBotResults?: (ApiBotInlineResult | ApiBotInlineMediaResult)[]; switchPm?: ApiBotInlineSwitchPm; + switchWebview?: ApiBotInlineSwitchWebview; isSavedMessages?: boolean; canSendGifs?: boolean; onSelectResult: ( @@ -51,6 +56,7 @@ const InlineBotTooltip: FC = ({ isGallery, inlineBotResults, switchPm, + switchWebview, isSavedMessages, canSendGifs, loadMore, @@ -61,6 +67,7 @@ const InlineBotTooltip: FC = ({ const { openChat, startBot, + requestSimpleWebView, } = getActions(); // eslint-disable-next-line no-null/no-null @@ -99,6 +106,17 @@ const InlineBotTooltip: FC = ({ startBot({ botId: botId!, param: switchPm!.startParam }); }, [botId, openChat, startBot, switchPm]); + const handleOpenWebview = useCallback(() => { + const theme = extractCurrentThemeParams(); + + requestSimpleWebView({ + botId: botId!, + url: switchWebview!.url, + buttonText: switchWebview!.text, + theme, + }); + }, [botId, switchWebview]); + const prevInlineBotResults = usePrevious( inlineBotResults?.length ? inlineBotResults @@ -126,6 +144,14 @@ const InlineBotTooltip: FC = ({ ); } + function renderSwitchWebview() { + return ( + + {switchWebview!.text} + + ); + } + function renderContent() { return renderedInlineBotResults!.map((inlineBotResult, index) => { switch (inlineBotResult.type) { @@ -203,6 +229,7 @@ const InlineBotTooltip: FC = ({ sensitiveArea={160} > {switchPm && renderSwitchPm()} + {switchWebview && renderSwitchWebview()} {Boolean(renderedInlineBotResults?.length) && renderContent()} ); diff --git a/src/components/middle/composer/hooks/useInlineBotTooltip.ts b/src/components/middle/composer/hooks/useInlineBotTooltip.ts index 72c394dfc..e62f62992 100644 --- a/src/components/middle/composer/hooks/useInlineBotTooltip.ts +++ b/src/components/middle/composer/hooks/useInlineBotTooltip.ts @@ -60,6 +60,7 @@ export default function useInlineBotTooltip( const { id: botId, switchPm, + switchWebview, offset, results, isGallery, @@ -87,6 +88,7 @@ export default function useInlineBotTooltip( botId, isGallery, switchPm, + switchWebview, results, closeTooltip: markManuallyClosed, help: canShowHelp && help ? `@${username} ${help}` : undefined, diff --git a/src/config.ts b/src/config.ts index fa7d330aa..071b6d274 100644 --- a/src/config.ts +++ b/src/config.ts @@ -45,7 +45,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-v17'; +export const LANG_CACHE_NAME = 'tt-lang-packs-v18'; 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'; @@ -233,7 +233,7 @@ export const SUPPORTED_TRANSLATION_LANGUAGES = [ // Official 'en', 'ar', 'be', 'ca', 'zh', 'nl', 'fr', 'de', 'id', 'it', 'ja', 'ko', 'pl', 'pt', 'ru', 'es', 'uk', - // Unnofficial + // Unofficial 'af', 'sq', 'am', 'hy', 'az', 'eu', 'bn', 'bs', 'bg', 'ceb', 'zh-CN', 'zh-TW', 'co', 'hr', 'cs', 'da', 'eo', 'et', 'fi', 'fy', 'gl', 'ka', 'el', 'gu', 'ht', 'ha', @@ -257,6 +257,7 @@ export const TME_LINK_PREFIX = 'https://t.me/'; export const USERNAME_PURCHASE_ERROR = 'USERNAME_PURCHASE_AVAILABLE'; export const PURCHASE_USERNAME = 'auction'; export const TME_WEB_DOMAINS = new Set(['t.me', 'web.t.me', 'a.t.me', 'k.t.me', 'z.t.me']); +export const WEB_APP_PLATFORM = 'weba'; // eslint-disable-next-line max-len export const COUNTRIES_WITH_12H_TIME_FORMAT = new Set(['AU', 'BD', 'CA', 'CO', 'EG', 'HN', 'IE', 'IN', 'JO', 'MX', 'MY', 'NI', 'NZ', 'PH', 'PK', 'SA', 'SV', 'US']); diff --git a/src/global/actions/api/bots.ts b/src/global/actions/api/bots.ts index 92311923d..0e0a142c9 100644 --- a/src/global/actions/api/bots.ts +++ b/src/global/actions/api/bots.ts @@ -24,6 +24,7 @@ import { extractCurrentThemeParams } from '../../../util/themeStyle'; import PopupManager from '../../../util/PopupManager'; import { updateTabState } from '../../reducers/tabs'; import { getCurrentTabId } from '../../../util/establishMultitabRole'; +import { translate } from '../../../util/langProvider'; const GAMEE_URL = 'https://prizes.gamee.com/'; const TOP_PEERS_REQUEST_COOLDOWN = 60; // 1 min @@ -290,18 +291,29 @@ addActionHandler('queryInlineBot', async (global, actions, payload): Promise { const { - query, isSamePeer, messageId, tabId = getCurrentTabId(), + query, isSamePeer, messageId, filter, tabId = getCurrentTabId(), + } = payload; + let { + botId, } = payload; const chat = selectCurrentChat(global, tabId); if (!chat) { return undefined; } - const message = selectChatMessage(global, chat.id, messageId); - if (!message) { + + if (!botId && messageId) { + const message = selectChatMessage(global, chat.id, messageId); + if (!message) { + return undefined; + } + botId = message.viaBotId || message.senderId; + } + + if (!botId) { return undefined; } - const botSender = selectUser(global, message.viaBotId || message.senderId!); + const botSender = selectUser(global, botId); if (!botSender) { return undefined; } @@ -309,6 +321,7 @@ addActionHandler('switchBotInline', (global, actions, payload): ActionReturnType actions.openChatWithDraft({ text: `@${botSender.usernames![0].username} ${query}`, chatId: isSamePeer ? chat.id : undefined, + filter, tabId, }); return undefined; @@ -513,6 +526,65 @@ addActionHandler('requestWebView', async (global, actions, payload): Promise => { + const { + botId, appName, startApp, theme, isWriteAllowed, + tabId = getCurrentTabId(), + } = payload; + + const bot = selectUser(global, botId); + if (!bot) return; + + const botApp = await callApi('fetchBotApp', { + bot, + appName, + }); + global = getGlobal(); + + if (!botApp) { + actions.showNotification({ message: translate('lng_username_app_not_found'), tabId }); + return; + } + + if (botApp.isInactive && !selectIsTrustedBot(global, botId)) { + global = updateTabState(global, { + botTrustRequest: { + botId, + shouldRequestWriteAccess: botApp.shouldRequestWriteAccess, + type: 'botApp', + onConfirm: { + action: 'requestAppWebView', + payload, + }, + }, + }, tabId); + setGlobal(global); + return; + } + + const peer = selectCurrentChat(global, tabId); + + const url = await callApi('requestAppWebView', { + peer: peer || bot, + app: botApp, + startParam: startApp, + isWriteAllowed, + theme, + }); + global = getGlobal(); + + if (!url) return; + + global = updateTabState(global, { + webApp: { + url, + botId, + buttonText: '', + }, + }, tabId); + setGlobal(global); +}); + addActionHandler('prolongWebView', async (global, actions, payload): Promise => { const { botId, peerId, isSilent, replyToMessageId, queryId, threadId, @@ -582,7 +654,7 @@ addActionHandler('cancelBotTrustRequest', (global, actions, payload): ActionRetu }); addActionHandler('markBotTrusted', (global, actions, payload): ActionReturnType => { - const { botId, tabId = getCurrentTabId() } = payload; + const { botId, isWriteAllowed, tabId = getCurrentTabId() } = payload; const { trustedBotIds } = global; const newTrustedBotIds = new Set(trustedBotIds); @@ -597,7 +669,10 @@ addActionHandler('markBotTrusted', (global, actions, payload): ActionReturnType if (tabState.botTrustRequest?.onConfirm) { const { action, payload: callbackPayload } = tabState.botTrustRequest.onConfirm; // @ts-ignore - actions[action](callbackPayload); + actions[action]({ + ...(callbackPayload as {}), + isWriteAllowed, + }); } global = updateTabState(global, { @@ -921,6 +996,7 @@ async function searchInlineBot(global: T, { cacheTime: Date.now() + result.cacheTime * 1000, ...(newResults.length && { isGallery: result.isGallery }), ...(result.switchPm && { switchPm: result.switchPm }), + ...(result.switchWebview && { switchWebview: result.switchWebview }), canLoadMore: result.results.length > 0 && Boolean(result.nextOffset), results: newInlineBotData.offset === '' || newInlineBotData.offset === result.nextOffset ? result.results diff --git a/src/global/actions/api/chats.ts b/src/global/actions/api/chats.ts index 9b8622e15..20559bbc5 100644 --- a/src/global/actions/api/chats.ts +++ b/src/global/actions/api/chats.ts @@ -77,6 +77,7 @@ import * as langProvider from '../../../util/langProvider'; import { selectCurrentLimit } from '../../selectors/limits'; import { updateTabState } from '../../reducers/tabs'; import { getCurrentTabId } from '../../../util/establishMultitabRole'; +import { extractCurrentThemeParams } from '../../../util/themeStyle'; const TOP_CHAT_MESSAGES_PRELOAD_INTERVAL = 100; const INFINITE_LOOP_MARKER = 100; @@ -1005,6 +1006,8 @@ addActionHandler('openTelegramLink', (global, actions, payload): ActionReturnTyp startParam: params.start, startAttach, attach: params.attach, + startApp: params.startapp, + originalParts: [part1, part2, part3], tabId, }); } @@ -1022,11 +1025,13 @@ addActionHandler('acceptInviteConfirmation', async (global, actions, payload): P addActionHandler('openChatByUsername', async (global, actions, payload): Promise => { const { - username, messageId, commentId, startParam, startAttach, attach, threadId, + username, messageId, commentId, startParam, startAttach, attach, threadId, originalParts, startApp, tabId = getCurrentTabId(), } = payload!; const chat = selectCurrentChat(global, tabId); + const webAppName = originalParts?.[1]; + const isWebApp = webAppName && !Number(webAppName); if (!commentId) { if (!startAttach && messageId && !startParam && chat?.usernames?.some((c) => c.username === username)) { @@ -1035,13 +1040,15 @@ addActionHandler('openChatByUsername', async (global, actions, payload): Promise }); return; } - await openChatByUsername(global, actions, username, threadId, messageId, startParam, startAttach, attach, tabId); - return; + if (!isWebApp) { + await openChatByUsername(global, actions, username, threadId, messageId, startParam, startAttach, attach, tabId); + return; + } } const { chatId, type } = selectCurrentMessageList(global, tabId) || {}; const usernameChat = selectChatByUsername(global, username); - if (chatId && messageId && usernameChat && type === 'thread') { + if (chatId && commentId && messageId && usernameChat && type === 'thread') { const threadInfo = selectThreadInfo(global, chatId, messageId); if (threadInfo && threadInfo.chatId === chatId) { @@ -1055,9 +1062,7 @@ addActionHandler('openChatByUsername', async (global, actions, payload): Promise } } - if (!messageId) return; - - actions.openChat({ id: TMP_CHAT_ID, tabId }); + if (!isWebApp) actions.openChat({ id: TMP_CHAT_ID, tabId }); const chatByUsername = await fetchChatByUsername(global, username); @@ -1065,6 +1070,21 @@ addActionHandler('openChatByUsername', async (global, actions, payload): Promise global = getGlobal(); + if (isWebApp && chatByUsername) { + const theme = extractCurrentThemeParams(); + + actions.requestAppWebView({ + appName: webAppName, + botId: chatByUsername.id, + tabId, + startApp, + theme, + }); + return; + } + + if (!messageId) return; + const threadInfo = selectThreadInfo(global, chatByUsername.id, messageId); let discussionChatId: string | undefined; diff --git a/src/global/actions/ui/chats.ts b/src/global/actions/ui/chats.ts index d5782cd7f..93e76ccb2 100644 --- a/src/global/actions/ui/chats.ts +++ b/src/global/actions/ui/chats.ts @@ -102,7 +102,7 @@ addActionHandler('openChatWithInfo', (global, actions, payload): ActionReturnTyp addActionHandler('openChatWithDraft', (global, actions, payload): ActionReturnType => { const { - chatId, text, threadId, files, tabId = getCurrentTabId(), + chatId, text, threadId, files, filter, tabId = getCurrentTabId(), } = payload; if (chatId) { @@ -114,6 +114,7 @@ addActionHandler('openChatWithDraft', (global, actions, payload): ActionReturnTy chatId, text, files, + filter, }, }, tabId); }); diff --git a/src/global/helpers/bots.ts b/src/global/helpers/bots.ts index b0e93c37c..5c5a8bebf 100644 --- a/src/global/helpers/bots.ts +++ b/src/global/helpers/bots.ts @@ -1,5 +1,13 @@ -import type { ApiPhoto } from '../../api/types'; +import type { ApiChatType, ApiPhoto } from '../../api/types'; export function getBotCoverMediaHash(photo: ApiPhoto) { return `photo${photo.id}?size=x`; } + +export function convertToApiChatType(type: string): ApiChatType | undefined { + if (type === 'channels') return 'channels'; + if (type === 'chats' || type === 'groups') return 'chats'; + if (type === 'users') return 'users'; + if (type === 'bots') return 'bots'; + return undefined; +} diff --git a/src/global/types.ts b/src/global/types.ts index e2a09469c..f844945dc 100644 --- a/src/global/types.ts +++ b/src/global/types.ts @@ -452,6 +452,7 @@ export type TabState = { chatId?: string; text: string; files?: File[]; + filter?: ApiChatType[]; }; pollModal: { @@ -471,7 +472,8 @@ export type TabState = { botTrustRequest?: { botId: string; - type: 'game' | 'webApp'; + type: 'game' | 'webApp' | 'botApp'; + shouldRequestWriteAccess?: boolean; onConfirm?: CallbackAction; }; requestedAttachBotInstall?: { @@ -1260,6 +1262,8 @@ export interface ActionPayloads { startParam?: string; startAttach?: string | boolean; attach?: string; + startApp?: string; + originalParts?: string[]; } & WithTabId; requestThreadInfoUpdate: { chatId: string; @@ -1644,6 +1648,7 @@ export interface ActionPayloads { threadId?: number; text: string; files?: File[]; + filter?: ApiChatType[]; } & WithTabId; resetOpenChatWithDraft: WithTabId | undefined; toggleJoinToSend: { @@ -2019,9 +2024,11 @@ export interface ActionPayloads { } & WithTabId; switchBotInline: { - messageId: number; + messageId?: number; + botId?: string; query: string; isSamePeer?: boolean; + filter?: ApiChatType[]; } & WithTabId; openGame: { @@ -2055,6 +2062,13 @@ export interface ActionPayloads { buttonText: string; theme?: ApiThemeParameters; } & WithTabId; + requestAppWebView: { + botId: string; + appName: string; + theme?: ApiThemeParameters; + startApp?: string; + isWriteAllowed?: boolean; + } & WithTabId; setWebAppPaymentSlug: { slug?: string; } & WithTabId; @@ -2062,6 +2076,7 @@ export interface ActionPayloads { cancelBotTrustRequest: WithTabId | undefined; markBotTrusted: { botId: string; + isWriteAllowed?: boolean; } & WithTabId; cancelAttachBotInstall: WithTabId | undefined; diff --git a/src/lib/gramjs/tl/apiTl.js b/src/lib/gramjs/tl/apiTl.js index 42711f53e..ff3dc53ac 100644 --- a/src/lib/gramjs/tl/apiTl.js +++ b/src/lib/gramjs/tl/apiTl.js @@ -1304,6 +1304,8 @@ messages.getTopReactions#bb8125ba limit:int hash:long = messages.Reactions; messages.getRecentReactions#39461db2 limit:int hash:long = messages.Reactions; messages.clearRecentReactions#9dfeefb4 = Bool; messages.getExtendedMedia#84f80814 peer:InputPeer id:Vector = Updates; +messages.getBotApp#34fdc5c3 app:InputBotApp hash:long = messages.BotApp; +messages.requestAppWebView#8c5a3b3c flags:# write_allowed:flags.0?true peer:InputPeer app:InputBotApp start_param:flags.1?string theme_params:flags.2?DataJSON platform:string = AppWebViewResult; updates.getState#edd4882a = updates.State; updates.getDifference#25939651 flags:# pts:int pts_total_limit:flags.0?int date:int qts:int = updates.Difference; updates.getChannelDifference#3173d78 flags:# force:flags.0?true channel:InputChannel filter:ChannelMessagesFilter pts:int limit:int = updates.ChannelDifference; diff --git a/src/lib/gramjs/tl/static/api.json b/src/lib/gramjs/tl/static/api.json index 411b1aaa8..a5f052cd0 100644 --- a/src/lib/gramjs/tl/static/api.json +++ b/src/lib/gramjs/tl/static/api.json @@ -163,6 +163,8 @@ "messages.toggleNoForwards", "messages.saveDefaultSendAs", "messages.getExtendedMedia", + "messages.getBotApp", + "messages.requestAppWebView", "updates.getState", "updates.getDifference", "updates.getChannelDifference", diff --git a/src/types/index.ts b/src/types/index.ts index bb70e6b5b..40d87e15c 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -1,6 +1,7 @@ import type { TeactNode } from '../lib/teact/teact'; import type { ApiBotInlineMediaResult, ApiBotInlineResult, ApiBotInlineSwitchPm, + ApiBotInlineSwitchWebview, ApiChatInviteImporter, ApiExportedInvite, ApiLanguage, ApiMessage, ApiReaction, ApiStickerSet, @@ -396,5 +397,6 @@ export type InlineBotSettings = { results?: (ApiBotInlineResult | ApiBotInlineMediaResult)[]; isGallery?: boolean; switchPm?: ApiBotInlineSwitchPm; + switchWebview?: ApiBotInlineSwitchWebview; cacheTime: number; }; diff --git a/src/util/deeplink.ts b/src/util/deeplink.ts index 4afedf25d..fe6fab73b 100644 --- a/src/util/deeplink.ts +++ b/src/util/deeplink.ts @@ -36,6 +36,7 @@ export const processDeepLink = (url: string) => { case 'resolve': { const { domain, phone, post, comment, voicechat, livestream, start, startattach, attach, thread, topic, + appname, startapp, } = params; const startAttach = params.hasOwnProperty('startattach') && !startattach ? true : startattach; @@ -43,7 +44,13 @@ export const processDeepLink = (url: string) => { const threadId = Number(thread) || Number(topic) || undefined; if (domain !== 'telegrampassport') { - if (startAttach && choose) { + if (appname) { + openChatByUsername({ + username: domain, + startApp: startapp, + originalParts: [domain, appname], + }); + } else if (startAttach && choose) { processAttachBotParameters({ username: domain, filter: choose,