kepka/Telegram/SourceFiles/history/history_location_manager.cpp

392 lines
12 KiB
C++

//
// This file is part of Kepka,
// an unofficial desktop version of Telegram messaging app,
// see https://github.com/procxx/kepka
//
// Kepka is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// It is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// In addition, as a special exception, the copyright holders give permission
// to link the code of portions of this program with the OpenSSL library.
//
// Full license: https://github.com/procxx/kepka/blob/master/LICENSE
// Copyright (c) 2014-2017 John Preston, https://desktop.telegram.org
// Copyright (c) 2017- Kepka Contributors, https://github.com/procxx
//
#include "history/history_location_manager.h"
#include <QBuffer>
#include <QDesktopServices>
#include <QImageReader>
#include <QJsonDocument>
#include <QJsonParseError>
#include "lang/lang_keys.h"
#include "mainwidget.h"
#include "platform/platform_specific.h"
namespace {
constexpr auto kMaxHttpRedirects = 5;
} // namespace
/// @brief Helper interface for getting location map tile and URL.
/// Used instead of hardcoded URLs.
class ILocationMapTileHelper {
public:
virtual QString locationUrl(const LocationCoords &loc) = 0;
/// @brief Returns image tile URL for requested location loc.
/// @param width Tile width, in px.
/// @param height Tile height, in px.
/// @param zoom The world map zoom. Usually varies from 1 to 16.
/// @param scale The map objects' scale for adopting map objects and
/// labels for HiDPI / Retina displays.
virtual QString locationTileImageUrl(const LocationCoords &loc, int width, int height, int zoom, int scale) = 0;
virtual ~ILocationMapTileHelper();
};
// Note: this dtor has been extracted to avoid the inlining and triggering a
// warning related to Weak vtables. If this dtor will be inside the class
// definition, then compiler will have to place multiple copies of vtables
// which could increase binary size and could make ABI clashes.
ILocationMapTileHelper::~ILocationMapTileHelper() = default;
/// @brief Yandex.Maps tile helper. Allows to use the Yandex.Maps as backend
/// for tile images and location URLs.
class YandexMapsLocationTileHelper : public ILocationMapTileHelper {
public:
QString locationUrl(const LocationCoords &loc) override;
/// @param zoom World map zoom (from 0 to 17)
/// @see https://tech.yandex.ru/maps/doc/staticapi/1.x/dg/concepts/map_scale_docpage/
QString locationTileImageUrl(const LocationCoords &loc, int width, int height, int zoom, int scale) override;
};
QString YandexMapsLocationTileHelper::locationUrl(const LocationCoords &loc) {
// Yandex.Maps accepts ll string in "longitude,latitude" format
auto latlon = loc.lonAsString() + "%2C" + loc.latAsString();
return qsl("https://maps.yandex.ru/?ll=") + latlon + qsl("&z=16");
}
QString YandexMapsLocationTileHelper::locationTileImageUrl(const LocationCoords &loc, int width, int height, int zoom,
int scale) {
// Map marker and API endpoint constants.
// See https://tech.yandex.ru/maps/doc/staticapi/1.x/dg/concepts/input_params-docpage/
// for API parameters reference.
const char *mapsApiUrl = "https://static-maps.yandex.ru/1.x/?ll=";
const char *mapsMarkerParams = ",pm2rdl"; // red large marker looking like "9"
// Tile image parameters format string
QString mapsApiParams = "&z=%1&size=%2,%3&l=map&scale=%4&pt=";
// Yandex.Maps accepts ll string in "longitude,latitude" format
auto coords = loc.lonAsString() + ',' + loc.latAsString();
QString url =
mapsApiUrl + coords + mapsApiParams.arg(zoom).arg(width).arg(height).arg(scale) + coords + mapsMarkerParams;
return url;
}
/// @brief Uses Google Maps Static API. Adopted from old upstream code.
class GoogleMapsLocationTileHelper : public ILocationMapTileHelper {
public:
QString locationUrl(const LocationCoords &loc) override;
QString locationTileImageUrl(const LocationCoords &loc, int width, int height, int zoom, int scale) override;
};
QString GoogleMapsLocationTileHelper::locationUrl(const LocationCoords &loc) {
auto latlon = loc.latAsString() + ',' + loc.lonAsString();
return qsl("https://maps.google.com/maps?q=") + latlon + qsl("&ll=") + latlon + qsl("&z=16");
}
QString GoogleMapsLocationTileHelper::locationTileImageUrl(const LocationCoords &loc, int width, int height, int zoom,
int scale) {
// Map marker, API options and endpoint constants.
const char *mapsApiUrl = "https://maps.googleapis.com/maps/api/staticmap?center=";
// additional marker params
const char *mapsMarkerParams = "&sensor=false";
// API format string with basic marker params (red and big)
QString mapsApiParams = "&zoom=%1&size=%2,%3&maptype=roadmap&scale=%4&markers=color:red|size:big|";
// Google uses lat,lon in query URLs
auto coords = loc.latAsString() + ',' + loc.lonAsString();
QString url =
mapsApiUrl + coords + mapsApiParams.arg(zoom).arg(width).arg(height).arg(scale) + coords + mapsMarkerParams;
return url;
}
// This option could be enabled in core CMakeLists.txt
#ifdef KEPKA_USE_YANDEX_MAPS
using LocationMapTileHelper = YandexMapsLocationTileHelper;
#else
using LocationMapTileHelper = GoogleMapsLocationTileHelper;
#endif
//
// Static variables
//
namespace {
LocationManager *locationManager = nullptr;
ILocationMapTileHelper *locationMapTileHelper = nullptr;
} // namespace
//
// LocationClickHandler routines
//
QString LocationClickHandler::copyToClipboardContextItemText() const {
return lang(lng_context_copy_link);
}
void LocationClickHandler::onClick([[maybe_unused]] Qt::MouseButton button) const {
if (!psLaunchMaps(_coords)) {
QDesktopServices::openUrl(_text);
}
}
void LocationClickHandler::setup() {
_text = locationMapTileHelper->locationUrl(_coords);
}
void initLocationManager() {
if (locationManager == nullptr) {
locationManager = new LocationManager();
locationManager->init();
}
if (locationMapTileHelper == nullptr) {
locationMapTileHelper = new LocationMapTileHelper();
}
}
void reinitLocationManager() {
if (locationManager != nullptr) {
locationManager->reinit();
}
}
void deinitLocationManager() {
if (locationManager != nullptr) {
locationManager->deinit();
delete locationManager;
locationManager = nullptr;
}
// if (ptr) is useless, because delete nullptr is valid.
delete locationMapTileHelper;
locationMapTileHelper = nullptr;
}
void LocationManager::init() {
delete manager;
manager = new QNetworkAccessManager();
App::setProxySettings(*manager);
connect(manager, SIGNAL(authenticationRequired(QNetworkReply *, QAuthenticator *)), this,
SLOT(onFailed(QNetworkReply *)));
#ifndef OS_MAC_OLD
connect(manager, SIGNAL(sslErrors(QNetworkReply *, const QList<QSslError> &)), this,
SLOT(onFailed(QNetworkReply *)));
#endif // OS_MAC_OLD
connect(manager, SIGNAL(finished(QNetworkReply *)), this, SLOT(onFinished(QNetworkReply *)));
if (notLoadedPlaceholder != nullptr) {
delete notLoadedPlaceholder->v();
delete notLoadedPlaceholder;
}
auto data = QImage(cIntRetinaFactor(), cIntRetinaFactor(), QImage::Format_ARGB32_Premultiplied);
data.fill(st::imageBgTransparent->c);
data.setDevicePixelRatio(cRetinaFactor());
notLoadedPlaceholder = new ImagePtr(App::pixmapFromImageInPlace(std::move(data)), "GIF");
}
void LocationManager::reinit() {
if (manager != nullptr) {
App::setProxySettings(*manager);
}
}
void LocationManager::deinit() {
if (manager != nullptr) {
delete manager;
manager = nullptr;
}
if (notLoadedPlaceholder != nullptr) {
delete notLoadedPlaceholder->v();
delete notLoadedPlaceholder;
notLoadedPlaceholder = nullptr;
}
dataLoadings.clear();
imageLoadings.clear();
}
void LocationManager::getData(LocationData *data) {
if (manager == nullptr) {
DEBUG_LOG(("App Error: getting image link data without manager init!"));
return failed(data);
}
qint32 w = st::locationSize.width(), h = st::locationSize.height();
qint32 zoom = 13, scale = 1;
if (cScale() == dbisTwo || cRetina()) {
scale = 2;
} else {
w = convertScale(w);
h = convertScale(h);
}
QString url = locationMapTileHelper->locationTileImageUrl(data->coords, w, h, zoom, scale);
QNetworkReply *reply = manager->get(QNetworkRequest(QUrl(url)));
imageLoadings[reply] = data;
}
void LocationManager::onFinished(QNetworkReply *reply) {
if (manager == nullptr) {
return;
}
if (reply->error() != QNetworkReply::NoError) {
return onFailed(reply);
}
QVariant statusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute);
if (statusCode.isValid()) {
int status = statusCode.toInt();
if (status == 301 || status == 302) {
QString loc = reply->header(QNetworkRequest::LocationHeader).toString();
if (!loc.isEmpty()) {
QMap<QNetworkReply *, LocationData *>::iterator i = dataLoadings.find(reply);
if (i != dataLoadings.cend()) {
LocationData *d = i.value();
if (serverRedirects.constFind(d) == serverRedirects.cend()) {
serverRedirects.insert(d, 1);
} else if (++serverRedirects[d] > kMaxHttpRedirects) {
DEBUG_LOG(
("Network Error: Too many HTTP redirects in onFinished() for image link: %1").arg(loc));
return onFailed(reply);
}
dataLoadings.erase(i);
dataLoadings.insert(manager->get(QNetworkRequest(loc)), d);
return;
}
if ((i = imageLoadings.find(reply)) != imageLoadings.cend()) {
LocationData *d = i.value();
if (serverRedirects.constFind(d) == serverRedirects.cend()) {
serverRedirects.insert(d, 1);
} else if (++serverRedirects[d] > kMaxHttpRedirects) {
DEBUG_LOG(
("Network Error: Too many HTTP redirects in onFinished() for image link: %1").arg(loc));
return onFailed(reply);
}
imageLoadings.erase(i);
imageLoadings.insert(manager->get(QNetworkRequest(loc)), d);
return;
}
}
}
if (status != 200) {
DEBUG_LOG(("Network Error: Bad HTTP status received in onFinished() for image link: %1").arg(status));
return onFailed(reply);
}
}
LocationData *d = nullptr;
QMap<QNetworkReply *, LocationData *>::iterator i = dataLoadings.find(reply);
if (i != dataLoadings.cend()) {
d = i.value();
dataLoadings.erase(i);
QJsonParseError e;
QJsonDocument doc = QJsonDocument::fromJson(reply->readAll(), &e);
if (e.error != QJsonParseError::NoError) {
DEBUG_LOG(("JSON Error: Bad json received in onFinished() for image link"));
return onFailed(reply);
}
failed(d);
if (App::main() != nullptr) {
App::main()->update();
}
} else {
i = imageLoadings.find(reply);
if (i != imageLoadings.cend()) {
d = i.value();
imageLoadings.erase(i);
QPixmap thumb;
QByteArray format;
QByteArray data(reply->readAll());
{
QBuffer buffer(&data);
QImageReader reader(&buffer);
#ifndef OS_MAC_OLD
reader.setAutoTransform(true);
#endif // OS_MAC_OLD
thumb = QPixmap::fromImageReader(&reader, Qt::ColorOnly);
format = reader.format();
thumb.setDevicePixelRatio(cRetinaFactor());
if (format.isEmpty()) {
format = QByteArray("JPG");
}
}
d->loading = false;
d->thumb = thumb.isNull() ? (*notLoadedPlaceholder) : ImagePtr(thumb, format);
serverRedirects.remove(d);
if (App::main() != nullptr) {
App::main()->update();
}
}
}
}
void LocationManager::onFailed(QNetworkReply *reply) {
if (manager == nullptr) {
return;
}
LocationData *d = nullptr;
QMap<QNetworkReply *, LocationData *>::iterator i = dataLoadings.find(reply);
if (i != dataLoadings.cend()) {
d = i.value();
dataLoadings.erase(i);
} else {
i = imageLoadings.find(reply);
if (i != imageLoadings.cend()) {
d = i.value();
imageLoadings.erase(i);
}
}
DEBUG_LOG(("Network Error: failed to get data for image link %1,%2 error %3")
.arg(d ? d->coords.latAsString() : QString())
.arg(d ? d->coords.lonAsString() : QString())
.arg(reply->errorString()));
if (d != nullptr) {
failed(d);
}
}
void LocationManager::failed(LocationData *data) {
data->loading = false;
data->thumb = *notLoadedPlaceholder;
serverRedirects.remove(data);
}
void LocationData::load() {
if (!thumb->isNull()) {
return thumb->load(false, false);
}
if (loading) {
return;
}
loading = true;
if (locationManager != nullptr) {
locationManager->getData(this);
}
}