From 6717e588e71d0586613053521b2dc591405410ba Mon Sep 17 00:00:00 2001 From: Oleg Morozenkov Date: Sun, 5 Jul 2015 01:17:15 +0300 Subject: First version --- .gitignore | 3 +- CMakeLists.txt | 44 +++ samples/CMakeLists.txt | 1 + samples/echobot/CMakeLists.txt | 4 + samples/echobot/src/main.cpp | 66 +++++ src/tgbot/Api.cpp | 270 +++++++++++++++++++ src/tgbot/Api.h | 77 ++++++ src/tgbot/Bot.cpp | 52 ++++ src/tgbot/Bot.h | 90 +++++++ src/tgbot/EventManager.cpp | 56 ++++ src/tgbot/EventManager.h | 75 ++++++ src/tgbot/Http.cpp | 172 ++++++++++++ src/tgbot/Http.h | 125 +++++++++ src/tgbot/Parser.cpp | 485 ++++++++++++++++++++++++++++++++++ src/tgbot/Parser.h | 178 +++++++++++++ src/tgbot/TgException.cpp | 30 +++ src/tgbot/TgException.h | 39 +++ src/tgbot/Url.cpp | 65 +++++ src/tgbot/Url.h | 43 +++ src/tgbot/tools/StringTools.cpp | 98 +++++++ src/tgbot/tools/StringTools.h | 46 ++++ src/tgbot/types/Audio.h | 44 +++ src/tgbot/types/Contact.h | 44 +++ src/tgbot/types/Document.h | 47 ++++ src/tgbot/types/ForceReply.h | 43 +++ src/tgbot/types/GenericChat.h | 42 +++ src/tgbot/types/GenericReply.h | 40 +++ src/tgbot/types/GroupChat.h | 43 +++ src/tgbot/types/InputFile.h | 43 +++ src/tgbot/types/Location.h | 41 +++ src/tgbot/types/Message.h | 73 +++++ src/tgbot/types/PhotoSize.h | 44 +++ src/tgbot/types/ReplyKeyboardHide.h | 43 +++ src/tgbot/types/ReplyKeyboardMarkup.h | 47 ++++ src/tgbot/types/Sticker.h | 47 ++++ src/tgbot/types/Update.h | 43 +++ src/tgbot/types/User.h | 45 ++++ src/tgbot/types/UserProfilePhotos.h | 44 +++ src/tgbot/types/Video.h | 50 ++++ 39 files changed, 2841 insertions(+), 1 deletion(-) create mode 100644 CMakeLists.txt create mode 100644 samples/CMakeLists.txt create mode 100644 samples/echobot/CMakeLists.txt create mode 100644 samples/echobot/src/main.cpp create mode 100644 src/tgbot/Api.cpp create mode 100644 src/tgbot/Api.h create mode 100644 src/tgbot/Bot.cpp create mode 100644 src/tgbot/Bot.h create mode 100644 src/tgbot/EventManager.cpp create mode 100644 src/tgbot/EventManager.h create mode 100644 src/tgbot/Http.cpp create mode 100644 src/tgbot/Http.h create mode 100644 src/tgbot/Parser.cpp create mode 100644 src/tgbot/Parser.h create mode 100644 src/tgbot/TgException.cpp create mode 100644 src/tgbot/TgException.h create mode 100644 src/tgbot/Url.cpp create mode 100644 src/tgbot/Url.h create mode 100644 src/tgbot/tools/StringTools.cpp create mode 100644 src/tgbot/tools/StringTools.h create mode 100644 src/tgbot/types/Audio.h create mode 100644 src/tgbot/types/Contact.h create mode 100644 src/tgbot/types/Document.h create mode 100644 src/tgbot/types/ForceReply.h create mode 100644 src/tgbot/types/GenericChat.h create mode 100644 src/tgbot/types/GenericReply.h create mode 100644 src/tgbot/types/GroupChat.h create mode 100644 src/tgbot/types/InputFile.h create mode 100644 src/tgbot/types/Location.h create mode 100644 src/tgbot/types/Message.h create mode 100644 src/tgbot/types/PhotoSize.h create mode 100644 src/tgbot/types/ReplyKeyboardHide.h create mode 100644 src/tgbot/types/ReplyKeyboardMarkup.h create mode 100644 src/tgbot/types/Sticker.h create mode 100644 src/tgbot/types/Update.h create mode 100644 src/tgbot/types/User.h create mode 100644 src/tgbot/types/UserProfilePhotos.h create mode 100644 src/tgbot/types/Video.h diff --git a/.gitignore b/.gitignore index 52540e7..8813d62 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ CMakeCache.txt -CMakeFiles +CMakeFiles/ Makefile cmake_install.cmake install_manifest.txt +.idea/ \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..a625dd8 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,44 @@ +cmake_minimum_required(VERSION 3.0) +project(TgBot) + +### options +option(ENABLE_TESTS "Set to ON to enable building of tests and samples" ON) + +### sources +set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++11") +include_directories("${PROJECT_SOURCE_DIR}/src") +set(SRC_LIST + src/tgbot/Bot.cpp + src/tgbot/Api.cpp + src/tgbot/EventManager.cpp + src/tgbot/Http.cpp + src/tgbot/Url.cpp + src/tgbot/Parser.cpp + src/tgbot/TgException.cpp + src/tgbot/tools/StringTools.cpp +) + +### libs +# openssl +find_package(OpenSSL REQUIRED) +include_directories(${OPENSSL_INCLUDE_DIR}) + +# boost +set(Boost_USE_MULTITHREADED ON) +find_package(Boost COMPONENTS system container iostreams REQUIRED) +include_directories(${Boost_INCLUDE_DIR}) + +set(LIB_LIST + ${Boost_LIBRARIES} + ${OPENSSL_LIBRARIES} +) + +### building project +add_library(${PROJECT_NAME} ${SRC_LIST}) +target_link_libraries(${PROJECT_NAME} ${LIB_LIST}) + +### tests +if (ENABLE_TESTS) + message(STATUS "Building of tests and sambles is enabled") + add_subdirectory(samples) +endif() diff --git a/samples/CMakeLists.txt b/samples/CMakeLists.txt new file mode 100644 index 0000000..35c00f2 --- /dev/null +++ b/samples/CMakeLists.txt @@ -0,0 +1 @@ +add_subdirectory(echobot) diff --git a/samples/echobot/CMakeLists.txt b/samples/echobot/CMakeLists.txt new file mode 100644 index 0000000..c8c02ea --- /dev/null +++ b/samples/echobot/CMakeLists.txt @@ -0,0 +1,4 @@ +set(SAMPLE_NAME echobot) + +add_executable(${SAMPLE_NAME} src/main.cpp) +target_link_libraries(${SAMPLE_NAME} TgBot) \ No newline at end of file diff --git a/samples/echobot/src/main.cpp b/samples/echobot/src/main.cpp new file mode 100644 index 0000000..4874aab --- /dev/null +++ b/samples/echobot/src/main.cpp @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2015 Oleg Morozenkov + * + * 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. + */ + +#include +#include +#include + +#include +#include +#include + +using namespace std; +using namespace TgBot; + +bool sigintGot = false; + +int main() { + signal(SIGINT, [](int s) { + printf("SIGINT got"); + sigintGot = true; + }); + + Bot bot("PLACE_YOUR_TOKEN_HERE"); + bot.getEvents().onCommand("start", [](Message::Ptr message, Bot* const bot) { + bot->getApi().sendMessage(message->chat->id, "Hi!"); + }); + bot.getEvents().onAnyMessage([](Message::Ptr message, Bot* const bot) { + printf("User wrote %s\n", message->text.c_str()); + if (StringTools::startsWith(message->text, "/start")) { + return; + } + bot->getApi().sendMessage(message->chat->id, "Your message is: " + message->text); + }); + + try { + printf("Bot username: %s\n", bot.getApi().getMe()->username.c_str()); + + while (!sigintGot) { + printf("Long poll started\n"); + bot.startLongPoll(); + } + } catch (TgException& e) { + printf("error: %s\n", e.what()); + } + + return 0; +} diff --git a/src/tgbot/Api.cpp b/src/tgbot/Api.cpp new file mode 100644 index 0000000..9dd7314 --- /dev/null +++ b/src/tgbot/Api.cpp @@ -0,0 +1,270 @@ +/* + * Copyright (c) 2015 Oleg Morozenkov + * + * 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. + */ + +#include "Api.h" + +#include + +#include "tgbot/Bot.h" +#include "tgbot/TgException.h" + +using namespace std; +using namespace boost::property_tree; + +namespace TgBot { + +Api::Api(Bot* const bot) : _bot(bot) { +} + +User::Ptr Api::getMe() const { + return _bot->getParser().parseUser(sendRequest("getMe").find("result")->second); +} + +Message::Ptr Api::sendMessage(int32_t chatId, const string& text, bool disableWebPagePreview, int32_t replyToMessageId, const GenericReply::Ptr& replyMarkup) const { + vector args; + args.push_back(Http::Argument("chat_id", chatId)); + args.push_back(Http::Argument("text", text)); + if (disableWebPagePreview) { + args.push_back(Http::Argument("disable_web_page_preview", disableWebPagePreview)); + } + if (replyToMessageId) { + args.push_back(Http::Argument("reply_to_message_id", replyToMessageId)); + } + if (replyMarkup) { + args.push_back(Http::Argument("reply_markup", _bot->getParser().parseGenericReply(replyMarkup))); + } + return _bot->getParser().parseMessage(sendRequest("sendMessage", args).find("result")->second); +} + +Message::Ptr Api::forwardMessage(int32_t chatId, int32_t fromChatId, int32_t messageId) const { + vector args; + args.push_back(Http::Argument("chat_id", chatId)); + args.push_back(Http::Argument("from_chat_id", fromChatId)); + args.push_back(Http::Argument("message_id", messageId)); + return _bot->getParser().parseMessage(sendRequest("forwardMessage", args).find("result")->second); +} + +Message::Ptr Api::sendPhoto(int32_t chatId, const InputFile::Ptr& photo, const string& caption, int32_t replyToMessageId, const GenericReply::Ptr& replyMarkup) const { + vector args; + args.push_back(Http::Argument("chat_id", chatId)); + args.push_back(Http::Argument("photo", photo->data, true, photo->mimeType)); + if (!caption.empty()) { + args.push_back(Http::Argument("caption", caption)); + } + if (replyToMessageId) { + args.push_back(Http::Argument("reply_to_message_id", replyToMessageId)); + } + if (replyMarkup) { + args.push_back(Http::Argument("reply_markup", _bot->getParser().parseGenericReply(replyMarkup))); + } + return _bot->getParser().parseMessage(sendRequest("sendPhoto", args).find("result")->second); +} + +Message::Ptr Api::sendPhoto(int32_t chatId, const string& photo, const string& caption, int32_t replyToMessageId, const GenericReply::Ptr& replyMarkup) const { + vector args; + args.push_back(Http::Argument("chat_id", chatId)); + args.push_back(Http::Argument("photo", photo)); + if (!caption.empty()) { + args.push_back(Http::Argument("caption", caption)); + } + if (replyToMessageId) { + args.push_back(Http::Argument("reply_to_message_id", replyToMessageId)); + } + if (replyMarkup) { + args.push_back(Http::Argument("reply_markup", _bot->getParser().parseGenericReply(replyMarkup))); + } + return _bot->getParser().parseMessage(sendRequest("sendPhoto", args).find("result")->second); +} + +Message::Ptr Api::sendAudio(int32_t chatId, const InputFile::Ptr& audio, int32_t replyToMessageId, const GenericReply::Ptr& replyMarkup) const { + vector args; + args.push_back(Http::Argument("chat_id", chatId)); + args.push_back(Http::Argument("audio", audio->data, true, audio->mimeType)); + if (replyToMessageId) { + args.push_back(Http::Argument("reply_to_message_id", replyToMessageId)); + } + if (replyMarkup) { + args.push_back(Http::Argument("reply_markup", _bot->getParser().parseGenericReply(replyMarkup))); + } + return _bot->getParser().parseMessage(sendRequest("sendAudio", args).find("result")->second); +} + +Message::Ptr Api::sendAudio(int32_t chatId, const string& audio, int32_t replyToMessageId, const GenericReply::Ptr& replyMarkup) const { + vector args; + args.push_back(Http::Argument("chat_id", chatId)); + args.push_back(Http::Argument("audio", audio)); + if (replyToMessageId) { + args.push_back(Http::Argument("reply_to_message_id", replyToMessageId)); + } + if (replyMarkup) { + args.push_back(Http::Argument("reply_markup", _bot->getParser().parseGenericReply(replyMarkup))); + } + return _bot->getParser().parseMessage(sendRequest("sendAudio", args).find("result")->second); +} + +Message::Ptr Api::sendDocument(int32_t chatId, const InputFile::Ptr& document, int32_t replyToMessageId, const GenericReply::Ptr& replyMarkup) const { + vector args; + args.push_back(Http::Argument("chat_id", chatId)); + args.push_back(Http::Argument("document", document->data, true, document->mimeType)); + if (replyToMessageId) { + args.push_back(Http::Argument("reply_to_message_id", replyToMessageId)); + } + if (replyMarkup) { + args.push_back(Http::Argument("reply_markup", _bot->getParser().parseGenericReply(replyMarkup))); + } + return _bot->getParser().parseMessage(sendRequest("sendDocument", args).find("result")->second); +} + +Message::Ptr Api::sendDocument(int32_t chatId, const string& document, int32_t replyToMessageId, const GenericReply::Ptr& replyMarkup) const { + vector args; + args.push_back(Http::Argument("chat_id", chatId)); + args.push_back(Http::Argument("document", document)); + if (replyToMessageId) { + args.push_back(Http::Argument("reply_to_message_id", replyToMessageId)); + } + if (replyMarkup) { + args.push_back(Http::Argument("reply_markup", _bot->getParser().parseGenericReply(replyMarkup))); + } + return _bot->getParser().parseMessage(sendRequest("sendDocument", args).find("result")->second); +} + +Message::Ptr Api::sendSticker(int32_t chatId, const InputFile::Ptr& sticker, int32_t replyToMessageId, const GenericReply::Ptr& replyMarkup) const { + vector args; + args.push_back(Http::Argument("chat_id", chatId)); + args.push_back(Http::Argument("sticker", sticker->data, true, sticker->mimeType)); + if (replyToMessageId) { + args.push_back(Http::Argument("reply_to_message_id", replyToMessageId)); + } + if (replyMarkup) { + args.push_back(Http::Argument("reply_markup", _bot->getParser().parseGenericReply(replyMarkup))); + } + return _bot->getParser().parseMessage(sendRequest("sendSticker", args).find("result")->second); +} + +Message::Ptr Api::sendSticker(int32_t chatId, const string& sticker, int32_t replyToMessageId, const GenericReply::Ptr& replyMarkup) const { + vector args; + args.push_back(Http::Argument("chat_id", chatId)); + args.push_back(Http::Argument("sticker", sticker)); + if (replyToMessageId) { + args.push_back(Http::Argument("reply_to_message_id", replyToMessageId)); + } + if (replyMarkup) { + args.push_back(Http::Argument("reply_markup", _bot->getParser().parseGenericReply(replyMarkup))); + } + return _bot->getParser().parseMessage(sendRequest("sendSticker", args).find("result")->second); +} + +Message::Ptr Api::sendVideo(int32_t chatId, const InputFile::Ptr& video, int32_t replyToMessageId, const GenericReply::Ptr& replyMarkup) const { + vector args; + args.push_back(Http::Argument("chat_id", chatId)); + args.push_back(Http::Argument("video", video->data, true, video->mimeType)); + if (replyToMessageId) { + args.push_back(Http::Argument("reply_to_message_id", replyToMessageId)); + } + if (replyMarkup) { + args.push_back(Http::Argument("reply_markup", _bot->getParser().parseGenericReply(replyMarkup))); + } + return _bot->getParser().parseMessage(sendRequest("sendVideo", args).find("result")->second); +} + +Message::Ptr Api::sendVideo(int32_t chatId, const string& video, int32_t replyToMessageId, const GenericReply::Ptr& replyMarkup) const { + vector args; + args.push_back(Http::Argument("chat_id", chatId)); + args.push_back(Http::Argument("video", video)); + if (replyToMessageId) { + args.push_back(Http::Argument("reply_to_message_id", replyToMessageId)); + } + if (replyMarkup) { + args.push_back(Http::Argument("reply_markup", _bot->getParser().parseGenericReply(replyMarkup))); + } + return _bot->getParser().parseMessage(sendRequest("sendVideo", args).find("result")->second); +} + +Message::Ptr Api::sendLocation(int32_t chatId, float latitude, float longitude, int32_t replyToMessageId, const GenericReply::Ptr& replyMarkup) const { + vector args; + args.push_back(Http::Argument("chat_id", chatId)); + args.push_back(Http::Argument("latitude", latitude)); + args.push_back(Http::Argument("longitude", longitude)); + if (replyToMessageId) { + args.push_back(Http::Argument("reply_to_message_id", replyToMessageId)); + } + if (replyMarkup) { + args.push_back(Http::Argument("reply_markup", _bot->getParser().parseGenericReply(replyMarkup))); + } + return _bot->getParser().parseMessage(sendRequest("sendLocation", args).find("result")->second); +} + +void Api::sendChatAction(int32_t chatId, const string& action) const { + vector args; + args.push_back(Http::Argument("chat_id", chatId)); + args.push_back(Http::Argument("action", action)); + sendRequest("sendChatAction", args); +} + +UserProfilePhotos::Ptr Api::getUserProfilePhotos(int32_t userId, int32_t offset, int32_t limit) const { + vector args; + args.push_back(Http::Argument("user_id", userId)); + if (offset) { + args.push_back(Http::Argument("offset", offset)); + } + limit = max(1, min(100, limit)); + args.push_back(Http::Argument("limit", limit)); + return _bot->getParser().parseUserProfilePhotos(sendRequest("getUserProfilePhotos", args).find("result")->second); +} + +vector Api::getUpdates(int32_t offset, int32_t limit, int32_t timeout) const { + vector args; + if (offset) { + args.push_back(Http::Argument("offset", offset)); + } + limit = max(1, min(100, limit)); + args.push_back(Http::Argument("limit", limit)); + if (timeout) { + args.push_back(Http::Argument("timeout", timeout)); + } + return _bot->getParser().parseArray(_bot->getParser().parseUpdate, sendRequest("getUpdates", args), "result"); +} + +void Api::setWebhook(const string& url) const { + vector args; + args.push_back(Http::Argument("url", url)); + sendRequest("setWebhook", args); +} + +boost::property_tree::ptree Api::sendRequest(const std::string& method, const std::vector& args) const { + std::string url = "https://api.telegram.org/bot"; + url += _bot->getToken(); + url += "/"; + url += method; + try { + ptree result = _bot->getParser().parseJson(_bot->getHttp().makeRequest(url, args)); + if (result.get("ok")) { + return result; + } else { + throw TgException(result.get("description", "")); + } + } catch (boost::property_tree::ptree_error& e) { + return ptree(); + } +} + +} diff --git a/src/tgbot/Api.h b/src/tgbot/Api.h new file mode 100644 index 0000000..c10fe56 --- /dev/null +++ b/src/tgbot/Api.h @@ -0,0 +1,77 @@ +/* + * Copyright (c) 2015 Oleg Morozenkov + * + * 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. + */ + +#ifndef TGBOT_CPP_API_H +#define TGBOT_CPP_API_H + +#include +#include + +#include + +#include "tgbot/Http.h" +#include "tgbot/types/User.h" +#include "tgbot/types/Message.h" +#include "tgbot/types/GenericReply.h" +#include "tgbot/types/InputFile.h" +#include "tgbot/types/UserProfilePhotos.h" +#include "tgbot/types/Update.h" + +namespace TgBot { + +class Bot; + +class Api { + +friend Bot; + +public: + User::Ptr getMe() const; + Message::Ptr sendMessage(int32_t chatId, const std::string& text, bool disableWebPagePreview = false, int32_t replyToMessageId = 0, const GenericReply::Ptr& replyMarkup = GenericReply::Ptr()) const; + Message::Ptr forwardMessage(int32_t chatId, int32_t fromChatId, int32_t messageId) const; + Message::Ptr sendPhoto(int32_t chatId, const InputFile::Ptr& photo, const std::string& caption = "", int32_t replyToMessageId = 0, const GenericReply::Ptr& replyMarkup = GenericReply::Ptr()) const; + Message::Ptr sendPhoto(int32_t chatId, const std::string& photo, const std::string& caption = "", int32_t replyToMessageId = 0, const GenericReply::Ptr& replyMarkup = GenericReply::Ptr()) const; + Message::Ptr sendAudio(int32_t chatId, const InputFile::Ptr& audio, int32_t replyToMessageId = 0, const GenericReply::Ptr& replyMarkup = GenericReply::Ptr()) const; + Message::Ptr sendAudio(int32_t chatId, const std::string& audio, int32_t replyToMessageId = 0, const GenericReply::Ptr& replyMarkup = GenericReply::Ptr()) const; + Message::Ptr sendDocument(int32_t chatId, const InputFile::Ptr& document, int32_t replyToMessageId = 0, const GenericReply::Ptr& replyMarkup = GenericReply::Ptr()) const; + Message::Ptr sendDocument(int32_t chatId, const std::string& document, int32_t replyToMessageId = 0, const GenericReply::Ptr& replyMarkup = GenericReply::Ptr()) const; + Message::Ptr sendSticker(int32_t chatId, const InputFile::Ptr& sticker, int32_t replyToMessageId = 0, const GenericReply::Ptr& replyMarkup = GenericReply::Ptr()) const; + Message::Ptr sendSticker(int32_t chatId, const std::string& sticker, int32_t replyToMessageId = 0, const GenericReply::Ptr& replyMarkup = GenericReply::Ptr()) const; + Message::Ptr sendVideo(int32_t chatId, const InputFile::Ptr& video, int32_t replyToMessageId = 0, const GenericReply::Ptr& replyMarkup = GenericReply::Ptr()) const; + Message::Ptr sendVideo(int32_t chatId, const std::string& video, int32_t replyToMessageId = 0, const GenericReply::Ptr& replyMarkup = GenericReply::Ptr()) const; + Message::Ptr sendLocation(int32_t chatId, float latitude, float longitude, int32_t replyToMessageId = 0, const GenericReply::Ptr& replyMarkup = GenericReply::Ptr()) const; + void sendChatAction(int32_t chatId, const std::string& action) const; + UserProfilePhotos::Ptr getUserProfilePhotos(int32_t userId, int32_t offset = 0, int32_t limit = 100) const; + std::vector getUpdates(int32_t offset = 0, int32_t limit = 100, int32_t timeout = 0) const; + void setWebhook(const std::string& url = "") const; + +private: + explicit Api(Bot* const bot); + + boost::property_tree::ptree sendRequest(const std::string& method, const std::vector& args = std::vector()) const; + + Bot* const _bot; +}; + +} + +#endif //TGBOT_CPP_API_H diff --git a/src/tgbot/Bot.cpp b/src/tgbot/Bot.cpp new file mode 100644 index 0000000..091b548 --- /dev/null +++ b/src/tgbot/Bot.cpp @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2015 Oleg Morozenkov + * + * 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. + */ + +#include "Bot.h" + +using namespace std; +using namespace boost::property_tree; + +namespace TgBot { + +Bot::Bot(const string& token) : + _token(token), + _api(this), + _events(this), + _http(), + _parser(), + _webhooksServerHandler([this](const string& data) { + _events.handleUpdate(_parser.parseUpdate(_parser.parseJson(data))); + }) +{ +} + +void Bot::startLongPoll() { + std::vector updates = _api.getUpdates(_lastUpdateId, 100, 60); + for (Update::Ptr& item : updates) { + if (item->updateId >= _lastUpdateId) { + _lastUpdateId = item->updateId + 1; + } + _events.handleUpdate(item); + } +} + +} diff --git a/src/tgbot/Bot.h b/src/tgbot/Bot.h new file mode 100644 index 0000000..488ceb1 --- /dev/null +++ b/src/tgbot/Bot.h @@ -0,0 +1,90 @@ +/* + * Copyright (c) 2015 Oleg Morozenkov + * + * 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. + */ + +#ifndef TGBOT_CPP_BOT_H +#define TGBOT_CPP_BOT_H + +#include + +#include "tgbot/Api.h" +#include "tgbot/EventManager.h" +#include "tgbot/Http.h" +#include "tgbot/Parser.h" + +namespace TgBot { + +class Bot { + +public: + explicit Bot(const std::string& token); + + void startLongPoll(); + + inline void startServer(unsigned short port) { + _http.startServer(port, _webhooksServerHandler); + } + + inline void startServer(const std::string& unixSocketPath) { + _http.startServer(unixSocketPath, _webhooksServerHandler); + } + + inline const std::string& getToken() const { + return _token; + } + + inline const Api& getApi() const { + return _api; + } + + inline const EventManager& getEvents() const { + return _events; + } + + inline EventManager& getEvents() { + return _events; + } + + inline const Http& getHttp() const { + return _http; + } + + inline Http& getHttp() { + return _http; + } + + inline const Parser& getParser() const { + return _parser; + } + +private: + const std::string _token; + const Api _api; + EventManager _events; + Http _http; + const Parser _parser; + const Http::ServerHandler _webhooksServerHandler; + int32_t _lastUpdateId = 0; +}; + +} + +#endif //TGBOT_CPP_BOT_H diff --git a/src/tgbot/EventManager.cpp b/src/tgbot/EventManager.cpp new file mode 100644 index 0000000..fdd4859 --- /dev/null +++ b/src/tgbot/EventManager.cpp @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2015 Oleg Morozenkov + * + * 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. + */ + +#include "EventManager.h" + +#include "tgbot/tools/StringTools.h" + +using namespace std; + +namespace TgBot { + +EventManager::EventManager(Bot* const bot) : _bot(bot) { +} + +void EventManager::handleUpdate(const Update::Ptr& update) { + for (EventManager::Listener& item : _onAnyMessageListeners) { + item(update->message, _bot); + } + if (StringTools::startsWith(update->message->text, "/")) { + string command = update->message->text.substr(1, update->message->text.find(' ') - 2); + for (pair& item : _onCommandListeners) { + if (item.first == command) { + item.second(update->message, _bot); + return; + } + } + for (EventManager::Listener& item : _onUnknownCommandListeners) { + item(update->message, _bot); + } + } else { + for (EventManager::Listener& item : _onNonCommandMessageListeners) { + item(update->message, _bot); + } + } +} + +} diff --git a/src/tgbot/EventManager.h b/src/tgbot/EventManager.h new file mode 100644 index 0000000..cfe0b04 --- /dev/null +++ b/src/tgbot/EventManager.h @@ -0,0 +1,75 @@ +/* + * Copyright (c) 2015 Oleg Morozenkov + * + * 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. + */ + +#ifndef TGBOT_CPP_EVENTMANAGER_H +#define TGBOT_CPP_EVENTMANAGER_H + +#include +#include +#include +#include + +#include "tgbot/types/Update.h" +#include "tgbot/types/Message.h" + +namespace TgBot { + +class Bot; + +class EventManager { + +friend Bot; + +public: + typedef std::function Listener; + + inline void onAnyMessage(const Listener& listener) { + _onAnyMessageListeners.push_back(listener); + } + + inline void onCommand(const std::string& commandName, const Listener& listener) { + _onCommandListeners[commandName] = listener; + } + + inline void onUnknownCommand(const Listener& listener) { + _onUnknownCommandListeners.push_back(listener); + } + + inline void onNonCommandMessage(const Listener& listener) { + _onNonCommandMessageListeners.push_back(listener); + } + +private: + explicit EventManager(Bot* const bot); + + void handleUpdate(const Update::Ptr& update); + + Bot* const _bot; + std::vector _onAnyMessageListeners; + std::map _onCommandListeners; + std::vector _onUnknownCommandListeners; + std::vector _onNonCommandMessageListeners; +}; + +} + +#endif //TGBOT_CPP_EVENTMANAGER_H diff --git a/src/tgbot/Http.cpp b/src/tgbot/Http.cpp new file mode 100644 index 0000000..d512e88 --- /dev/null +++ b/src/tgbot/Http.cpp @@ -0,0 +1,172 @@ +/* + * Copyright (c) 2015 Oleg Morozenkov + * + * 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. + */ + +#include "Http.h" + +#include + +#include + +#include "tgbot/tools/StringTools.h" + +using namespace std; +using namespace boost::asio; +using namespace boost::asio::ip; +using namespace boost::asio::local; +using boost::lexical_cast; + +namespace TgBot { + +string Http::makeRequest(const Url& url, const vector& args) { + string result; + + ssl::context context(ssl::context::sslv23); + context.set_default_verify_paths(); + + ssl::stream socket(_ioService, context); + tcp::resolver resolver(_ioService); + tcp::resolver::query query(url.host, url.protocol); + connect(socket.lowest_layer(), resolver.resolve(query)); + + socket.set_verify_mode(ssl::verify_none); + socket.set_verify_callback(ssl::rfc2818_verification(url.host)); + socket.handshake(ssl::stream::client); + + string requestText; + if (args.empty()) { + requestText += "GET "; + } else { + requestText += "POST "; + } + requestText += url.path; + requestText += url.query.empty() ? "" : "?" + url.query; + requestText += " HTTP/1.1\r\n"; + requestText += "Host: "; + requestText += url.host; + requestText += "\r\nConnection: close\r\n"; + if (args.empty()) { + requestText += "\r\n"; + } else { + string requestData; + + bool isMultipart = false; + string bondary; + srand((unsigned int) time(nullptr)); + for (const Argument& item : args) { + if (item.isFile) { + isMultipart = true; + while (bondary.empty() || item.value.find(bondary) != item.value.npos) { + bondary += StringTools::generateRandomString(4); + } + } + } + if (isMultipart) { + requestText += "Content-Type: multipart/form-data; boundary="; + requestText += bondary; + requestText += "\r\n"; + for (const Argument& item : args) { + requestData += "--"; + requestData += bondary; + requestData += "\r\nContent-Disposition: form-data; name=\""; + requestData += item.name; + requestData += "\"\r\n"; + if (item.isFile) { + requestData += "Content-Type: "; + requestData += item.mimeType; + requestData += "\r\n"; + } + requestData += "\r\n"; + requestData += item.value; + requestData += "\r\n\r\n"; + } + } else { + requestText += "Content-Type: application/x-www-form-urlencoded\r\n"; + bool firstRun = true; + for (const Argument& item : args) { + if (firstRun) { + firstRun = false; + } else { + requestData += '&'; + } + requestData += StringTools::urlEncode(item.name); + requestData += '='; + requestData += StringTools::urlEncode(item.value); + } + } + + requestText += "Content-Length: "; + requestText += lexical_cast(requestData.length()); + requestText += "\r\n\r\n"; + requestText += requestData; + } + write(socket, buffer(requestText.c_str(), requestText.length())); + + char buff[1024]; + boost::system::error_code error; + while (!error) { + size_t bytes = read(socket, buffer(buff), error); + result += string(buff, bytes); + } + + cout << "REQUEST" << endl << requestText << endl << "RESPONSE" << endl << result << endl; + + size_t headerEnd = result.find("\r\n\r\n"); + if (headerEnd == result.npos) { + headerEnd = result.find("\n\n"); + } + if (headerEnd == result.npos) { + headerEnd = 0; + } + result.erase(0, headerEnd); + + return result; +} + +void Http::startServer(unsigned short port, const Http::ServerHandler& handler) { + stopServer(); + _serverHandler = handler; + tcp::acceptor* acceptor = new tcp::acceptor(_ioService, tcp::endpoint(tcp::v4(), port)); + _tcpServer = new Server(this, acceptor); + _ioService.run(); +} + +void Http::startServer(const std::string& unixSocketPath, const Http::ServerHandler& handler) { + stopServer(); + _serverHandler = handler; + stream_protocol::acceptor* acceptor = new stream_protocol::acceptor(_ioService, stream_protocol::endpoint(unixSocketPath)); + _unixSocketServer = new Server(this, acceptor); + _ioService.run(); +} + +void Http::stopServer() { + _ioService.stop(); + if (_tcpServer != nullptr) { + delete _tcpServer; + _tcpServer = nullptr; + } + if (_unixSocketServer != nullptr) { + delete _unixSocketServer; + _unixSocketServer = nullptr; + } +} + +} \ No newline at end of file diff --git a/src/tgbot/Http.h b/src/tgbot/Http.h new file mode 100644 index 0000000..aedcd93 --- /dev/null +++ b/src/tgbot/Http.h @@ -0,0 +1,125 @@ +/* + * Copyright (c) 2015 Oleg Morozenkov + * + * 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. + */ + +#ifndef TGBOT_CPP_HTTP_H +#define TGBOT_CPP_HTTP_H + +#include + +#include +#include + +#include "tgbot/Url.h" + +namespace TgBot { + +class Http { + +public: + typedef std::function ServerHandler; + + class Argument { + + public: + template + Argument(const std::string& name, const T& value, bool isFile = false, const std::string& mimeType = "") : + name(name), value(boost::lexical_cast(value)), isFile(isFile), mimeType(mimeType) + { + } + + std::string name; + std::string value; + bool isFile = false; + std::string mimeType; + }; + + std::string makeRequest(const Url& url, const std::vector& args); + void startServer(unsigned short port, const ServerHandler& handler); + void startServer(const std::string& unixSocketPath, const ServerHandler& handler); + void stopServer(); + +private: + template + class Connection { + + public: + Connection(Http* http, boost::asio::basic_stream_socket* socket) : http(http), socket(socket) { + } + + ~Connection() { + delete socket; + } + + void start() { + data.reserve(10240); + socket->async_receive(data, [this]() { + size_t headerEnd = data.find("\r\n\r\n"); + if (headerEnd == data.npos) { + headerEnd = data.find("\n\n"); + } + if (headerEnd == data.npos) { + headerEnd = 0; + } + data.erase(0, headerEnd); + http->_serverHandler(data); + }); + } + + Http* http; + boost::asio::basic_stream_socket* socket; + std::string data; + }; + + template + class Server { + + public: + Server(Http* http, boost::asio::basic_socket_acceptor* acceptor) : http(http), acceptor(acceptor) { + } + + ~Server() { + delete acceptor; + } + + void start() { + boost::asio::basic_stream_socket* socket = new boost::asio::basic_stream_socket(acceptor->get_io_service()); + std::shared_ptr> connection(new Connection(http, socket)); + acceptor->async_accept(*connection->socket, [this, connection]() { + connection->start(); + start(); + }); + } + + Http* http; + boost::asio::basic_socket_acceptor* acceptor; + }; + + + boost::asio::io_service _ioService; + Server* _tcpServer = nullptr; + Server* _unixSocketServer = nullptr; + ServerHandler _serverHandler; +}; + +} + +#endif //TGBOT_CPP_HTTP_H diff --git a/src/tgbot/Parser.cpp b/src/tgbot/Parser.cpp new file mode 100644 index 0000000..3730607 --- /dev/null +++ b/src/tgbot/Parser.cpp @@ -0,0 +1,485 @@ +/* + * Copyright (c) 2015 Oleg Morozenkov + * + * 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. + */ + +#include "Parser.h" + +using namespace std; +using namespace boost::property_tree; + +namespace TgBot { + +User::Ptr Parser::parseUser(const ptree& data) const { + User::Ptr result(new User); + result->id = data.get("id"); + result->firstName = data.get("first_name"); + result->lastName = data.get("last_name", ""); + result->username = data.get("username", ""); + return result; +} + +string Parser::parseUser(const User::Ptr& object) const { + if (!object) { + return ""; + } + string result; + result += '{'; + appendToJson(result, "id", object->id); + appendToJson(result, "first_name", object->firstName); + appendToJson(result, "last_name", object->lastName); + appendToJson(result, "username", object->username); + result.erase(result.length() - 1); + result += '}'; + return result; +} + +GroupChat::Ptr Parser::parseGroupChat(const ptree& data) const { + GroupChat::Ptr result(new GroupChat); + result->id = data.get("id"); + result->title = data.get("title"); + return result; +} + +string Parser::parseGroupChat(const GroupChat::Ptr& object) const { + if (!object) { + return ""; + } + string result; + result += '{'; + appendToJson(result, "id", object->id); + appendToJson(result, "title", object->title); + result.erase(result.length() - 1); + result += '}'; + return result; +} + +Message::Ptr Parser::parseMessage(const ptree& data) const { + Message::Ptr result(new Message); + result->messageId = data.get("message_id"); + result->from = parseUser(data.find("from")->second); + result->date = data.get("date"); + result->chat = parseGenericChat(data.find("chat")->second); + result->forwardFrom = tryParse(parseUser, data, "forward_from"); + result->forwardDate = data.get("forward_date", 0); + result->replyToMessage = tryParse(parseMessage, data, "reply_to_message"); + result->text = data.get("text", ""); + result->audio = tryParse