about summary refs log tree commit diff stats
path: root/src/client.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'src/client.cpp')
-rw-r--r--src/client.cpp1066
1 files changed, 378 insertions, 688 deletions
diff --git a/src/client.cpp b/src/client.cpp index 2c655e2..f8908fd 100644 --- a/src/client.cpp +++ b/src/client.cpp
@@ -5,10 +5,8 @@
5#include <liboauthcpp/liboauthcpp.h> 5#include <liboauthcpp/liboauthcpp.h>
6#include <curl_easy.h> 6#include <curl_easy.h>
7#include <curl_header.h> 7#include <curl_header.h>
8#include "util.h"
9#include <json.hpp> 8#include <json.hpp>
10 9#include <thread>
11using nlohmann::json;
12 10
13// These are here for debugging curl stuff 11// These are here for debugging curl stuff
14 12
@@ -84,99 +82,315 @@ int my_trace(CURL *handle, curl_infotype type,
84 82
85namespace twitter { 83namespace twitter {
86 84
87 int client_stream_progress_callback_wrapper(void* cdata, curl_off_t, curl_off_t, curl_off_t, curl_off_t) 85 class request
88 { 86 {
89 return static_cast<client::stream*>(cdata)->progress(); 87 public:
90 } 88
89 explicit request(std::string url) try
90 : _ios(_output), _conn(_ios)
91 {
92 _conn.add<CURLOPT_URL>(url.c_str());
93 } catch (const curl::curl_easy_exception& error)
94 {
95 error.print_traceback();
96
97 assert(false);
98 }
99
100 std::string perform()
101 {
102 try
103 {
104 _conn.perform();
105 } catch (const curl::curl_easy_exception& error)
106 {
107 std::throw_with_nested(connection_error());
108 }
109
110 int response_code = _conn.get_info<CURLINFO_RESPONSE_CODE>().get();
111 std::string result = _output.str();
112
113 if (response_code / 100 != 2)
114 {
115 nlohmann::json response_json;
116
117 try
118 {
119 response_json = nlohmann::json::parse(result);
120 } catch (const std::invalid_argument& e)
121 {
122 std::throw_with_nested(invalid_response(result));
123 }
124
125 for (nlohmann::json& error : response_json["errors"])
126 {
127 int error_code;
128 std::string error_message;
129
130 try
131 {
132 error_code = error["code"].get<int>();
133 error_message = error["message"].get<std::string>();
134 } catch (const std::domain_error& e)
135 {
136 std::throw_with_nested(invalid_response(result));
137 }
138
139 switch (error_code)
140 {
141 case 32:
142 case 135:
143 case 215:
144 throw bad_auth(error_message);
145
146 case 44:
147 throw invalid_media(error_message);
148
149 case 64:
150 throw account_suspended(error_message);
151
152 case 88:
153 throw rate_limit_exceeded(error_message);
154
155 case 89:
156 throw bad_token(error_message);
157
158 case 130:
159 throw server_overloaded(error_message);
160
161 case 131:
162 throw server_error(error_message);
163
164 case 185:
165 throw update_limit_exceeded(error_message);
166
167 case 186:
168 throw bad_length(error_message);
169
170 case 187:
171 throw duplicate_status(error_message);
172
173 case 226:
174 throw suspected_spam(error_message);
175
176 case 261:
177 throw write_restricted(error_message);
178 }
179 }
180
181 if (response_code == 429)
182 {
183 throw rate_limit_exceeded("HTTP 429 Too Many Requests");
184 } else if (response_code == 500)
185 {
186 throw server_error("HTTP 500 Internal Server Error");
187 } else if (response_code == 502)
188 {
189 throw server_unavailable("HTTP 502 Bad Gateway");
190 } else if (response_code == 503)
191 {
192 throw server_overloaded("HTTP 503 Service Unavailable");
193 } else if (response_code == 504)
194 {
195 throw server_timeout("HTTP 504 Gateway Timeout");
196 }
197
198 throw unknown_error(response_code, result);
199 }
200
201 return result;
202 }
203
204 private:
205
206 std::ostringstream _output;
207 curl::curl_ios<std::ostringstream> _ios;
208
209 protected:
210
211 curl::curl_easy _conn;
212 };
91 213
92 size_t client_stream_write_callback_wrapper(void* ptr, size_t size, size_t nmemb, void* cdata) 214 class get : public request
93 { 215 {
94 return static_cast<client::stream*>(cdata)->write(static_cast<char*>(ptr), size, nmemb); 216 public:
95 } 217
218 get(const OAuth::Client& oauth_client, std::string url) try
219 : request(url)
220 {
221 std::string oauth_header = oauth_client.getFormattedHttpHeader(OAuth::Http::Get, url, "");
222 if (!oauth_header.empty())
223 {
224 _headers.add(std::move(oauth_header));
225 }
96 226
97 client::client(const auth& _arg) 227 _conn.add<CURLOPT_HTTPHEADER>(_headers.get());
228 } catch (const OAuth::ParseError& error)
229 {
230 std::cout << "Error generating OAuth header:" << std::endl;
231 std::cout << error.what() << std::endl;
232 std::cout << "This is likely due to a malformed URL." << std::endl;
233
234 assert(false);
235 } catch (const curl::curl_easy_exception& error)
236 {
237 error.print_traceback();
238
239 assert(false);
240 }
241
242 private:
243
244 curl::curl_header _headers;
245 };
246
247 class post : public request
98 { 248 {
99 _oauth_consumer = new OAuth::Consumer(_arg.getConsumerKey(), _arg.getConsumerSecret()); 249 public:
100 _oauth_token = new OAuth::Token(_arg.getAccessKey(), _arg.getAccessSecret()); 250
101 _oauth_client = new OAuth::Client(_oauth_consumer, _oauth_token); 251 post(const OAuth::Client& oauth_client, std::string url, std::string datastr) try
102 252 : request(url)
103 std::string url = "https://api.twitter.com/1.1/account/verify_credentials.json";
104 long response_code;
105 std::string response_data;
106 if (performGet(url, response_code, response_data) && (response_code == 200))
107 { 253 {
108 try { 254 std::string oauth_header = oauth_client.getFormattedHttpHeader(OAuth::Http::Post, url, datastr);
109 _current_user = user(response_data); 255 if (!oauth_header.empty())
110 } catch (std::invalid_argument e)
111 { 256 {
112 // Ignore 257 _headers.add(std::move(oauth_header));
113 } 258 }
259
260 _conn.add<CURLOPT_HTTPHEADER>(_headers.get());
261 _conn.add<CURLOPT_COPYPOSTFIELDS>(datastr.c_str());
262 } catch (const OAuth::ParseError& error)
263 {
264 std::cout << "Error generating OAuth header:" << std::endl;
265 std::cout << error.what() << std::endl;
266 std::cout << "This is likely due to a malformed URL." << std::endl;
267
268 assert(false);
269 } catch (const curl::curl_easy_exception& error)
270 {
271 error.print_traceback();
272
273 assert(false);
114 } 274 }
115 } 275
276 private:
277
278 curl::curl_header _headers;
279 };
280
281 class multipost : public request
282 {
283 public:
284
285 multipost(const OAuth::Client& oauth_client, std::string url, const curl_httppost* fields) try
286 : request(url)
287 {
288 std::string oauth_header = oauth_client.getFormattedHttpHeader(OAuth::Http::Post, url, "");
289 if (!oauth_header.empty())
290 {
291 _headers.add(std::move(oauth_header));
292 }
293
294 _conn.add<CURLOPT_HTTPHEADER>(_headers.get());
295 _conn.add<CURLOPT_HTTPPOST>(fields);
296 } catch (const OAuth::ParseError& error)
297 {
298 std::cout << "Error generating OAuth header:" << std::endl;
299 std::cout << error.what() << std::endl;
300 std::cout << "This is likely due to a malformed URL." << std::endl;
301
302 assert(false);
303 } catch (const curl::curl_easy_exception& error)
304 {
305 error.print_traceback();
116 306
117 client::~client() 307 assert(false);
308 }
309
310 private:
311
312 curl::curl_header _headers;
313 };
314
315 client::client(const auth& _arg)
118 { 316 {
119 delete _oauth_client; 317 _oauth_consumer =
120 delete _oauth_token; 318 make_unique<OAuth::Consumer>(
121 delete _oauth_consumer; 319 _arg.getConsumerKey(),
320 _arg.getConsumerSecret());
321
322 _oauth_token =
323 make_unique<OAuth::Token>(
324 _arg.getAccessKey(),
325 _arg.getAccessSecret());
326
327 _oauth_client =
328 make_unique<OAuth::Client>(
329 _oauth_consumer.get(),
330 _oauth_token.get());
331
332 _current_user =
333 make_unique<user>(*this,
334 get(*_oauth_client,
335 "https://api.twitter.com/1.1/account/verify_credentials.json")
336 .perform());
122 } 337 }
123 338
124 response client::updateStatus(std::string msg, tweet& result, tweet in_response_to, std::list<long> media_ids) 339 client::~client() = default;
340
341 tweet client::updateStatus(std::string msg, std::list<long> media_ids) const
125 { 342 {
126 std::stringstream datastrstream; 343 std::stringstream datastrstream;
127 datastrstream << "status=" << OAuth::PercentEncode(msg); 344 datastrstream << "status=" << OAuth::PercentEncode(msg);
128 345
129 if (in_response_to)
130 {
131 datastrstream << "&in_reply_to_status_id=";
132 datastrstream << in_response_to.getID();
133 }
134
135 if (!media_ids.empty()) 346 if (!media_ids.empty())
136 { 347 {
137 datastrstream << "&media_ids="; 348 datastrstream << "&media_ids=";
138 datastrstream << twitter::implode(std::begin(media_ids), std::end(media_ids), ","); 349 datastrstream << twitter::implode(std::begin(media_ids), std::end(media_ids), ",");
139 } 350 }
140 351
141 std::string datastr = datastrstream.str(); 352 return tweet(*this,
142 std::string url = "https://api.twitter.com/1.1/statuses/update.json"; 353 post(*_oauth_client,
354 "https://api.twitter.com/1.1/statuses/update.json",
355 datastrstream.str())
356 .perform());
357 }
358
359 tweet client::replyToTweet(std::string msg, tweet_id in_response_to, std::list<long> media_ids) const
360 {
361 std::stringstream datastrstream;
362 datastrstream << "status=" << OAuth::PercentEncode(msg);
363 datastrstream << "&in_reply_to_status_id=";
364 datastrstream << in_response_to;
143 365
144 long response_code; 366 if (!media_ids.empty())
145 std::string response_data;
146 if (!performPost(url, datastr, response_code, response_data))
147 { 367 {
148 return response::curl_error; 368 datastrstream << "&media_ids=";
369 datastrstream << twitter::implode(std::begin(media_ids), std::end(media_ids), ",");
149 } 370 }
150 371
151 if (response_code == 200) 372 return tweet(*this,
152 { 373 post(*_oauth_client,
153 try { 374 "https://api.twitter.com/1.1/statuses/update.json",
154 result = tweet(response_data); 375 datastrstream.str())
155 return response::ok; 376 .perform());
156 } catch (std::invalid_argument e)
157 {
158 return response::invalid_response;
159 }
160 } else {
161 return codeForError(response_code, response_data);
162 }
163 } 377 }
164 378
165 response client::uploadMedia(std::string media_type, const char* data, long data_length, long& media_id) 379 long client::uploadMedia(std::string media_type, const char* data, long data_length) const try
166 { 380 {
167 curl::curl_form form; 381 curl::curl_form form;
382 std::string str_data_length = std::to_string(data_length);
168 383
169 curl::curl_pair<CURLformoption, std::string> command_name(CURLFORM_COPYNAME, "command"); 384 curl::curl_pair<CURLformoption, std::string> command_name(CURLFORM_COPYNAME, "command");
170 curl::curl_pair<CURLformoption, std::string> command_cont(CURLFORM_COPYCONTENTS, "INIT"); 385 curl::curl_pair<CURLformoption, std::string> command_cont(CURLFORM_COPYCONTENTS, "INIT");
171 curl::curl_pair<CURLformoption, std::string> bytes_name(CURLFORM_COPYNAME, "total_bytes"); 386 curl::curl_pair<CURLformoption, std::string> bytes_name(CURLFORM_COPYNAME, "total_bytes");
172 std::string str_data_length = std::to_string(data_length);
173 curl::curl_pair<CURLformoption, std::string> bytes_cont(CURLFORM_COPYCONTENTS, str_data_length); 387 curl::curl_pair<CURLformoption, std::string> bytes_cont(CURLFORM_COPYCONTENTS, str_data_length);
174 curl::curl_pair<CURLformoption, std::string> type_name(CURLFORM_COPYNAME, "media_type"); 388 curl::curl_pair<CURLformoption, std::string> type_name(CURLFORM_COPYNAME, "media_type");
175 curl::curl_pair<CURLformoption, std::string> type_cont(CURLFORM_COPYCONTENTS, media_type); 389 curl::curl_pair<CURLformoption, std::string> type_cont(CURLFORM_COPYCONTENTS, media_type);
176 form.add(command_name, command_cont); 390 form.add(command_name, command_cont);
177 form.add(bytes_name, bytes_cont); 391 form.add(bytes_name, bytes_cont);
178 form.add(type_name, type_cont); 392 form.add(type_name, type_cont);
179 393
180 if (media_type == "image/gif") 394 if (media_type == "image/gif")
181 { 395 {
182 curl::curl_pair<CURLformoption, std::string> category_name(CURLFORM_COPYNAME, "media_category"); 396 curl::curl_pair<CURLformoption, std::string> category_name(CURLFORM_COPYNAME, "media_category");
@@ -184,212 +398,109 @@ namespace twitter {
184 form.add(category_name, category_cont); 398 form.add(category_name, category_cont);
185 } 399 }
186 400
187 long response_code; 401 std::string init_response =
188 std::string response_data; 402 multipost(*_oauth_client,
189 if (!performMultiPost("https://upload.twitter.com/1.1/media/upload.json", form.get(), response_code, response_data)) 403 "https://upload.twitter.com/1.1/media/upload.json",
190 { 404 form.get())
191 return response::curl_error; 405 .perform();
192 } 406
407 long media_id;
193 408
194 if (response_code / 100 != 2) 409 try
195 { 410 {
196 return codeForError(response_code, response_data); 411 nlohmann::json response_json = nlohmann::json::parse(init_response);
197 } 412 media_id = response_json["media_id"].get<long>();
198 413 } catch (const std::invalid_argument& error)
199 json response_json; 414 {
200 try { 415 std::throw_with_nested(invalid_response(init_response));
201 response_json = json::parse(response_data); 416 } catch (const std::domain_error& error)
202 } catch (std::invalid_argument e)
203 { 417 {
204 return response::invalid_response; 418 std::throw_with_nested(invalid_response(init_response));
205 } 419 }
206
207 media_id = response_json["media_id"].get<long>();
208 420
421 // TODO: Currently have to use the C libcurl API to create this form because it uses a buffer and
422 // libcurlcpp currently messes that up.
209 curl_httppost* append_form_post = nullptr; 423 curl_httppost* append_form_post = nullptr;
210 curl_httppost* append_form_last = nullptr; 424 curl_httppost* append_form_last = nullptr;
211 curl_formadd(&append_form_post, &append_form_last, CURLFORM_COPYNAME, "command", CURLFORM_COPYCONTENTS, "APPEND", CURLFORM_END); 425 if ( curl_formadd(&append_form_post, &append_form_last, CURLFORM_COPYNAME, "command", CURLFORM_COPYCONTENTS, "APPEND", CURLFORM_END)
212 curl_formadd(&append_form_post, &append_form_last, CURLFORM_COPYNAME, "media_id", CURLFORM_COPYCONTENTS, std::to_string(media_id).c_str(), CURLFORM_END); 426 || curl_formadd(&append_form_post, &append_form_last, CURLFORM_COPYNAME, "media_id", CURLFORM_COPYCONTENTS, std::to_string(media_id).c_str(), CURLFORM_END)
213 curl_formadd(&append_form_post, &append_form_last, CURLFORM_COPYNAME, "media", CURLFORM_BUFFER, "media", CURLFORM_BUFFERPTR, data, CURLFORM_BUFFERLENGTH, data_length, CURLFORM_CONTENTTYPE, "application/octet-stream", CURLFORM_END); 427 || curl_formadd(&append_form_post, &append_form_last, CURLFORM_COPYNAME, "media", CURLFORM_BUFFER, "media", CURLFORM_BUFFERPTR, data, CURLFORM_BUFFERLENGTH, data_length, CURLFORM_CONTENTTYPE, "application/octet-stream", CURLFORM_END)
214 curl_formadd(&append_form_post, &append_form_last, CURLFORM_COPYNAME, "segment_index", CURLFORM_COPYCONTENTS, std::to_string(0).c_str(), CURLFORM_END); 428 || curl_formadd(&append_form_post, &append_form_last, CURLFORM_COPYNAME, "segment_index", CURLFORM_COPYCONTENTS, std::to_string(0).c_str(), CURLFORM_END))
215 if (!performMultiPost("https://upload.twitter.com/1.1/media/upload.json", append_form_post, response_code, response_data))
216 { 429 {
217 return response::curl_error; 430 assert(false);
218 } 431 }
219 432
220 curl_formfree(append_form_post); 433 multipost(*_oauth_client, "https://upload.twitter.com/1.1/media/upload.json", append_form_post).perform();
221 434
222 if (response_code / 100 != 2) 435 curl_formfree(append_form_post);
223 {
224 return codeForError(response_code, response_data);
225 }
226 436
227 curl::curl_form finalize_form; 437 curl::curl_form finalize_form;
438 std::string str_media_id = std::to_string(media_id);
439
228 curl::curl_pair<CURLformoption, std::string> command3_name(CURLFORM_COPYNAME, "command"); 440 curl::curl_pair<CURLformoption, std::string> command3_name(CURLFORM_COPYNAME, "command");
229 curl::curl_pair<CURLformoption, std::string> command3_cont(CURLFORM_COPYCONTENTS, "FINALIZE"); 441 curl::curl_pair<CURLformoption, std::string> command3_cont(CURLFORM_COPYCONTENTS, "FINALIZE");
230 curl::curl_pair<CURLformoption, std::string> media_id_name(CURLFORM_COPYNAME, "media_id"); 442 curl::curl_pair<CURLformoption, std::string> media_id_name(CURLFORM_COPYNAME, "media_id");
231 std::string str_media_id = std::to_string(media_id);
232 curl::curl_pair<CURLformoption, std::string> media_id_cont(CURLFORM_COPYCONTENTS, str_media_id); 443 curl::curl_pair<CURLformoption, std::string> media_id_cont(CURLFORM_COPYCONTENTS, str_media_id);
233 finalize_form.add(command3_name, command3_cont); 444 finalize_form.add(command3_name, command3_cont);
234 finalize_form.add(media_id_name, media_id_cont); 445 finalize_form.add(media_id_name, media_id_cont);
235 446
236 if (!performMultiPost("https://upload.twitter.com/1.1/media/upload.json", finalize_form.get(), response_code, response_data)) 447 std::string finalize_response =
237 { 448 multipost(*_oauth_client,
238 return response::curl_error; 449 "https://upload.twitter.com/1.1/media/upload.json",
239 } 450 finalize_form.get())
451 .perform();
452
453 nlohmann::json finalize_json;
240 454
241 if (response_code / 100 != 2) 455 try
242 { 456 {
243 return codeForError(response_code, response_data); 457 finalize_json = nlohmann::json::parse(finalize_response);
244 } 458 } catch (const std::invalid_argument& error)
245
246 try {
247 response_json = json::parse(response_data);
248 } catch (std::invalid_argument e)
249 { 459 {
250 return response::invalid_response; 460 std::throw_with_nested(invalid_response(finalize_response));
251 } 461 }
252 462
253 if (response_json.find("processing_info") != response_json.end()) 463 if (finalize_json.find("processing_info") != finalize_json.end())
254 { 464 {
255 std::stringstream datastr; 465 std::stringstream datastr;
256 datastr << "https://upload.twitter.com/1.1/media/upload.json?command=STATUS&media_id=" << media_id; 466 datastr << "https://upload.twitter.com/1.1/media/upload.json?command=STATUS&media_id=" << media_id;
257 467
258 for (;;) 468 for (;;)
259 { 469 {
260 if (!performGet(datastr.str(), response_code, response_data)) 470 std::string status_response = get(*_oauth_client, datastr.str()).perform();
261 {
262 return response::curl_error;
263 }
264 471
265 if (response_code / 100 != 2) 472 try
266 { 473 {
267 return codeForError(response_code, response_data); 474 nlohmann::json status_json = nlohmann::json::parse(status_response);
268 } 475 std::string state = status_json["processing_info"]["state"].get<std::string>();
269 476
270 try { 477 if (state == "succeeded")
271 response_json = json::parse(response_data); 478 {
272 } catch (std::invalid_argument e) 479 break;
480 }
481
482 int ttw = status_json["processing_info"]["check_after_secs"].get<int>();
483 std::this_thread::sleep_for(std::chrono::seconds(ttw));
484 } catch (const std::invalid_argument& error)
273 { 485 {
274 return response::invalid_response; 486 std::throw_with_nested(invalid_response(status_response));
275 } 487 } catch (const std::domain_error& error)
276
277 if (response_json["processing_info"]["state"] == "succeeded")
278 { 488 {
279 break; 489 std::throw_with_nested(invalid_response(status_response));
280 } 490 }
281
282 int ttw = response_json["processing_info"]["check_after_secs"].get<int>();
283 std::this_thread::sleep_for(std::chrono::seconds(ttw));
284 } 491 }
285 } 492 }
286 493
287 return response::ok; 494 return media_id;
288 } 495 } catch (const curl::curl_exception& error)
289
290 response client::follow(user_id toFollow)
291 { 496 {
292 std::stringstream datastrstream; 497 error.print_traceback();
293 datastrstream << "follow=true&user_id=";
294 datastrstream << toFollow;
295 498
296 std::string datastr = datastrstream.str(); 499 assert(false);
297 std::string url = "https://api.twitter.com/1.1/friendships/create.json";
298
299 long response_code;
300 std::string response_data;
301 if (!performPost(url, datastr, response_code, response_data))
302 {
303 return response::curl_error;
304 }
305
306 if (response_code == 200)
307 {
308 return response::ok;
309 } else {
310 return codeForError(response_code, response_data);
311 }
312 }
313
314 response client::follow(user toFollow)
315 {
316 return follow(toFollow.getID());
317 }
318
319 response client::unfollow(user_id toUnfollow)
320 {
321 std::stringstream datastrstream;
322 datastrstream << "user_id=";
323 datastrstream << toUnfollow;
324
325 std::string datastr = datastrstream.str();
326 std::string url = "https://api.twitter.com/1.1/friendships/destroy.json";
327
328 long response_code;
329 std::string response_data;
330 if (!performPost(url, datastr, response_code, response_data))
331 {
332 return response::curl_error;
333 }
334
335 if (response_code == 200)
336 {
337 return response::ok;
338 } else {
339 return codeForError(response_code, response_data);
340 }
341 }
342
343 response client::unfollow(user toUnfollow)
344 {
345 return unfollow(toUnfollow.getID());
346 }
347
348 response client::getUser(user& result)
349 {
350 if (!_current_user)
351 {
352 std::string url = "https://api.twitter.com/1.1/account/verify_credentials.json";
353 long response_code;
354 std::string response_data;
355 if (performGet(url, response_code, response_data) && (response_code == 200))
356 {
357 try {
358 _current_user = user(response_data);
359 } catch (std::invalid_argument e)
360 {
361 return response::invalid_response;
362 }
363 }
364 }
365
366 result = _current_user;
367 return response::ok;
368 }
369
370 configuration client::getConfiguration()
371 {
372 if (!_configuration || (difftime(time(NULL), _last_configuration_update) > 60*60*24))
373 {
374 long response_code;
375 std::string response_data;
376 if (performGet("https://api.twitter.com/1.1/help/configuration.json", response_code, response_data))
377 {
378 _configuration = configuration(response_data);
379 _last_configuration_update = time(NULL);
380 }
381 }
382
383 return _configuration;
384 } 500 }
385 501
386 response client::getFriends(std::set<user_id>& _ret) 502 std::set<user_id> client::getFriends(user_id id) const
387 { 503 {
388 if (!_current_user)
389 {
390 return response::unknown_error;
391 }
392
393 long long cursor = -1; 504 long long cursor = -1;
394 std::set<user_id> result; 505 std::set<user_id> result;
395 506
@@ -397,48 +508,33 @@ namespace twitter {
397 { 508 {
398 std::stringstream urlstream; 509 std::stringstream urlstream;
399 urlstream << "https://api.twitter.com/1.1/friends/ids.json?user_id="; 510 urlstream << "https://api.twitter.com/1.1/friends/ids.json?user_id=";
400 urlstream << _current_user.getID(); 511 urlstream << id;
401 urlstream << "&cursor="; 512 urlstream << "&cursor=";
402 urlstream << cursor; 513 urlstream << cursor;
403 514
404 std::string url = urlstream.str(); 515 std::string url = urlstream.str();
405 516 std::string response_data = get(*_oauth_client, url).perform();
406 long response_code; 517
407 std::string response_data; 518 try
408 if (!performGet(url, response_code, response_data))
409 {
410 return response::curl_error;
411 }
412
413 if (response_code == 200)
414 { 519 {
415 json rjs; 520 nlohmann::json rjs = nlohmann::json::parse(response_data);
416 try {
417 rjs = json::parse(response_data);
418 } catch (std::invalid_argument e)
419 {
420 return response::invalid_response;
421 }
422 521
423 cursor = rjs.at("next_cursor"); 522 cursor = rjs["next_cursor"].get<long long>();
424 result.insert(std::begin(rjs.at("ids")), std::end(rjs.at("ids"))); 523 result.insert(std::begin(rjs["ids"]), std::end(rjs["ids"]));
425 } else { 524 } catch (const std::invalid_argument& error)
426 return codeForError(response_code, response_data); 525 {
526 std::throw_with_nested(invalid_response(response_data));
527 } catch (const std::domain_error& error)
528 {
529 std::throw_with_nested(invalid_response(response_data));
427 } 530 }
428 } 531 }
429 532
430 _ret = result; 533 return result;
431
432 return response::ok;
433 } 534 }
434 535
435 response client::getFollowers(std::set<user_id>& _ret) 536 std::set<user_id> client::getFollowers(user_id id) const
436 { 537 {
437 if (!_current_user)
438 {
439 return response::unknown_error;
440 }
441
442 long long cursor = -1; 538 long long cursor = -1;
443 std::set<user_id> result; 539 std::set<user_id> result;
444 540
@@ -446,474 +542,68 @@ namespace twitter {
446 { 542 {
447 std::stringstream urlstream; 543 std::stringstream urlstream;
448 urlstream << "https://api.twitter.com/1.1/followers/ids.json?user_id="; 544 urlstream << "https://api.twitter.com/1.1/followers/ids.json?user_id=";
449 urlstream << _current_user.getID(); 545 urlstream << id;
450 urlstream << "&cursor="; 546 urlstream << "&cursor=";
451 urlstream << cursor; 547 urlstream << cursor;
452 548
453 std::string url = urlstream.str(); 549 std::string url = urlstream.str();
454 550 std::string response_data = get(*_oauth_client, url).perform();
455 long response_code; 551
456 std::string response_data; 552 try
457 if (!performGet(url, response_code, response_data))
458 {
459 return response::curl_error;
460 }
461
462 if (response_code == 200)
463 { 553 {
464 json rjs; 554 nlohmann::json rjs = nlohmann::json::parse(response_data);
465 try {
466 rjs = json::parse(response_data);
467 } catch (std::invalid_argument e)
468 {
469 return response::invalid_response;
470 }
471 555
472 cursor = rjs.at("next_cursor"); 556 cursor = rjs["next_cursor"].get<long long>();
473 result.insert(std::begin(rjs.at("ids")), std::end(rjs.at("ids"))); 557 result.insert(std::begin(rjs["ids"]), std::end(rjs["ids"]));
474 } else { 558 } catch (const std::invalid_argument& error)
475 return codeForError(response_code, response_data); 559 {
476 } 560 std::throw_with_nested(invalid_response(response_data));
477 } 561 } catch (const std::domain_error& error)
478
479 _ret = result;
480
481 return response::ok;
482 }
483
484 void client::setUserStreamNotifyCallback(stream::notify_callback callback)
485 {
486 _user_stream.setNotifyCallback(callback);
487 }
488
489 void client::setUserStreamReceiveAllReplies(bool _arg)
490 {
491 _user_stream.setReceiveAllReplies(_arg);
492 }
493
494 void client::startUserStream()
495 {
496 _user_stream.start();
497 }
498
499 void client::stopUserStream()
500 {
501 _user_stream.stop();
502 }
503
504 std::string client::generateReplyPrefill(tweet _tweet) const
505 {
506 std::ostringstream output;
507 output << "@" << _tweet.getAuthor().getScreenName() << " ";
508
509 for (auto mention : _tweet.getMentions())
510 {
511 if ((mention.first != _tweet.getAuthor().getID()) && (mention.first != _current_user.getID()))
512 { 562 {
513 output << "@" << mention.second << " "; 563 std::throw_with_nested(invalid_response(response_data));
514 } 564 }
515 } 565 }
516 566
517 return output.str(); 567 return result;
518 }
519
520 bool client::performGet(std::string url, long& response_code, std::string& result)
521 {
522 std::ostringstream output;
523 curl::curl_ios<std::ostringstream> ios(output);
524 curl::curl_easy conn(ios);
525
526 curl::curl_header headers;
527 std::string oauth_header = _oauth_client->getFormattedHttpHeader(OAuth::Http::Get, url, "");
528 if (!oauth_header.empty())
529 {
530 headers.add(oauth_header);
531 }
532
533 try {
534 //conn.add<CURLOPT_VERBOSE>(1);
535 //conn.add<CURLOPT_DEBUGFUNCTION>(my_trace);
536 conn.add<CURLOPT_URL>(url.c_str());
537 conn.add<CURLOPT_HTTPHEADER>(headers.get());
538
539 conn.perform();
540 } catch (curl::curl_easy_exception error)
541 {
542 error.print_traceback();
543
544 return false;
545 }
546
547 response_code = conn.get_info<CURLINFO_RESPONSE_CODE>().get();
548 result = output.str();
549
550 return true;
551 }
552
553 bool client::performPost(std::string url, std::string datastr, long& response_code, std::string& result)
554 {
555 std::ostringstream output;
556 curl::curl_ios<std::ostringstream> ios(output);
557 curl::curl_easy conn(ios);
558
559 curl::curl_header headers;
560 std::string oauth_header = _oauth_client->getFormattedHttpHeader(OAuth::Http::Post, url, datastr);
561 if (!oauth_header.empty())
562 {
563 headers.add(oauth_header);
564 }
565
566 try {
567 //conn.add<CURLOPT_VERBOSE>(1);
568 //conn.add<CURLOPT_DEBUGFUNCTION>(my_trace);
569 conn.add<CURLOPT_URL>(url.c_str());
570 conn.add<CURLOPT_COPYPOSTFIELDS>(datastr.c_str());
571 conn.add<CURLOPT_HTTPHEADER>(headers.get());
572
573 conn.perform();
574 } catch (curl::curl_easy_exception error)
575 {
576 error.print_traceback();
577
578 return false;
579 }
580
581 response_code = conn.get_info<CURLINFO_RESPONSE_CODE>().get();
582 result = output.str();
583
584 return true;
585 }
586
587 bool client::performMultiPost(std::string url, const curl_httppost* fields, long& response_code, std::string& result)
588 {
589 std::ostringstream output;
590 curl::curl_ios<std::ostringstream> ios(output);
591 curl::curl_easy conn(ios);
592
593 curl::curl_header headers;
594 std::string oauth_header = _oauth_client->getFormattedHttpHeader(OAuth::Http::Post, url, "");
595 if (!oauth_header.empty())
596 {
597 headers.add(oauth_header);
598 }
599
600 try {
601 //conn.add<CURLOPT_VERBOSE>(1);
602 //conn.add<CURLOPT_DEBUGFUNCTION>(my_trace);
603 conn.add<CURLOPT_HTTPHEADER>(headers.get());
604 conn.add<CURLOPT_URL>(url.c_str());
605 conn.add<CURLOPT_HTTPPOST>(fields);
606
607 conn.perform();
608 } catch (curl::curl_easy_exception error)
609 {
610 error.print_traceback();
611
612 return false;
613 }
614
615 response_code = conn.get_info<CURLINFO_RESPONSE_CODE>().get();
616 result = output.str();
617
618 return true;
619 }
620
621 response client::codeForError(int response_code, std::string response_data) const
622 {
623 json response_json;
624 try {
625 response_json = json::parse(response_data);
626 } catch (std::invalid_argument e)
627 {
628 return response::invalid_response;
629 }
630
631 std::set<int> error_codes;
632 if (response_json.find("errors") != response_json.end())
633 {
634 std::transform(std::begin(response_json["errors"]), std::end(response_json["errors"]), std::inserter(error_codes, std::begin(error_codes)), [] (const json& error) {
635 return error["code"].get<int>();
636 });
637 }
638
639 if (error_codes.count(32) == 1 || error_codes.count(135) == 1 || error_codes.count(215) == 1)
640 {
641 return response::bad_auth;
642 } else if (error_codes.count(64) == 1)
643 {
644 return response::suspended;
645 } else if (error_codes.count(88) == 1 || error_codes.count(185) == 1)
646 {
647 return response::limited;
648 } else if (error_codes.count(89) == 1)
649 {
650 return response::bad_token;
651 } else if (error_codes.count(130) == 1)
652 {
653 return response::server_overloaded;
654 } else if (error_codes.count(131) == 1)
655 {
656 return response::server_error;
657 } else if (error_codes.count(186) == 1)
658 {
659 return response::bad_length;
660 } else if (error_codes.count(187) == 1)
661 {
662 return response::duplicate_status;
663 } else if (error_codes.count(226) == 1)
664 {
665 return response::suspected_spam;
666 } else if (error_codes.count(261) == 1)
667 {
668 return response::write_restricted;
669 } else if (error_codes.count(44) == 1)
670 {
671 return response::invalid_media;
672 } else if (response_code == 429)
673 {
674 return response::limited;
675 } else if (response_code == 500)
676 {
677 return response::server_error;
678 } else if (response_code == 502)
679 {
680 return response::server_unavailable;
681 } else if (response_code == 503)
682 {
683 return response::server_overloaded;
684 } else if (response_code == 504)
685 {
686 return response::server_timeout;
687 } else {
688 return response::unknown_error;
689 }
690 }
691
692 client::stream::stream(client& _client) : _client(_client)
693 {
694
695 }
696
697 bool client::stream::isRunning() const
698 {
699 return _thread.joinable();
700 } 568 }
701 569
702 void client::stream::setNotifyCallback(notify_callback _n) 570 void client::follow(user_id toFollow) const
703 { 571 {
704 std::lock_guard<std::mutex> _running_lock(_running_mutex); 572 std::stringstream datastrstream;
573 datastrstream << "follow=true&user_id=";
574 datastrstream << toFollow;
705 575
706 if (!_thread.joinable()) 576 post(*_oauth_client, "https://api.twitter.com/1.1/friendships/create.json", datastrstream.str()).perform();
707 {
708 _notify = _n;
709 }
710 } 577 }
711 578
712 void client::stream::setReceiveAllReplies(bool _arg) 579 void client::unfollow(user_id toUnfollow) const
713 { 580 {
714 std::lock_guard<std::mutex> _running_lock(_running_mutex); 581 std::stringstream datastrstream;
715 582 datastrstream << "user_id=";
716 if (!_thread.joinable()) 583 datastrstream << toUnfollow;
717 {
718 _receive_all_replies = _arg;
719 }
720 }
721
722 void client::stream::start()
723 {
724 std::lock_guard<std::mutex> _running_lock(_running_mutex);
725 584
726 if (!_thread.joinable()) 585 post(*_oauth_client, "https://api.twitter.com/1.1/friendships/destroy.json", datastrstream.str()).perform();
727 {
728 _thread = std::thread(&stream::run, this);
729 }
730 } 586 }
731
732 void client::stream::stop()
733 {
734 std::lock_guard<std::mutex> _running_lock(_running_mutex);
735
736 if (_thread.joinable())
737 {
738 _stop = true;
739 _thread.join();
740 _stop = false;
741 }
742 }
743
744 void client::stream::run()
745 {
746 std::ostringstream urlstr;
747 urlstr << "https://userstream.twitter.com/1.1/user.json";
748
749 if (_receive_all_replies)
750 {
751 urlstr << "?replies=all";
752 }
753
754 std::string url = urlstr.str();
755
756 _backoff_type = backoff::none;
757 _backoff_amount = std::chrono::milliseconds(0);
758 for (;;)
759 {
760 curl::curl_easy conn;
761 curl::curl_header headers;
762 std::string oauth_header = _client._oauth_client->getFormattedHttpHeader(OAuth::Http::Get, url, "");
763 if (!oauth_header.empty())
764 {
765 headers.add(oauth_header);
766 }
767
768 conn.add<CURLOPT_WRITEFUNCTION>(client_stream_write_callback_wrapper);
769 conn.add<CURLOPT_WRITEDATA>(this);
770 conn.add<CURLOPT_HEADERFUNCTION>(nullptr);
771 conn.add<CURLOPT_HEADERDATA>(nullptr);
772 conn.add<CURLOPT_XFERINFOFUNCTION>(client_stream_progress_callback_wrapper);
773 conn.add<CURLOPT_XFERINFODATA>(this);
774 conn.add<CURLOPT_NOPROGRESS>(0);
775 //conn.add<CURLOPT_VERBOSE>(1);
776 //conn.add<CURLOPT_DEBUGFUNCTION>(my_trace);
777 conn.add<CURLOPT_URL>(url.c_str());
778 conn.add<CURLOPT_HTTPHEADER>(headers.get());
779
780 bool failure = false;
781 try {
782 conn.perform();
783 } catch (curl::curl_easy_exception error)
784 {
785 failure = true;
786 if ((error.get_code() == CURLE_ABORTED_BY_CALLBACK) && _stop)
787 {
788 break;
789 } else {
790 if (_backoff_type == backoff::none)
791 {
792 _established = false;
793 _backoff_type = backoff::network;
794 _backoff_amount = std::chrono::milliseconds(0);
795 }
796 }
797 }
798
799 if (!failure)
800 {
801 long response_code = conn.get_info<CURLINFO_RESPONSE_CODE>().get();
802 if (response_code == 420)
803 {
804 if (_backoff_type == backoff::none)
805 {
806 _established = false;
807 _backoff_type = backoff::rate_limit;
808 _backoff_amount = std::chrono::minutes(1);
809 }
810 } else if (response_code != 200)
811 {
812 if (_backoff_type == backoff::none)
813 {
814 _established = false;
815 _backoff_type = backoff::http;
816 _backoff_amount = std::chrono::seconds(5);
817 }
818 } else {
819 if (_backoff_type == backoff::none)
820 {
821 _established = false;
822 _backoff_type = backoff::network;
823 _backoff_amount = std::chrono::milliseconds(0);
824 }
825 }
826 }
827
828 std::this_thread::sleep_for(_backoff_amount);
829 587
830 switch (_backoff_type) 588 const user& client::getUser() const
831 {
832 case backoff::network:
833 {
834 if (_backoff_amount < std::chrono::seconds(16))
835 {
836 _backoff_amount += std::chrono::milliseconds(250);
837 }
838
839 break;
840 }
841
842 case backoff::http:
843 {
844 if (_backoff_amount < std::chrono::seconds(320))
845 {
846 _backoff_amount *= 2;
847 }
848
849 break;
850 }
851
852 case backoff::rate_limit:
853 {
854 _backoff_amount *= 2;
855
856 break;
857 }
858 }
859 }
860 }
861
862 size_t client::stream::write(char* ptr, size_t size, size_t nmemb)
863 { 589 {
864 for (size_t i = 0; i < size*nmemb; i++) 590 return *_current_user;
865 {
866 if (ptr[i] == '\r')
867 {
868 i++; // Skip the \n
869
870 if (!_buffer.empty())
871 {
872 notification n(_buffer, _client._current_user);
873 if (n.getType() == notification::type::friends)
874 {
875 _established = true;
876 _backoff_type = backoff::none;
877 _backoff_amount = std::chrono::milliseconds(0);
878 }
879
880 if (_notify)
881 {
882 _notify(n);
883 }
884
885 _buffer = "";
886 }
887 } else {
888 _buffer.push_back(ptr[i]);
889 }
890 }
891
892 {
893 std::lock_guard<std::mutex> _stall_lock(_stall_mutex);
894 time(&_last_write);
895 }
896
897 return size*nmemb;
898 } 591 }
899 592
900 int client::stream::progress() 593 const configuration& client::getConfiguration() const
901 { 594 {
902 if (_stop) 595 if (!_configuration || (difftime(time(NULL), _last_configuration_update) > 60*60*24))
903 {
904 return 1;
905 }
906
907 if (_established)
908 { 596 {
909 std::lock_guard<std::mutex> _stall_lock(_stall_mutex); 597 _configuration =
910 if (difftime(time(NULL), _last_write) >= 90) 598 make_unique<configuration>(
911 { 599 get(*_oauth_client,
912 return 1; 600 "https://api.twitter.com/1.1/help/configuration.json")
913 } 601 .perform());
602
603 _last_configuration_update = time(NULL);
914 } 604 }
915 605
916 return 0; 606 return *_configuration;
917 } 607 }
918 608
919}; 609};