Added streaming support

This commit is contained in:
tastytea 2018-02-26 07:57:30 +01:00
parent f9b6009b57
commit e73a5a765b
Signed by: tastytea
GPG Key ID: 59346E0EA35C67E5
7 changed files with 317 additions and 24 deletions

View File

@ -1,6 +1,6 @@
cmake_minimum_required (VERSION 3.7) cmake_minimum_required (VERSION 3.7)
project (mastodon-cpp project (mastodon-cpp
VERSION 0.3.3 VERSION 0.4.0
LANGUAGES CXX LANGUAGES CXX
) )
@ -63,7 +63,7 @@ if(WITH_EXAMPLES)
foreach(src ${sources_examples}) foreach(src ${sources_examples})
get_filename_component(bin ${src} NAME_WE) get_filename_component(bin ${src} NAME_WE)
add_executable(${bin} ${src}) add_executable(${bin} ${src})
target_link_libraries(${bin} ${Boost_LIBRARIES} mastodon-cpp) target_link_libraries(${bin} -lpthread ${Boost_LIBRARIES} mastodon-cpp)
endforeach() endforeach()
endif() endif()

View File

@ -75,6 +75,8 @@ After you did a `make install`, a project consisting of one file can be compiled
| 1 | Invalid call | | 1 | Invalid call |
| 2 | Not implemented | | 2 | Not implemented |
| 3 | URL changed (HTTP 301 or 308) | | 3 | URL changed (HTTP 301 or 308) |
| 4 | Aborted by user |
| 10 | Failed to connect |
| 100 - 999 | HTTP status codes | | 100 - 999 | HTTP status codes |
| 65535 | Unknown error | | 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 * [x] Support registering as an application
* Version 0.4.0 * Version 0.4.0
* [x] Handle X-RateLimit header * [x] Handle X-RateLimit header
* [ ] Streaming API * [x] Streaming API
* Later * Later
* [ ] Asynchronous I/O * [ ] Asynchronous I/O
* [ ] Improve error reporting * [ ] 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/public
* [x] GET /api/v1/timelines/tag/:hashtag * [x] GET /api/v1/timelines/tag/:hashtag
* [x] GET /api/v1/timelines/list/:list_id * [x] GET /api/v1/timelines/list/:list_id
* [ ] GET /api/v1/streaming/user * [x] GET /api/v1/streaming/user
* [ ] GET /api/v1/streaming/public * [x] GET /api/v1/streaming/public
* [ ] GET /api/v1/streaming/public/local * [x] GET /api/v1/streaming/public/local
* [ ] GET /api/v1/streaming/hashtag * [x] GET /api/v1/streaming/hashtag
* [ ] GET /api/v1/streaming/list * [x] GET /api/v1/streaming/list
# Copyright # Copyright

View File

@ -17,6 +17,7 @@
#include <iostream> #include <iostream>
#include <string> #include <string>
#include <vector> #include <vector>
#include <memory>
#include <curlpp/cURLpp.hpp> #include <curlpp/cURLpp.hpp>
#include "macros.hpp" #include "macros.hpp"
#include "mastodon-cpp.hpp" #include "mastodon-cpp.hpp"
@ -24,6 +25,7 @@
using namespace Mastodon; using namespace Mastodon;
using std::string; using std::string;
using std::cerr; using std::cerr;
const std::uint16_t API::get(const Mastodon::API::v1 &call, string &answer) const std::uint16_t API::get(const Mastodon::API::v1 &call, string &answer)
{ {
const parametermap p; const parametermap p;

85
src/api_get_stream.cpp Normal file
View File

@ -0,0 +1,85 @@
/* This file is part of mastodon-cpp.
* Copyright © 2018 tastytea <tastytea@tastytea.de>
*
* 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 <http://www.gnu.org/licenses/>.
*/
#include <iostream>
#include <string>
#include <memory>
#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<Mastodon::API::http> &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<http>(*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<Mastodon::API::http> &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<http>(*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<http> &ptr)
{
ptr = std::make_unique<http>(*this, _instance, _access_token);
return ptr->request_sync(http::method::GET_STREAM, call, answer);
}

View File

@ -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 <iostream>
#include <string>
#include <cstdint>
#include <thread>
#include <chrono>
#include <memory>
#include "../mastodon-cpp.hpp"
using Mastodon::API;
int main(int argc, char *argv[])
{
if (argc < 3)
{
std::cerr << "usage: " << argv[0] << " <instance> <access_token>\n";
return 1;
}
static std::string answer;
static std::unique_ptr<Mastodon::API::http> 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;
}

View File

@ -17,7 +17,9 @@
#include <string> #include <string>
#include <cstdint> #include <cstdint>
#include <iostream> #include <iostream>
#include <sstream> #include <functional> // std::bind
#include <list>
#include <cstring> // std::strncmp
#include <curlpp/cURLpp.hpp> #include <curlpp/cURLpp.hpp>
#include <curlpp/Easy.hpp> #include <curlpp/Easy.hpp>
#include <curlpp/Options.hpp> #include <curlpp/Options.hpp>
@ -36,10 +38,16 @@ API::http::http(const API &api, const string &instance,
: parent(api) : parent(api)
, _instance(instance) , _instance(instance)
, _access_token(access_token) , _access_token(access_token)
, _abort_stream(false)
{ {
curlpp::initialize(); curlpp::initialize();
} }
API::http::~http()
{
curlpp::terminate();
}
const std::uint16_t API::http::request_sync(const method &meth, const std::uint16_t API::http::request_sync(const method &meth,
const string &path, const string &path,
string &answer) string &answer)
@ -52,23 +60,34 @@ const std::uint16_t API::http::request_sync(const method &meth,
const curlpp::Forms &formdata, const curlpp::Forms &formdata,
string &answer) string &answer)
{ {
using namespace std::placeholders; // _1, _2, _3
std::uint16_t ret;
ttdebug << "Path is: " << path << '\n'; ttdebug << "Path is: " << path << '\n';
try try
{ {
std::ostringstream oss;
curlpp::Easy request; curlpp::Easy request;
std::list<string> headers;
request.setOpt<curlopts::Url>("https://" + _instance + path); request.setOpt<curlopts::Url>("https://" + _instance + path);
request.setOpt<curlopts::UserAgent>(parent.get_useragent()); request.setOpt<curlopts::UserAgent>(parent.get_useragent());
request.setOpt<curlopts::HttpHeader>(
headers.push_back("Connection: close");
if (!_access_token.empty())
{ {
"Connection: close", headers.push_back("Authorization: Bearer " + _access_token);
"Authorization: Bearer " + _access_token }
}); request.setOpt<curlopts::HttpHeader>(headers);
// Get headers from server // Get headers from server
request.setOpt<curlpp::options::Header>(true); if (meth != http::method::GET_STREAM)
{
request.setOpt<curlpp::options::Header>(true);
}
request.setOpt<curlopts::FollowLocation>(true); request.setOpt<curlopts::FollowLocation>(true);
request.setOpt<curlopts::WriteStream>(&oss); request.setOpt<curlpp::options::WriteFunction>
(std::bind(&http::callback, this, _1, _2, _3, &answer));
if (!formdata.empty()) if (!formdata.empty())
{ {
request.setOpt<curlopts::HttpPost>(formdata); request.setOpt<curlopts::HttpPost>(formdata);
@ -93,15 +112,15 @@ const std::uint16_t API::http::request_sync(const method &meth,
} }
request.perform(); request.perform();
std::uint16_t ret = curlpp::infos::ResponseCode::get(request); ret = curlpp::infos::ResponseCode::get(request);
ttdebug << "Response code: " << ret << '\n'; ttdebug << "Response code: " << ret << '\n';
size_t pos = oss.str().find("\r\n\r\n"); size_t pos = answer.find("\r\n\r\n");
_headers = oss.str().substr(0, pos); _headers = answer.substr(0, pos);
if (ret == 200 || ret == 302 || ret == 307) if (ret == 200 || ret == 302 || ret == 307)
{ // OK or Found or Temporary Redirect { // OK or Found or Temporary Redirect
// Only return body // Only return body
answer = oss.str().substr(pos + 4); answer = answer.substr(pos + 4);
} }
else if (ret == 301 || ret == 308) else if (ret == 301 || ret == 308)
{ // Moved Permanently or Permanent Redirect { // Moved Permanently or Permanent Redirect
@ -116,9 +135,25 @@ const std::uint16_t API::http::request_sync(const method &meth,
} }
catch (curlpp::RuntimeError &e) 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) catch (curlpp::LogicError &e)
{ {
@ -133,3 +168,21 @@ const void API::http::get_headers(string &headers) const
{ {
headers = _headers; 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;
}

View File

@ -21,6 +21,7 @@
#include <vector> #include <vector>
#include <cstdint> #include <cstdint>
#include <map> #include <map>
#include <memory>
#include <curlpp/cURLpp.hpp> #include <curlpp/cURLpp.hpp>
#include <curlpp/Easy.hpp> #include <curlpp/Easy.hpp>
@ -33,6 +34,7 @@
* @example example6_toot_delete-toot.cpp * @example example6_toot_delete-toot.cpp
* @example example7_register_app.cpp * @example example7_register_app.cpp
* @example example8_rate_limiting.cpp * @example example8_rate_limiting.cpp
* @example example9_streaming_api.cpp
*/ */
namespace Mastodon namespace Mastodon
{ {
@ -52,6 +54,8 @@ namespace Mastodon
class API class API
{ {
public: public:
class http;
/*! /*!
* @brief Used for passing (most of the time) optional parameters. * @brief Used for passing (most of the time) optional parameters.
* *
@ -127,7 +131,13 @@ public:
statuses_id_pin, statuses_id_pin,
statuses_id_unpin, statuses_id_unpin,
statuses_id_mute, 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, const std::uint16_t get(const std::string &call,
std::string &answer); 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<Mastodon::API::http> &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<Mastodon::API::http> &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<Mastodon::API::http> &ptr);
/*! /*!
* @brief Make a PATCH request. * @brief Make a PATCH request.
* *
@ -507,6 +567,10 @@ private:
*/ */
const curlpp::Forms maptoformdata(const parametermap &map); const curlpp::Forms maptoformdata(const parametermap &map);
public:
/*!
* @brief http class. Do not use this directly.
*/
class http class http
{ {
public: public:
@ -516,17 +580,21 @@ private:
PATCH, PATCH,
POST, POST,
PUT, PUT,
DELETE DELETE,
GET_STREAM
}; };
explicit http(const API &api, const std::string &instance, explicit http(const API &api, const std::string &instance,
const std::string &access_token); const std::string &access_token);
~http();
const std::uint16_t request_sync(const method &meth, const std::uint16_t request_sync(const method &meth,
const std::string &path, const std::string &path,
std::string &answer); std::string &answer);
/*! /*!
* @brief Blocking request. * @brief Blocking request.
*
*
* *
* @param meth The method defined in http::method * @param meth The method defined in http::method
* @param path The api call as string * @param path The api call as string
@ -543,11 +611,25 @@ private:
const void get_headers(std::string &headers) const; 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: private:
const API &parent; const API &parent;
const std::string _instance; const std::string _instance;
const std::string _access_token; const std::string _access_token;
std::string _headers; std::string _headers;
bool _abort_stream;
} _http; } _http;
}; };
} }