Support Bot Apps (#3041)

This commit is contained in:
Alexander Zinchuk 2023-04-25 17:24:03 +04:00
parent ac28fe0162
commit 920b046d2e
25 changed files with 415 additions and 60 deletions

View File

@ -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,
};
}

View File

@ -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;

View File

@ -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),
});
}

View File

@ -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;

View File

@ -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 {

View File

@ -43,6 +43,11 @@ export interface ApiBotInlineSwitchPm {
startParam: string;
}
export interface ApiBotInlineSwitchWebview {
text: string;
url: string;
}
export interface ApiBotCommand {
botId: string;
command: string;

View File

@ -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

View File

@ -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<OwnProps> = ({ bot, type }) => {
const BotTrustModal: FC<OwnProps> = ({ 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<OwnProps> = ({ 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 (
<ConfirmDialog
isOpen={Boolean(bot)}
onClose={cancelBotTrustRequest}
confirmHandler={handleBotTrustAccept}
onClose={handleBotTrustDecline}
title={title}
textParts={renderText(text, ['br', 'simple_markdown', 'emoji'])}
/>
confirmHandler={handleBotTrustAccept}
>
{text}
{shouldRequestWriteAccess && (
<Checkbox
className="dialog-checkbox"
checked={isWriteAllowed}
label={renderText(
lang('WebApp.AddToAttachmentAllowMessages', currentBot?.firstName),
['simple_markdown'],
)}
onCheck={setIsWriteAllowed}
/>
)}
</ConfirmDialog>
);
};

View File

@ -54,6 +54,7 @@ const DraftRecipientPicker: FC<OwnProps> = ({
<RecipientPicker
isOpen={isOpen}
searchPlaceholder={lang('ForwardTo')}
filter={requestedDraft?.filter}
onSelectRecipient={handleSelectRecipient}
onClose={handleClose}
onCloseAnimationEnd={unmarkIsShown}

View File

@ -511,7 +511,11 @@ const Main: FC<OwnProps & StateProps> = ({
<PhoneCall isActive={isPhoneCallActive} />
<UnreadCount isForAppBadge />
<RatePhoneCallModal isOpen={isRatePhoneCallModalOpen} />
<BotTrustModal bot={botTrustRequestBot} type={botTrustRequest?.type} />
<BotTrustModal
bot={botTrustRequestBot}
type={botTrustRequest?.type}
shouldRequestWriteAccess={botTrustRequest?.shouldRequestWriteAccess}
/>
<AttachBotInstallModal bot={attachBotToInstall} />
<AttachBotRecipientPicker requestedAttachBotInChat={requestedAttachBotInChat} />
<MessageListHistoryHandler />

View File

@ -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<OwnProps & StateProps> = ({
openInvoice,
setWebAppPaymentSlug,
showNotification,
switchBotInline,
} = getActions();
const [mainButton, setMainButton] = useState<WebAppButton | undefined>();
const [isBackButtonVisible, setIsBackButtonVisible] = useState(false);
const [backgroundColor, setBackgroundColor] = useState(extractCurrentThemeParams().bg_color);
const [headerColor, setHeaderColor] = useState(extractCurrentThemeParams().bg_color);
const [backgroundColor, setBackgroundColor] = useState<string>();
const [headerColor, setHeaderColor] = useState<string>();
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<PopupOptions | undefined>();
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<HTMLIFrameElement>(null);
@ -115,7 +123,7 @@ const WebAppModal: FC<OwnProps & StateProps> = ({
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<OwnProps & StateProps> = ({
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<OwnProps & StateProps> = ({
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<OwnProps & StateProps> = ({
{confirmClose && (
<ConfirmDialog
isOpen={isCloseModalOpen}
onClose={closeCloseModal}
onClose={closeModal}
title={lang('lng_bot_close_warning_title')}
text={lang('lng_bot_close_warning')}
confirmHandler={closeWebApp}

View File

@ -1,4 +1,5 @@
import { useCallback, useEffect, useRef } from '../../../lib/teact/teact';
import { getActions } from '../../../lib/teact/teactn';
import { extractCurrentThemeParams } from '../../../util/themeStyle';
import useWindowSize from '../../../hooks/useWindowSize';
@ -83,6 +84,12 @@ export type WebAppInboundEvent = {
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';
@ -171,6 +178,10 @@ const useWebAppFrame = (
onEvent: (event: WebAppInboundEvent) => void,
onLoad?: () => void,
) => {
const {
showNotification,
} = getActions();
const ignoreEventsRef = useRef<boolean>(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;

View File

@ -496,6 +496,7 @@ const Composer: FC<OwnProps & StateProps> = ({
botId: inlineBotId,
isGallery: isInlineBotTooltipGallery,
switchPm: inlineBotSwitchPm,
switchWebview: inlineBotSwitchWebview,
results: inlineBotResults,
closeTooltip: closeInlineBotTooltip,
help: inlineBotHelp,
@ -1322,6 +1323,7 @@ const Composer: FC<OwnProps & StateProps> = ({
isGallery={isInlineBotTooltipGallery}
inlineBotResults={inlineBotResults}
switchPm={inlineBotSwitchPm}
switchWebview={inlineBotSwitchWebview}
loadMore={loadMoreForInlineBot}
isSavedMessages={isChatWithSelf}
canSendGifs={canSendGifs}

View File

@ -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<OwnProps> = ({
isGallery,
inlineBotResults,
switchPm,
switchWebview,
isSavedMessages,
canSendGifs,
loadMore,
@ -61,6 +67,7 @@ const InlineBotTooltip: FC<OwnProps> = ({
const {
openChat,
startBot,
requestSimpleWebView,
} = getActions();
// eslint-disable-next-line no-null/no-null
@ -99,6 +106,17 @@ const InlineBotTooltip: FC<OwnProps> = ({
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<OwnProps> = ({
);
}
function renderSwitchWebview() {
return (
<ListItem ripple className="switch-pm scroll-item" onClick={handleOpenWebview}>
<span className="title">{switchWebview!.text}</span>
</ListItem>
);
}
function renderContent() {
return renderedInlineBotResults!.map((inlineBotResult, index) => {
switch (inlineBotResult.type) {
@ -203,6 +229,7 @@ const InlineBotTooltip: FC<OwnProps> = ({
sensitiveArea={160}
>
{switchPm && renderSwitchPm()}
{switchWebview && renderSwitchWebview()}
{Boolean(renderedInlineBotResults?.length) && renderContent()}
</InfiniteScroll>
);

View File

@ -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,

View File

@ -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']);

View File

@ -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<voi
addActionHandler('switchBotInline', (global, actions, payload): ActionReturnType => {
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<voi
setGlobal(global);
});
addActionHandler('requestAppWebView', async (global, actions, payload): Promise<void> => {
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<void> => {
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<T extends GlobalState>(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

View File

@ -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<void> => {
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;

View File

@ -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);
});

View File

@ -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;
}

View File

@ -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;

View File

@ -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<int> = 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;

View File

@ -163,6 +163,8 @@
"messages.toggleNoForwards",
"messages.saveDefaultSendAs",
"messages.getExtendedMedia",
"messages.getBotApp",
"messages.requestAppWebView",
"updates.getState",
"updates.getDifference",
"updates.getChannelDifference",

View File

@ -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;
};

View File

@ -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,