init
Some checks failed
Docker. / Ubuntu (push) Has been cancelled
User-agent updater. / User-agent (push) Failing after 15s
Lock Threads / lock (push) Failing after 10s
Waiting for answer. / waiting-for-answer (push) Failing after 22s
Needs user action. / needs-user-action (push) Failing after 8s
Can't reproduce. / cant-reproduce (push) Failing after 8s
Close stale issues and PRs / stale (push) Has been cancelled

This commit is contained in:
allhaileris
2026-02-16 15:50:16 +03:00
commit afb81b8278
13816 changed files with 3689732 additions and 0 deletions

View File

@@ -0,0 +1,177 @@
/*
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 "smartglocal/smartglocal_api_client.h"
#include "smartglocal/smartglocal_error.h"
#include "smartglocal/smartglocal_token.h"
#include <QtCore/QJsonObject>
#include <QtCore/QJsonDocument>
#include <QtNetwork/QNetworkRequest>
#include <QtNetwork/QNetworkReply>
#include <crl/crl_on_main.h>
namespace SmartGlocal {
namespace {
[[nodiscard]] QString APIURLBase(bool isTest) {
return isTest
? "tgb-playground.smart-glocal.com/cds/v1"
: "tgb.smart-glocal.com/cds/v1";
}
[[nodiscard]] QString TokenEndpoint() {
return "tokenize/card";
}
[[nodiscard]] QByteArray ToJson(const Stripe::CardParams &card) {
const auto zero = QChar('0');
const auto month = QString("%1").arg(card.expMonth, 2, 10, zero);
const auto year = QString("%1").arg(card.expYear % 100, 2, 10, zero);
return QJsonDocument(QJsonObject{
{ "card", QJsonObject{
{ "number", card.number },
{ "expiration_month", month },
{ "expiration_year", year },
{ "security_code", card.cvc },
} },
}).toJson(QJsonDocument::Compact);
}
[[nodiscard]] QString ComputeApiUrl(PaymentConfiguration configuration) {
const auto url = configuration.tokenizeUrl;
if (url.startsWith("https://")
&& url.endsWith(".smart-glocal.com/cds/v1/tokenize/card")) {
return url;
}
return QString("https://%1/%2")
.arg(APIURLBase(configuration.isTest))
.arg(TokenEndpoint());
}
} // namespace
APIClient::APIClient(PaymentConfiguration configuration)
: _apiUrl(ComputeApiUrl(configuration))
, _configuration(configuration) {
_additionalHttpHeaders = {
{ "X-PUBLIC-TOKEN", _configuration.publicToken },
};
}
APIClient::~APIClient() {
const auto destroy = std::move(_old);
}
void APIClient::createTokenWithCard(
Stripe::CardParams card,
TokenCompletionCallback completion) {
createTokenWithData(ToJson(card), std::move(completion));
}
void APIClient::createTokenWithData(
QByteArray data,
TokenCompletionCallback completion) {
const auto url = QUrl(_apiUrl);
auto request = QNetworkRequest(url);
request.setHeader(
QNetworkRequest::ContentTypeHeader,
"application/json");
for (const auto &[name, value] : _additionalHttpHeaders) {
request.setRawHeader(name.toUtf8(), value.toUtf8());
}
destroyReplyDelayed(std::move(_reply));
_reply.reset(_manager.post(request, data));
const auto finish = [=](Token token, Error error) {
crl::on_main([
completion,
token = std::move(token),
error = std::move(error)
] {
completion(std::move(token), std::move(error));
});
};
const auto finishWithError = [=](Error error) {
finish(Token::Empty(), std::move(error));
};
const auto finishWithToken = [=](Token token) {
finish(std::move(token), Error::None());
};
QObject::connect(_reply.get(), &QNetworkReply::finished, [=] {
const auto replyError = int(_reply->error());
const auto replyErrorString = _reply->errorString();
const auto bytes = _reply->readAll();
destroyReplyDelayed(std::move(_reply));
auto parseError = QJsonParseError();
const auto document = QJsonDocument::fromJson(bytes, &parseError);
if (!bytes.isEmpty()) {
if (parseError.error != QJsonParseError::NoError) {
const auto code = int(parseError.error);
finishWithError({
Error::Code::JsonParse,
QString("InvalidJson%1").arg(code),
parseError.errorString(),
});
return;
} else if (!document.isObject()) {
finishWithError({
Error::Code::JsonFormat,
"InvalidJsonRoot",
"Not an object in JSON reply.",
});
return;
}
const auto object = document.object();
if (auto error = Error::DecodedObjectFromResponse(object)) {
finishWithError(std::move(error));
return;
}
}
if (replyError != QNetworkReply::NoError) {
finishWithError({
Error::Code::Network,
QString("RequestError%1").arg(replyError),
replyErrorString,
});
return;
}
auto token = Token::DecodedObjectFromAPIResponse(
document.object().value("data").toObject());
if (!token) {
finishWithError({
Error::Code::JsonFormat,
"InvalidTokenJson",
"Could not parse token.",
});
}
finishWithToken(std::move(token));
});
}
void APIClient::destroyReplyDelayed(std::unique_ptr<QNetworkReply> reply) {
if (!reply) {
return;
}
const auto raw = reply.get();
_old.push_back(std::move(reply));
QObject::disconnect(raw, &QNetworkReply::finished, nullptr, nullptr);
raw->deleteLater();
QObject::connect(raw, &QObject::destroyed, [=] {
for (auto i = begin(_old); i != end(_old); ++i) {
if (i->get() == raw) {
i->release();
_old.erase(i);
break;
}
}
});
}
} // namespace SmartGlocal

View File

@@ -0,0 +1,50 @@
/*
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 "stripe/stripe_card_params.h"
#include "smartglocal/smartglocal_callbacks.h"
#include <QtNetwork/QNetworkAccessManager>
#include <QtCore/QString>
#include <map>
#include <memory>
namespace SmartGlocal {
struct PaymentConfiguration {
QString publicToken;
QString tokenizeUrl;
bool isTest = false;
};
class APIClient final {
public:
explicit APIClient(PaymentConfiguration configuration);
~APIClient();
void createTokenWithCard(
Stripe::CardParams card,
TokenCompletionCallback completion);
void createTokenWithData(
QByteArray data,
TokenCompletionCallback completion);
private:
void destroyReplyDelayed(std::unique_ptr<QNetworkReply> reply);
QString _apiUrl;
PaymentConfiguration _configuration;
std::map<QString, QString> _additionalHttpHeaders;
QNetworkAccessManager _manager;
std::unique_ptr<QNetworkReply> _reply;
std::vector<std::unique_ptr<QNetworkReply>> _old;
};
} // namespace SmartGlocal

View File

@@ -0,0 +1,19 @@
/*
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 <functional>
namespace SmartGlocal {
class Token;
class Error;
using TokenCompletionCallback = std::function<void(Token, Error)>;
} // namespace SmartGlocal

View File

@@ -0,0 +1,63 @@
/*
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 "smartglocal/smartglocal_card.h"
#include <QtCore/QRegularExpression>
namespace SmartGlocal {
Card::Card(
QString type,
QString network,
QString maskedNumber)
: _type(type)
, _network(network)
, _maskedNumber(maskedNumber) {
}
Card Card::Empty() {
return Card(QString(), QString(), QString());
}
Card Card::DecodedObjectFromAPIResponse(QJsonObject object) {
const auto string = [&](QStringView key) {
return object.value(key).toString();
};
const auto type = string(u"card_type");
const auto network = string(u"card_network");
const auto maskedNumber = string(u"masked_card_number");
if (type.isEmpty() || maskedNumber.isEmpty()) {
return Card::Empty();
}
return Card(type, network, maskedNumber);
}
QString Card::type() const {
return _type;
}
QString Card::network() const {
return _network;
}
QString Card::maskedNumber() const {
return _maskedNumber;
}
bool Card::empty() const {
return _type.isEmpty() || _maskedNumber.isEmpty();
}
QString Last4(const Card &card) {
static const auto RegExp = QRegularExpression("[^\\d]\\d*(\\d{4})$");
const auto masked = card.maskedNumber();
const auto m = RegExp.match(masked);
return m.hasMatch() ? m.captured(1) : QString();
}
} // namespace SmartGlocal

View File

@@ -0,0 +1,51 @@
/*
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 <QtCore/QString>
class QJsonObject;
namespace SmartGlocal {
class Card final {
public:
Card(const Card &other) = default;
Card &operator=(const Card &other) = default;
Card(Card &&other) = default;
Card &operator=(Card &&other) = default;
~Card() = default;
[[nodiscard]] static Card Empty();
[[nodiscard]] static Card DecodedObjectFromAPIResponse(
QJsonObject object);
[[nodiscard]] QString type() const;
[[nodiscard]] QString network() const;
[[nodiscard]] QString maskedNumber() const;
[[nodiscard]] bool empty() const;
[[nodiscard]] explicit operator bool() const {
return !empty();
}
private:
Card(
QString type,
QString network,
QString maskedNumber);
QString _type;
QString _network;
QString _maskedNumber;
};
[[nodiscard]] QString Last4(const Card &card);
} // namespace SmartGlocal

View File

@@ -0,0 +1,69 @@
/*
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 "smartglocal/smartglocal_error.h"
namespace SmartGlocal {
Error::Code Error::code() const {
return _code;
}
QString Error::description() const {
return _description;
}
QString Error::message() const {
return _message;
}
QString Error::parameter() const {
return _parameter;
}
Error Error::None() {
return Error(Code::None, {}, {}, {});
}
Error Error::DecodedObjectFromResponse(QJsonObject object) {
if (object.value("status").toString() == "ok") {
return Error::None();
}
const auto entry = object.value("error");
if (!entry.isObject()) {
return {
Code::Unknown,
"GenericError",
"Could not read the error response "
"that was returned from SmartGlocal."
};
}
const auto error = entry.toObject();
const auto string = [&](QStringView key) {
return error.value(key).toString();
};
const auto code = string(u"code");
const auto description = string(u"description");
// There should always be a message and type for the error
if (code.isEmpty() || description.isEmpty()) {
return {
Code::Unknown,
"GenericError",
"Could not interpret the error response "
"that was returned from SmartGlocal."
};
}
return { Code::Unknown, code, description };
}
bool Error::empty() const {
return (_code == Code::None);
}
} // namespace SmartGlocal

View File

@@ -0,0 +1,59 @@
/*
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 <QtCore/QString>
class QJsonObject;
namespace SmartGlocal {
class Error {
public:
enum class Code {
None = 0, // Non-SmartGlocal errors.
JsonParse = -1,
JsonFormat = -2,
Network = -3,
Unknown = 8,
};
Error(
Code code,
const QString &description,
const QString &message,
const QString &parameter = QString())
: _code(code)
, _description(description)
, _message(message)
, _parameter(parameter) {
}
[[nodiscard]] Code code() const;
[[nodiscard]] QString description() const;
[[nodiscard]] QString message() const;
[[nodiscard]] QString parameter() const;
[[nodiscard]] static Error None();
[[nodiscard]] static Error DecodedObjectFromResponse(QJsonObject object);
[[nodiscard]] bool empty() const;
[[nodiscard]] explicit operator bool() const {
return !empty();
}
private:
Code _code = Code::None;
QString _description;
QString _message;
QString _parameter;
};
} // namespace SmartGlocal

View File

@@ -0,0 +1,46 @@
/*
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 "smartglocal/smartglocal_token.h"
namespace SmartGlocal {
QString Token::tokenId() const {
return _tokenId;
}
Card Token::card() const {
return _card;
}
Token Token::Empty() {
return Token(QString());
}
Token Token::DecodedObjectFromAPIResponse(QJsonObject object) {
const auto tokenId = object.value("token").toString();
if (tokenId.isEmpty()) {
return Token::Empty();
}
auto result = Token(tokenId);
const auto card = object.value("info");
if (card.isObject()) {
result._card = Card::DecodedObjectFromAPIResponse(card.toObject());
}
return result;
}
bool Token::empty() const {
return _tokenId.isEmpty();
}
Token::Token(QString tokenId)
: _tokenId(std::move(tokenId)) {
}
} // namespace SmartGlocal

View File

@@ -0,0 +1,47 @@
/*
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 "smartglocal/smartglocal_card.h"
#include <QtCore/QDateTime>
class QJsonObject;
namespace SmartGlocal {
class Token {
public:
Token(const Token &other) = default;
Token &operator=(const Token &other) = default;
Token(Token &&other) = default;
Token &operator=(Token &&other) = default;
~Token() = default;
[[nodiscard]] QString tokenId() const;
[[nodiscard]] bool livemode() const;
[[nodiscard]] Card card() const;
[[nodiscard]] static Token Empty();
[[nodiscard]] static Token DecodedObjectFromAPIResponse(
QJsonObject object);
[[nodiscard]] bool empty() const;
[[nodiscard]] explicit operator bool() const {
return !empty();
}
private:
explicit Token(QString tokenId);
QString _tokenId;
Card _card = Card::Empty();
};
} // namespace SmartGlocal