From e73a5a765b50c36d3356a44c81cad0d0630c7e44 Mon Sep 17 00:00:00 2001 From: tastytea Date: Mon, 26 Feb 2018 07:57:30 +0100 Subject: [PATCH] Added streaming support --- CMakeLists.txt | 4 +- README.md | 14 ++-- src/api_get.cpp | 2 + src/api_get_stream.cpp | 85 ++++++++++++++++++++++++ src/examples/example9_streaming_api.cpp | 69 ++++++++++++++++++++ src/http_sync.cpp | 81 +++++++++++++++++++---- src/mastodon-cpp.hpp | 86 ++++++++++++++++++++++++- 7 files changed, 317 insertions(+), 24 deletions(-) create mode 100644 src/api_get_stream.cpp create mode 100644 src/examples/example9_streaming_api.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index fbafff3..a5f1d26 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,6 +1,6 @@ cmake_minimum_required (VERSION 3.7) project (mastodon-cpp - VERSION 0.3.3 + VERSION 0.4.0 LANGUAGES CXX ) @@ -63,7 +63,7 @@ if(WITH_EXAMPLES) foreach(src ${sources_examples}) get_filename_component(bin ${src} NAME_WE) add_executable(${bin} ${src}) - target_link_libraries(${bin} ${Boost_LIBRARIES} mastodon-cpp) + target_link_libraries(${bin} -lpthread ${Boost_LIBRARIES} mastodon-cpp) endforeach() endif() diff --git a/README.md b/README.md index 904ebed..68af273 100644 --- a/README.md +++ b/README.md @@ -75,6 +75,8 @@ After you did a `make install`, a project consisting of one file can be compiled | 1 | Invalid call | | 2 | Not implemented | | 3 | URL changed (HTTP 301 or 308) | +| 4 | Aborted by user | +| 10 | Failed to connect | | 100 - 999 | HTTP status codes | | 65535 | Unknown error | @@ -109,7 +111,7 @@ If you use a debug build, you get more verbose error messages. * [x] Support registering as an application * Version 0.4.0 * [x] Handle X-RateLimit header - * [ ] Streaming API + * [x] Streaming API * Later * [ ] Asynchronous I/O * [ ] Improve error reporting @@ -179,11 +181,11 @@ If you use a debug build, you get more verbose error messages. * [x] GET /api/v1/timelines/public * [x] GET /api/v1/timelines/tag/:hashtag * [x] GET /api/v1/timelines/list/:list_id - * [ ] GET /api/v1/streaming/user - * [ ] GET /api/v1/streaming/public - * [ ] GET /api/v1/streaming/public/local - * [ ] GET /api/v1/streaming/hashtag - * [ ] GET /api/v1/streaming/list + * [x] GET /api/v1/streaming/user + * [x] GET /api/v1/streaming/public + * [x] GET /api/v1/streaming/public/local + * [x] GET /api/v1/streaming/hashtag + * [x] GET /api/v1/streaming/list # Copyright diff --git a/src/api_get.cpp b/src/api_get.cpp index 1f7a286..f749ac9 100644 --- a/src/api_get.cpp +++ b/src/api_get.cpp @@ -17,6 +17,7 @@ #include #include #include +#include #include #include "macros.hpp" #include "mastodon-cpp.hpp" @@ -24,6 +25,7 @@ using namespace Mastodon; using std::string; using std::cerr; + const std::uint16_t API::get(const Mastodon::API::v1 &call, string &answer) { const parametermap p; diff --git a/src/api_get_stream.cpp b/src/api_get_stream.cpp new file mode 100644 index 0000000..6ccb483 --- /dev/null +++ b/src/api_get_stream.cpp @@ -0,0 +1,85 @@ +/* This file is part of mastodon-cpp. + * Copyright © 2018 tastytea + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include +#include +#include +#include "macros.hpp" +#include "mastodon-cpp.hpp" + +using namespace Mastodon; +using std::string; +using std::cerr; + +const std::uint16_t API::get_stream(const Mastodon::API::v1 &call, + const string &argument, + string &answer, + std::unique_ptr &ptr) +{ + string strcall = ""; + const string argument_encoded = curlpp::escape(argument); + + switch (call) + { + case v1::streaming_hashtag: + strcall = "/api/v1/streaming/hashtag?tag=" + argument_encoded; + break; + case v1::streaming_list: + strcall = "/api/v1/streaming/list?list=" + argument_encoded; + break; + default: + ttdebug << "ERROR: Invalid call.\n"; + return 1; + break; + } + + ptr = std::make_unique(*this, _instance, _access_token); + return ptr->request_sync(http::method::GET_STREAM, strcall, answer); +} + +const std::uint16_t API::get_stream(const Mastodon::API::v1 &call, + string &answer, + std::unique_ptr &ptr) +{ + string strcall = ""; + + switch (call) + { + case v1::streaming_user: + strcall = "/api/v1/streaming/user"; + break; + case v1::streaming_public: + strcall = "/api/v1/streaming/public"; + break; + case v1::streaming_public_local: + strcall = "/api/v1/streaming/public/local"; + break; + default: + ttdebug << "ERROR: Invalid call.\n"; + return 1; + break; + } + + ptr = std::make_unique(*this, _instance, _access_token); + return ptr->request_sync(http::method::GET_STREAM, strcall, answer); +} + +const std::uint16_t API::get_stream(const std::string &call, string &answer, + std::unique_ptr &ptr) +{ + ptr = std::make_unique(*this, _instance, _access_token); + return ptr->request_sync(http::method::GET_STREAM, call, answer); +} diff --git a/src/examples/example9_streaming_api.cpp b/src/examples/example9_streaming_api.cpp new file mode 100644 index 0000000..7a4dbf9 --- /dev/null +++ b/src/examples/example9_streaming_api.cpp @@ -0,0 +1,69 @@ +/* This file is part of mastodon-cpp. + * How to use the streaming API + */ + +/* This file is part of mastodon-cpp. + */ + +#include +#include +#include +#include +#include +#include +#include "../mastodon-cpp.hpp" + +using Mastodon::API; + +int main(int argc, char *argv[]) +{ + if (argc < 3) + { + std::cerr << "usage: " << argv[0] << " \n"; + return 1; + } + + static std::string answer; + static std::unique_ptr ptr; + + std::cout << "Dumping public timeline...\n"; + std::thread pub([=] + { + Mastodon::API masto(argv[1], argv[2]); + masto.set_useragent("mastodon-cpp-example/1.3.3.7"); + masto.get_stream(API::v1::streaming_public, answer, ptr); + }); + + std::uint8_t counter = 0; + while (true) + { + ++counter; + std::cout << answer; + answer.clear(); + if (counter == 10) + { + std::cerr << "Aborting...\n"; + ptr->abort_stream(); + break; + } + std::this_thread::sleep_for(std::chrono::seconds(2)); + } + pub.join(); + std::cout << '\n'; + + std::cout << "Dumping the #np tag...\n"; + answer = ""; + std::thread tag([=] + { + Mastodon::API masto(argv[1], argv[2]); + masto.set_useragent("mastodon-cpp-example/1.3.3.7"); + masto.get_stream(API::v1::streaming_hashtag, "np", answer, ptr); + }); + std::this_thread::sleep_for(std::chrono::seconds(20)); + ptr->abort_stream(); + std::cout << answer; + std::cout << '\n'; + tag.join(); + + return 0; +} diff --git a/src/http_sync.cpp b/src/http_sync.cpp index b3a4cec..1565f20 100644 --- a/src/http_sync.cpp +++ b/src/http_sync.cpp @@ -17,7 +17,9 @@ #include #include #include -#include +#include // std::bind +#include +#include // std::strncmp #include #include #include @@ -36,10 +38,16 @@ API::http::http(const API &api, const string &instance, : parent(api) , _instance(instance) , _access_token(access_token) +, _abort_stream(false) { curlpp::initialize(); } +API::http::~http() +{ + curlpp::terminate(); +} + const std::uint16_t API::http::request_sync(const method &meth, const string &path, string &answer) @@ -52,23 +60,34 @@ const std::uint16_t API::http::request_sync(const method &meth, const curlpp::Forms &formdata, string &answer) { + using namespace std::placeholders; // _1, _2, _3 + + std::uint16_t ret; ttdebug << "Path is: " << path << '\n'; try { - std::ostringstream oss; curlpp::Easy request; + std::list headers; + request.setOpt("https://" + _instance + path); request.setOpt(parent.get_useragent()); - request.setOpt( + + headers.push_back("Connection: close"); + if (!_access_token.empty()) { - "Connection: close", - "Authorization: Bearer " + _access_token - }); + headers.push_back("Authorization: Bearer " + _access_token); + } + request.setOpt(headers); + // Get headers from server - request.setOpt(true); + if (meth != http::method::GET_STREAM) + { + request.setOpt(true); + } request.setOpt(true); - request.setOpt(&oss); + request.setOpt + (std::bind(&http::callback, this, _1, _2, _3, &answer)); if (!formdata.empty()) { request.setOpt(formdata); @@ -93,15 +112,15 @@ const std::uint16_t API::http::request_sync(const method &meth, } request.perform(); - std::uint16_t ret = curlpp::infos::ResponseCode::get(request); + ret = curlpp::infos::ResponseCode::get(request); ttdebug << "Response code: " << ret << '\n'; - size_t pos = oss.str().find("\r\n\r\n"); - _headers = oss.str().substr(0, pos); + size_t pos = answer.find("\r\n\r\n"); + _headers = answer.substr(0, pos); if (ret == 200 || ret == 302 || ret == 307) { // OK or Found or Temporary Redirect // Only return body - answer = oss.str().substr(pos + 4); + answer = answer.substr(pos + 4); } else if (ret == 301 || ret == 308) { // Moved Permanently or Permanent Redirect @@ -116,9 +135,25 @@ const std::uint16_t API::http::request_sync(const method &meth, } catch (curlpp::RuntimeError &e) { - cerr << "RUNTIME ERROR: " << e.what() << std::endl; + if (std::strncmp(e.what(), + "Failed writing body", 19) == 0) + { + ttdebug << "Request was aborted by user\n"; + return 4; + } + else if (std::strncmp(e.what(), + "Failed to connect to", 20) == 0) + { + ret = 10; + } + else + { + cerr << "RUNTIME ERROR: " << e.what() << std::endl; + ret = 0xffff; + } + ttdebug << e.what() << std::endl; - return 0xffff; + return ret; } catch (curlpp::LogicError &e) { @@ -133,3 +168,21 @@ const void API::http::get_headers(string &headers) const { headers = _headers; } + +const size_t API::http::callback(char* data, size_t size, size_t nmemb, + string *str) +{ + if (_abort_stream) + { + // This throws the runtime error: Failed writing body + return 0; + } + str->append(data); + // ttdebug << "Received " << size * nmemb << " Bytes\n"; + return size * nmemb; +}; + +const void API::http::abort_stream() +{ + _abort_stream = true; +} diff --git a/src/mastodon-cpp.hpp b/src/mastodon-cpp.hpp index f2102fd..ab8cdf8 100644 --- a/src/mastodon-cpp.hpp +++ b/src/mastodon-cpp.hpp @@ -21,6 +21,7 @@ #include #include #include +#include #include #include @@ -33,6 +34,7 @@ * @example example6_toot_delete-toot.cpp * @example example7_register_app.cpp * @example example8_rate_limiting.cpp + * @example example9_streaming_api.cpp */ namespace Mastodon { @@ -52,6 +54,8 @@ namespace Mastodon class API { public: + class http; + /*! * @brief Used for passing (most of the time) optional parameters. * @@ -127,7 +131,13 @@ public: statuses_id_pin, statuses_id_unpin, statuses_id_mute, - statuses_id_unmute + statuses_id_unmute, + // Streaming + streaming_user, + streaming_public, + streaming_public_local, + streaming_hashtag, + streaming_list }; /*! @@ -288,6 +298,56 @@ public: const std::uint16_t get(const std::string &call, std::string &answer); + /*! + * @brief Make a streaming GET request. + * + * @param call A call defined in Mastodon::API::v1 + * @param argument The non-optional argument + * @param answer The answer from the server. Usually JSON. On error an + * empty string. + * @param ptr Pointer to the http object. Can be used to call + * ptr->abort_stream() + * + * @return @ref error "Error code". If the URL has permanently changed, 3 + * is returned and answer is set to the new URL. + */ + const std::uint16_t get_stream(const Mastodon::API::v1 &call, + const std::string &argument, + std::string &answer, + std::unique_ptr &ptr); + + /*! + * @brief Make a streaming GET request. + * + * @param call A call defined in Mastodon::API::v1 + * @param answer The answer from the server. Usually JSON. On error an + * empty string. + * @param ptr Pointer to the http object. Can be used to call + * ptr->abort_stream() + * + * @return @ref error "Error code". If the URL has permanently changed, 3 + * is returned and answer is set to the new URL. + */ + const std::uint16_t get_stream(const Mastodon::API::v1 &call, + std::string &answer, + std::unique_ptr &ptr); + + /*! + * @brief Make a streaming GET request. + * + * @param call String in the form `/api/v1/example` + * @param answer The answer from the server. Usually JSON. On error an + * empty string. + * @param ptr Pointer to the http object. Can be used to call + * ptr->abort_stream() + * + * @return @ref error "Error code". If the URL has permanently changed, 3 + * is returned and answer is set to the new URL. + */ + const std::uint16_t get_stream(const std::string &call, + std::string &answer, + std::unique_ptr &ptr); + /*! * @brief Make a PATCH request. * @@ -507,6 +567,10 @@ private: */ const curlpp::Forms maptoformdata(const parametermap &map); +public: + /*! + * @brief http class. Do not use this directly. + */ class http { public: @@ -516,17 +580,21 @@ private: PATCH, POST, PUT, - DELETE + DELETE, + GET_STREAM }; explicit http(const API &api, const std::string &instance, const std::string &access_token); + ~http(); const std::uint16_t request_sync(const method &meth, const std::string &path, std::string &answer); /*! * @brief Blocking request. + * + * * * @param meth The method defined in http::method * @param path The api call as string @@ -543,11 +611,25 @@ private: const void get_headers(std::string &headers) const; + const size_t callback(char* data, size_t size, size_t nmemb, + std::string *oss); + + /*! + * @brief Aborts the stream. Use only with streams. + * + * Aborts the stream next time data comes in. Can take a few + * seconds. + * This works only with streams, because only streams have an + * own http object. + */ + const void abort_stream(); + private: const API &parent; const std::string _instance; const std::string _access_token; std::string _headers; + bool _abort_stream; } _http; }; }