From 3663fd38ce7ccf05ded32bb008119fe5e8442ae9 Mon Sep 17 00:00:00 2001 From: Moritz Sternemann Date: Tue, 29 Dec 2015 16:44:16 +0100 Subject: [PATCH] Initial commit --- .gitignore | 36 +++ LICENSE | 22 ++ QtTelegramBot.pri | 43 ++++ README.md | 27 ++ examples/echo/.gitignore | 73 ++++++ examples/echo/echo.pro | 12 + examples/echo/main.cpp | 27 ++ examples/examples.pro | 3 + networking.cpp | 157 ++++++++++++ networking.h | 69 +++++ qttelegrambot.cpp | 402 ++++++++++++++++++++++++++++++ qttelegrambot.h | 309 +++++++++++++++++++++++ types/audio.cpp | 13 + types/audio.h | 39 +++ types/chat.cpp | 15 ++ types/chat.h | 43 ++++ types/contact.cpp | 11 + types/contact.h | 35 +++ types/document.cpp | 12 + types/document.h | 38 +++ types/file.h | 32 +++ types/location.cpp | 9 + types/location.h | 31 +++ types/message.cpp | 116 +++++++++ types/message.h | 79 ++++++ types/photosize.cpp | 11 + types/photosize.h | 35 +++ types/reply/forcereply.h | 31 +++ types/reply/genericreply.h | 44 ++++ types/reply/replykeyboardhide.h | 30 +++ types/reply/replykeyboardmarkup.h | 54 ++++ types/sticker.cpp | 12 + types/sticker.h | 38 +++ types/update.cpp | 9 + types/update.h | 31 +++ types/user.cpp | 11 + types/user.h | 35 +++ types/video.cpp | 14 ++ types/video.h | 41 +++ types/voice.cpp | 11 + types/voice.h | 35 +++ 41 files changed, 2095 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 QtTelegramBot.pri create mode 100644 README.md create mode 100644 examples/echo/.gitignore create mode 100644 examples/echo/echo.pro create mode 100644 examples/echo/main.cpp create mode 100644 examples/examples.pro create mode 100644 networking.cpp create mode 100644 networking.h create mode 100644 qttelegrambot.cpp create mode 100644 qttelegrambot.h create mode 100644 types/audio.cpp create mode 100644 types/audio.h create mode 100644 types/chat.cpp create mode 100644 types/chat.h create mode 100644 types/contact.cpp create mode 100644 types/contact.h create mode 100644 types/document.cpp create mode 100644 types/document.h create mode 100644 types/file.h create mode 100644 types/location.cpp create mode 100644 types/location.h create mode 100644 types/message.cpp create mode 100644 types/message.h create mode 100644 types/photosize.cpp create mode 100644 types/photosize.h create mode 100644 types/reply/forcereply.h create mode 100644 types/reply/genericreply.h create mode 100644 types/reply/replykeyboardhide.h create mode 100644 types/reply/replykeyboardmarkup.h create mode 100644 types/sticker.cpp create mode 100644 types/sticker.h create mode 100644 types/update.cpp create mode 100644 types/update.h create mode 100644 types/user.cpp create mode 100644 types/user.h create mode 100644 types/video.cpp create mode 100644 types/video.h create mode 100644 types/voice.cpp create mode 100644 types/voice.h diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f220c5c --- /dev/null +++ b/.gitignore @@ -0,0 +1,36 @@ +### Qt ### +# C++ objects and libs + +*.slo +*.lo +*.o +*.a +*.la +*.lai +*.so +*.dll +*.dylib + +# Qt-es + +/.qmake.cache +/.qmake.stash +*.pro.user +*.pro.user.* +*.qbs.user +*.qbs.user.* +*.moc +moc_*.cpp +qrc_*.cpp +ui_*.h +Makefile* +*-build-* + +# QtCreator + +*.autosave + +#QtCtreator Qml +*.qmlproject.user +*.qmlproject.user.* +**/bin diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..aa08e01 --- /dev/null +++ b/LICENSE @@ -0,0 +1,22 @@ +The MIT License (MIT) + +Copyright (c) 2015 Moritz Sternemann + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + diff --git a/QtTelegramBot.pri b/QtTelegramBot.pri new file mode 100644 index 0000000..9c1f7ae --- /dev/null +++ b/QtTelegramBot.pri @@ -0,0 +1,43 @@ +INCLUDEPATH += $$PWD + +QT += core network + +SOURCES += \ + $$PWD/qttelegrambot.cpp \ + $$PWD/networking.cpp \ + $$PWD/types/message.cpp \ + $$PWD/types/update.cpp \ + $$PWD/types/chat.cpp \ + $$PWD/types/user.cpp \ + $$PWD/types/document.cpp \ + $$PWD/types/photosize.cpp \ + $$PWD/types/audio.cpp \ + $$PWD/types/sticker.cpp \ + $$PWD/types/video.cpp \ + $$PWD/types/voice.cpp \ + $$PWD/types/contact.cpp \ + $$PWD/types/location.cpp + +HEADERS += \ + $$PWD/qttelegrambot.h \ + $$PWD/networking.h \ + $$PWD/types/message.h \ + $$PWD/types/update.h \ + $$PWD/types/chat.h \ + $$PWD/types/user.h \ + $$PWD/types/file.h \ + $$PWD/types/document.h \ + $$PWD/types/photosize.h \ + $$PWD/types/audio.h \ + $$PWD/types/sticker.h \ + $$PWD/types/video.h \ + $$PWD/types/voice.h \ + $$PWD/types/contact.h \ + $$PWD/types/location.h \ + $$PWD/types/reply/genericreply.h \ + $$PWD/types/reply/replykeyboardmarkup.h \ + $$PWD/types/reply/replykeyboardhide.h \ + $$PWD/types/reply/forcereply.h + +OTHER_FILES += \ + $$PWD/README.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..0d23727 --- /dev/null +++ b/README.md @@ -0,0 +1,27 @@ +## Introduction +Qt5 library for the Telegram Bot API ([https://core.telegram.org/bots/api]()). +It can be used to interact with the API directly and/or configured to automatically notify you on updates using Qt’s signals and slots. +You need a bot token to make API calls. Text the [@botfather](https://telegram.me/BotFather) to create a new bot and obtain a token. + +## Usage +Create a lib directory in your project directory and cd into it: + mkdir lib && cd lib + +Clone the repo to your projects source directory: + git clone https://github.com/iMoritz/QtTelegramBot.git + +Alternatively you can add QtTelegramBot as a git submodule to always stay up-to-date: + git submodule add https://github.com/iMoritz/QtTelegramBot.git + +In your project file (*project*.pro) add this include: + include($$PWD/lib/QtTelegramBot/QtTelegramBot.pri) + +Now you can use the Bot classes using: + #include "qttelegrambot.h" + + +Everything in *QtTelegramBot* is in the `Telegram` namespace. + + +## Examples +Examples can be found in the `examples` directory. You can build them in QtCreator or using the command line. \ No newline at end of file diff --git a/examples/echo/.gitignore b/examples/echo/.gitignore new file mode 100644 index 0000000..5439c79 --- /dev/null +++ b/examples/echo/.gitignore @@ -0,0 +1,73 @@ +# This file is used to ignore files which are generated +# ---------------------------------------------------------------------------- + +*~ +*.autosave +*.a +*.core +*.moc +*.o +*.obj +*.orig +*.rej +*.so +*.so.* +*_pch.h.cpp +*_resource.rc +*.qm +.#* +*.*# +core +!core/ +tags +.DS_Store +*.debug +Makefile* +*.prl +*.app +moc_*.cpp +ui_*.h +qrc_*.cpp +Thumbs.db +*.res +*.rc +/.qmake.cache +/.qmake.stash + +# qtcreator generated files +*.pro.user* + +# xemacs temporary files +*.flc + +# Vim temporary files +.*.swp + +# Visual Studio generated files +*.ib_pdb_index +*.idb +*.ilk +*.pdb +*.sln +*.suo +*.vcproj +*vcproj.*.*.user +*.ncb +*.sdf +*.opensdf +*.vcxproj +*vcxproj.* + +# MinGW generated files +*.Debug +*.Release + +# Python byte code +*.pyc + +# Binaries +# -------- +*.dll +*.exe + + diff --git a/examples/echo/echo.pro b/examples/echo/echo.pro new file mode 100644 index 0000000..6049b15 --- /dev/null +++ b/examples/echo/echo.pro @@ -0,0 +1,12 @@ +QT += core +QT -= gui + +TARGET = echo +CONFIG += console +CONFIG -= app_bundle + +TEMPLATE = app + +SOURCES += main.cpp + +include(../../QtTelegramBot.pri) diff --git a/examples/echo/main.cpp b/examples/echo/main.cpp new file mode 100644 index 0000000..c04170b --- /dev/null +++ b/examples/echo/main.cpp @@ -0,0 +1,27 @@ +#include +#include +#include "qttelegrambot.h" + +#define TOKEN "YOUR BOT TOKEN" + +Telegram::Bot *bot; + +void newMessage(Telegram::Message message) +{ + qDebug() << "new message:" << message; + + if (bot && message.type == Telegram::Message::TextType) { + bot->sendMessage(message.from.id, message.string); + } +} + +int main(int argc, char *argv[]) +{ + QCoreApplication a(argc, argv); + + bot = new Telegram::Bot(TOKEN, false, 500, 4); + QObject::connect(bot, &Telegram::Bot::message, &newMessage); + qDebug() << "Started Telegram Bot"; + + return a.exec(); +} diff --git a/examples/examples.pro b/examples/examples.pro new file mode 100644 index 0000000..2c813dc --- /dev/null +++ b/examples/examples.pro @@ -0,0 +1,3 @@ +TEMPLATE = subdirs +SUBDIRS += \ + echo diff --git a/networking.cpp b/networking.cpp new file mode 100644 index 0000000..ee87b1f --- /dev/null +++ b/networking.cpp @@ -0,0 +1,157 @@ +#include "networking.h" + +using namespace Telegram; + +Networking::Networking(QString token, QObject *parent) : + QObject(parent), + m_nam(new QNetworkAccessManager(this)), + m_token(token) +{ +} + +Networking::~Networking() +{ + delete m_nam; +} + +QByteArray Networking::request(QString endpoint, ParameterList params, Networking::Method method) +{ + if (endpoint.isEmpty()) { + qWarning("Cannot do request without endpoint"); + return QByteArray(); + } + if (m_token.isEmpty()) { + qWarning("Cannot do request without a Telegram Bot Token"); + return QByteArray(); + } + + QNetworkRequest req; + QUrl url = buildUrl(endpoint); + req.setUrl(url); + +#ifdef DEBUG + qDebug("HTTP request: %s", qUtf8Printable(req.url().toString())); +#endif + + QEventLoop loop; + + QNetworkReply *reply; + + if (method == GET) { + url.setQuery(parameterListToString(params)); + req.setUrl(url); + reply = m_nam->get(req); + } else if (method == POST) { + req.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded"); + reply = m_nam->post(req, parameterListToString(params)); + } else if (method == UPLOAD) { + QByteArray boundary = generateMultipartBoundary(params); + req.setHeader(QNetworkRequest::ContentTypeHeader, "multipart/form-data; boundary=" + boundary); + QByteArray requestData = generateMultipartFormData(params, boundary); + req.setHeader(QNetworkRequest::ContentLengthHeader, requestData.length()); + reply = m_nam->post(req, requestData); + } else { + qCritical("No valid method!"); + reply = NULL; + } + + if (reply == NULL) { + qWarning("Reply is NULL"); + delete reply; + return QByteArray(); + } + + QObject::connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit); + loop.exec(); + + if (reply->error() != QNetworkReply::NoError) { + qCritical("%s", qPrintable(QString("[%1] %2").arg(reply->error()).arg(reply->errorString()))); + delete reply; + return QByteArray(); + } + + QByteArray ret = reply->readAll(); + delete reply; + return ret; +} + +QUrl Networking::buildUrl(QString endpoint) +{ + QUrl url = QUrl(); + url.setScheme("https"); + url.setHost(API_HOST); + url.setPath("/bot" + m_token + endpoint); + + return url; +} + +QByteArray Networking::parameterListToString(ParameterList list) +{ + QByteArray ret; + + ParameterList::iterator i = list.begin(); + while (i != list.end()) { + ret.append(i.key() + "=" + i.value().value + "&"); + ++i; + } + ret = ret.left(ret.length() - 1); + + return ret; +} + +QByteArray Networking::generateMultipartBoundary(ParameterList list) +{ + // Generates a boundary that is not existent in the data + QByteArray result; + + srand((unsigned int) time(NULL)); + ParameterList::iterator i = list.begin(); + while (i != list.end()) { + if (i.value().isFile) { + while (result.isEmpty() || i.value().value.contains(result)) { + result.append(generateRandomString(4)); + } + } + ++i; + } + + return result; +} + +QByteArray Networking::generateMultipartFormData(ParameterList list, QByteArray boundary) +{ + QByteArray result; + + ParameterList::iterator i = list.begin(); + while (i != list.end()) { + HttpParameter param = i.value(); + result.append("--" + boundary + "\r\n"); + result.append("Content-Disposition: form-data; name=\"" + i.key()); + if (param.isFile) { + result.append("\"; filename=\"" + param.filename); + } + result.append("\"\r\n"); + if (param.isFile) { + result.append("Content-Type: " + param.mimeType + "\r\n"); + } + result.append("\r\n"); + result.append(param.value); + result.append("\r\n"); + + ++i; + } + result.append("--" + boundary + "--"); + + return result; +} + +QString Networking::generateRandomString(int length) +{ + static const std::string chars("qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM1234567890-=[]\\;',./!@#$%^&*()_+{}|:\"<>?`~"); + static const size_t charsLen = chars.length(); + QString result; + for (int i = 0; i < length; ++i) { + result += QChar(chars[rand() % charsLen]); + } + return result; +} diff --git a/networking.h b/networking.h new file mode 100644 index 0000000..3b45722 --- /dev/null +++ b/networking.h @@ -0,0 +1,69 @@ +#ifndef NETWORKING_H +#define NETWORKING_H + +#include +#include +#include +#include +#include + +#define API_HOST "api.telegram.org" + +#define ENDPOINT_GET_ME "/getMe" +#define ENDPOINT_SEND_MESSAGE "/sendMessage" +#define ENDPOINT_FORWARD_MESSAGE "/forwardMessage" +#define ENDPOINT_SEND_PHOTO "/sendPhoto" +#define ENDPOINT_SEND_AUDIO "/sendAudio" +#define ENDPOINT_SEND_DOCUMENT "/sendDocument" +#define ENDPOINT_SEND_STICKER "/sendSticker" +#define ENDPOINT_SEND_VIDEO "/sendVideo" +#define ENDPOINT_SEND_VOICE "/sendVoice" +#define ENDPOINT_SEND_LOCATION "/sendLocation" +#define ENDPOINT_SEND_CHAT_ACTION "/sendChatAction" +#define ENDPOINT_GET_USER_PROFILE_PHOTOS "/getUserProfilePhotos" +#define ENDPOINT_GET_UPDATES "/getUpdates" +#define ENDPOINT_SET_WEBHOOK "/setWebhook" +#define ENDPOINT_GET_FILE "/getFile" + +namespace Telegram { + +class HttpParameter { +public: + HttpParameter() {} + HttpParameter(QVariant value, bool isFile = false, QString mimeType = "text/plain", QString filename = "") : + value(value.toByteArray()), isFile(isFile), mimeType(mimeType), filename(filename) {} + + QByteArray value; + bool isFile; + QString mimeType; + QString filename; +}; + +typedef QMap ParameterList; + +class Networking : public QObject +{ + Q_OBJECT +public: + Networking(QString token, QObject *parent = 0); + ~Networking(); + + enum Method { GET, POST, UPLOAD }; + + QByteArray request(QString endpoint, ParameterList params, Method method); + +private: + QNetworkAccessManager *m_nam; + QString m_token; + + QUrl buildUrl(QString endpoint); + QByteArray parameterListToString(ParameterList list); + + QByteArray generateMultipartBoundary(ParameterList list); + QByteArray generateMultipartFormData(ParameterList list, QByteArray boundary); + QString generateRandomString(int length); +}; + +} + +#endif // NETWORKING_H diff --git a/qttelegrambot.cpp b/qttelegrambot.cpp new file mode 100644 index 0000000..e16e7a7 --- /dev/null +++ b/qttelegrambot.cpp @@ -0,0 +1,402 @@ +#include "qttelegrambot.h" + +using namespace Telegram; + +Bot::Bot(QString token, bool updates, quint32 updateInterval, quint32 pollingTimeout, QObject *parent) : + QObject(parent), + m_net(new Networking(token)), + m_internalUpdateTimer(new QTimer(this)), + m_updateInterval(updateInterval), + m_pollingTimeout(pollingTimeout) +{ + QLoggingCategory::setFilterRules("qt.network.ssl.warning=false"); + + if (updates) { + m_internalUpdateTimer->setSingleShot(true); + connect(m_internalUpdateTimer, &QTimer::timeout, this, &Bot::internalGetUpdates); + internalGetUpdates(); + } +} + +Bot::~Bot() +{ + delete m_net; +} + +User Bot::getMe() +{ + QJsonObject json = this->jsonObjectFromByteArray( + m_net->request(ENDPOINT_GET_ME, ParameterList(), Networking::GET)); + + User ret; + ret.id = json.value("id").toInt(); + ret.firstname = json.value("first_name").toString(); + ret.lastname = json.value("last_name").toString(); + ret.username = json.value("username").toString(); + + if (ret.id == 0 || ret.firstname.isEmpty()) { + qCritical("%s", qPrintable("Got invalid user in " + QString(ENDPOINT_GET_ME))); + return User(); + } + + return ret; +} + +bool Bot::sendMessage(QVariant chatId, QString text, bool markdown, bool disableWebPagePreview, qint32 replyToMessageId, const GenericReply &replyMarkup) +{ + ParameterList params; + if (markdown) params.insert("parse_mode", HttpParameter("Markdown")); + if (disableWebPagePreview) params.insert("disable_web_page_preview", HttpParameter(disableWebPagePreview)); + + return this->_sendPayload(chatId, text, ParameterList(), replyToMessageId, replyMarkup, "text", ENDPOINT_SEND_MESSAGE); +} + +bool Bot::forwardMessage(QVariant chatId, quint32 fromChatId, quint32 messageId) +{ + if (chatId.type() != QVariant::String && chatId.type() != QVariant::Int) { + qCritical("Please provide a QString or int as chatId"); + return false; + } + ParameterList params; + params.insert("chat_id", HttpParameter(chatId)); + params.insert("from_chat_id", HttpParameter(fromChatId)); + params.insert("message_id", HttpParameter(messageId)); + + bool success = this->responseOk(m_net->request(ENDPOINT_FORWARD_MESSAGE, params, Networking::POST)); + + return success; +} + +bool Bot::sendPhoto(QVariant chatId, QFile *file, QString caption, qint32 replyToMessageId, const GenericReply &replyMarkup) +{ + ParameterList params; + if (!caption.isEmpty()) params.insert("caption", HttpParameter(caption)); + + return this->_sendPayload(chatId, file, params, replyToMessageId, replyMarkup, "photo", ENDPOINT_SEND_PHOTO); +} + +bool Bot::sendPhoto(QVariant chatId, QString fileId, QString caption, qint32 replyToMessageId, const GenericReply &replyMarkup) +{ + ParameterList params; + if (!caption.isEmpty()) params.insert("caption", HttpParameter(caption)); + + return this->_sendPayload(chatId, fileId, params, replyToMessageId, replyMarkup, "photo", ENDPOINT_SEND_PHOTO); +} + +bool Bot::sendAudio(QVariant chatId, QFile *file, qint64 duration, QString performer, QString title, qint32 replyToMessageId, const GenericReply &replyMarkup) +{ + ParameterList params; + if (duration >= 0) params.insert("duration", HttpParameter(duration)); + if (!performer.isEmpty()) params.insert("performer", HttpParameter(performer)); + if (!title.isEmpty()) params.insert("title", HttpParameter(title)); + + return this->_sendPayload(chatId, file, params, replyToMessageId, replyMarkup, "audio", ENDPOINT_SEND_AUDIO); +} + +bool Bot::sendAudio(QVariant chatId, QString fileId, qint64 duration, QString performer, QString title, qint32 replyToMessageId, const GenericReply &replyMarkup) +{ + ParameterList params; + if (duration >= 0) params.insert("duration", HttpParameter(duration)); + if (!performer.isEmpty()) params.insert("performer", HttpParameter(performer)); + if (!title.isEmpty()) params.insert("title", HttpParameter(title)); + + return this->_sendPayload(chatId, fileId, params, replyToMessageId, replyMarkup, "audio", ENDPOINT_SEND_AUDIO); +} + +bool Bot::sendDocument(QVariant chatId, QFile *file, qint32 replyToMessageId, const GenericReply &replyMarkup) +{ + return this->_sendPayload(chatId, file, ParameterList(), replyToMessageId, replyMarkup, "document", ENDPOINT_SEND_DOCUMENT); +} + +bool Bot::sendDocument(QVariant chatId, QString fileId, qint32 replyToMessageId, const GenericReply &replyMarkup) +{ + return this->_sendPayload(chatId, fileId, ParameterList(), replyToMessageId, replyMarkup, "document", ENDPOINT_SEND_DOCUMENT); +} + +bool Bot::sendSticker(QVariant chatId, QFile *file, qint32 replyToMessageId, const GenericReply &replyMarkup) +{ + return this->_sendPayload(chatId, file, ParameterList(), replyToMessageId, replyMarkup, "sticker", ENDPOINT_SEND_STICKER); +} + +bool Bot::sendSticker(QVariant chatId, QString fileId, qint32 replyToMessageId, const GenericReply &replyMarkup) +{ + return this->_sendPayload(chatId, fileId, ParameterList(), replyToMessageId, replyMarkup, "sticker", ENDPOINT_SEND_STICKER); +} + +bool Bot::sendVideo(QVariant chatId, QFile *file, qint64 duration, QString caption, qint32 replyToMessageId, const GenericReply &replyMarkup) +{ + ParameterList params; + params.insert("duration", HttpParameter(duration)); + params.insert("caption", HttpParameter(caption)); + + return this->_sendPayload(chatId, file, params, replyToMessageId, replyMarkup, "video", ENDPOINT_SEND_VIDEO); +} + +bool Bot::sendVideo(QVariant chatId, QString fileId, qint64 duration, QString caption, qint32 replyToMessageId, const GenericReply &replyMarkup) +{ + ParameterList params; + params.insert("duration", HttpParameter(duration)); + params.insert("caption", HttpParameter(caption)); + + return this->_sendPayload(chatId, fileId, params, replyToMessageId, replyMarkup, "video", ENDPOINT_SEND_VIDEO); +} + +bool Bot::sendVoice(QVariant chatId, QFile *file, qint64 duration, qint32 replyToMessageId, const GenericReply &replyMarkup) +{ + ParameterList params; + params.insert("duration", HttpParameter(duration)); + + return this->_sendPayload(chatId, file, params, replyToMessageId, replyMarkup, "voice", ENDPOINT_SEND_VOICE); +} + +bool Bot::sendVoice(QVariant chatId, QString fileId, qint64 duration, qint32 replyToMessageId, const GenericReply &replyMarkup) +{ + ParameterList params; + params.insert("duration", HttpParameter(duration)); + + return this->_sendPayload(chatId, fileId, params, replyToMessageId, replyMarkup, "voice", ENDPOINT_SEND_VOICE); +} + +bool Bot::sendLocation(QVariant chatId, float latitude, float longitude, qint32 replyToMessageId, const GenericReply &replyMarkup) +{ + Q_UNUSED(replyMarkup); // TODO + if (chatId.type() != QVariant::String && chatId.type() != QVariant::Int) { + qCritical("Please provide a QString or int as chatId"); + return false; + } + ParameterList params; + params.insert("chat_id", HttpParameter(chatId)); + params.insert("latitude", HttpParameter(latitude)); + params.insert("longitude", HttpParameter(longitude)); + if (replyToMessageId >= 0) params.insert("reply_to_message_id", HttpParameter(replyToMessageId)); + + bool success = this->responseOk(m_net->request(ENDPOINT_SEND_LOCATION, params, Networking::POST)); + + return success; +} + +bool Bot::sendChatAction(QVariant chatId, Bot::ChatAction action) +{ + if (chatId.type() != QVariant::String && chatId.type() != QVariant::Int) { + qCritical("Please provide a QString or int as chatId"); + return false; + } + + ParameterList params; + params.insert("chat_id", HttpParameter(chatId)); + switch (action) { + case Typing: + params.insert("action", HttpParameter("typing")); + break; + case UploadingPhoto: + params.insert("action", HttpParameter("upload_photo")); + break; + case RecordingVideo: + params.insert("action", HttpParameter("record_video")); + break; + case UploadingVideo: + params.insert("action", HttpParameter("upload_video")); + break; + case RecordingAudio: + params.insert("action", HttpParameter("record_audio")); + break; + case UploadingAudio: + params.insert("action", HttpParameter("upload_audio")); + break; + case UploadingDocument: + params.insert("action", HttpParameter("upload_document")); + break; + case FindingLocation: + params.insert("action", HttpParameter("find_location")); + break; + default: + return false; + } + + bool success = this->responseOk(m_net->request(ENDPOINT_SEND_CHAT_ACTION, params, Networking::POST)); + + return success; +} + +UserProfilePhotos Bot::getUserProfilePhotos(quint32 userId, qint16 offset, qint8 limit) +{ + ParameterList params; + params.insert("user_id", HttpParameter(userId)); + if (offset > -1) params.insert("offset", HttpParameter(offset)); + if (limit > -1) params.insert("limit", HttpParameter(limit)); + + QJsonObject json = this->jsonObjectFromByteArray(m_net->request(ENDPOINT_GET_USER_PROFILE_PHOTOS, params, Networking::GET)); + + UserProfilePhotos ret; + + QList photo; + foreach (QJsonValue val, json.value("photos").toArray()) { + photo = QList(); + + foreach (QJsonValue p, val.toArray()) { + PhotoSize ps; + ps.fileId = p.toObject().value("file_id").toString(); + ps.width = p.toObject().value("width").toInt(); + ps.height = p.toObject().value("height").toInt(); + if (p.toObject().contains("file_size")) ps.fileSize = p.toObject().value("file_size").toInt(); + photo.append(ps); + } + + ret.append(photo); + } + + return ret; +} + +QList Bot::getUpdates(quint32 timeout, quint32 limit, quint32 offset) +{ + ParameterList params; + params.insert("offset", HttpParameter(offset)); + params.insert("limit", HttpParameter(limit)); + params.insert("timeout", HttpParameter(timeout)); + QJsonArray json = this->jsonArrayFromByteArray(m_net->request(ENDPOINT_GET_UPDATES, params, Networking::GET)); + + QList ret = QList(); + foreach (QJsonValue value, json) { + ret.append(Update(value.toObject())); + } + + return ret; +} + +bool Bot::setWebhook(QString url, QFile *certificate) +{ + ParameterList params; + params.insert("url", HttpParameter(url)); + + QMimeDatabase db; + bool openedFile = false; + if (!certificate->isOpen()) { + if (!certificate->open(QFile::ReadOnly)) { + qCritical("Could not open file %s [%s]", qPrintable(certificate->fileName()), qPrintable(certificate->errorString())); + return false; + } + openedFile = true; + } + QByteArray data = certificate->readAll(); + if (openedFile) certificate->close(); + params.insert("certificate", HttpParameter(data, true, db.mimeTypeForData(data).name(), certificate->fileName())); + + bool success = this->responseOk(m_net->request(ENDPOINT_SET_WEBHOOK, params, Networking::UPLOAD)); + + return success; +} + +File Bot::getFile(QString fileId) +{ + ParameterList params; + params.insert("file_id", HttpParameter(fileId)); + + QJsonObject json = this->jsonObjectFromByteArray(m_net->request(ENDPOINT_GET_FILE, params, Networking::GET)); + + return File(json.value("file_id").toString(), json.value("file_size").toInt(-1), json.value("file_path").toString()); +} + +bool Bot::_sendPayload(QVariant chatId, QFile *filePayload, ParameterList params, qint32 replyToMessageId, const GenericReply &replyMarkup, QString payloadField, QString endpoint) +{ + if (chatId.type() != QVariant::String && chatId.type() != QVariant::Int) { + qCritical("Please provide a QString or int as chatId"); + return false; + } + + params.insert("chat_id", HttpParameter(chatId)); + + QMimeDatabase db; + bool openedFile = false; + if (!filePayload->isOpen()) { + if (!filePayload->open(QFile::ReadOnly)) { + qCritical("Could not open file %s [%s]", qPrintable(filePayload->fileName()), qPrintable(filePayload->errorString())); + return false; + } + openedFile = true; + } + QByteArray data = filePayload->readAll(); + if (openedFile) filePayload->close(); + params.insert(payloadField, HttpParameter(data, true, db.mimeTypeForData(data).name(), filePayload->fileName())); + + if (replyToMessageId >= 0) params.insert("reply_to_message_id", HttpParameter(replyToMessageId)); + if (replyMarkup.isValid()) params.insert("reply_markup", HttpParameter(replyMarkup.serialize())); + + bool success = this->responseOk(m_net->request(endpoint, params, Networking::UPLOAD)); + + return success; +} + +bool Bot::_sendPayload(QVariant chatId, QString textPayload, ParameterList params, qint32 replyToMessageId, const GenericReply &replyMarkup, QString payloadField, QString endpoint) +{ + if (chatId.type() != QVariant::String && chatId.type() != QVariant::Int) { + qCritical("Please provide a QString or int as chatId"); + return false; + } + params.insert("chat_id", HttpParameter(chatId)); + params.insert(payloadField, HttpParameter(textPayload)); + if (replyToMessageId >= 0) params.insert("reply_to_message_id", HttpParameter(replyToMessageId)); + if (replyMarkup.isValid()) params.insert("reply_markup", HttpParameter(replyMarkup.serialize())); + + bool success = this->responseOk(m_net->request(endpoint, params, Networking::POST)); + + return success; +} + +QJsonObject Bot::jsonObjectFromByteArray(QByteArray json) +{ + QJsonDocument d = QJsonDocument::fromJson(json); + QJsonObject obj = d.object(); + + if (obj.isEmpty()) { + qCritical("Got an empty response object"); + return obj; + } + + if (obj.value("ok").toBool() != true) { + qWarning("Result is not Ok"); + return obj; + } + + return obj.value("result").toObject(); +} + +QJsonArray Bot::jsonArrayFromByteArray(QByteArray json) +{ + QJsonDocument d = QJsonDocument::fromJson(json); + QJsonObject obj = d.object(); + + if (obj.isEmpty()) { + qCritical("Got an empty response object"); + return QJsonArray(); + } + + if (obj.value("ok").toBool() != true) { + qWarning("Result is not Ok"); + return QJsonArray(); + } + + return obj.value("result").toArray(); +} + +bool Bot::responseOk(QByteArray json) +{ + QJsonDocument d = QJsonDocument::fromJson(json); + QJsonObject obj = d.object(); + + return (!obj.isEmpty() && obj.value("ok").toBool() == true); +} + +void Bot::internalGetUpdates() +{ + QList updates = getUpdates(m_pollingTimeout, 50, m_updateOffset); + + foreach (Update u, updates) { + // change updateOffset to u.id to avoid duplicate updates + m_updateOffset = (u.id >= m_updateOffset ? u.id + 1 : m_updateOffset); + + emit message(u.message); + } + + m_internalUpdateTimer->start(m_updateInterval); +} diff --git a/qttelegrambot.h b/qttelegrambot.h new file mode 100644 index 0000000..e2f3403 --- /dev/null +++ b/qttelegrambot.h @@ -0,0 +1,309 @@ +#ifndef QTTELEGRAMBOT_H +#define QTTELEGRAMBOT_H + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "networking.h" +#include "types/chat.h" +#include "types/update.h" +#include "types/user.h" +#include "types/file.h" +#include "types/message.h" +#include "types/reply/genericreply.h" +#include "types/reply/replykeyboardmarkup.h" +#include "types/reply/replykeyboardhide.h" +#include "types/reply/forcereply.h" + +namespace Telegram { + +typedef QList > UserProfilePhotos; + +class Bot : public QObject +{ + Q_OBJECT +public: + /** + * Bot constructor + * @param token + * @param updates - enable automatic update polling + * @param updateInterval - interval between update polls in msec + * @param pollingTimeout - timeout in sec + * @param parent + */ + explicit Bot(QString token, bool updates = false, quint32 updateInterval = 1000, quint32 pollingTimeout = 0, QObject *parent = 0); + ~Bot(); + + enum ChatAction { Typing, UploadingPhoto, RecordingVideo, UploadingVideo, RecordingAudio, UploadingAudio, UploadingDocument, FindingLocation }; + + /** + * Returns basic information about the bot in form of a `User` object. + * @return User Object + * @see https://core.telegram.org/bots/api#getme + */ + User getMe(); + + /** + * Send text message. + * @param chatId - Unique identifier for the message recipient or @channelname + * @param text - Text of the message to be sent + * @param markdown - Use markdown in message display (only Telegram for Android supports this) + * @param disableWebPagePreview - Disables link previews for links in this message + * @param replyToMessageId - If the message is a reply, ID of the original message + * @param replyMarkup - Additional interface options + * @return success + * @see https://core.telegram.org/bots/api#sendmessage + */ + bool sendMessage(QVariant chatId, QString text, bool markdown = false, bool disableWebPagePreview = false, qint32 replyToMessageId = -1, const GenericReply &replyMarkup = GenericReply()); + + /** + * Forward messages of any kind. + * @param chatId - Unique identifier for the message recipient or @channelname + * @param fromChatId - Unique identifier for the chat where the original message was sent + * @param messageId - Unique message identifier + * @return success + * @see https://core.telegram.org/bots/api#forwardmessage + */ + bool forwardMessage(QVariant chatId, quint32 fromChatId, quint32 messageId); + + /** + * Send a photo + * @param chatId - Unique identifier for the message recipient or @channelname + * @param file - A file to send + * @param caption - Photo caption + * @param replyToMessageId - If the message is a reply, ID of the original message + * @param replyMarkup - Additional interface options + * @return success + * @see https://core.telegram.org/bots/api#sendphoto + */ + bool sendPhoto(QVariant chatId, QFile *file, QString caption = QString(), qint32 replyToMessageId = -1, const GenericReply &replyMarkup = GenericReply()); + + /** + * Send a photo + * @param chatId - Unique identifier for the message recipient or @channelname + * @param fileId - Telegram file_id of already sent photo + * @param caption - Photo caption + * @param replyToMessageId - If the message is a reply, ID of the original message + * @param replyMarkup - Additional interface options + * @return success + * @see https://core.telegram.org/bots/api#sendphoto + */ + bool sendPhoto(QVariant chatId, QString fileId, QString caption = QString(), qint32 replyToMessageId = -1, const GenericReply &replyMarkup = GenericReply()); + + /** + * Send audio + * @param chatId - Unique identifier for the message recipient or @channelname + * @param file - A file to send + * @param duration - Duration of the audio in seconds + * @param performer - Performer of the audio + * @param title - Track name of the audio + * @param replyToMessageId - If the message is a reply, ID of the original message + * @param replyMarkup - Additional interface options + * @return success + * @see https://core.telegram.org/bots/api#sendaudio + */ + bool sendAudio(QVariant chatId, QFile *file, qint64 duration = -1, QString performer = QString(), QString title = QString(), qint32 replyToMessageId = -1, const GenericReply &replyMarkup = GenericReply()); + + /** + * Send audio + * @param chatId - Unique identifier for the message recipient or @channelname + * @param fileId - Telegram file_id of already sent audio + * @param duration - Duration of the audio in seconds + * @param performer - Performer of the audio + * @param title - Track name of the audio + * @param replyToMessageId - If the message is a reply, ID of the original message + * @param replyMarkup - Additional interface options + * @return success + * @see https://core.telegram.org/bots/api#sendaudio + */ + bool sendAudio(QVariant chatId, QString fileId, qint64 duration = -1, QString performer = QString(), QString title = QString(), qint32 replyToMessageId = -1, const GenericReply &replyMarkup = GenericReply()); + + /** + * Send a document + * @param chatId - Unique identifier for the message recipient or @channelname + * @param file - A file to send + * @param replyToMessageId - If the message is a reply, ID of the original message + * @param replyMarkup - Additional interface options + * @return success + * @see https://core.telegram.org/bots/api#senddocument + */ + bool sendDocument(QVariant chatId, QFile *file, qint32 replyToMessageId = -1, const GenericReply &replyMarkup = GenericReply()); + + /** + * Send a document + * @param chatId - Unique identifier for the message recipient or @channelname + * @param fileId - Telegram file_id of already sent photo + * @param replyToMessageId - If the message is a reply, ID of the original message + * @param replyMarkup - Additional interface options + * @return success + * @see https://core.telegram.org/bots/api#senddocument + */ + bool sendDocument(QVariant chatId, QString fileId, qint32 replyToMessageId = -1, const GenericReply &replyMarkup = GenericReply()); + + /** + * Send a sticker + * @param chatId - Unique identifier for the message recipient or @channelname + * @param file - A file to send + * @param replyToMessageId - If the message is a reply, ID of the original message + * @param replyMarkup - Additional interface options + * @return success + * @see https://core.telegram.org/bots/api#sendsticker + */ + bool sendSticker(QVariant chatId, QFile *file, qint32 replyToMessageId = -1, const GenericReply &replyMarkup = GenericReply()); + + /** + * Send a sticker + * @param chatId - Unique identifier for the message recipient or @channelname + * @param fileId - Telegram file_id of already sent photo + * @param replyToMessageId - If the message is a reply, ID of the original message + * @param replyMarkup - Additional interface options + * @return success + * @see https://core.telegram.org/bots/api#sendsticker + */ + bool sendSticker(QVariant chatId, QString fileId, qint32 replyToMessageId = -1, const GenericReply &replyMarkup = GenericReply()); + + /** + * Send a video + * @param chatId - Unique identifier for the message recipient or @channelname + * @param fileId - Telegram file_id of already sent photo + * @param duration - Duration of sent video in seconds + * @param caption - Video caption + * @param replyToMessageId - If the message is a reply, ID of the original message + * @param replyMarkup - Additional interface options + * @return success + * @see https://core.telegram.org/bots/api#sendvideo + */ + bool sendVideo(QVariant chatId, QFile *file, qint64 duration = -1, QString caption = QString(), qint32 replyToMessageId = -1, const GenericReply &replyMarkup = GenericReply()); + + /** + * Send a video + * @param chatId - Unique identifier for the message recipient or @channelname + * @param fileId - Telegram file_id of already sent photo + * @param duration - Duration of sent video in seconds + * @param caption - Video caption + * @param replyToMessageId - If the message is a reply, ID of the original message + * @param replyMarkup - Additional interface options + * @return success + * @see https://core.telegram.org/bots/api#sendvideo + */ + bool sendVideo(QVariant chatId, QString fileId, qint64 duration = -1, QString caption = QString(), qint32 replyToMessageId = -1, const GenericReply &replyMarkup = GenericReply()); + + /** + * Send a voice + * @param chatId - Unique identifier for the message recipient or @channelname + * @param fileId - Telegram file_id of already sent photo + * @param duration - Duration of sent audio in seconds + * @param replyToMessageId - If the message is a reply, ID of the original message + * @param replyMarkup - Additional interface options + * @return success + * @see https://core.telegram.org/bots/api#sendvoice + */ + bool sendVoice(QVariant chatId, QFile *file, qint64 duration = -1, qint32 replyToMessageId = -1, const GenericReply &replyMarkup = GenericReply()); + + /** + * Send a voice + * @param chatId - Unique identifier for the message recipient or @channelname + * @param fileId - Telegram file_id of already sent photo + * @param duration - Duration of sent audio in seconds + * @param replyToMessageId - If the message is a reply, ID of the original message + * @param replyMarkup - Additional interface options + * @return success + * @see https://core.telegram.org/bots/api#sendvoice + */ + bool sendVoice(QVariant chatId, QString fileId, qint64 duration = -1, qint32 replyToMessageId = -1, const GenericReply &replyMarkup = GenericReply()); + + /** + * Send a location + * @param chatId - Unique identifier for the message recipient or @channelname + * @param latitude - latitude of the location + * @param longitude - longitude of the location + * @param replyToMessageId - If the message is a reply, ID of the original message + * @param replyMarkup - Additional interface options + * @return success + * @see https://core.telegram.org/bots/api#sendlocation + */ + bool sendLocation(QVariant chatId, float latitude, float longitude, qint32 replyToMessageId = -1, const GenericReply &replyMarkup = GenericReply()); + + /** + * Use this method when you need to tell the user that something is happening on the bot's side. + * @param chatId - Unique identifier for the message recipient or @channelname + * @param action - Type of action to broadcast + * @return success + * @see https://core.telegram.org/bots/api#sendchataction + */ + bool sendChatAction(QVariant chatId, ChatAction action); + + /** + * Use this method to get a list of profile pictures for a user. + * @param userId - Unique identifier of the target user + * @param offset - Sequential number of the first photo to be returned. + * @param limit - Limits the number of photos to be retrieved. Values between 1—100 are accepted. Defaults to 100. + * @return UserProfilePhotos list + * @see Use this method to get a list of profile pictures for a user. + */ + UserProfilePhotos getUserProfilePhotos(quint32 userId, qint16 offset = -1, qint8 limit = -1); + + /** + * Use this method to receive incoming updates using long polling + * @param timeout - Timeout in seconds for long polling. + * @param limit - Limits the number of updates to be retrieved. + * @param offset - Identifier of the first update to be returned. + * @return List of Update objects + * @see https://core.telegram.org/bots/api#getupdates + */ + QList getUpdates(quint32 timeout, quint32 limit, quint32 offset); + + /** + * Use this method to specify a url and receive incoming updates via an outgoing webhook. + * @param url - HTTPS url to send updates to. Use an empty string to remove webhook integration + * @param certificate - Upload your public key certificate so that the root certificate in use can be checked. + * @return success + * @see https://core.telegram.org/bots/api#setwebhook + */ + bool setWebhook(QString url, QFile *certificate); + + /** + * Use this method to get basic info about a file and prepare it for downloading. + * @param fileId - File identifier to get info about + * @return File object + * @see https://core.telegram.org/bots/api#getfile + */ + File getFile(QString fileId); + +private: + Networking *m_net; + + bool _sendPayload(QVariant chatId, QFile *filePayload, ParameterList params, qint32 replyToMessageId, const GenericReply &replyMarkup, QString payloadField, QString endpoint); + bool _sendPayload(QVariant chatId, QString textPayload, ParameterList params, qint32 replyToMessageId, const GenericReply &replyMarkup, QString payloadField, QString endpoint); + + QJsonObject jsonObjectFromByteArray(QByteArray json); + QJsonArray jsonArrayFromByteArray(QByteArray json); + bool responseOk(QByteArray json); + + void internalGetUpdates(); + QTimer *m_internalUpdateTimer; + quint32 m_updateInterval; + quint32 m_updateOffset; + quint32 m_pollingTimeout; + + + +signals: + void message(Message message); +}; + +} + +#endif // QTTELEGRAMBOT_H diff --git a/types/audio.cpp b/types/audio.cpp new file mode 100644 index 0000000..2ba2ece --- /dev/null +++ b/types/audio.cpp @@ -0,0 +1,13 @@ +#include "audio.h" + +using namespace Telegram; + +Audio::Audio(QJsonObject audio) +{ + fileId = audio.value("file_id").toString(); + duration = audio.value("duration").toInt(); + performer = audio.value("performer").toString(); + title = audio.value("title").toString(); + mimeType = audio.value("mime_type").toString(); + fileSize = audio.value("file_size").toInt(); +} diff --git a/types/audio.h b/types/audio.h new file mode 100644 index 0000000..1246b92 --- /dev/null +++ b/types/audio.h @@ -0,0 +1,39 @@ +#ifndef AUDIO_H +#define AUDIO_H + +#include +#include +#include + +namespace Telegram { + +class Audio +{ +public: + Audio() {} + Audio(QJsonObject audio); + + QString fileId; + quint64 duration; + QString performer; + QString title; + QString mimeType; + quint64 fileSize; +}; + +inline QDebug operator<< (QDebug dbg, const Audio &audio) +{ + dbg.nospace() << qUtf8Printable(QString("Telegram::Audio(fileId=%1; duration=%2; performer=%3; title=%4; mimeType=%5; fileSize=%6)") + .arg(audio.fileId) + .arg(audio.duration) + .arg(audio.performer) + .arg(audio.title) + .arg(audio.mimeType) + .arg(audio.fileSize)); + + return dbg.maybeSpace(); +} + +} + +#endif // AUDIO_H diff --git a/types/chat.cpp b/types/chat.cpp new file mode 100644 index 0000000..da0674d --- /dev/null +++ b/types/chat.cpp @@ -0,0 +1,15 @@ +#include "chat.h" + +using namespace Telegram; + +Chat::Chat(QJsonObject chat) +{ + id = chat.value("id").toInt(); + QString chatType = chat.value("type").toString(); + if (chatType == "private") type = Private; + else if (chatType == "group") type = Group; + else if (chatType == "channel") type = Channel; + username = chat.value("username").toString(); + firstname = chat.value("first_name").toString(); + lastname = chat.value("last_name").toString(); +} diff --git a/types/chat.h b/types/chat.h new file mode 100644 index 0000000..eec2bfb --- /dev/null +++ b/types/chat.h @@ -0,0 +1,43 @@ +#ifndef CHAT_H +#define CHAT_H + +#include +#include +#include + +namespace Telegram { + +class Chat +{ +public: + Chat() {} + Chat(QJsonObject chat); + + enum ChatType { + Private, Group, Channel + }; + + quint32 id; + ChatType type; + QString title; + QString username; + QString firstname; + QString lastname; +}; + +inline QDebug operator<< (QDebug dbg, const Chat &chat) +{ + dbg.nospace() << qUtf8Printable(QString("Telegram::Chat(id=%1; type=%2; title=%3; username=%4; firstname=%5; lastname=%6)") + .arg(chat.id) + .arg(chat.type) + .arg(chat.title) + .arg(chat.username) + .arg(chat.firstname) + .arg(chat.lastname)); + + return dbg.maybeSpace(); +} + +} + +#endif // CHAT_H diff --git a/types/contact.cpp b/types/contact.cpp new file mode 100644 index 0000000..e1bc6bb --- /dev/null +++ b/types/contact.cpp @@ -0,0 +1,11 @@ +#include "contact.h" + +using namespace Telegram; + +Contact::Contact(QJsonObject contact) +{ + phoneNumber = contact.value("phone_number").toString(); + firstname = contact.value("first_name").toString(); + lastname = contact.value("last_name").toString(); + userId = contact.value("user_id").toInt(); +} diff --git a/types/contact.h b/types/contact.h new file mode 100644 index 0000000..e05f5c4 --- /dev/null +++ b/types/contact.h @@ -0,0 +1,35 @@ +#ifndef CONTACT_H +#define CONTACT_H + +#include +#include +#include + +namespace Telegram { + +class Contact +{ +public: + Contact() {} + Contact(QJsonObject contact); + + QString phoneNumber; + QString firstname; + QString lastname; + quint32 userId; +}; + +inline QDebug operator<< (QDebug dbg, const Contact &contact) +{ + dbg.nospace() << qUtf8Printable(QString("Telegram::Contact(phoneNumber=%1; firstname=%2; lastname=%3; userId=%4)") + .arg(contact.phoneNumber) + .arg(contact.firstname) + .arg(contact.lastname) + .arg(contact.userId)); + + return dbg.maybeSpace(); +} + +} + +#endif // CONTACT_H diff --git a/types/document.cpp b/types/document.cpp new file mode 100644 index 0000000..e8d7def --- /dev/null +++ b/types/document.cpp @@ -0,0 +1,12 @@ +#include "document.h" + +using namespace Telegram; + +Document::Document(QJsonObject document) +{ + fileId = document.value("file_id").toString(); + thumb = PhotoSize(document.value("thumb").toObject()); + fileName = document.value("file_name").toString(); + mimeType = document.value("mime_type").toString(); + fileSize = document.value("file_size").toInt(); +} diff --git a/types/document.h b/types/document.h new file mode 100644 index 0000000..c0e98f4 --- /dev/null +++ b/types/document.h @@ -0,0 +1,38 @@ +#ifndef DOCUMENT_H +#define DOCUMENT_H + +#include +#include +#include +#include "photosize.h" + +namespace Telegram { + +class Document +{ +public: + Document() {} + Document(QJsonObject document); + + QString fileId; + PhotoSize thumb; + QString fileName; + QString mimeType; + quint64 fileSize; +}; + +inline QDebug operator<< (QDebug dbg, const Document &document) +{ + dbg.nospace() << qUtf8Printable(QString("Telegram::Document(fileId=%1; thumb=%2; fileName=%3; mimeType=%4; fileSize=%5)") + .arg(document.fileId) + .arg("PhotoSize(" + document.thumb.fileId + ")") + .arg(document.fileName) + .arg(document.mimeType) + .arg(document.fileSize)); + + return dbg.maybeSpace(); +} + +} + +#endif // DOCUMENT_H diff --git a/types/file.h b/types/file.h new file mode 100644 index 0000000..78c0c19 --- /dev/null +++ b/types/file.h @@ -0,0 +1,32 @@ +#ifndef FILE_H +#define FILE_H + +#include +#include + +namespace Telegram { + +class File +{ +public: + File(QString fileId, qint64 fileSize = -1, QString filePath = QString()) : + fileId(fileId), fileSize(fileSize), filePath(filePath) {} + + QString fileId; + qint64 fileSize; + QString filePath; +}; + +inline QDebug operator<< (QDebug dbg, const File &file) +{ + dbg.nospace() << qUtf8Printable(QString("Telegram::File(fileId=%1; fileSize=%2; filePath=%3)") + .arg(file.fileId) + .arg(file.fileSize) + .arg(file.filePath)); + + return dbg.maybeSpace(); +} + +} + +#endif // FILE_H diff --git a/types/location.cpp b/types/location.cpp new file mode 100644 index 0000000..1fe7c1c --- /dev/null +++ b/types/location.cpp @@ -0,0 +1,9 @@ +#include "location.h" + +using namespace Telegram; + +Location::Location(QJsonObject location) +{ + longitude = location.value("longitude").toDouble(); + latitude = location.value("latitude").toDouble(); +} diff --git a/types/location.h b/types/location.h new file mode 100644 index 0000000..2b16846 --- /dev/null +++ b/types/location.h @@ -0,0 +1,31 @@ +#ifndef LOCATION_H +#define LOCATION_H + +#include +#include +#include + +namespace Telegram { + +class Location +{ +public: + Location() {} + Location(QJsonObject location); + + float longitude; + float latitude; +}; + +inline QDebug operator<< (QDebug dbg, const Location &location) +{ + dbg.nospace() << qUtf8Printable(QString("Telegram::Location(longitude=%1; latitude=%2)") + .arg(location.longitude) + .arg(location.latitude)); + + return dbg.maybeSpace(); +} + +} + +#endif // LOCATION_H diff --git a/types/message.cpp b/types/message.cpp new file mode 100644 index 0000000..9825721 --- /dev/null +++ b/types/message.cpp @@ -0,0 +1,116 @@ +#include "message.h" + +using namespace Telegram; + +Message::Message(QJsonObject message) +{ + id = message.value("message_id").toInt(); + date = QDateTime::fromMSecsSinceEpoch(message.value("date").toInt()); + chat = Chat(message.value("chat").toObject()); + + /** + x audio Audio Optional. Message is an audio file, information about the file + document Document Optional. Message is a general file, information about the file + photo Array of PhotoSize Optional. Message is a photo, available sizes of the photo + sticker Sticker Optional. Message is a sticker, information about the sticker + video Video Optional. Message is a video, information about the video + voice Voice Optional. Message is a voice message, information about the file + caption String Optional. Caption for the photo or video + contact Contact Optional. Message is a shared contact, information about the contact + location Location Optional. Message is a shared location, information about the location + new_chat_participant User Optional. A new member was added to the group, information about them (this member may be bot itself) + left_chat_participant User Optional. A member was removed from the group, information about them (this member may be bot itself) + new_chat_photo Array of PhotoSize Optional. A chat photo was change to this value + delete_chat_photo True Optional. Informs that the chat photo was deleted + group_chat_created True Optional. Informs that the group has been created + */ + + if (message.contains("from")) { + from = User(message.value("from").toObject()); + } + if (message.contains("forward_from")) { + forwardFrom = User(message.value("forward_from").toObject()); + } + if (message.contains("forward_date")) { + forwardDate = QDateTime::fromMSecsSinceEpoch(message.value("forward_date").toInt()); + } + if (message.contains("reply_to_message")) { + replyToMessage = new Message(message.value("reply_to_message").toObject()); + } + + // Parse payload + QJsonObject obj; + if (message.contains("text")) { + string = message.value("text").toString(); + type = Message::TextType; + } + if (message.contains("audio")) { + obj = message.value("audio").toObject(); + audio = Audio(obj); + type = Message::AudioType; + } + if (message.contains("document")) { + obj = message.value("document").toObject(); + document = Document(obj); + type = Message::DocumentType; + } + if (message.contains("photo")) { + foreach (QJsonValue val, message.value("photo").toArray()) { + photo.append(PhotoSize(val.toObject())); + } + type = Message::PhotoType; + } + if (message.contains("sticker")) { + obj = message.value("sticker").toObject(); + sticker = Sticker(obj); + type = Message::StickerType; + } + if (message.contains("video")) { + obj = message.value("video").toObject(); + video = Video(obj); + type = Message::VideoType; + } + if (message.contains("voice")) { + obj = message.value("voice").toObject(); + voice = Voice(obj); + type = Message::VoiceType; + } + if (message.contains("contact")) { + obj = message.value("contact").toObject(); + contact = Contact(obj); + type = Message::ContactType; + } + if (message.contains("location")) { + obj = message.value("location").toObject(); + location = Location(obj); + type = Message::LocationType; + } + if (message.contains("new_chat_participant")) { + obj = message.value("new_chat_participant").toObject(); + user = User(obj); + type = Message::NewChatParticipantType; + } + if (message.contains("left_chat_participant")) { + obj = message.value("left_chat_participant").toObject(); + user = User(obj); + type = Message::LeftChatParticipantType; + } + if (message.contains("new_chat_title")) { + string = message.value("new_chat_title").toString(); + type = Message::NewChatTitleType; + } + if (message.contains("new_chat_photo")) { + foreach (QJsonValue val, message.value("new_chat_photo").toArray()) { + photo.append(PhotoSize(val.toObject())); + } + type = Message::NewChatPhotoType; + } + if (message.contains("delete_chat_photo")) { + boolean = true; + type = Message::DeleteChatPhotoType; + } + if (message.contains("group_chat_created")) { + boolean = true; + type = Message::GroupChatCreatedType; + } +} diff --git a/types/message.h b/types/message.h new file mode 100644 index 0000000..c537e5f --- /dev/null +++ b/types/message.h @@ -0,0 +1,79 @@ +#ifndef MESSAGE_H +#define MESSAGE_H + +#include + +#include +#include +#include +#include + +#include "audio.h" +#include "document.h" +#include "photosize.h" +#include "sticker.h" +#include "video.h" +#include "voice.h" +#include "contact.h" +#include "location.h" +#include "chat.h" +#include "user.h" + +namespace Telegram { + +class Message +{ +public: + Message() {} + Message(QJsonObject message); + + /** + * @brief Telegram message events + */ + enum MessageType { + TextType, AudioType, DocumentType, PhotoType, StickerType, VideoType, VoiceType, ContactType, + LocationType, NewChatParticipantType, LeftChatParticipantType, NewChatTitleType, + NewChatPhotoType, DeleteChatPhotoType, GroupChatCreatedType + }; + + // required + quint32 id; + QDateTime date; + Chat chat; + + // optional + User from; + User forwardFrom; + QDateTime forwardDate; + Message *replyToMessage; + + MessageType type; + + // payload + QString string; + User user; + Audio audio; + Document document; + QList photo; + Sticker sticker; + Video video; + Voice voice; + Contact contact; + Location location; + bool boolean; +}; + +inline QDebug operator<< (QDebug dbg, const Message &message) +{ + dbg.nospace() << qUtf8Printable(QString("Telegram::Message(id=%1; date=%2; chat=%3; type=%4)") + .arg(message.id) + .arg(message.date.toString("dd.MM.yyyy hh:mm:ss")) + .arg("Chat(" + QString::number(message.chat.id) + ")") + .arg(message.type)); + + return dbg.maybeSpace(); +} + +} + +#endif // MESSAGE_H diff --git a/types/photosize.cpp b/types/photosize.cpp new file mode 100644 index 0000000..9710508 --- /dev/null +++ b/types/photosize.cpp @@ -0,0 +1,11 @@ +#include "photosize.h" + +using namespace Telegram; + +PhotoSize::PhotoSize(QJsonObject photoSize) +{ + fileId = photoSize.value("file_id").toString(); + width = photoSize.value("width").toInt(); + height = photoSize.value("height").toInt(); + fileSize = photoSize.value("file_size").toInt(); +} diff --git a/types/photosize.h b/types/photosize.h new file mode 100644 index 0000000..d9268d7 --- /dev/null +++ b/types/photosize.h @@ -0,0 +1,35 @@ +#ifndef PHOTOSIZE_H +#define PHOTOSIZE_H + +#include +#include +#include + +namespace Telegram { + +class PhotoSize +{ +public: + PhotoSize() {} + PhotoSize(QJsonObject photoSize); + + QString fileId; + quint16 width; + quint16 height; + quint64 fileSize; +}; + +inline QDebug operator<< (QDebug dbg, const PhotoSize &photoSize) +{ + dbg.nospace() << qUtf8Printable(QString("Telegram::PhotoSize(fileId=%1; width=%2; height=%3; fileSize=%4)") + .arg(photoSize.fileId) + .arg(photoSize.width) + .arg(photoSize.height) + .arg(photoSize.fileSize)); + + return dbg.maybeSpace(); +} + +} + +#endif // PHOTOSIZE_H diff --git a/types/reply/forcereply.h b/types/reply/forcereply.h new file mode 100644 index 0000000..e763c8a --- /dev/null +++ b/types/reply/forcereply.h @@ -0,0 +1,31 @@ +#ifndef FORCEREPLY +#define FORCEREPLY + +#include "genericreply.h" + +namespace Telegram { + +class ForceReply : public GenericReply +{ +public: + ForceReply(bool selective = false) + : GenericReply(selective), + forceReply(true) {} + + /** + * Shows reply interface to the user, as if they manually selected the bot‘s message and tapped ’Reply' + */ + const bool forceReply; + + virtual QString serialize() const { + QJsonObject o = QJsonObject(); + o.insert("force_reply", forceReply); + o.insert("selective", selective); + return serializeJson(o); + } +}; + +} + +#endif // FORCEREPLY + diff --git a/types/reply/genericreply.h b/types/reply/genericreply.h new file mode 100644 index 0000000..bfaf7be --- /dev/null +++ b/types/reply/genericreply.h @@ -0,0 +1,44 @@ +#ifndef GENERICREPLY_H +#define GENERICREPLY_H + +#include +#include +#include +#include + +namespace Telegram { + +class GenericReply +{ +public: + GenericReply() : valid(false) {} + GenericReply(bool selective) : selective(selective), valid(true) {} + + /** + * Optional. Use this parameter if you want to show the keyboard to specific users only. + * Targets: 1) users that are @mentioned in the text of the Message object; + * 2) if the bot's message is a reply (has reply_to_message_id), sender of the original message. + */ + bool selective; + + virtual QString serialize() const { + return QString(); + } + + bool isValid() const { + return valid; + } + +private: + bool valid; + +protected: + QByteArray serializeJson(QJsonObject o) const { + QJsonDocument d = QJsonDocument(o); + return d.toJson(QJsonDocument::Compact); + } +}; + +} + +#endif // GENERICREPLY_H diff --git a/types/reply/replykeyboardhide.h b/types/reply/replykeyboardhide.h new file mode 100644 index 0000000..79ce9ad --- /dev/null +++ b/types/reply/replykeyboardhide.h @@ -0,0 +1,30 @@ +#ifndef REPLYKEYBOARDHIDE +#define REPLYKEYBOARDHIDE + +#include "genericreply.h" + +namespace Telegram { + +class ReplyKeyboardHide : public GenericReply +{ +public: + ReplyKeyboardHide(bool selective = false) + : GenericReply(selective), + hideKeyboard(true) {} + + /** + * Requests clients to hide the custom keyboard + */ + const bool hideKeyboard; + + virtual QString serialize() const { + QJsonObject o = QJsonObject(); + o.insert("hide_keyboard", hideKeyboard); + o.insert("selective", selective); + return serializeJson(o); + } +}; + +} + +#endif // REPLYKEYBOARDHIDE diff --git a/types/reply/replykeyboardmarkup.h b/types/reply/replykeyboardmarkup.h new file mode 100644 index 0000000..c5ce388 --- /dev/null +++ b/types/reply/replykeyboardmarkup.h @@ -0,0 +1,54 @@ +#ifndef REPLYKEYBOARDMARKUP_H +#define REPLYKEYBOARDMARKUP_H + +#include + +#include "genericreply.h" + +namespace Telegram { + +typedef QList KeyboardMarkup; + +class ReplyKeyboardMarkup : public GenericReply +{ +public: + ReplyKeyboardMarkup(KeyboardMarkup keyboard, bool resizeKeyboard = false, bool oneTimeKeyboard = false, bool selective = false) + : GenericReply(selective), + keyboard(keyboard), + resizeKeyboard(resizeKeyboard), + oneTimeKeyboard(oneTimeKeyboard) {} + + /** + * Array of button rows, each represented by an Array of Strings + */ + KeyboardMarkup keyboard; + + /** + * Optional. Requests clients to resize the keyboard vertically for optimal fit (e.g., make the keyboard smaller if there are just two rows of buttons). + * Defaults to false, in which case the custom keyboard is always of the same height as the app's standard keyboard. + */ + bool resizeKeyboard; + + /** + * Optional. Requests clients to hide the keyboard as soon as it's been used. + * Defaults to false. + */ + bool oneTimeKeyboard; + + virtual QString serialize() const { + QJsonObject o = QJsonObject(); + QJsonArray keyboardMarkup = QJsonArray(); + foreach (QStringList list, keyboard) { + keyboardMarkup.append(QJsonArray::fromStringList(list)); + } + o.insert("keyboard", keyboardMarkup); + o.insert("resize_keyboard", resizeKeyboard); + o.insert("one_time_keyboard", oneTimeKeyboard); + o.insert("selective", selective); + return serializeJson(o); + } +}; + +} + +#endif // REPLYKEYBOARDMARKUP_H diff --git a/types/sticker.cpp b/types/sticker.cpp new file mode 100644 index 0000000..cfc5aaa --- /dev/null +++ b/types/sticker.cpp @@ -0,0 +1,12 @@ +#include "sticker.h" + +using namespace Telegram; + +Sticker::Sticker(QJsonObject sticker) +{ + fileId = sticker.value("file_id").toString(); + width = sticker.value("width").toInt(); + height = sticker.value("height").toInt(); + thumb = PhotoSize(sticker.value("thumb").toObject()); + fileSize = sticker.value("file_size").toInt(); +} diff --git a/types/sticker.h b/types/sticker.h new file mode 100644 index 0000000..b2c8ee2 --- /dev/null +++ b/types/sticker.h @@ -0,0 +1,38 @@ +#ifndef STICKER_H +#define STICKER_H + +#include +#include +#include +#include "photosize.h" + +namespace Telegram { + +class Sticker +{ +public: + Sticker() {} + Sticker(QJsonObject sticker); + + QString fileId; + quint16 width; + quint16 height; + PhotoSize thumb; + quint64 fileSize; +}; + +inline QDebug operator<< (QDebug dbg, const Sticker &sticker) +{ + dbg.nospace() << qUtf8Printable(QString("Telegram::Sticker(fileId=%1; width=%2; height=%3; thumb=%4; fileSize=%5)") + .arg(sticker.fileId) + .arg(sticker.width) + .arg(sticker.height) + .arg("PhotoSize(" + sticker.thumb.fileId + ")") + .arg(sticker.fileSize)); + + return dbg.maybeSpace(); +} + +} + +#endif // STICKER_H diff --git a/types/update.cpp b/types/update.cpp new file mode 100644 index 0000000..03697f6 --- /dev/null +++ b/types/update.cpp @@ -0,0 +1,9 @@ +#include "update.h" + +using namespace Telegram; + +Update::Update(QJsonObject update) +{ + id = update.value("update_id").toInt(); + message = Message(update.value("message").toObject()); +} diff --git a/types/update.h b/types/update.h new file mode 100644 index 0000000..8bf4f27 --- /dev/null +++ b/types/update.h @@ -0,0 +1,31 @@ +#ifndef UPDATE_H +#define UPDATE_H + +#include +#include +#include "message.h" + +namespace Telegram { + +class Update +{ +public: + Update() {} + Update(QJsonObject update); + + quint32 id; + Message message; +}; + +inline QDebug operator<< (QDebug dbg, const Update &update) +{ + dbg.nospace() << qUtf8Printable(QString("Telegram::Update(id=%1; message=%2)") + .arg(update.id) + .arg("Message(" + QString::number(update.message.id) + ")")); + + return dbg.maybeSpace(); +} + +} + +#endif // UPDATE_H diff --git a/types/user.cpp b/types/user.cpp new file mode 100644 index 0000000..37e6c68 --- /dev/null +++ b/types/user.cpp @@ -0,0 +1,11 @@ +#include "user.h" + +using namespace Telegram; + +Telegram::User::User(QJsonObject user) +{ + id = user.value("id").toInt(); + firstname = user.value("first_name").toString(); + lastname = user.value("last_name").toString(); + username = user.value("username").toString(); +} diff --git a/types/user.h b/types/user.h new file mode 100644 index 0000000..1ab3682 --- /dev/null +++ b/types/user.h @@ -0,0 +1,35 @@ +#ifndef USER_H +#define USER_H + +#include +#include +#include + +namespace Telegram { + +class User +{ +public: + User() : id(0), firstname(QString()), lastname(QString()), username(QString()) {} + User(QJsonObject user); + + quint32 id; + QString firstname; + QString lastname; + QString username; +}; + +inline QDebug operator<< (QDebug dbg, const User &user) +{ + dbg.nospace() << qUtf8Printable(QString("Telegram::User(id=%1; firstname=%2; lastname=%3; username=%4)") + .arg(user.id) + .arg(user.firstname) + .arg(user.lastname) + .arg(user.username)); + + return dbg.maybeSpace(); +} + +} + +#endif // USER_H diff --git a/types/video.cpp b/types/video.cpp new file mode 100644 index 0000000..f5c9c59 --- /dev/null +++ b/types/video.cpp @@ -0,0 +1,14 @@ +#include "video.h" + +using namespace Telegram; + +Video::Video(QJsonObject video) +{ + fileId = video.value("file_id").toString(); + width = video.value("width").toInt(); + height = video.value("height").toInt(); + duration = video.value("duration").toInt(); + thumb = PhotoSize(video.value("thumb").toObject()); + mimeType = video.value("mime_type").toString(); + fileSize = video.value("file_size").toInt(); +} diff --git a/types/video.h b/types/video.h new file mode 100644 index 0000000..a35c770 --- /dev/null +++ b/types/video.h @@ -0,0 +1,41 @@ +#ifndef VIDEO_H +#define VIDEO_H + +#include +#include +#include "photosize.h" + +namespace Telegram { + +class Video +{ +public: + Video() {} + Video(QJsonObject video); + + QString fileId; + quint16 width; + quint16 height; + quint64 duration; + PhotoSize thumb; + QString mimeType; + QString fileSize; +}; + +inline QDebug operator<< (QDebug dbg, const Video &video) +{ + dbg.nospace() << qUtf8Printable(QString("Telegram::Video(fileId=%1; width=%2; height=%3; duration=%4; thumb=%5; mimeType=%6; fileSize=%7)") + .arg(video.fileId) + .arg(video.width) + .arg(video.height) + .arg(video.duration) + .arg("PhotoSize(" + video.thumb.fileId + ")") + .arg(video.mimeType) + .arg(video.fileSize)); + + return dbg.maybeSpace(); +} + +} + +#endif // VIDEO_H diff --git a/types/voice.cpp b/types/voice.cpp new file mode 100644 index 0000000..c41a2fe --- /dev/null +++ b/types/voice.cpp @@ -0,0 +1,11 @@ +#include "voice.h" + +using namespace Telegram; + +Voice::Voice(QJsonObject voice) +{ + fileId = voice.value("file_id").toString(); + duration = voice.value("duration").toInt(); + mimeType = voice.value("mime_type").toString(); + fileSize = voice.value("file_size").toInt(); +} diff --git a/types/voice.h b/types/voice.h new file mode 100644 index 0000000..86bb5e0 --- /dev/null +++ b/types/voice.h @@ -0,0 +1,35 @@ +#ifndef VOICE_H +#define VOICE_H + +#include +#include +#include + +namespace Telegram { + +class Voice +{ +public: + Voice() {} + Voice(QJsonObject voice); + + QString fileId; + quint64 duration; + QString mimeType; + quint64 fileSize; +}; + +inline QDebug operator<< (QDebug dbg, const Voice &voice) +{ + dbg.nospace() << qUtf8Printable(QString("Telegram::Voice(fileId=%1; duration=%2; mimeType=%3; fileSize=%4)") + .arg(voice.fileId) + .arg(voice.duration) + .arg(voice.mimeType) + .arg(voice.fileSize)); + + return dbg.maybeSpace(); +} + +} + +#endif // VOICE_H