mirror of
https://github.com/Qz3rK/tdesktop.git
synced 2026-07-03 14:15:08 +02:00
Harden interaction app<->shell<->webapp.
This commit is contained in:
@@ -45,25 +45,80 @@
|
||||
verifiedBadge: null,
|
||||
menuPalette: null
|
||||
};
|
||||
const shellToken = TDESKTOP_SHELL_TOKEN_PLACEHOLDER;
|
||||
const nativeMessageType = 'tdesktop_external_bot_webapp';
|
||||
const maxPendingEvents = 64;
|
||||
let iframe = null;
|
||||
let frameLoaded = false;
|
||||
let frameUrl = 'about:blank';
|
||||
let frameGeneration = 0;
|
||||
let reloadSupported = false;
|
||||
let reloadTimeout = null;
|
||||
let viewportScheduled = false;
|
||||
let resizeObserver = null;
|
||||
const pendingEvents = [];
|
||||
|
||||
function invoke(eventType, eventData) {
|
||||
if (window.external && window.external.invoke) {
|
||||
window.external.invoke(JSON.stringify([
|
||||
eventType,
|
||||
JSON.stringify(eventData || {})
|
||||
]));
|
||||
function normalizeEventData(eventData) {
|
||||
if (typeof eventData === 'string') {
|
||||
try {
|
||||
const parsed = JSON.parse(eventData);
|
||||
return parsed
|
||||
&& typeof parsed === 'object'
|
||||
&& !Array.isArray(parsed)
|
||||
? parsed
|
||||
: {};
|
||||
} catch (e) {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
return eventData
|
||||
&& typeof eventData === 'object'
|
||||
&& !Array.isArray(eventData)
|
||||
? eventData
|
||||
: {};
|
||||
}
|
||||
|
||||
function sendToFrame(eventType, eventData) {
|
||||
function isShellOrigin() {
|
||||
return window.location.protocol === 'https:'
|
||||
&& window.location.hostname === 'web.telegram.org'
|
||||
&& (!window.location.port || window.location.port === '443');
|
||||
}
|
||||
|
||||
function invokeNative(source, eventType, eventData) {
|
||||
if (!window.external
|
||||
|| typeof window.external.invoke !== 'function'
|
||||
|| typeof shellToken !== 'string'
|
||||
|| !shellToken
|
||||
|| typeof eventType !== 'string') {
|
||||
return;
|
||||
}
|
||||
if (source === 'shell' && !isShellOrigin()) {
|
||||
return;
|
||||
}
|
||||
window.external.invoke(JSON.stringify({
|
||||
type: nativeMessageType,
|
||||
source: source,
|
||||
token: shellToken,
|
||||
origin: window.location.origin,
|
||||
eventType: eventType,
|
||||
eventData: normalizeEventData(eventData)
|
||||
}));
|
||||
}
|
||||
|
||||
function invokeShell(eventType, eventData) {
|
||||
invokeNative('shell', eventType, eventData);
|
||||
}
|
||||
|
||||
function invokeWebApp(eventType, eventData) {
|
||||
invokeNative('webapp', eventType, eventData);
|
||||
}
|
||||
|
||||
function sendToFrame(eventType, eventData, generation) {
|
||||
if (!iframe
|
||||
|| !iframe.contentWindow
|
||||
|| generation !== frameGeneration) {
|
||||
return;
|
||||
}
|
||||
iframe.contentWindow.postMessage(JSON.stringify({
|
||||
eventType: eventType,
|
||||
eventData: eventData || {}
|
||||
@@ -71,14 +126,19 @@
|
||||
}
|
||||
|
||||
function postToFrame(eventType, eventData) {
|
||||
const generation = frameGeneration;
|
||||
if (!iframe || !iframe.contentWindow || !frameLoaded) {
|
||||
pendingEvents.push({
|
||||
generation: generation,
|
||||
eventType: eventType,
|
||||
eventData: eventData || {}
|
||||
});
|
||||
while (pendingEvents.length > maxPendingEvents) {
|
||||
pendingEvents.shift();
|
||||
}
|
||||
return;
|
||||
}
|
||||
sendToFrame(eventType, eventData);
|
||||
sendToFrame(eventType, eventData, generation);
|
||||
}
|
||||
|
||||
function shellPointerPayload(event, extra) {
|
||||
@@ -102,11 +162,12 @@
|
||||
if (shellState.blocked
|
||||
|| shellState.isFullscreen
|
||||
|| event.defaultPrevented
|
||||
|| !event.isTrusted
|
||||
|| event.button !== 0) {
|
||||
return;
|
||||
}
|
||||
closeMenu();
|
||||
invoke(command, shellPointerPayload(event, extra));
|
||||
invokeShell(command, shellPointerPayload(event, extra));
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
@@ -117,11 +178,11 @@
|
||||
&& target.closest('.title-control, #menu')) {
|
||||
return;
|
||||
}
|
||||
beginShellControl('tdesktop_shell_begin_move', event);
|
||||
beginShellControl('shell_begin_move', event);
|
||||
}
|
||||
|
||||
function beginShellResize(edge, event) {
|
||||
beginShellControl('tdesktop_shell_begin_resize', event, {
|
||||
beginShellControl('shell_begin_resize', event, {
|
||||
edge: edge
|
||||
});
|
||||
}
|
||||
@@ -129,7 +190,9 @@
|
||||
function flushPendingEvents() {
|
||||
const pending = pendingEvents.splice(0);
|
||||
for (const event of pending) {
|
||||
postToFrame(event.eventType, event.eventData);
|
||||
if (event.generation === frameGeneration) {
|
||||
sendToFrame(event.eventType, event.eventData, event.generation);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -497,7 +560,7 @@
|
||||
return;
|
||||
}
|
||||
state.iconRequestGeneration = state.iconGeneration;
|
||||
invoke('tdesktop_shell_request_button_icon', {
|
||||
invokeShell('shell_request_button_icon', {
|
||||
name: state.name
|
||||
});
|
||||
}
|
||||
@@ -585,9 +648,9 @@
|
||||
if (clickable) {
|
||||
node.type = 'button';
|
||||
setupRipple(node);
|
||||
node.addEventListener('click', function() {
|
||||
if (!shellState.blocked) {
|
||||
invoke('tdesktop_shell_menu_action', { id: item.id });
|
||||
node.addEventListener('click', function(event) {
|
||||
if (!shellState.blocked && event.isTrusted) {
|
||||
invokeShell('shell_menu_action', { id: item.id });
|
||||
closeMenu();
|
||||
}
|
||||
});
|
||||
@@ -692,7 +755,10 @@
|
||||
renderMenu();
|
||||
}
|
||||
|
||||
function toggleMenu() {
|
||||
function toggleMenu(event) {
|
||||
if (event && !event.isTrusted) {
|
||||
return;
|
||||
}
|
||||
if (shellState.blocked) {
|
||||
return;
|
||||
}
|
||||
@@ -700,7 +766,7 @@
|
||||
closeMenu();
|
||||
return;
|
||||
}
|
||||
invoke('tdesktop_shell_menu_request', {});
|
||||
invokeShell('shell_menu_request', {});
|
||||
shellState.menuOpen = true;
|
||||
renderMenu();
|
||||
}
|
||||
@@ -713,7 +779,9 @@
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return data && typeof data === 'object' ? data : null;
|
||||
return data && typeof data === 'object' && !Array.isArray(data)
|
||||
? data
|
||||
: null;
|
||||
}
|
||||
|
||||
function addRipple(button, x, y) {
|
||||
@@ -764,23 +832,27 @@
|
||||
}
|
||||
|
||||
window.addEventListener('message', function(event) {
|
||||
if (!iframe || event.source !== iframe.contentWindow) {
|
||||
if (!iframe
|
||||
|| !iframe.contentWindow
|
||||
|| event.source !== iframe.contentWindow) {
|
||||
return;
|
||||
}
|
||||
const message = parseFrameMessage(event.data);
|
||||
if (!message || !message.eventType) {
|
||||
if (!message || typeof message.eventType !== 'string') {
|
||||
return;
|
||||
}
|
||||
if (message.eventType === 'iframe_ready') {
|
||||
reloadSupported = !!(message.eventData && message.eventData.reload_supported);
|
||||
return;
|
||||
} else if (message.eventType === 'iframe_will_reload') {
|
||||
if (reloadTimeout) {
|
||||
window.clearTimeout(reloadTimeout);
|
||||
reloadTimeout = null;
|
||||
}
|
||||
frameLoaded = false;
|
||||
return;
|
||||
}
|
||||
invoke(message.eventType, message.eventData || {});
|
||||
invokeWebApp(message.eventType, message.eventData);
|
||||
});
|
||||
|
||||
menuBackdrop.addEventListener('mousedown', closeMenu);
|
||||
@@ -812,8 +884,10 @@
|
||||
resizeObserver.observe(root);
|
||||
}
|
||||
|
||||
controls.close.addEventListener('click', function() {
|
||||
invoke('tdesktop_shell_close', {});
|
||||
controls.close.addEventListener('click', function(event) {
|
||||
if (event.isTrusted) {
|
||||
invokeShell('shell_close', {});
|
||||
}
|
||||
});
|
||||
controls.back.addEventListener('click', function() {
|
||||
postToFrame('back_button_pressed', {});
|
||||
@@ -838,10 +912,13 @@
|
||||
window.clearTimeout(reloadTimeout);
|
||||
reloadTimeout = null;
|
||||
}
|
||||
const generation = ++frameGeneration;
|
||||
pendingEvents.splice(0);
|
||||
const next = document.createElement('iframe');
|
||||
next.setAttribute('allow', 'clipboard-read; clipboard-write; fullscreen');
|
||||
next.referrerPolicy = 'no-referrer';
|
||||
next.addEventListener('load', function() {
|
||||
if (iframe !== next) {
|
||||
if (iframe !== next || generation !== frameGeneration) {
|
||||
return;
|
||||
}
|
||||
frameLoaded = true;
|
||||
@@ -867,7 +944,7 @@
|
||||
return;
|
||||
}
|
||||
if (reloadSupported && frameLoaded && iframe.contentWindow) {
|
||||
sendToFrame('reload_iframe', {});
|
||||
sendToFrame('reload_iframe', {}, frameGeneration);
|
||||
if (reloadTimeout) {
|
||||
window.clearTimeout(reloadTimeout);
|
||||
}
|
||||
@@ -880,8 +957,15 @@
|
||||
fallbackReloadFrame();
|
||||
}
|
||||
|
||||
window.TelegramDesktopShell = {
|
||||
bootstrap: function(data) {
|
||||
function isNativeToken(token) {
|
||||
return !!shellToken && token === shellToken;
|
||||
}
|
||||
|
||||
const api = {
|
||||
bootstrap: function(data, token) {
|
||||
if (!isNativeToken(token)) {
|
||||
return;
|
||||
}
|
||||
applyMetrics(data && data.metrics);
|
||||
applyColors(data && data.colors);
|
||||
applyChrome(data || {});
|
||||
@@ -893,7 +977,10 @@
|
||||
renderButtons();
|
||||
renderMenu();
|
||||
},
|
||||
nativeEvent: function(eventType, eventData) {
|
||||
nativeEvent: function(eventType, eventData, token) {
|
||||
if (!isNativeToken(token) || typeof eventType !== 'string') {
|
||||
return;
|
||||
}
|
||||
if (eventType === 'fullscreen_changed' && eventData) {
|
||||
shellState.isFullscreen = !!eventData.is_fullscreen;
|
||||
root.classList.toggle('fullscreen', shellState.isFullscreen);
|
||||
@@ -901,31 +988,52 @@
|
||||
}
|
||||
postToFrame(eventType, eventData || {});
|
||||
},
|
||||
setTitle: function(data) {
|
||||
setTitle: function(data, token) {
|
||||
if (!isNativeToken(token)) {
|
||||
return;
|
||||
}
|
||||
title.textContent = (data && data.title) || '';
|
||||
document.title = (data && data.title) || 'Telegram';
|
||||
},
|
||||
setChrome: function(data) {
|
||||
setChrome: function(data, token) {
|
||||
if (!isNativeToken(token)) {
|
||||
return;
|
||||
}
|
||||
applyChrome(data || {});
|
||||
},
|
||||
setColors: function(data) {
|
||||
setColors: function(data, token) {
|
||||
if (!isNativeToken(token)) {
|
||||
return;
|
||||
}
|
||||
applyColors(data || {});
|
||||
},
|
||||
setAssets: function(data) {
|
||||
setAssets: function(data, token) {
|
||||
if (!isNativeToken(token)) {
|
||||
return;
|
||||
}
|
||||
applyAssets(data || {});
|
||||
},
|
||||
setMenu: function(data) {
|
||||
setMenu: function(data, token) {
|
||||
if (!isNativeToken(token)) {
|
||||
return;
|
||||
}
|
||||
shellState.menuItems = Array.isArray(data && data.items)
|
||||
? data.items
|
||||
: [];
|
||||
applyChrome({});
|
||||
renderMenu();
|
||||
},
|
||||
setBottomText: function(data) {
|
||||
setBottomText: function(data, token) {
|
||||
if (!isNativeToken(token)) {
|
||||
return;
|
||||
}
|
||||
shellState.bottomText = '';
|
||||
updateFooter();
|
||||
},
|
||||
setButton: function(data) {
|
||||
setButton: function(data, token) {
|
||||
if (!isNativeToken(token)) {
|
||||
return;
|
||||
}
|
||||
if (!data || !data.name) {
|
||||
return;
|
||||
}
|
||||
@@ -945,7 +1053,10 @@
|
||||
shellState.buttons[data.name] = next;
|
||||
renderButtons();
|
||||
},
|
||||
setButtonIcon: function(data) {
|
||||
setButtonIcon: function(data, token) {
|
||||
if (!isNativeToken(token)) {
|
||||
return;
|
||||
}
|
||||
if (!data || !data.name) {
|
||||
return;
|
||||
}
|
||||
@@ -961,17 +1072,38 @@
|
||||
state.iconUrl = (icon && icon.url) ? icon.url : '';
|
||||
renderButtons();
|
||||
},
|
||||
setBlocked: function(data) {
|
||||
setBlocked: function(data, token) {
|
||||
if (!isNativeToken(token)) {
|
||||
return;
|
||||
}
|
||||
shellState.blocked = !!(data && data.blocked);
|
||||
root.classList.toggle('blocked', shellState.blocked);
|
||||
if (shellState.blocked) {
|
||||
closeMenu();
|
||||
}
|
||||
},
|
||||
setProgress: function(data) {
|
||||
setProgress: function(data, token) {
|
||||
if (!isNativeToken(token)) {
|
||||
return;
|
||||
}
|
||||
root.classList.toggle('loading', !!(data && data.shown));
|
||||
},
|
||||
reloadFrame: reloadFrame,
|
||||
sendViewport: scheduleViewport
|
||||
reloadFrame: function(data, token) {
|
||||
if (!isNativeToken(token)) {
|
||||
return;
|
||||
}
|
||||
reloadFrame();
|
||||
},
|
||||
sendViewport: function(data, token) {
|
||||
if (!isNativeToken(token)) {
|
||||
return;
|
||||
}
|
||||
scheduleViewport();
|
||||
}
|
||||
};
|
||||
Object.defineProperty(window, 'TelegramDesktopShell', {
|
||||
value: Object.freeze(api),
|
||||
configurable: false,
|
||||
writable: false
|
||||
});
|
||||
})();
|
||||
|
||||
@@ -36,6 +36,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#include "base/invoke_queued.h"
|
||||
#include "base/options.h"
|
||||
#include "base/qt_signal_producer.h"
|
||||
#include "base/random.h"
|
||||
#include "styles/style_chat.h"
|
||||
#include "styles/style_info.h"
|
||||
#include "styles/style_payments.h"
|
||||
@@ -46,6 +47,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#include <QtCore/QJsonDocument>
|
||||
#include <QtCore/QJsonObject>
|
||||
#include <QtCore/QJsonArray>
|
||||
#include <QtCore/QUrl>
|
||||
#include <QtGui/QGuiApplication>
|
||||
#include <QtGui/QClipboard>
|
||||
#include <QtGui/QWindow>
|
||||
@@ -66,6 +68,43 @@ constexpr auto kProgressOpacity = 0.3;
|
||||
constexpr auto kLightnessThreshold = 128;
|
||||
constexpr auto kLightnessDelta = 32;
|
||||
constexpr auto kExternalShellButtonIconSize = 20;
|
||||
constexpr auto kMaxNativeMessageBytes = 1024 * 1024;
|
||||
constexpr auto kExternalMessageType = "tdesktop_external_bot_webapp";
|
||||
|
||||
enum class NativeMessageSource {
|
||||
LegacyWebApp,
|
||||
ExternalWebApp,
|
||||
ExternalShell,
|
||||
};
|
||||
|
||||
struct NativeMessage {
|
||||
NativeMessageSource source = NativeMessageSource::LegacyWebApp;
|
||||
QString command;
|
||||
QJsonObject arguments;
|
||||
};
|
||||
|
||||
[[nodiscard]] QString GenerateExternalShellToken() {
|
||||
auto bytes = QByteArray();
|
||||
bytes.resize(32);
|
||||
base::RandomFill(bytes.data(), bytes.size());
|
||||
return QString::fromLatin1(bytes.toHex());
|
||||
}
|
||||
|
||||
[[nodiscard]] QString ExternalShellTopUrl() {
|
||||
return u"https://web.telegram.org:443/blank.html"_q;
|
||||
}
|
||||
|
||||
[[nodiscard]] bool IsExternalShellOrigin(const QString &origin) {
|
||||
const auto url = QUrl(origin);
|
||||
return url.isValid()
|
||||
&& url.scheme() == u"https"_q
|
||||
&& url.host() == u"web.telegram.org"_q
|
||||
&& url.port(443) == 443
|
||||
&& url.userInfo().isEmpty()
|
||||
&& url.path().isEmpty()
|
||||
&& url.query().isEmpty()
|
||||
&& url.fragment().isEmpty();
|
||||
}
|
||||
|
||||
base::options::toggle OptionLinuxExternalBotWebApps({
|
||||
.id = kOptionLinuxExternalBotWebApps,
|
||||
@@ -89,17 +128,181 @@ base::options::toggle OptionLinuxExternalBotWebApps({
|
||||
return RectPart::Left;
|
||||
}
|
||||
|
||||
[[nodiscard]] QJsonObject ParseMethodArgs(const QString &json) {
|
||||
if (json.isEmpty()) {
|
||||
[[nodiscard]] bool CanParseArguments(QJsonValue value) {
|
||||
if (value.isObject()) {
|
||||
return true;
|
||||
} else if (!value.isString()) {
|
||||
return false;
|
||||
}
|
||||
auto error = QJsonParseError();
|
||||
const auto document = QJsonDocument::fromJson(
|
||||
value.toString().toUtf8(),
|
||||
&error);
|
||||
return (error.error == QJsonParseError::NoError) && document.isObject();
|
||||
}
|
||||
|
||||
[[nodiscard]] QJsonObject ParseArguments(QJsonValue value) {
|
||||
if (value.isObject()) {
|
||||
return value.toObject();
|
||||
} else if (!value.isString()) {
|
||||
return {};
|
||||
}
|
||||
auto error = QJsonParseError();
|
||||
const auto dictionary = QJsonDocument::fromJson(json.toUtf8(), &error);
|
||||
if (error.error != QJsonParseError::NoError) {
|
||||
LOG(("BotWebView Error: Could not parse \"%1\".").arg(json));
|
||||
const auto document = QJsonDocument::fromJson(
|
||||
value.toString().toUtf8(),
|
||||
&error);
|
||||
return (error.error == QJsonParseError::NoError && document.isObject())
|
||||
? document.object()
|
||||
: QJsonObject();
|
||||
}
|
||||
|
||||
[[nodiscard]] bool IsShellNamespaceCommand(const QString &command) {
|
||||
return command.startsWith(u"shell_"_q)
|
||||
|| command.startsWith(u"tdesktop_shell_"_q);
|
||||
}
|
||||
|
||||
[[nodiscard]] QString SafeCommandForLog(const QString &command) {
|
||||
if (command.isEmpty() || command.size() > 80) {
|
||||
return {};
|
||||
}
|
||||
return dictionary.object();
|
||||
if (!command.startsWith(u"web_app_"_q)
|
||||
&& !command.startsWith(u"shell_"_q)
|
||||
&& !command.startsWith(u"tdesktop_shell_"_q)
|
||||
&& command != u"share_score"_q) {
|
||||
return {};
|
||||
}
|
||||
const auto safe = std::all_of(
|
||||
command.begin(),
|
||||
command.end(),
|
||||
[](QChar ch) {
|
||||
return ch.isLetterOrNumber() || ch == QChar('_');
|
||||
});
|
||||
return safe ? command : QString();
|
||||
}
|
||||
|
||||
void LogNativeMessageRejected(
|
||||
const QString &reason,
|
||||
quint64 bytes,
|
||||
const QString &command = QString()) {
|
||||
const auto safeCommand = SafeCommandForLog(command);
|
||||
if (safeCommand.isEmpty()) {
|
||||
LOG(("BotWebView Error: Native message rejected: %1 (%2 bytes)."
|
||||
).arg(reason, QString::number(bytes)));
|
||||
} else {
|
||||
LOG(("BotWebView Error: Native message rejected: %1 "
|
||||
"(%2 bytes, command: %3)."
|
||||
).arg(reason, QString::number(bytes), safeCommand));
|
||||
}
|
||||
}
|
||||
|
||||
[[nodiscard]] std::optional<NativeMessage> ParseNativeMessage(
|
||||
const QByteArray &bytes,
|
||||
bool externalShell,
|
||||
const QString &shellToken) {
|
||||
const auto byteCount = quint64(bytes.size());
|
||||
if (bytes.size() > kMaxNativeMessageBytes) {
|
||||
LogNativeMessageRejected(u"payload too large"_q, byteCount);
|
||||
return std::nullopt;
|
||||
}
|
||||
auto error = QJsonParseError();
|
||||
const auto document = QJsonDocument::fromJson(bytes, &error);
|
||||
if (error.error != QJsonParseError::NoError) {
|
||||
LogNativeMessageRejected(u"invalid json"_q, byteCount);
|
||||
return std::nullopt;
|
||||
}
|
||||
if (externalShell) {
|
||||
if (!document.isObject()) {
|
||||
LogNativeMessageRejected(
|
||||
u"external payload is not an object"_q,
|
||||
byteCount);
|
||||
return std::nullopt;
|
||||
}
|
||||
const auto object = document.object();
|
||||
const auto command = object.value(u"eventType"_q).toString();
|
||||
const auto reject = [&](const QString &reason) {
|
||||
LogNativeMessageRejected(reason, byteCount, command);
|
||||
return std::optional<NativeMessage>();
|
||||
};
|
||||
if (object.value(u"type"_q).toString()
|
||||
!= QString::fromLatin1(kExternalMessageType)) {
|
||||
return reject(u"bad external type"_q);
|
||||
}
|
||||
const auto sourceText = object.value(u"source"_q).toString();
|
||||
auto source = NativeMessageSource::ExternalWebApp;
|
||||
if (sourceText == u"webapp"_q) {
|
||||
source = NativeMessageSource::ExternalWebApp;
|
||||
} else if (sourceText == u"shell"_q) {
|
||||
source = NativeMessageSource::ExternalShell;
|
||||
} else {
|
||||
return reject(u"bad external source"_q);
|
||||
}
|
||||
const auto token = object.value(u"token"_q);
|
||||
if (!token.isString()
|
||||
|| shellToken.isEmpty()
|
||||
|| token.toString() != shellToken) {
|
||||
return reject(u"bad external token"_q);
|
||||
}
|
||||
if (source == NativeMessageSource::ExternalShell) {
|
||||
const auto origin = object.value(u"origin"_q);
|
||||
if (!origin.isString()
|
||||
|| !IsExternalShellOrigin(origin.toString())) {
|
||||
return reject(u"bad shell origin"_q);
|
||||
}
|
||||
}
|
||||
if (!object.value(u"eventType"_q).isString() || command.isEmpty()) {
|
||||
return reject(u"bad command"_q);
|
||||
}
|
||||
const auto data = object.value(u"eventData"_q);
|
||||
if (!CanParseArguments(data)) {
|
||||
return reject(u"bad arguments"_q);
|
||||
}
|
||||
const auto arguments = ParseArguments(data);
|
||||
const auto shellCommand = IsShellNamespaceCommand(command);
|
||||
if (source == NativeMessageSource::ExternalShell) {
|
||||
if (!shellCommand) {
|
||||
return reject(u"non-shell command from shell"_q);
|
||||
}
|
||||
} else if (shellCommand) {
|
||||
return reject(u"shell command from webapp"_q);
|
||||
}
|
||||
return NativeMessage{
|
||||
.source = source,
|
||||
.command = command,
|
||||
.arguments = arguments,
|
||||
};
|
||||
}
|
||||
if (!document.isArray()) {
|
||||
LogNativeMessageRejected(
|
||||
u"legacy payload is not an array"_q,
|
||||
byteCount);
|
||||
return std::nullopt;
|
||||
}
|
||||
const auto list = document.array();
|
||||
const auto command = list.at(0).toString();
|
||||
const auto reject = [&](const QString &reason) {
|
||||
LogNativeMessageRejected(reason, byteCount, command);
|
||||
return std::optional<NativeMessage>();
|
||||
};
|
||||
if (!list.at(0).isString() || command.isEmpty()) {
|
||||
return reject(u"bad command"_q);
|
||||
} else if (IsShellNamespaceCommand(command)) {
|
||||
return reject(u"shell command from legacy"_q);
|
||||
}
|
||||
auto arguments = QJsonObject();
|
||||
if (list.size() > 1) {
|
||||
const auto value = list.at(1);
|
||||
if (!value.isNull()
|
||||
&& !value.isUndefined()
|
||||
&& !CanParseArguments(value)) {
|
||||
return reject(u"bad arguments"_q);
|
||||
}
|
||||
arguments = ParseArguments(value);
|
||||
}
|
||||
return NativeMessage{
|
||||
.source = NativeMessageSource::LegacyWebApp,
|
||||
.command = command,
|
||||
.arguments = arguments,
|
||||
};
|
||||
}
|
||||
|
||||
[[nodiscard]] bool UseExternalBotWebApps() {
|
||||
@@ -112,6 +315,12 @@ base::options::toggle OptionLinuxExternalBotWebApps({
|
||||
return (color.alpha() == 255) ? color : st::windowBg->c;
|
||||
}
|
||||
|
||||
[[nodiscard]] QJsonObject ThemeChangedPayload(
|
||||
const Webview::ThemeParams ¶ms) {
|
||||
const auto parsed = QJsonDocument::fromJson(params.json);
|
||||
return { { u"theme_params"_q, parsed.object() } };
|
||||
}
|
||||
|
||||
enum class SharedPanelMenuAction {
|
||||
None,
|
||||
Settings,
|
||||
@@ -1262,6 +1471,9 @@ void Panel::hideWebviewProgress() {
|
||||
bool Panel::showWebview(Args &&args, const Webview::ThemeParams ¶ms) {
|
||||
_bottomText = std::move(args.bottom);
|
||||
_externalUrl = args.url;
|
||||
if (_externalShell && !_webview) {
|
||||
resetExternalShellIdentity();
|
||||
}
|
||||
if (!_webview && !createWebview(params)) {
|
||||
return false;
|
||||
}
|
||||
@@ -1274,8 +1486,7 @@ bool Panel::showWebview(Args &&args, const Webview::ThemeParams ¶ms) {
|
||||
const auto url = args.url;
|
||||
if (_externalShell) {
|
||||
_externalShellBootstrapped = false;
|
||||
_webview->window.navigate(
|
||||
u"https://web.telegram.org/blank.html"_q);
|
||||
_webview->window.navigate(ExternalShellTopUrl());
|
||||
} else {
|
||||
_webview->window.navigate(url);
|
||||
_widget->setBackAllowed(allowBack);
|
||||
@@ -1390,11 +1601,20 @@ void Panel::sendExternalShellColors(const Webview::ThemeParams ¶ms) {
|
||||
LinuxShell::ColorPayload(externalShellColors(params)));
|
||||
}
|
||||
|
||||
void Panel::resetExternalShellIdentity() {
|
||||
_externalShellToken = GenerateExternalShellToken();
|
||||
++_externalShellGeneration;
|
||||
_externalShellBootstrapped = false;
|
||||
}
|
||||
|
||||
void Panel::installExternalShellDocument() {
|
||||
if (!_webview) {
|
||||
if (!_externalShell
|
||||
|| !_externalShellBootstrapped
|
||||
|| !_webview
|
||||
|| _externalShellToken.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
_webview->window.eval(LinuxShell::InstallScript());
|
||||
_webview->window.eval(LinuxShell::InstallScript(_externalShellToken));
|
||||
}
|
||||
|
||||
void Panel::sendExternalShellBootstrap() {
|
||||
@@ -1411,7 +1631,7 @@ void Panel::sendExternalShellBootstrap() {
|
||||
});
|
||||
sendExternalShellAssets();
|
||||
sendExternalShellMenu();
|
||||
postEvent("theme_changed", "{\"theme_params\": " + params.json + "}");
|
||||
postEvent("theme_changed", ThemeChangedPayload(params));
|
||||
sendFullScreen();
|
||||
sendSafeArea();
|
||||
sendContentSafeArea();
|
||||
@@ -1420,19 +1640,27 @@ void Panel::sendExternalShellBootstrap() {
|
||||
void Panel::sendExternalShellMethod(
|
||||
const QByteArray &method,
|
||||
const QJsonObject &data) {
|
||||
if (!_webview) {
|
||||
if (!_externalShell
|
||||
|| !_externalShellBootstrapped
|
||||
|| !_webview
|
||||
|| _externalShellToken.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
_webview->window.eval(LinuxShell::MethodCallScript(method, data));
|
||||
_webview->window.eval(
|
||||
LinuxShell::MethodCallScript(method, data, _externalShellToken));
|
||||
}
|
||||
|
||||
void Panel::sendExternalShellEvent(
|
||||
const QString &event,
|
||||
const QByteArray &data) {
|
||||
if (!_webview) {
|
||||
const QJsonObject &data) {
|
||||
if (!_externalShell
|
||||
|| !_externalShellBootstrapped
|
||||
|| !_webview
|
||||
|| _externalShellToken.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
_webview->window.eval(LinuxShell::EventScript(event, data));
|
||||
_webview->window.eval(
|
||||
LinuxShell::EventScript(event, data, _externalShellToken));
|
||||
}
|
||||
|
||||
void Panel::sendExternalShellButton(
|
||||
@@ -1591,13 +1819,15 @@ void Panel::handleExternalShellMenuAction(const QString &id) {
|
||||
.reload = [=] {
|
||||
if (_webview && _webview->window.widget()) {
|
||||
sendExternalShellMethod("reloadFrame", {});
|
||||
} else if (const auto params = _delegate->botThemeParams()
|
||||
; createWebview(params)) {
|
||||
} else {
|
||||
const auto params = _delegate->botThemeParams();
|
||||
resetExternalShellIdentity();
|
||||
if (!createWebview(params)) {
|
||||
return;
|
||||
}
|
||||
showWebviewProgress();
|
||||
updateThemeParams(params);
|
||||
_externalShellBootstrapped = false;
|
||||
_webview->window.navigate(
|
||||
u"https://web.telegram.org/blank.html"_q);
|
||||
_webview->window.navigate(ExternalShellTopUrl());
|
||||
}
|
||||
},
|
||||
.terms = [=] {
|
||||
@@ -1669,6 +1899,11 @@ Panel::ExternalShellAnchor Panel::externalShellAnchor() const {
|
||||
return result;
|
||||
}
|
||||
|
||||
QWidget *Panel::webviewWindowForPopup() const {
|
||||
const auto widget = _webview ? _webview->window.widget() : nullptr;
|
||||
return widget ? widget->window() : nullptr;
|
||||
}
|
||||
|
||||
Webview::PopupResult Panel::showBlockingPopup(Webview::PopupArgs &&args) {
|
||||
if (!_externalShell) {
|
||||
return Webview::ShowBlockingPopup(std::move(args));
|
||||
@@ -1757,12 +1992,19 @@ bool Panel::createWebview(const Webview::ThemeParams ¶ms) {
|
||||
.windowMargins = _externalShell
|
||||
? st::botWebViewShellShadowPadding
|
||||
: QMargins(),
|
||||
.shellMessageToken = _externalShell
|
||||
? _externalShellToken
|
||||
: QString(),
|
||||
});
|
||||
const auto raw = &_webview->window;
|
||||
|
||||
const auto bottom = _webviewBottom.get();
|
||||
QObject::connect(container, &QObject::destroyed, [=] {
|
||||
if (_webview && &_webview->window == raw) {
|
||||
if (_externalShell) {
|
||||
_externalShellBootstrapped = false;
|
||||
++_externalShellGeneration;
|
||||
}
|
||||
base::take(_webview);
|
||||
if (_webviewProgress) {
|
||||
hideWebviewProgress();
|
||||
@@ -1805,6 +2047,10 @@ bool Panel::createWebview(const Webview::ThemeParams ¶ms) {
|
||||
// we don't show any message, nothing crashed.
|
||||
return;
|
||||
}
|
||||
if (_externalShell) {
|
||||
_externalShellBootstrapped = false;
|
||||
++_externalShellGeneration;
|
||||
}
|
||||
crl::on_main(this, [=] {
|
||||
if (_externalShell) {
|
||||
_delegate->botClose();
|
||||
@@ -1832,30 +2078,54 @@ bool Panel::createWebview(const Webview::ThemeParams ¶ms) {
|
||||
}, _webview->lifetime);
|
||||
}
|
||||
|
||||
raw->setMessageHandler([=](const QJsonDocument &message) {
|
||||
if (!message.isArray()) {
|
||||
LOG(("BotWebView Error: "
|
||||
"Not an array received in buy_callback arguments."));
|
||||
raw->setMessageHandler([=](std::string text) {
|
||||
if (text.size() > size_t(kMaxNativeMessageBytes)) {
|
||||
LogNativeMessageRejected(
|
||||
u"payload too large"_q,
|
||||
quint64(text.size()));
|
||||
return;
|
||||
}
|
||||
const auto bytes = QByteArray::fromRawData(
|
||||
text.data(),
|
||||
int(text.size()));
|
||||
const auto parsed = ParseNativeMessage(
|
||||
bytes,
|
||||
_externalShell,
|
||||
_externalShellToken);
|
||||
if (!parsed) {
|
||||
return;
|
||||
}
|
||||
const auto &command = parsed->command;
|
||||
const auto &arguments = parsed->arguments;
|
||||
if (parsed->source == NativeMessageSource::ExternalShell) {
|
||||
if (!_externalShell || !_externalShellBootstrapped) {
|
||||
return;
|
||||
}
|
||||
if (command == "shell_close") {
|
||||
if (_closeNeedConfirmation) {
|
||||
scheduleCloseWithConfirmation();
|
||||
} else {
|
||||
_delegate->botClose();
|
||||
}
|
||||
} else if (command == "shell_menu_request") {
|
||||
if (_externalBlockCount <= 0) {
|
||||
sendExternalShellAssets();
|
||||
sendExternalShellMenu();
|
||||
}
|
||||
} else if (command == "shell_menu_action") {
|
||||
if (_externalBlockCount <= 0) {
|
||||
handleExternalShellMenuAction(arguments["id"].toString());
|
||||
}
|
||||
} else if (command == "shell_request_button_icon") {
|
||||
const auto name = arguments["name"];
|
||||
if (name.isString()) {
|
||||
requestExternalShellButtonEmoji(name.toString());
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
const auto list = message.array();
|
||||
const auto command = list.at(0).toString();
|
||||
const auto arguments = ParseMethodArgs(list.at(1).toString());
|
||||
if (command == "web_app_close") {
|
||||
_delegate->botClose();
|
||||
} else if (command == "tdesktop_shell_close") {
|
||||
if (_closeNeedConfirmation) {
|
||||
scheduleCloseWithConfirmation();
|
||||
} else {
|
||||
_delegate->botClose();
|
||||
}
|
||||
} else if (command == "tdesktop_shell_menu_request") {
|
||||
sendExternalShellAssets();
|
||||
sendExternalShellMenu();
|
||||
} else if (command == "tdesktop_shell_menu_action") {
|
||||
handleExternalShellMenuAction(arguments["id"].toString());
|
||||
} else if (command == "tdesktop_shell_request_button_icon") {
|
||||
requestExternalShellButtonEmoji(arguments["name"].toString());
|
||||
} else if (command == "web_app_data_send") {
|
||||
sendDataMessage(arguments);
|
||||
} else if (command == "web_app_switch_inline_query") {
|
||||
@@ -1891,21 +2161,33 @@ bool Panel::createWebview(const Webview::ThemeParams ¶ms) {
|
||||
sendFullScreen();
|
||||
}
|
||||
} else if (command == "web_app_check_home_screen") {
|
||||
postEvent("home_screen_checked", "{ status: \"unsupported\" }");
|
||||
postEvent("home_screen_checked", QJsonObject{
|
||||
{ u"status"_q, u"unsupported"_q },
|
||||
});
|
||||
} else if (command == "web_app_start_accelerometer") {
|
||||
postEvent("accelerometer_failed", "{ error: \"UNSUPPORTED\" }");
|
||||
postEvent("accelerometer_failed", QJsonObject{
|
||||
{ u"error"_q, u"UNSUPPORTED"_q },
|
||||
});
|
||||
} else if (command == "web_app_start_device_orientation") {
|
||||
postEvent(
|
||||
"device_orientation_failed",
|
||||
"{ error: \"UNSUPPORTED\" }");
|
||||
postEvent("device_orientation_failed", QJsonObject{
|
||||
{ u"error"_q, u"UNSUPPORTED"_q },
|
||||
});
|
||||
} else if (command == "web_app_start_gyroscope") {
|
||||
postEvent("gyroscope_failed", "{ error: \"UNSUPPORTED\" }");
|
||||
postEvent("gyroscope_failed", QJsonObject{
|
||||
{ u"error"_q, u"UNSUPPORTED"_q },
|
||||
});
|
||||
} else if (command == "web_app_check_location") {
|
||||
postEvent("location_checked", "{ available: false }");
|
||||
postEvent("location_checked", QJsonObject{
|
||||
{ u"available"_q, false },
|
||||
});
|
||||
} else if (command == "web_app_request_location") {
|
||||
postEvent("location_requested", "{ available: false }");
|
||||
postEvent("location_requested", QJsonObject{
|
||||
{ u"available"_q, false },
|
||||
});
|
||||
} else if (command == "web_app_biometry_get_info") {
|
||||
postEvent("biometry_info_received", "{ available: false }");
|
||||
postEvent("biometry_info_received", QJsonObject{
|
||||
{ u"available"_q, false },
|
||||
});
|
||||
} else if (command == "web_app_open_tg_link") {
|
||||
openTgLink(arguments);
|
||||
} else if (command == "web_app_open_link") {
|
||||
@@ -2059,14 +2341,18 @@ void Panel::sendViewport() {
|
||||
}
|
||||
|
||||
void Panel::sendFullScreen() {
|
||||
postEvent("fullscreen_changed", _fullscreen.current()
|
||||
? "{ is_fullscreen: true }"
|
||||
: "{ is_fullscreen: false }");
|
||||
postEvent("fullscreen_changed", QJsonObject{
|
||||
{ u"is_fullscreen"_q, _fullscreen.current() },
|
||||
});
|
||||
}
|
||||
|
||||
void Panel::sendSafeArea() {
|
||||
postEvent("safe_area_changed",
|
||||
"{ top: 0, right: 0, bottom: 0, left: 0 }");
|
||||
postEvent("safe_area_changed", QJsonObject{
|
||||
{ u"top"_q, 0 },
|
||||
{ u"right"_q, 0 },
|
||||
{ u"bottom"_q, 0 },
|
||||
{ u"left"_q, 0 },
|
||||
});
|
||||
}
|
||||
|
||||
void Panel::sendContentSafeArea() {
|
||||
@@ -2084,8 +2370,12 @@ void Panel::sendContentSafeArea() {
|
||||
const auto systemScreenScale = dpi * ratio / base;
|
||||
report = int(base::SafeRound(scaled / systemScreenScale));
|
||||
}
|
||||
postEvent("content_safe_area_changed",
|
||||
u"{ top: %1, right: 0, bottom: 0, left: 0 }"_q.arg(report));
|
||||
postEvent("content_safe_area_changed", QJsonObject{
|
||||
{ u"top"_q, report },
|
||||
{ u"right"_q, 0 },
|
||||
{ u"bottom"_q, 0 },
|
||||
{ u"left"_q, 0 },
|
||||
});
|
||||
}
|
||||
|
||||
void Panel::setTitle(rpl::producer<QString> title) {
|
||||
@@ -2155,9 +2445,9 @@ void Panel::processSendMessageRequest(const QJsonObject &args) {
|
||||
if (error.isEmpty()) {
|
||||
postEvent("prepared_message_sent");
|
||||
} else {
|
||||
postEvent(
|
||||
"prepared_message_failed",
|
||||
u"{ error: \"%1\" }"_q.arg(error));
|
||||
postEvent("prepared_message_failed", QJsonObject{
|
||||
{ u"error"_q, error },
|
||||
});
|
||||
}
|
||||
});
|
||||
_delegate->botSendPreparedMessage({
|
||||
@@ -2177,15 +2467,14 @@ void Panel::processRequestChat(const QJsonObject &args) {
|
||||
}
|
||||
auto callback = crl::guard(this, [=](QString error) {
|
||||
if (error.isEmpty()) {
|
||||
postEvent(
|
||||
"requested_chat_sent",
|
||||
u"{ req_id: \"%1\" }"_q.arg(requestId));
|
||||
postEvent("requested_chat_sent", QJsonObject{
|
||||
{ u"req_id"_q, requestId },
|
||||
});
|
||||
} else {
|
||||
postEvent(
|
||||
"requested_chat_failed",
|
||||
u"{ req_id: \"%1\", error: \"%2\" }"_q.arg(
|
||||
requestId,
|
||||
error));
|
||||
postEvent("requested_chat_failed", QJsonObject{
|
||||
{ u"req_id"_q, requestId },
|
||||
{ u"error"_q, error },
|
||||
});
|
||||
}
|
||||
});
|
||||
_delegate->botRequestChat({
|
||||
@@ -2203,23 +2492,23 @@ void Panel::processEmojiStatusRequest(const QJsonObject &args) {
|
||||
const auto duration = TimeId(base::SafeRound(
|
||||
args["duration"].toDouble()));
|
||||
if (!emojiId) {
|
||||
postEvent(
|
||||
"emoji_status_failed",
|
||||
"{ error: \"SUGGESTED_EMOJI_INVALID\" }");
|
||||
postEvent("emoji_status_failed", QJsonObject{
|
||||
{ u"error"_q, u"SUGGESTED_EMOJI_INVALID"_q },
|
||||
});
|
||||
return;
|
||||
} else if (duration < 0) {
|
||||
postEvent(
|
||||
"emoji_status_failed",
|
||||
"{ error: \"DURATION_INVALID\" }");
|
||||
postEvent("emoji_status_failed", QJsonObject{
|
||||
{ u"error"_q, u"DURATION_INVALID"_q },
|
||||
});
|
||||
return;
|
||||
}
|
||||
auto callback = crl::guard(this, [=](QString error) {
|
||||
if (error.isEmpty()) {
|
||||
postEvent("emoji_status_set");
|
||||
} else {
|
||||
postEvent(
|
||||
"emoji_status_failed",
|
||||
u"{ error: \"%1\" }"_q.arg(error));
|
||||
postEvent("emoji_status_failed", QJsonObject{
|
||||
{ u"error"_q, error },
|
||||
});
|
||||
}
|
||||
});
|
||||
_delegate->botSetEmojiStatus({
|
||||
@@ -2231,9 +2520,9 @@ void Panel::processEmojiStatusRequest(const QJsonObject &args) {
|
||||
|
||||
void Panel::processEmojiStatusAccessRequest() {
|
||||
auto callback = crl::guard(this, [=](bool allowed) {
|
||||
postEvent("emoji_status_access_requested", allowed
|
||||
? "{ status: \"allowed\" }"
|
||||
: "{ status: \"cancelled\" }");
|
||||
postEvent("emoji_status_access_requested", QJsonObject{
|
||||
{ u"status"_q, allowed ? u"allowed"_q : u"cancelled"_q },
|
||||
});
|
||||
});
|
||||
_delegate->botRequestEmojiStatusAccess(std::move(callback));
|
||||
}
|
||||
@@ -2384,10 +2673,9 @@ void Panel::openPopup(const QJsonObject &args) {
|
||||
_delegate->botClose();
|
||||
return;
|
||||
}
|
||||
const auto widget = _webview->window.widget();
|
||||
const auto weak = base::make_weak(this);
|
||||
const auto result = showBlockingPopup({
|
||||
.parent = widget ? widget->window() : nullptr,
|
||||
.parent = webviewWindowForPopup(),
|
||||
.title = args["title"].toString(),
|
||||
.text = message,
|
||||
.buttons = std::move(buttons),
|
||||
@@ -2400,9 +2688,8 @@ void Panel::openPopup(const QJsonObject &args) {
|
||||
}
|
||||
|
||||
void Panel::openScanQrPopup(const QJsonObject &args) {
|
||||
const auto widget = _webview->window.widget();
|
||||
[[maybe_unused]] const auto ok = showBlockingPopup({
|
||||
.parent = widget ? widget->window() : nullptr,
|
||||
.parent = webviewWindowForPopup(),
|
||||
.text = tr::lng_bot_no_scan_qr(tr::now),
|
||||
.buttons = { {
|
||||
.id = "ok",
|
||||
@@ -2413,9 +2700,8 @@ void Panel::openScanQrPopup(const QJsonObject &args) {
|
||||
}
|
||||
|
||||
void Panel::openShareStory(const QJsonObject &args) {
|
||||
const auto widget = _webview->window.widget();
|
||||
[[maybe_unused]] const auto ok = showBlockingPopup({
|
||||
.parent = widget ? widget->window() : nullptr,
|
||||
.parent = webviewWindowForPopup(),
|
||||
.text = tr::lng_bot_no_share_story(tr::now),
|
||||
.buttons = { {
|
||||
.id = "ok",
|
||||
@@ -2444,10 +2730,13 @@ void Panel::requestWriteAccess() {
|
||||
return;
|
||||
}
|
||||
using Button = Webview::PopupArgs::Button;
|
||||
const auto widget = _webview->window.widget();
|
||||
if (!_webview) {
|
||||
_inBlockingRequest = false;
|
||||
return;
|
||||
}
|
||||
const auto integration = &Ui::Integration::Instance();
|
||||
const auto result = showBlockingPopup({
|
||||
.parent = widget ? widget->window() : nullptr,
|
||||
.parent = webviewWindowForPopup(),
|
||||
.title = integration->phraseBotAllowWriteTitle(),
|
||||
.text = integration->phraseBotAllowWrite(),
|
||||
.buttons = {
|
||||
@@ -2485,11 +2774,14 @@ void Panel::requestPhone() {
|
||||
replyRequestPhone(shared);
|
||||
};
|
||||
using Button = Webview::PopupArgs::Button;
|
||||
const auto widget = _webview->window.widget();
|
||||
const auto weak = base::make_weak(this);
|
||||
if (!_webview) {
|
||||
_inBlockingRequest = false;
|
||||
return;
|
||||
}
|
||||
const auto integration = &Ui::Integration::Instance();
|
||||
const auto result = showBlockingPopup({
|
||||
.parent = widget ? widget->window() : nullptr,
|
||||
.parent = webviewWindowForPopup(),
|
||||
.title = integration->phraseBotSharePhoneTitle(),
|
||||
.text = integration->phraseBotSharePhone(),
|
||||
.buttons = {
|
||||
@@ -2590,17 +2882,28 @@ bool Panel::allowClipboardQuery() const {
|
||||
void Panel::scheduleCloseWithConfirmation() {
|
||||
if (!_closeWithConfirmationScheduled) {
|
||||
_closeWithConfirmationScheduled = true;
|
||||
InvokeQueued(_widget.get(), [=] { closeWithConfirmation(); });
|
||||
const auto generation = _externalShellGeneration;
|
||||
InvokeQueued(_widget.get(), [=] {
|
||||
if (_externalShell && generation != _externalShellGeneration) {
|
||||
_closeWithConfirmationScheduled = false;
|
||||
return;
|
||||
}
|
||||
closeWithConfirmation();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void Panel::closeWithConfirmation() {
|
||||
if (!_webview) {
|
||||
_closeWithConfirmationScheduled = false;
|
||||
_delegate->botClose();
|
||||
return;
|
||||
}
|
||||
using Button = Webview::PopupArgs::Button;
|
||||
const auto widget = _webview->window.widget();
|
||||
const auto weak = base::make_weak(this);
|
||||
const auto integration = &Ui::Integration::Instance();
|
||||
const auto result = showBlockingPopup({
|
||||
.parent = widget ? widget->window() : nullptr,
|
||||
.parent = webviewWindowForPopup(),
|
||||
.title = integration->phrasePanelCloseWarning(),
|
||||
.text = integration->phrasePanelCloseUnsaved(),
|
||||
.buttons = {
|
||||
@@ -2845,9 +3148,9 @@ void Panel::processDownloadRequest(const QJsonObject &args) {
|
||||
return;
|
||||
}
|
||||
const auto done = crl::guard(this, [=](bool started) {
|
||||
postEvent("file_download_requested", started
|
||||
? "{ status: \"downloading\" }"
|
||||
: "{ status: \"cancelled\" }");
|
||||
postEvent("file_download_requested", QJsonObject{
|
||||
{ u"status"_q, started ? u"downloading"_q : u"cancelled"_q },
|
||||
});
|
||||
});
|
||||
_delegate->botDownloadFile({
|
||||
.url = url,
|
||||
@@ -3124,7 +3427,7 @@ void Panel::updateThemeParams(const Webview::ThemeParams ¶ms) {
|
||||
params.scrollBarBgOver);
|
||||
sendExternalShellColors(params);
|
||||
sendExternalShellAssets();
|
||||
postEvent("theme_changed", "{\"theme_params\": " + params.json + "}");
|
||||
postEvent("theme_changed", ThemeChangedPayload(params));
|
||||
return;
|
||||
}
|
||||
_webview->window.updateTheme(
|
||||
@@ -3186,14 +3489,21 @@ void Panel::postEvent(const QString &event, EventData data) {
|
||||
).arg(event));
|
||||
return;
|
||||
}
|
||||
if (_externalShell) {
|
||||
if (v::is<QJsonObject>(data)) {
|
||||
sendExternalShellEvent(event, v::get<QJsonObject>(data));
|
||||
} else if (v::get<QString>(data).isEmpty()) {
|
||||
sendExternalShellEvent(event, {});
|
||||
} else {
|
||||
LOG(("BotWebView Error: Drop raw external event \"%1\"."
|
||||
).arg(event));
|
||||
}
|
||||
return;
|
||||
}
|
||||
auto written = v::is<QString>(data)
|
||||
? v::get<QString>(data).toUtf8()
|
||||
: QJsonDocument(
|
||||
v::get<QJsonObject>(data)).toJson(QJsonDocument::Compact);
|
||||
if (_externalShell) {
|
||||
sendExternalShellEvent(event, written);
|
||||
return;
|
||||
}
|
||||
_webview->window.eval(R"(
|
||||
if (window.TelegramGameProxy) {
|
||||
window.TelegramGameProxy.receiveEvent(
|
||||
|
||||
@@ -218,6 +218,8 @@ private:
|
||||
bool showWebview(Args &&args, const Webview::ThemeParams ¶ms);
|
||||
|
||||
bool createWebview(const Webview::ThemeParams ¶ms);
|
||||
void resetExternalShellIdentity();
|
||||
[[nodiscard]] QWidget *webviewWindowForPopup() const;
|
||||
void installExternalShellDocument();
|
||||
void sendExternalShellBootstrap();
|
||||
void sendExternalShellMethod(
|
||||
@@ -225,7 +227,7 @@ private:
|
||||
const QJsonObject &data);
|
||||
void sendExternalShellEvent(
|
||||
const QString &event,
|
||||
const QByteArray &data);
|
||||
const QJsonObject &data);
|
||||
void sendExternalShellButton(
|
||||
const char *name,
|
||||
const QJsonObject &args);
|
||||
@@ -323,6 +325,8 @@ private:
|
||||
bool _externalTitleBadgeVisible = false;
|
||||
bool _externalShell = false;
|
||||
bool _externalShellBootstrapped = false;
|
||||
QString _externalShellToken;
|
||||
uint64 _externalShellGeneration = 0;
|
||||
bool _externalBackVisible = false;
|
||||
ExternalShellColorState _externalShellColorState;
|
||||
MenuButtons _menuButtons = {};
|
||||
|
||||
@@ -60,58 +60,62 @@ namespace {
|
||||
|
||||
} // namespace
|
||||
|
||||
QByteArray InstallScript() {
|
||||
static const auto result = [] {
|
||||
const auto css = QString::fromUtf8(PageCss());
|
||||
const auto body = QString::fromUtf8(BodyHtml());
|
||||
auto script = QByteArray();
|
||||
script += "if (window === window.top"
|
||||
" && !window.TelegramDesktopShell"
|
||||
" && !window.TelegramDesktopShellInstalling) {"
|
||||
"window.TelegramDesktopShellInstalling = true;"
|
||||
"try {"
|
||||
"if (!document.head) {"
|
||||
"document.documentElement.insertBefore("
|
||||
"document.createElement('head'),"
|
||||
"document.documentElement.firstChild);"
|
||||
"}"
|
||||
"if (!document.body) {"
|
||||
"document.documentElement.appendChild("
|
||||
"document.createElement('body'));"
|
||||
"}"
|
||||
"document.title = 'Telegram';"
|
||||
"const metaRobots = document.createElement('meta');"
|
||||
"metaRobots.name = 'robots';"
|
||||
"metaRobots.content = 'noindex, nofollow';"
|
||||
"document.head.appendChild(metaRobots);"
|
||||
"const metaViewport = document.createElement('meta');"
|
||||
"metaViewport.name = 'viewport';"
|
||||
"metaViewport.content = 'width=device-width, initial-scale=1.0';"
|
||||
"document.head.appendChild(metaViewport);"
|
||||
"const style = document.createElement('style');"
|
||||
"style.textContent = ";
|
||||
script += JsonValue(css);
|
||||
script += ";"
|
||||
"document.head.appendChild(style);"
|
||||
"document.body.insertAdjacentHTML('beforeend', ";
|
||||
script += JsonValue(body);
|
||||
script += ");";
|
||||
script += PageJs();
|
||||
script += "} finally {"
|
||||
"window.TelegramDesktopShellInstalling = false;"
|
||||
"}"
|
||||
"}";
|
||||
return script;
|
||||
}();
|
||||
return result;
|
||||
QByteArray InstallScript(const QString &shellToken) {
|
||||
const auto css = QString::fromUtf8(PageCss());
|
||||
const auto body = QString::fromUtf8(BodyHtml());
|
||||
auto pageJs = PageJs();
|
||||
pageJs.replace(
|
||||
QByteArray("TDESKTOP_SHELL_TOKEN_PLACEHOLDER"),
|
||||
JsonValue(shellToken));
|
||||
|
||||
auto script = QByteArray();
|
||||
script += "if (window === window.top"
|
||||
" && !window.TelegramDesktopShell"
|
||||
" && !window.TelegramDesktopShellInstalling) {"
|
||||
"window.TelegramDesktopShellInstalling = true;"
|
||||
"try {"
|
||||
"if (!document.head) {"
|
||||
"document.documentElement.insertBefore("
|
||||
"document.createElement('head'),"
|
||||
"document.documentElement.firstChild);"
|
||||
"}"
|
||||
"if (!document.body) {"
|
||||
"document.documentElement.appendChild("
|
||||
"document.createElement('body'));"
|
||||
"}"
|
||||
"document.title = 'Telegram';"
|
||||
"const metaRobots = document.createElement('meta');"
|
||||
"metaRobots.name = 'robots';"
|
||||
"metaRobots.content = 'noindex, nofollow';"
|
||||
"document.head.appendChild(metaRobots);"
|
||||
"const metaViewport = document.createElement('meta');"
|
||||
"metaViewport.name = 'viewport';"
|
||||
"metaViewport.content = 'width=device-width, initial-scale=1.0';"
|
||||
"document.head.appendChild(metaViewport);"
|
||||
"const style = document.createElement('style');"
|
||||
"style.textContent = ";
|
||||
script += JsonValue(css);
|
||||
script += ";"
|
||||
"document.head.appendChild(style);"
|
||||
"document.body.insertAdjacentHTML('beforeend', ";
|
||||
script += JsonValue(body);
|
||||
script += ");";
|
||||
script += pageJs;
|
||||
script += "} finally {"
|
||||
"window.TelegramDesktopShellInstalling = false;"
|
||||
"}"
|
||||
"}";
|
||||
return script;
|
||||
}
|
||||
|
||||
QByteArray MethodCallScript(
|
||||
const QByteArray &method,
|
||||
const QJsonObject &data) {
|
||||
const QJsonObject &data,
|
||||
const QString &shellToken) {
|
||||
const auto payload = JsonObject(data);
|
||||
const auto token = JsonValue(shellToken);
|
||||
auto script = QByteArray();
|
||||
script.reserve(method.size() * 2 + payload.size() + 96);
|
||||
script.reserve(method.size() * 2 + payload.size() + token.size() + 98);
|
||||
script += "if (window.TelegramDesktopShell"
|
||||
" && window.TelegramDesktopShell.";
|
||||
script += method;
|
||||
@@ -119,17 +123,27 @@ QByteArray MethodCallScript(
|
||||
script += method;
|
||||
script += "(";
|
||||
script += payload;
|
||||
script += ", ";
|
||||
script += token;
|
||||
script += "); }";
|
||||
return script;
|
||||
}
|
||||
|
||||
QByteArray EventScript(const QString &event, const QByteArray &data) {
|
||||
QByteArray EventScript(
|
||||
const QString &event,
|
||||
const QJsonObject &data,
|
||||
const QString &shellToken) {
|
||||
const auto eventValue = JsonValue(event);
|
||||
const auto payload = JsonObject(data);
|
||||
const auto token = JsonValue(shellToken);
|
||||
auto script = QByteArray();
|
||||
script += "if (window.TelegramDesktopShell) {"
|
||||
"window.TelegramDesktopShell.nativeEvent(";
|
||||
script += JsonValue(event);
|
||||
script += eventValue;
|
||||
script += ", ";
|
||||
script += (data.isEmpty() ? QByteArray("{}") : data);
|
||||
script += payload;
|
||||
script += ", ";
|
||||
script += token;
|
||||
script += "); }";
|
||||
return script;
|
||||
}
|
||||
|
||||
@@ -23,13 +23,15 @@ struct ResolvedColors {
|
||||
|
||||
#ifdef Q_OS_LINUX
|
||||
|
||||
[[nodiscard]] QByteArray InstallScript();
|
||||
[[nodiscard]] QByteArray InstallScript(const QString &shellToken);
|
||||
[[nodiscard]] QByteArray MethodCallScript(
|
||||
const QByteArray &method,
|
||||
const QJsonObject &data);
|
||||
const QJsonObject &data,
|
||||
const QString &shellToken);
|
||||
[[nodiscard]] QByteArray EventScript(
|
||||
const QString &event,
|
||||
const QByteArray &data);
|
||||
const QJsonObject &data,
|
||||
const QString &shellToken);
|
||||
[[nodiscard]] QJsonObject Metrics();
|
||||
[[nodiscard]] QSize WindowSize(QSize contentSize);
|
||||
[[nodiscard]] QJsonObject MenuPalette();
|
||||
@@ -37,19 +39,21 @@ struct ResolvedColors {
|
||||
|
||||
#else // Q_OS_LINUX
|
||||
|
||||
[[nodiscard]] inline QByteArray InstallScript() {
|
||||
[[nodiscard]] inline QByteArray InstallScript(const QString &) {
|
||||
return {};
|
||||
}
|
||||
|
||||
[[nodiscard]] inline QByteArray MethodCallScript(
|
||||
const QByteArray &,
|
||||
const QJsonObject &) {
|
||||
const QJsonObject &,
|
||||
const QString &) {
|
||||
return {};
|
||||
}
|
||||
|
||||
[[nodiscard]] inline QByteArray EventScript(
|
||||
const QString &,
|
||||
const QByteArray &) {
|
||||
const QJsonObject &,
|
||||
const QString &) {
|
||||
return {};
|
||||
}
|
||||
|
||||
|
||||
+1
-1
Submodule Telegram/lib_webview updated: 401b08210d...fb06e7a6a9
Reference in New Issue
Block a user