mirror of
https://github.com/AyuGram/AyuGramDesktop.git
synced 2025-06-05 06:33:57 +02:00
Implement opening of t.me/bot/app-s.
This commit is contained in:
parent
ae5f2add0e
commit
af51307aa6
21 changed files with 358 additions and 44 deletions
|
@ -444,6 +444,8 @@ PRIVATE
|
||||||
data/data_audio_msg_id.h
|
data/data_audio_msg_id.h
|
||||||
data/data_auto_download.cpp
|
data/data_auto_download.cpp
|
||||||
data/data_auto_download.h
|
data/data_auto_download.h
|
||||||
|
data/data_bot_app.cpp
|
||||||
|
data/data_bot_app.h
|
||||||
data/data_chat.cpp
|
data/data_chat.cpp
|
||||||
data/data_chat.h
|
data/data_chat.h
|
||||||
data/data_chat_filters.cpp
|
data/data_chat_filters.cpp
|
||||||
|
|
|
@ -396,6 +396,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||||
"lng_username_available" = "This username is available.";
|
"lng_username_available" = "This username is available.";
|
||||||
"lng_username_not_found" = "User @{user} not found.";
|
"lng_username_not_found" = "User @{user} not found.";
|
||||||
"lng_username_by_phone_not_found" = "User {phone} not found.";
|
"lng_username_by_phone_not_found" = "User {phone} not found.";
|
||||||
|
"lng_username_app_not_found" = "Bot application not found.";
|
||||||
"lng_username_link" = "This link opens a chat with you:";
|
"lng_username_link" = "This link opens a chat with you:";
|
||||||
"lng_username_copied" = "Link copied to clipboard.";
|
"lng_username_copied" = "Link copied to clipboard.";
|
||||||
|
|
||||||
|
@ -1463,6 +1464,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||||
"lng_action_took_screenshot" = "{from} took a screenshot!";
|
"lng_action_took_screenshot" = "{from} took a screenshot!";
|
||||||
"lng_action_you_took_screenshot" = "You took a screenshot!";
|
"lng_action_you_took_screenshot" = "You took a screenshot!";
|
||||||
"lng_action_bot_allowed_from_domain" = "You allowed this bot to message you when you logged in on {domain}.";
|
"lng_action_bot_allowed_from_domain" = "You allowed this bot to message you when you logged in on {domain}.";
|
||||||
|
"lng_action_bot_allowed_from_app" = "You allowed this bot to message you when you opened {app}.";
|
||||||
"lng_action_secure_values_sent" = "{user} received the following documents: {documents}";
|
"lng_action_secure_values_sent" = "{user} received the following documents: {documents}";
|
||||||
"lng_action_secure_personal_details" = "personal details";
|
"lng_action_secure_personal_details" = "personal details";
|
||||||
"lng_action_secure_proof_of_identity" = "proof of identity";
|
"lng_action_secure_proof_of_identity" = "proof of identity";
|
||||||
|
|
|
@ -108,7 +108,11 @@ void HiddenUrlClickHandler::Open(QString url, QVariant context) {
|
||||||
};
|
};
|
||||||
if (url.startsWith(u"tg://"_q, Qt::CaseInsensitive)
|
if (url.startsWith(u"tg://"_q, Qt::CaseInsensitive)
|
||||||
|| url.startsWith(u"internal:"_q, Qt::CaseInsensitive)) {
|
|| url.startsWith(u"internal:"_q, Qt::CaseInsensitive)) {
|
||||||
open();
|
UrlClickHandler::Open(url, QVariant::fromValue([&] {
|
||||||
|
auto result = context.value<ClickHandlerContext>();
|
||||||
|
result.mayShowConfirmation = !base::IsCtrlPressed();
|
||||||
|
return result;
|
||||||
|
}()));
|
||||||
} else {
|
} else {
|
||||||
const auto parsedUrl = QUrl::fromUserInput(url);
|
const auto parsedUrl = QUrl::fromUserInput(url);
|
||||||
if (UrlRequiresConfirmation(parsedUrl) && !base::IsCtrlPressed()) {
|
if (UrlRequiresConfirmation(parsedUrl) && !base::IsCtrlPressed()) {
|
||||||
|
|
|
@ -42,6 +42,7 @@ struct ClickHandlerContext {
|
||||||
Fn<HistoryView::ElementDelegate*()> elementDelegate;
|
Fn<HistoryView::ElementDelegate*()> elementDelegate;
|
||||||
base::weak_ptr<Window::SessionController> sessionWindow;
|
base::weak_ptr<Window::SessionController> sessionWindow;
|
||||||
std::shared_ptr<Ui::Show> show;
|
std::shared_ptr<Ui::Show> show;
|
||||||
|
bool mayShowConfirmation = false;
|
||||||
bool skipBotAutoLogin = false;
|
bool skipBotAutoLogin = false;
|
||||||
bool botStartAutoSubmit = false;
|
bool botStartAutoSubmit = false;
|
||||||
// Is filled from peer info.
|
// Is filled from peer info.
|
||||||
|
|
|
@ -373,6 +373,8 @@ bool ResolveUsernameOrPhone(
|
||||||
if (const auto postId = postParam.toInt()) {
|
if (const auto postId = postParam.toInt()) {
|
||||||
post = postId;
|
post = postId;
|
||||||
}
|
}
|
||||||
|
const auto appname = params.value(u"appname"_q);
|
||||||
|
const auto appstart = params.value(u"startapp"_q);
|
||||||
const auto commentParam = params.value(u"comment"_q);
|
const auto commentParam = params.value(u"comment"_q);
|
||||||
const auto commentId = commentParam.toInt();
|
const auto commentId = commentParam.toInt();
|
||||||
const auto topicParam = params.value(u"topic"_q);
|
const auto topicParam = params.value(u"topic"_q);
|
||||||
|
@ -384,6 +386,12 @@ bool ResolveUsernameOrPhone(
|
||||||
startToken = gameParam;
|
startToken = gameParam;
|
||||||
resolveType = ResolveType::ShareGame;
|
resolveType = ResolveType::ShareGame;
|
||||||
}
|
}
|
||||||
|
if (startToken.isEmpty() && params.contains(u"startapp"_q)) {
|
||||||
|
startToken = params.value(u"startapp"_q);
|
||||||
|
}
|
||||||
|
if (!appname.isEmpty()) {
|
||||||
|
resolveType = ResolveType::BotApp;
|
||||||
|
}
|
||||||
const auto myContext = context.value<ClickHandlerContext>();
|
const auto myContext = context.value<ClickHandlerContext>();
|
||||||
using Navigation = Window::SessionNavigation;
|
using Navigation = Window::SessionNavigation;
|
||||||
controller->showPeerByLink(Navigation::PeerByLinkInfo{
|
controller->showPeerByLink(Navigation::PeerByLinkInfo{
|
||||||
|
@ -403,6 +411,8 @@ bool ResolveUsernameOrPhone(
|
||||||
.startToken = startToken,
|
.startToken = startToken,
|
||||||
.startAdminRights = adminRights,
|
.startAdminRights = adminRights,
|
||||||
.startAutoSubmit = myContext.botStartAutoSubmit,
|
.startAutoSubmit = myContext.botStartAutoSubmit,
|
||||||
|
.botAppName = appname.isEmpty() ? postParam : appname,
|
||||||
|
.botAppForceConfirmation = myContext.mayShowConfirmation,
|
||||||
.attachBotUsername = params.value(u"attach"_q),
|
.attachBotUsername = params.value(u"attach"_q),
|
||||||
.attachBotToggleCommand = (params.contains(u"startattach"_q)
|
.attachBotToggleCommand = (params.contains(u"startattach"_q)
|
||||||
? params.value(u"startattach"_q)
|
? params.value(u"startattach"_q)
|
||||||
|
@ -1004,6 +1014,7 @@ QString TryConvertUrlToLocal(QString url) {
|
||||||
"("
|
"("
|
||||||
"/?\\?|"
|
"/?\\?|"
|
||||||
"/?$|"
|
"/?$|"
|
||||||
|
"/[a-zA-Z0-9\\.\\_]+|"
|
||||||
"/\\d+/?(\\?|$)|"
|
"/\\d+/?(\\?|$)|"
|
||||||
"/\\d+/\\d+/?(\\?|$)"
|
"/\\d+/\\d+/?(\\?|$)"
|
||||||
")"_q, query, matchOptions)) {
|
")"_q, query, matchOptions)) {
|
||||||
|
@ -1014,6 +1025,8 @@ QString TryConvertUrlToLocal(QString url) {
|
||||||
added = u"&topic=%1&post=%2"_q.arg(threadPostMatch->captured(1)).arg(threadPostMatch->captured(2));
|
added = u"&topic=%1&post=%2"_q.arg(threadPostMatch->captured(1)).arg(threadPostMatch->captured(2));
|
||||||
} else if (const auto postMatch = regex_match(u"^/(\\d+)(/?\\?|/?$)"_q, usernameMatch->captured(2))) {
|
} else if (const auto postMatch = regex_match(u"^/(\\d+)(/?\\?|/?$)"_q, usernameMatch->captured(2))) {
|
||||||
added = u"&post="_q + postMatch->captured(1);
|
added = u"&post="_q + postMatch->captured(1);
|
||||||
|
} else if (const auto appNameMatch = regex_match(u"^/([a-zA-Z0-9\\.\\_]+)(/?\\?|/?$)"_q, usernameMatch->captured(2))) {
|
||||||
|
added = u"&appname="_q + appNameMatch->captured(1);
|
||||||
}
|
}
|
||||||
return base + added + (params.isEmpty() ? QString() : '&' + params);
|
return base + added + (params.isEmpty() ? QString() : '&' + params);
|
||||||
}
|
}
|
||||||
|
|
13
Telegram/SourceFiles/data/data_bot_app.cpp
Normal file
13
Telegram/SourceFiles/data/data_bot_app.cpp
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
/*
|
||||||
|
This file is part of Telegram Desktop,
|
||||||
|
the official desktop application for the Telegram messaging service.
|
||||||
|
|
||||||
|
For license and copyright information please follow this link:
|
||||||
|
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||||
|
*/
|
||||||
|
#include "data/data_bot_app.h"
|
||||||
|
|
||||||
|
BotAppData::BotAppData(not_null<Data::Session*> owner, const BotAppId &id)
|
||||||
|
: owner(owner)
|
||||||
|
, id(id) {
|
||||||
|
}
|
27
Telegram/SourceFiles/data/data_bot_app.h
Normal file
27
Telegram/SourceFiles/data/data_bot_app.h
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
/*
|
||||||
|
This file is part of Telegram Desktop,
|
||||||
|
the official desktop application for the Telegram messaging service.
|
||||||
|
|
||||||
|
For license and copyright information please follow this link:
|
||||||
|
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||||
|
*/
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "data/data_photo.h"
|
||||||
|
#include "data/data_document.h"
|
||||||
|
|
||||||
|
struct BotAppData {
|
||||||
|
BotAppData(not_null<Data::Session*> owner, const BotAppId &id);
|
||||||
|
|
||||||
|
const not_null<Data::Session*> owner;
|
||||||
|
BotAppId id = 0;
|
||||||
|
PeerId botId = 0;
|
||||||
|
QString shortName;
|
||||||
|
QString title;
|
||||||
|
QString description;
|
||||||
|
PhotoData *photo = nullptr;
|
||||||
|
DocumentData *document = nullptr;
|
||||||
|
|
||||||
|
uint64 accessHash = 0;
|
||||||
|
uint64 hash = 0;
|
||||||
|
};
|
|
@ -21,5 +21,4 @@ struct GameData {
|
||||||
QString description;
|
QString description;
|
||||||
PhotoData *photo = nullptr;
|
PhotoData *photo = nullptr;
|
||||||
DocumentData *document = nullptr;
|
DocumentData *document = nullptr;
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
|
@ -41,6 +41,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||||
#include "lang/lang_keys.h" // tr::lng_deleted(tr::now) in user name
|
#include "lang/lang_keys.h" // tr::lng_deleted(tr::now) in user name
|
||||||
#include "data/stickers/data_stickers.h"
|
#include "data/stickers/data_stickers.h"
|
||||||
#include "data/notify/data_notify_settings.h"
|
#include "data/notify/data_notify_settings.h"
|
||||||
|
#include "data/data_bot_app.h"
|
||||||
#include "data/data_changes.h"
|
#include "data/data_changes.h"
|
||||||
#include "data/data_group_call.h"
|
#include "data/data_group_call.h"
|
||||||
#include "data/data_media_types.h"
|
#include "data/data_media_types.h"
|
||||||
|
@ -3450,6 +3451,45 @@ void Session::gameApplyFields(
|
||||||
notifyGameUpdateDelayed(game);
|
notifyGameUpdateDelayed(game);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
not_null<BotAppData*> Session::botApp(BotAppId id) {
|
||||||
|
const auto i = _botApps.find(id);
|
||||||
|
return (i != end(_botApps))
|
||||||
|
? i->second.get()
|
||||||
|
: _botApps.emplace(
|
||||||
|
id,
|
||||||
|
std::make_unique<BotAppData>(this, id)).first->second.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
BotAppData *Session::findBotApp(PeerId botId, const QString &appName) const {
|
||||||
|
for (const auto &[id, app] : _botApps) {
|
||||||
|
if (app->botId == botId && app->shortName == appName) {
|
||||||
|
return app.get();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
BotAppData *Session::processBotApp(
|
||||||
|
PeerId botId,
|
||||||
|
const MTPBotApp &data) {
|
||||||
|
return data.match([&](const MTPDbotApp &data) {
|
||||||
|
const auto result = botApp(data.vid().v);
|
||||||
|
result->botId = botId;
|
||||||
|
result->shortName = qs(data.vshort_name());
|
||||||
|
result->title = qs(data.vtitle());
|
||||||
|
result->description = qs(data.vdescription());
|
||||||
|
result->photo = processPhoto(data.vphoto());
|
||||||
|
result->document = data.vdocument()
|
||||||
|
? processDocument(*data.vdocument()).get()
|
||||||
|
: nullptr;
|
||||||
|
result->accessHash = data.vaccess_hash().v;
|
||||||
|
result->hash = data.vhash().v;
|
||||||
|
return result.get();
|
||||||
|
}, [](const MTPDbotAppNotModified &) {
|
||||||
|
return (BotAppData*)nullptr;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
not_null<PollData*> Session::poll(PollId id) {
|
not_null<PollData*> Session::poll(PollId id) {
|
||||||
auto i = _polls.find(id);
|
auto i = _polls.find(id);
|
||||||
if (i == _polls.cend()) {
|
if (i == _polls.cend()) {
|
||||||
|
|
|
@ -570,6 +570,12 @@ public:
|
||||||
not_null<GameData*> original,
|
not_null<GameData*> original,
|
||||||
const MTPGame &data);
|
const MTPGame &data);
|
||||||
|
|
||||||
|
[[nodiscard]] not_null<BotAppData*> botApp(BotAppId id);
|
||||||
|
BotAppData *findBotApp(PeerId botId, const QString &appName) const;
|
||||||
|
BotAppData *processBotApp(
|
||||||
|
PeerId botId,
|
||||||
|
const MTPBotApp &data);
|
||||||
|
|
||||||
[[nodiscard]] not_null<PollData*> poll(PollId id);
|
[[nodiscard]] not_null<PollData*> poll(PollId id);
|
||||||
not_null<PollData*> processPoll(const MTPPoll &data);
|
not_null<PollData*> processPoll(const MTPPoll &data);
|
||||||
not_null<PollData*> processPoll(const MTPDmessageMediaPoll &data);
|
not_null<PollData*> processPoll(const MTPDmessageMediaPoll &data);
|
||||||
|
@ -922,6 +928,9 @@ private:
|
||||||
std::unordered_map<
|
std::unordered_map<
|
||||||
GameId,
|
GameId,
|
||||||
std::unique_ptr<GameData>> _games;
|
std::unique_ptr<GameData>> _games;
|
||||||
|
std::unordered_map<
|
||||||
|
BotAppId,
|
||||||
|
std::unique_ptr<BotAppData>> _botApps;
|
||||||
std::unordered_map<
|
std::unordered_map<
|
||||||
not_null<const GameData*>,
|
not_null<const GameData*>,
|
||||||
base::flat_set<not_null<ViewElement*>>> _gameViews;
|
base::flat_set<not_null<ViewElement*>>> _gameViews;
|
||||||
|
|
|
@ -118,6 +118,7 @@ class DocumentData;
|
||||||
class PhotoData;
|
class PhotoData;
|
||||||
struct WebPageData;
|
struct WebPageData;
|
||||||
struct GameData;
|
struct GameData;
|
||||||
|
struct BotAppData;
|
||||||
struct PollData;
|
struct PollData;
|
||||||
|
|
||||||
using PhotoId = uint64;
|
using PhotoId = uint64;
|
||||||
|
@ -129,6 +130,7 @@ using GameId = uint64;
|
||||||
using PollId = uint64;
|
using PollId = uint64;
|
||||||
using WallPaperId = uint64;
|
using WallPaperId = uint64;
|
||||||
using CallId = uint64;
|
using CallId = uint64;
|
||||||
|
using BotAppId = uint64;
|
||||||
constexpr auto CancelledWebPageId = WebPageId(0xFFFFFFFFFFFFFFFFULL);
|
constexpr auto CancelledWebPageId = WebPageId(0xFFFFFFFFFFFFFFFFULL);
|
||||||
|
|
||||||
struct PreparedPhotoThumb {
|
struct PreparedPhotoThumb {
|
||||||
|
|
|
@ -1059,6 +1059,12 @@ ServiceAction ParseServiceAction(
|
||||||
result.content = content;
|
result.content = content;
|
||||||
}, [&](const MTPDmessageActionBotAllowed &data) {
|
}, [&](const MTPDmessageActionBotAllowed &data) {
|
||||||
auto content = ActionBotAllowed();
|
auto content = ActionBotAllowed();
|
||||||
|
if (const auto app = data.vapp()) {
|
||||||
|
app->match([&](const MTPDbotApp &data) {
|
||||||
|
content.appId = data.vid().v;
|
||||||
|
content.app = ParseString(data.vtitle());
|
||||||
|
}, [](const MTPDbotAppNotModified &) {});
|
||||||
|
}
|
||||||
if (const auto domain = data.vdomain()) {
|
if (const auto domain = data.vdomain()) {
|
||||||
content.domain = ParseString(*domain);
|
content.domain = ParseString(*domain);
|
||||||
}
|
}
|
||||||
|
|
|
@ -431,6 +431,8 @@ struct ActionCustomAction {
|
||||||
};
|
};
|
||||||
|
|
||||||
struct ActionBotAllowed {
|
struct ActionBotAllowed {
|
||||||
|
uint64 appId = 0;
|
||||||
|
Utf8String app;
|
||||||
Utf8String domain;
|
Utf8String domain;
|
||||||
bool attachMenu = false;
|
bool attachMenu = false;
|
||||||
};
|
};
|
||||||
|
|
|
@ -1033,6 +1033,9 @@ auto HtmlWriter::Wrap::pushMessage(
|
||||||
return data.attachMenu
|
return data.attachMenu
|
||||||
? "You allowed this bot to message you "
|
? "You allowed this bot to message you "
|
||||||
"when you added it in the attachment menu."_q
|
"when you added it in the attachment menu."_q
|
||||||
|
: data.app.isEmpty()
|
||||||
|
? ("You allowed this bot to message you when you opened "
|
||||||
|
+ SerializeString(data.app))
|
||||||
: ("You allowed this bot to message you when you logged in on "
|
: ("You allowed this bot to message you when you logged in on "
|
||||||
+ SerializeString(data.domain));
|
+ SerializeString(data.domain));
|
||||||
}, [&](const ActionSecureValuesSent &data) {
|
}, [&](const ActionSecureValuesSent &data) {
|
||||||
|
|
|
@ -477,6 +477,10 @@ QByteArray SerializeMessage(
|
||||||
}, [&](const ActionBotAllowed &data) {
|
}, [&](const ActionBotAllowed &data) {
|
||||||
if (data.attachMenu) {
|
if (data.attachMenu) {
|
||||||
pushAction("attach_menu_bot_allowed");
|
pushAction("attach_menu_bot_allowed");
|
||||||
|
} else if (data.appId) {
|
||||||
|
pushAction("allow_sending_messages");
|
||||||
|
push("reason_app_id", data.appId);
|
||||||
|
push("reason_app_name", data.app);
|
||||||
} else {
|
} else {
|
||||||
pushAction("allow_sending_messages");
|
pushAction("allow_sending_messages");
|
||||||
push("reason_domain", data.domain);
|
push("reason_domain", data.domain);
|
||||||
|
|
|
@ -49,6 +49,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||||
#include "api/api_updates.h"
|
#include "api/api_updates.h"
|
||||||
#include "dialogs/ui/dialogs_message_view.h"
|
#include "dialogs/ui/dialogs_message_view.h"
|
||||||
#include "data/notify/data_notify_settings.h"
|
#include "data/notify/data_notify_settings.h"
|
||||||
|
#include "data/data_bot_app.h"
|
||||||
#include "data/data_scheduled_messages.h" // kScheduledUntilOnlineTimestamp
|
#include "data/data_scheduled_messages.h" // kScheduledUntilOnlineTimestamp
|
||||||
#include "data/data_changes.h"
|
#include "data/data_changes.h"
|
||||||
#include "data/data_session.h"
|
#include "data/data_session.h"
|
||||||
|
@ -3689,6 +3690,21 @@ void HistoryItem::setServiceMessageByAction(const MTPmessageAction &action) {
|
||||||
result.text = {
|
result.text = {
|
||||||
tr::lng_action_attach_menu_bot_allowed(tr::now)
|
tr::lng_action_attach_menu_bot_allowed(tr::now)
|
||||||
};
|
};
|
||||||
|
} else if (const auto app = action.vapp()) {
|
||||||
|
const auto bot = history()->peer->asUser();
|
||||||
|
const auto botId = bot ? bot->id : PeerId();
|
||||||
|
const auto info = history()->owner().processBotApp(botId, *app);
|
||||||
|
const auto url = (bot && info)
|
||||||
|
? history()->session().createInternalLinkFull(
|
||||||
|
bot->username() + '/' + info->shortName)
|
||||||
|
: QString();
|
||||||
|
result.text = tr::lng_action_bot_allowed_from_app(
|
||||||
|
tr::now,
|
||||||
|
lt_app,
|
||||||
|
(url.isEmpty()
|
||||||
|
? TextWithEntities{ u"App"_q }
|
||||||
|
: Ui::Text::Link(info->title, url)),
|
||||||
|
Ui::Text::WithEntities);
|
||||||
} else {
|
} else {
|
||||||
const auto domain = qs(action.vdomain().value_or_empty());
|
const auto domain = qs(action.vdomain().value_or_empty());
|
||||||
result.text = tr::lng_action_bot_allowed_from_domain(
|
result.text = tr::lng_action_bot_allowed_from_domain(
|
||||||
|
|
|
@ -8,6 +8,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||||
#include "inline_bots/bot_attach_web_view.h"
|
#include "inline_bots/bot_attach_web_view.h"
|
||||||
|
|
||||||
#include "api/api_common.h"
|
#include "api/api_common.h"
|
||||||
|
#include "data/data_bot_app.h"
|
||||||
#include "data/data_user.h"
|
#include "data/data_user.h"
|
||||||
#include "data/data_file_origin.h"
|
#include "data/data_file_origin.h"
|
||||||
#include "data/data_document.h"
|
#include "data/data_document.h"
|
||||||
|
@ -424,6 +425,7 @@ struct AttachWebView::Context {
|
||||||
Dialogs::EntryState dialogsEntryState;
|
Dialogs::EntryState dialogsEntryState;
|
||||||
Api::SendAction action;
|
Api::SendAction action;
|
||||||
bool fromSwitch = false;
|
bool fromSwitch = false;
|
||||||
|
bool fromBotApp = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
AttachWebView::AttachWebView(not_null<Main::Session*> session)
|
AttachWebView::AttachWebView(not_null<Main::Session*> session)
|
||||||
|
@ -551,13 +553,12 @@ void AttachWebView::request(const WebViewButton &button) {
|
||||||
: MTP_inputPeerEmpty())
|
: MTP_inputPeerEmpty())
|
||||||
)).done([=](const MTPWebViewResult &result) {
|
)).done([=](const MTPWebViewResult &result) {
|
||||||
_requestId = 0;
|
_requestId = 0;
|
||||||
result.match([&](const MTPDwebViewResultUrl &data) {
|
const auto &data = result.data();
|
||||||
show(
|
show(
|
||||||
data.vquery_id().v,
|
data.vquery_id().v,
|
||||||
qs(data.vurl()),
|
qs(data.vurl()),
|
||||||
button.text,
|
button.text,
|
||||||
button.fromMenu || button.url.isEmpty());
|
button.fromMenu || button.url.isEmpty());
|
||||||
});
|
|
||||||
}).fail([=](const MTP::Error &error) {
|
}).fail([=](const MTP::Error &error) {
|
||||||
_requestId = 0;
|
_requestId = 0;
|
||||||
if (error.type() == u"BOT_INVALID"_q) {
|
if (error.type() == u"BOT_INVALID"_q) {
|
||||||
|
@ -573,7 +574,9 @@ void AttachWebView::cancel() {
|
||||||
_panel = nullptr;
|
_panel = nullptr;
|
||||||
_context = nullptr;
|
_context = nullptr;
|
||||||
_bot = nullptr;
|
_bot = nullptr;
|
||||||
|
_app = nullptr;
|
||||||
_botUsername = QString();
|
_botUsername = QString();
|
||||||
|
_botAppName = QString();
|
||||||
_startCommand = QString();
|
_startCommand = QString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -624,9 +627,7 @@ void AttachWebView::requestAddToMenu(
|
||||||
Expects(controller != nullptr || _context != nullptr);
|
Expects(controller != nullptr || _context != nullptr);
|
||||||
|
|
||||||
if (!bot->isBot() || !bot->botInfo->supportsAttachMenu) {
|
if (!bot->isBot() || !bot->botInfo->supportsAttachMenu) {
|
||||||
Ui::ShowMultilineToast({
|
showToast(tr::lng_bot_menu_not_supported(tr::now), controller);
|
||||||
.text = { tr::lng_bot_menu_not_supported(tr::now) },
|
|
||||||
});
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const auto wasController = (controller != nullptr);
|
const auto wasController = (controller != nullptr);
|
||||||
|
@ -694,10 +695,8 @@ void AttachWebView::requestAddToMenu(
|
||||||
} else {
|
} else {
|
||||||
requestBots();
|
requestBots();
|
||||||
if (!open(types)) {
|
if (!open(types)) {
|
||||||
Ui::ShowMultilineToast({
|
showToast(
|
||||||
.text = {
|
tr::lng_bot_menu_already_added(tr::now));
|
||||||
tr::lng_bot_menu_already_added(tr::now) },
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -708,17 +707,13 @@ void AttachWebView::requestAddToMenu(
|
||||||
_addToMenuBot = nullptr;
|
_addToMenuBot = nullptr;
|
||||||
_addToMenuContext = nullptr;
|
_addToMenuContext = nullptr;
|
||||||
_addToMenuStartCommand = QString();
|
_addToMenuStartCommand = QString();
|
||||||
Ui::ShowMultilineToast({
|
showToast(tr::lng_bot_menu_not_supported(tr::now));
|
||||||
.text = { tr::lng_bot_menu_not_supported(tr::now) },
|
|
||||||
});
|
|
||||||
}).send();
|
}).send();
|
||||||
}
|
}
|
||||||
|
|
||||||
void AttachWebView::removeFromMenu(not_null<UserData*> bot) {
|
void AttachWebView::removeFromMenu(not_null<UserData*> bot) {
|
||||||
toggleInMenu(bot, ToggledState::Removed, [=] {
|
toggleInMenu(bot, ToggledState::Removed, [=] {
|
||||||
Ui::ShowMultilineToast({
|
showToast(tr::lng_bot_remove_from_menu_done(tr::now));
|
||||||
.text = { tr::lng_bot_remove_from_menu_done(tr::now) },
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -729,9 +724,7 @@ void AttachWebView::resolve() {
|
||||||
}
|
}
|
||||||
_bot = bot->asUser();
|
_bot = bot->asUser();
|
||||||
if (!_bot) {
|
if (!_bot) {
|
||||||
Ui::ShowMultilineToast({
|
showToast(tr::lng_bot_menu_not_supported(tr::now));
|
||||||
.text = { tr::lng_bot_menu_not_supported(tr::now) }
|
|
||||||
});
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
requestAddToMenu(_bot, _startCommand);
|
requestAddToMenu(_bot, _startCommand);
|
||||||
|
@ -760,11 +753,8 @@ void AttachWebView::resolveUsername(
|
||||||
}).fail([=](const MTP::Error &error) {
|
}).fail([=](const MTP::Error &error) {
|
||||||
_requestId = 0;
|
_requestId = 0;
|
||||||
if (error.code() == 400) {
|
if (error.code() == 400) {
|
||||||
Ui::ShowMultilineToast({
|
showToast(
|
||||||
.text = {
|
tr::lng_username_not_found(tr::now, lt_user, username));
|
||||||
tr::lng_username_not_found(tr::now, lt_user, username),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}).send();
|
}).send();
|
||||||
}
|
}
|
||||||
|
@ -838,9 +828,8 @@ void AttachWebView::requestMenu(
|
||||||
: MTP_inputPeerEmpty())
|
: MTP_inputPeerEmpty())
|
||||||
)).done([=](const MTPWebViewResult &result) {
|
)).done([=](const MTPWebViewResult &result) {
|
||||||
_requestId = 0;
|
_requestId = 0;
|
||||||
result.match([&](const MTPDwebViewResultUrl &data) {
|
const auto &data = result.data();
|
||||||
show(data.vquery_id().v, qs(data.vurl()), text);
|
show(data.vquery_id().v, qs(data.vurl()), text);
|
||||||
});
|
|
||||||
}).fail([=](const MTP::Error &error) {
|
}).fail([=](const MTP::Error &error) {
|
||||||
_requestId = 0;
|
_requestId = 0;
|
||||||
if (error.type() == u"BOT_INVALID"_q) {
|
if (error.type() == u"BOT_INVALID"_q) {
|
||||||
|
@ -850,6 +839,129 @@ void AttachWebView::requestMenu(
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void AttachWebView::requestApp(
|
||||||
|
not_null<Window::SessionController*> controller,
|
||||||
|
const Api::SendAction &action,
|
||||||
|
not_null<UserData*> bot,
|
||||||
|
const QString &appName,
|
||||||
|
const QString &startParam,
|
||||||
|
bool forceConfirmation) {
|
||||||
|
const auto context = LookupContext(controller, action);
|
||||||
|
if (_requestId
|
||||||
|
&& _bot == bot
|
||||||
|
&& _startCommand == startParam
|
||||||
|
&& _botAppName == appName
|
||||||
|
&& IsSame(_context, context)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
cancel();
|
||||||
|
_bot = bot;
|
||||||
|
_startCommand = startParam;
|
||||||
|
_botAppName = appName;
|
||||||
|
_context = std::make_unique<Context>(context);
|
||||||
|
_context->fromBotApp = true;
|
||||||
|
const auto already = _session->data().findBotApp(_bot->id, appName);
|
||||||
|
_requestId = _session->api().request(MTPmessages_GetBotApp(
|
||||||
|
MTP_inputBotAppShortName(
|
||||||
|
bot->inputUser,
|
||||||
|
MTP_string(appName)),
|
||||||
|
MTP_long(already ? already->hash : 0)
|
||||||
|
)).done([=](const MTPmessages_BotApp &result) {
|
||||||
|
_requestId = 0;
|
||||||
|
if (!_bot || !_context) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const auto &data = result.data();
|
||||||
|
const auto firstTime = data.is_inactive();
|
||||||
|
const auto received = _session->data().processBotApp(
|
||||||
|
_bot->id,
|
||||||
|
data.vapp());
|
||||||
|
_app = received ? received : already;
|
||||||
|
if (!_app) {
|
||||||
|
cancel();
|
||||||
|
showToast(tr::lng_username_app_not_found(tr::now));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const auto confirm = firstTime || forceConfirmation;
|
||||||
|
if (confirm) {
|
||||||
|
confirmAppOpen(result.data().is_request_write_access());
|
||||||
|
} else {
|
||||||
|
requestAppView(false);
|
||||||
|
}
|
||||||
|
}).fail([=] {
|
||||||
|
cancel();
|
||||||
|
showToast(tr::lng_username_app_not_found(tr::now));
|
||||||
|
}).send();
|
||||||
|
}
|
||||||
|
|
||||||
|
void AttachWebView::confirmAppOpen(bool requestWriteAccess) {
|
||||||
|
const auto controller = _context ? _context->controller.get() : nullptr;
|
||||||
|
if (!controller || !_bot) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
controller->show(Box([=](not_null<Ui::GenericBox*> box) {
|
||||||
|
const auto allowed = std::make_shared<Ui::Checkbox*>();
|
||||||
|
const auto done = [=](Fn<void()> close) {
|
||||||
|
requestAppView((*allowed) && (*allowed)->checked());
|
||||||
|
close();
|
||||||
|
};
|
||||||
|
Ui::ConfirmBox(box, {
|
||||||
|
tr::lng_allow_bot_webview(
|
||||||
|
tr::now,
|
||||||
|
lt_bot_name,
|
||||||
|
Ui::Text::Bold(_bot->name()),
|
||||||
|
Ui::Text::RichLangValue),
|
||||||
|
done,
|
||||||
|
});
|
||||||
|
if (requestWriteAccess) {
|
||||||
|
(*allowed) = box->addRow(
|
||||||
|
object_ptr<Ui::Checkbox>(
|
||||||
|
box,
|
||||||
|
tr::lng_url_auth_allow_messages(
|
||||||
|
tr::now,
|
||||||
|
lt_bot,
|
||||||
|
Ui::Text::Bold(_bot->name()),
|
||||||
|
Ui::Text::WithEntities),
|
||||||
|
true,
|
||||||
|
st::urlAuthCheckbox),
|
||||||
|
style::margins(
|
||||||
|
st::boxRowPadding.left(),
|
||||||
|
st::boxPhotoCaptionSkip,
|
||||||
|
st::boxRowPadding.right(),
|
||||||
|
st::boxPhotoCaptionSkip));
|
||||||
|
(*allowed)->setAllowTextLines();
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
void AttachWebView::requestAppView(bool allowWrite) {
|
||||||
|
if (!_context || !_app) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
using Flag = MTPmessages_RequestAppWebView::Flag;
|
||||||
|
const auto flags = Flag::f_theme_params
|
||||||
|
| (_startCommand.isEmpty() ? Flag(0) : Flag::f_start_param)
|
||||||
|
| (allowWrite ? Flag::f_write_allowed : Flag(0));
|
||||||
|
_requestId = _session->api().request(MTPmessages_RequestAppWebView(
|
||||||
|
MTP_flags(flags),
|
||||||
|
_context->action.history->peer->input,
|
||||||
|
MTP_inputBotAppID(MTP_long(_app->id), MTP_long(_app->accessHash)),
|
||||||
|
MTP_string(_startCommand),
|
||||||
|
MTP_dataJSON(MTP_bytes(Window::Theme::WebViewParams().json)),
|
||||||
|
MTP_string("tdesktop")
|
||||||
|
)).done([=](const MTPAppWebViewResult &result) {
|
||||||
|
_requestId = 0;
|
||||||
|
const auto &data = result.data();
|
||||||
|
const auto queryId = uint64();
|
||||||
|
show(queryId, qs(data.vurl()));
|
||||||
|
}).fail([=](const MTP::Error &error) {
|
||||||
|
_requestId = 0;
|
||||||
|
if (error.type() == u"BOT_INVALID"_q) {
|
||||||
|
requestBots();
|
||||||
|
}
|
||||||
|
}).send();
|
||||||
|
}
|
||||||
|
|
||||||
void AttachWebView::confirmOpen(
|
void AttachWebView::confirmOpen(
|
||||||
not_null<Window::SessionController*> controller,
|
not_null<Window::SessionController*> controller,
|
||||||
Fn<void()> done) {
|
Fn<void()> done) {
|
||||||
|
@ -895,6 +1007,7 @@ void AttachWebView::show(
|
||||||
const auto sendData = crl::guard(this, [=](QByteArray data) {
|
const auto sendData = crl::guard(this, [=](QByteArray data) {
|
||||||
if (!_context
|
if (!_context
|
||||||
|| _context->fromSwitch
|
|| _context->fromSwitch
|
||||||
|
|| _context->fromBotApp
|
||||||
|| _context->action.history->peer != _bot
|
|| _context->action.history->peer != _bot
|
||||||
|| queryId) {
|
|| queryId) {
|
||||||
return;
|
return;
|
||||||
|
@ -1061,7 +1174,7 @@ void AttachWebView::show(
|
||||||
void AttachWebView::started(uint64 queryId) {
|
void AttachWebView::started(uint64 queryId) {
|
||||||
Expects(_bot != nullptr && _context != nullptr);
|
Expects(_bot != nullptr && _context != nullptr);
|
||||||
|
|
||||||
if (_context->fromSwitch) {
|
if (_context->fromSwitch || !queryId) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1098,6 +1211,24 @@ void AttachWebView::started(uint64 queryId) {
|
||||||
}, _panel->lifetime());
|
}, _panel->lifetime());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void AttachWebView::showToast(
|
||||||
|
const QString &text,
|
||||||
|
Window::SessionController *controller) {
|
||||||
|
const auto strong = controller
|
||||||
|
? controller
|
||||||
|
: _context
|
||||||
|
? _context->controller.get()
|
||||||
|
: _addToMenuContext
|
||||||
|
? _addToMenuContext->controller.get()
|
||||||
|
: nullptr;
|
||||||
|
Ui::ShowMultilineToast({
|
||||||
|
.parentOverride = (strong
|
||||||
|
? Window::Show(strong).toastParent().get()
|
||||||
|
: nullptr),
|
||||||
|
.text = { text },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
void AttachWebView::confirmAddToMenu(
|
void AttachWebView::confirmAddToMenu(
|
||||||
AttachWebViewBot bot,
|
AttachWebViewBot bot,
|
||||||
Fn<void()> callback) {
|
Fn<void()> callback) {
|
||||||
|
@ -1115,9 +1246,7 @@ void AttachWebView::confirmAddToMenu(
|
||||||
if (callback) {
|
if (callback) {
|
||||||
callback();
|
callback();
|
||||||
}
|
}
|
||||||
Ui::ShowMultilineToast({
|
showToast(tr::lng_bot_add_to_menu_done(tr::now));
|
||||||
.text = { tr::lng_bot_add_to_menu_done(tr::now) },
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
close();
|
close();
|
||||||
};
|
};
|
||||||
|
|
|
@ -93,6 +93,13 @@ public:
|
||||||
void requestMenu(
|
void requestMenu(
|
||||||
not_null<Window::SessionController*> controller,
|
not_null<Window::SessionController*> controller,
|
||||||
not_null<UserData*> bot);
|
not_null<UserData*> bot);
|
||||||
|
void requestApp(
|
||||||
|
not_null<Window::SessionController*> controller,
|
||||||
|
const Api::SendAction &action,
|
||||||
|
not_null<UserData*> bot,
|
||||||
|
const QString &appName,
|
||||||
|
const QString &startParam,
|
||||||
|
bool forceConfirmation);
|
||||||
|
|
||||||
void cancel();
|
void cancel();
|
||||||
|
|
||||||
|
@ -162,14 +169,22 @@ private:
|
||||||
void confirmAddToMenu(
|
void confirmAddToMenu(
|
||||||
AttachWebViewBot bot,
|
AttachWebViewBot bot,
|
||||||
Fn<void()> callback = nullptr);
|
Fn<void()> callback = nullptr);
|
||||||
|
void confirmAppOpen(bool requestWriteAccess);
|
||||||
|
void requestAppView(bool allowWrite);
|
||||||
void started(uint64 queryId);
|
void started(uint64 queryId);
|
||||||
|
|
||||||
|
void showToast(
|
||||||
|
const QString &text,
|
||||||
|
Window::SessionController *controller = nullptr);
|
||||||
|
|
||||||
const not_null<Main::Session*> _session;
|
const not_null<Main::Session*> _session;
|
||||||
|
|
||||||
std::unique_ptr<Context> _context;
|
std::unique_ptr<Context> _context;
|
||||||
UserData *_bot = nullptr;
|
UserData *_bot = nullptr;
|
||||||
QString _botUsername;
|
QString _botUsername;
|
||||||
|
QString _botAppName;
|
||||||
QString _startCommand;
|
QString _startCommand;
|
||||||
|
BotAppData *_app = nullptr;
|
||||||
QPointer<Ui::GenericBox> _confirmAddBox;
|
QPointer<Ui::GenericBox> _confirmAddBox;
|
||||||
|
|
||||||
mtpRequestId _requestId = 0;
|
mtpRequestId _requestId = 0;
|
||||||
|
|
|
@ -447,7 +447,7 @@ void Inner::refreshMosaicOffset() {
|
||||||
const auto top = _switchPmButton
|
const auto top = _switchPmButton
|
||||||
? (_switchPmButton->height() + st::inlineResultsSkip)
|
? (_switchPmButton->height() + st::inlineResultsSkip)
|
||||||
: 0;
|
: 0;
|
||||||
_mosaic.setPadding(st::gifsPadding + QMargins(0, top, 0, 0));
|
_mosaic.setPadding(st::emojiPanMargins + QMargins(0, top, 0, 0));
|
||||||
}
|
}
|
||||||
|
|
||||||
void Inner::refreshSwitchPmButton(const CacheEntry *entry) {
|
void Inner::refreshSwitchPmButton(const CacheEntry *entry) {
|
||||||
|
|
|
@ -357,6 +357,15 @@ void SessionNavigation::showPeerByLinkResolved(
|
||||||
using Scope = AddBotToGroupBoxController::Scope;
|
using Scope = AddBotToGroupBoxController::Scope;
|
||||||
const auto user = peer->asUser();
|
const auto user = peer->asUser();
|
||||||
const auto bot = (user && user->isBot()) ? user : nullptr;
|
const auto bot = (user && user->isBot()) ? user : nullptr;
|
||||||
|
|
||||||
|
// t.me/username/012345 - we thought it was a channel post link, but
|
||||||
|
// after resolving the username we found out it is a bot.
|
||||||
|
const auto resolveType = (bot
|
||||||
|
&& !info.botAppName.isEmpty()
|
||||||
|
&& info.resolveType == ResolveType::Default)
|
||||||
|
? ResolveType::BotApp
|
||||||
|
: info.resolveType;
|
||||||
|
|
||||||
const auto &replies = info.repliesInfo;
|
const auto &replies = info.repliesInfo;
|
||||||
if (const auto threadId = std::get_if<ThreadId>(&replies)) {
|
if (const auto threadId = std::get_if<ThreadId>(&replies)) {
|
||||||
showRepliesForMessage(
|
showRepliesForMessage(
|
||||||
|
@ -389,14 +398,29 @@ void SessionNavigation::showPeerByLinkResolved(
|
||||||
info.messageId,
|
info.messageId,
|
||||||
callback);
|
callback);
|
||||||
}
|
}
|
||||||
} else if (bot && info.resolveType == ResolveType::ShareGame) {
|
} else if (bot && resolveType == ResolveType::BotApp) {
|
||||||
|
const auto itemId = info.clickFromMessageId;
|
||||||
|
const auto item = _session->data().message(itemId);
|
||||||
|
const auto contextPeer = item
|
||||||
|
? item->history()->peer
|
||||||
|
: bot;
|
||||||
|
crl::on_main(this, [=] {
|
||||||
|
bot->session().attachWebView().requestApp(
|
||||||
|
parentController(),
|
||||||
|
Api::SendAction(bot->owner().history(contextPeer)),
|
||||||
|
bot,
|
||||||
|
info.botAppName,
|
||||||
|
info.startToken,
|
||||||
|
info.botAppForceConfirmation);
|
||||||
|
});
|
||||||
|
} else if (bot && resolveType == ResolveType::ShareGame) {
|
||||||
Window::ShowShareGameBox(parentController(), bot, info.startToken);
|
Window::ShowShareGameBox(parentController(), bot, info.startToken);
|
||||||
} else if (bot
|
} else if (bot
|
||||||
&& (info.resolveType == ResolveType::AddToGroup
|
&& (resolveType == ResolveType::AddToGroup
|
||||||
|| info.resolveType == ResolveType::AddToChannel)) {
|
|| resolveType == ResolveType::AddToChannel)) {
|
||||||
const auto scope = (info.resolveType == ResolveType::AddToGroup)
|
const auto scope = (resolveType == ResolveType::AddToGroup)
|
||||||
? (info.startAdminRights ? Scope::GroupAdmin : Scope::All)
|
? (info.startAdminRights ? Scope::GroupAdmin : Scope::All)
|
||||||
: (info.resolveType == ResolveType::AddToChannel)
|
: (resolveType == ResolveType::AddToChannel)
|
||||||
? Scope::ChannelAdmin
|
? Scope::ChannelAdmin
|
||||||
: Scope::None;
|
: Scope::None;
|
||||||
Assert(scope != Scope::None);
|
Assert(scope != Scope::None);
|
||||||
|
@ -407,7 +431,7 @@ void SessionNavigation::showPeerByLinkResolved(
|
||||||
scope,
|
scope,
|
||||||
info.startToken,
|
info.startToken,
|
||||||
info.startAdminRights);
|
info.startAdminRights);
|
||||||
} else if (info.resolveType == ResolveType::Mention) {
|
} else if (resolveType == ResolveType::Mention) {
|
||||||
if (bot || peer->isChannel()) {
|
if (bot || peer->isChannel()) {
|
||||||
crl::on_main(this, [=] {
|
crl::on_main(this, [=] {
|
||||||
showPeerHistory(peer, params);
|
showPeerHistory(peer, params);
|
||||||
|
|
|
@ -97,6 +97,7 @@ inline constexpr bool is_flag_type(GifPauseReason) { return true; };
|
||||||
|
|
||||||
enum class ResolveType {
|
enum class ResolveType {
|
||||||
Default,
|
Default,
|
||||||
|
BotApp,
|
||||||
BotStart,
|
BotStart,
|
||||||
AddToGroup,
|
AddToGroup,
|
||||||
AddToChannel,
|
AddToChannel,
|
||||||
|
@ -208,6 +209,8 @@ public:
|
||||||
QString startToken;
|
QString startToken;
|
||||||
ChatAdminRights startAdminRights;
|
ChatAdminRights startAdminRights;
|
||||||
bool startAutoSubmit = false;
|
bool startAutoSubmit = false;
|
||||||
|
QString botAppName;
|
||||||
|
bool botAppForceConfirmation = false;
|
||||||
QString attachBotUsername;
|
QString attachBotUsername;
|
||||||
std::optional<QString> attachBotToggleCommand;
|
std::optional<QString> attachBotToggleCommand;
|
||||||
InlineBots::PeerTypes attachBotChooseTypes;
|
InlineBots::PeerTypes attachBotChooseTypes;
|
||||||
|
|
Loading…
Add table
Reference in a new issue