diff options
Diffstat (limited to 'difference.cpp')
| -rw-r--r-- | difference.cpp | 263 |
1 files changed, 73 insertions, 190 deletions
| diff --git a/difference.cpp b/difference.cpp index 9c060d4..86a0779 100644 --- a/difference.cpp +++ b/difference.cpp | |||
| @@ -1,12 +1,14 @@ | |||
| 1 | #include "difference.h" | 1 | #include "difference.h" |
| 2 | #include <curl_easy.h> | ||
| 3 | #include <curl_header.h> | ||
| 4 | #include <vector> | 2 | #include <vector> |
| 5 | #include <iostream> | 3 | #include <iostream> |
| 6 | #include <yaml-cpp/yaml.h> | 4 | #include <yaml-cpp/yaml.h> |
| 7 | #include <chrono> | 5 | #include <chrono> |
| 8 | #include <thread> | 6 | #include <thread> |
| 9 | #include <deque> | 7 | #include <deque> |
| 8 | #include <json.hpp> | ||
| 9 | #include "imagenet.h" | ||
| 10 | |||
| 11 | #define ENABLE_BOT | ||
| 10 | 12 | ||
| 11 | difference::difference( | 13 | difference::difference( |
| 12 | std::string configFile, | 14 | std::string configFile, |
| @@ -16,20 +18,22 @@ difference::difference( | |||
| 16 | // Load the config file. | 18 | // Load the config file. |
| 17 | YAML::Node config = YAML::LoadFile(configFile); | 19 | YAML::Node config = YAML::LoadFile(configFile); |
| 18 | 20 | ||
| 19 | // Set up the Twitter client. | 21 | #ifdef ENABLE_BOT |
| 20 | twitter::auth auth; | 22 | // Set up the Mastodon client. |
| 21 | auth.setConsumerKey(config["consumer_key"].as<std::string>()); | 23 | instance_ = std::make_unique<mastodonpp::Instance>( |
| 22 | auth.setConsumerSecret(config["consumer_secret"].as<std::string>()); | 24 | config["mastodon_instance"].as<std::string>(), |
| 23 | auth.setAccessKey(config["access_key"].as<std::string>()); | 25 | config["mastodon_token"].as<std::string>()); |
| 24 | auth.setAccessSecret(config["access_secret"].as<std::string>()); | 26 | connection_ = std::make_unique<mastodonpp::Connection>(*instance_); |
| 25 | 27 | #endif | |
| 26 | client_ = std::unique_ptr<twitter::client>(new twitter::client(auth)); | 28 | |
| 27 | |||
| 28 | // Set up the verbly database. | 29 | // Set up the verbly database. |
| 29 | database_ = std::unique_ptr<verbly::database>( | 30 | database_ = std::unique_ptr<verbly::database>( |
| 30 | new verbly::database(config["verbly_datafile"].as<std::string>())); | 31 | new verbly::database(config["verbly_datafile"].as<std::string>())); |
| 31 | 32 | ||
| 33 | imagenet_ = std::make_unique<imagenet>(config["imagenet"].as<std::string>()); | ||
| 34 | |||
| 32 | fontfile_ = "@" + config["font"].as<std::string>(); | 35 | fontfile_ = "@" + config["font"].as<std::string>(); |
| 36 | tempfile_ = config["tempfile"].as<std::string>(); | ||
| 33 | } | 37 | } |
| 34 | 38 | ||
| 35 | void difference::run() const | 39 | void difference::run() const |
| @@ -73,19 +77,19 @@ void difference::run() const | |||
| 73 | // Generate the tweet text. | 77 | // Generate the tweet text. |
| 74 | std::cout << "Generating text..." << std::endl; | 78 | std::cout << "Generating text..." << std::endl; |
| 75 | 79 | ||
| 76 | std::string text = generateTweetText(std::move(word1), std::move(word2)); | 80 | std::string text = generatePostText(std::move(word1), std::move(word2)); |
| 77 | 81 | ||
| 78 | std::cout << "Tweet text: " << text << std::endl; | 82 | std::cout << "Post text: " << text << std::endl; |
| 79 | 83 | ||
| 80 | // Send the tweet. | 84 | // Send the post. |
| 81 | std::cout << "Sending tweet..." << std::endl; | 85 | std::cout << "Sending post..." << std::endl; |
| 82 | 86 | ||
| 83 | sendTweet(std::move(text), std::move(image)); | 87 | sendPost(std::move(text), std::move(image)); |
| 84 | 88 | ||
| 85 | std::cout << "Tweeted!" << std::endl; | 89 | std::cout << "Posted!" << std::endl; |
| 86 | 90 | ||
| 87 | // Wait. | 91 | // Wait. |
| 88 | std::this_thread::sleep_for(std::chrono::hours(1)); | 92 | std::this_thread::sleep_for(std::chrono::hours(9)); |
| 89 | } catch (const could_not_get_images& ex) | 93 | } catch (const could_not_get_images& ex) |
| 90 | { | 94 | { |
| 91 | std::cout << ex.what() << std::endl; | 95 | std::cout << ex.what() << std::endl; |
| @@ -95,9 +99,9 @@ void difference::run() const | |||
| 95 | } catch (const Magick::ErrorCorruptImage& ex) | 99 | } catch (const Magick::ErrorCorruptImage& ex) |
| 96 | { | 100 | { |
| 97 | std::cout << "Corrupt image: " << ex.what() << std::endl; | 101 | std::cout << "Corrupt image: " << ex.what() << std::endl; |
| 98 | } catch (const twitter::twitter_error& ex) | 102 | } catch (const std::exception& ex) |
| 99 | { | 103 | { |
| 100 | std::cout << "Twitter error: " << ex.what() << std::endl; | 104 | std::cout << "Other error: " << ex.what() << std::endl; |
| 101 | 105 | ||
| 102 | std::this_thread::sleep_for(std::chrono::hours(1)); | 106 | std::this_thread::sleep_for(std::chrono::hours(1)); |
| 103 | } | 107 | } |
| @@ -153,175 +157,22 @@ verbly::word difference::getPicturedNoun() const | |||
| 153 | std::pair<Magick::Image, Magick::Image> | 157 | std::pair<Magick::Image, Magick::Image> |
| 154 | difference::getImagesForNoun(verbly::word pictured) const | 158 | difference::getImagesForNoun(verbly::word pictured) const |
| 155 | { | 159 | { |
| 156 | int backoff = 0; | ||
| 157 | |||
| 158 | std::cout << "Getting URLs..." << std::endl; | 160 | std::cout << "Getting URLs..." << std::endl; |
| 159 | 161 | ||
| 160 | std::string lstdata; | 162 | std::vector<std::tuple<std::string, std::string>> images = |
| 161 | while (lstdata.empty()) | 163 | imagenet_->getImagesForNotion(pictured.getNotion().getId(), rng_, 2); |
| 162 | { | ||
| 163 | std::ostringstream lstbuf; | ||
| 164 | curl::curl_ios<std::ostringstream> lstios(lstbuf); | ||
| 165 | curl::curl_easy lsthandle(lstios); | ||
| 166 | std::string lsturl = pictured.getNotion().getImageNetUrl(); | ||
| 167 | lsthandle.add<CURLOPT_URL>(lsturl.c_str()); | ||
| 168 | lsthandle.add<CURLOPT_CONNECTTIMEOUT>(30); | ||
| 169 | lsthandle.add<CURLOPT_TIMEOUT>(300); | ||
| 170 | |||
| 171 | try | ||
| 172 | { | ||
| 173 | lsthandle.perform(); | ||
| 174 | } catch (const curl::curl_easy_exception& e) | ||
| 175 | { | ||
| 176 | e.print_traceback(); | ||
| 177 | |||
| 178 | backoff++; | ||
| 179 | std::cout << "Waiting for " << backoff << " seconds..." << std::endl; | ||
| 180 | |||
| 181 | std::this_thread::sleep_for(std::chrono::seconds(backoff)); | ||
| 182 | |||
| 183 | continue; | ||
| 184 | } | ||
| 185 | |||
| 186 | backoff = 0; | ||
| 187 | |||
| 188 | if (lsthandle.get_info<CURLINFO_RESPONSE_CODE>().get() != 200) | ||
| 189 | { | ||
| 190 | throw could_not_get_images(); | ||
| 191 | } | ||
| 192 | |||
| 193 | std::cout << "Got URLs." << std::endl; | ||
| 194 | lstdata = lstbuf.str(); | ||
| 195 | } | ||
| 196 | |||
| 197 | std::vector<std::string> lstvec = verbly::split<std::vector<std::string>>(lstdata, "\r\n"); | ||
| 198 | if (lstvec.size() < 2) | ||
| 199 | { | ||
| 200 | throw could_not_get_images(); | ||
| 201 | } | ||
| 202 | |||
| 203 | std::shuffle(std::begin(lstvec), std::end(lstvec), rng_); | ||
| 204 | |||
| 205 | std::deque<std::string> urls; | ||
| 206 | for (std::string& url : lstvec) | ||
| 207 | { | ||
| 208 | urls.push_back(url); | ||
| 209 | } | ||
| 210 | |||
| 211 | Magick::Image image1; | ||
| 212 | bool success = false; | ||
| 213 | while (!urls.empty()) | ||
| 214 | { | ||
| 215 | std::string url = urls.front(); | ||
| 216 | urls.pop_front(); | ||
| 217 | |||
| 218 | try | ||
| 219 | { | ||
| 220 | image1 = getImageAtUrl(url); | ||
| 221 | |||
| 222 | success = true; | ||
| 223 | break; | ||
| 224 | } catch (const could_not_get_images& ex) | ||
| 225 | { | ||
| 226 | // Just try the next one. | ||
| 227 | } | ||
| 228 | } | ||
| 229 | |||
| 230 | if (!success) | ||
| 231 | { | ||
| 232 | throw could_not_get_images(); | ||
| 233 | } | ||
| 234 | 164 | ||
| 235 | Magick::Image image2; | 165 | const std::string& imgdata1 = std::get<0>(images[0]); |
| 236 | success = false; | 166 | Magick::Blob image1(imgdata1.c_str(), imgdata1.length()); |
| 237 | while (!urls.empty()) | 167 | Magick::Image pic1; |
| 238 | { | 168 | pic1.read(image1); |
| 239 | std::string url = urls.front(); | ||
| 240 | urls.pop_front(); | ||
| 241 | 169 | ||
| 242 | try | 170 | const std::string& imgdata2 = std::get<0>(images[1]); |
| 243 | { | 171 | Magick::Blob image2(imgdata2.c_str(), imgdata2.length()); |
| 244 | image2 = getImageAtUrl(url); | 172 | Magick::Image pic2; |
| 173 | pic2.read(image2); | ||
| 245 | 174 | ||
| 246 | success = true; | 175 | return {std::move(pic1), std::move(pic2)}; |
| 247 | break; | ||
| 248 | } catch (const could_not_get_images& ex) | ||
| 249 | { | ||
| 250 | // Just try the next one. | ||
| 251 | } | ||
| 252 | } | ||
| 253 | |||
| 254 | if (!success) | ||
| 255 | { | ||
| 256 | throw could_not_get_images(); | ||
| 257 | } | ||
| 258 | |||
| 259 | return {std::move(image1), std::move(image2)}; | ||
| 260 | } | ||
| 261 | |||
| 262 | Magick::Image difference::getImageAtUrl(std::string url) const | ||
| 263 | { | ||
| 264 | // willyfogg.com is a thumbnail generator known to return 200 even if the target image no longer exists | ||
| 265 | if (url.find("willyfogg.com/thumb.php") != std::string::npos) | ||
| 266 | { | ||
| 267 | throw could_not_get_images(); | ||
| 268 | } | ||
| 269 | |||
| 270 | // Accept string from Google Chrome | ||
| 271 | std::string accept = "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8"; | ||
| 272 | curl::curl_header headers; | ||
| 273 | headers.add(accept); | ||
| 274 | |||
| 275 | std::ostringstream imgbuf; | ||
| 276 | curl::curl_ios<std::ostringstream> imgios(imgbuf); | ||
| 277 | curl::curl_easy imghandle(imgios); | ||
| 278 | |||
| 279 | imghandle.add<CURLOPT_HTTPHEADER>(headers.get()); | ||
| 280 | imghandle.add<CURLOPT_URL>(url.c_str()); | ||
| 281 | imghandle.add<CURLOPT_CONNECTTIMEOUT>(30); | ||
| 282 | imghandle.add<CURLOPT_TIMEOUT>(300); | ||
| 283 | |||
| 284 | try | ||
| 285 | { | ||
| 286 | imghandle.perform(); | ||
| 287 | } catch (const curl::curl_easy_exception& error) { | ||
| 288 | error.print_traceback(); | ||
| 289 | |||
| 290 | throw could_not_get_images(); | ||
| 291 | } | ||
| 292 | |||
| 293 | if (imghandle.get_info<CURLINFO_RESPONSE_CODE>().get() != 200) | ||
| 294 | { | ||
| 295 | throw could_not_get_images(); | ||
| 296 | } | ||
| 297 | |||
| 298 | std::string content_type = imghandle.get_info<CURLINFO_CONTENT_TYPE>().get(); | ||
| 299 | if (content_type.substr(0, 6) != "image/") | ||
| 300 | { | ||
| 301 | throw could_not_get_images(); | ||
| 302 | } | ||
| 303 | |||
| 304 | std::string imgstr = imgbuf.str(); | ||
| 305 | Magick::Blob img(imgstr.c_str(), imgstr.length()); | ||
| 306 | Magick::Image pic; | ||
| 307 | |||
| 308 | try | ||
| 309 | { | ||
| 310 | pic.read(img); | ||
| 311 | |||
| 312 | if ((pic.rows() > 0) && (pic.columns() >= 800)) | ||
| 313 | { | ||
| 314 | std::cout << url << std::endl; | ||
| 315 | } | ||
| 316 | } catch (const Magick::ErrorOption& e) | ||
| 317 | { | ||
| 318 | // Occurs when the the data downloaded from the server is malformed | ||
| 319 | std::cout << "Magick: " << e.what() << std::endl; | ||
| 320 | |||
| 321 | throw could_not_get_images(); | ||
| 322 | } | ||
| 323 | |||
| 324 | return pic; | ||
| 325 | } | 176 | } |
| 326 | 177 | ||
| 327 | std::pair<verbly::word, verbly::word> difference::getOppositeIdentifiers() const | 178 | std::pair<verbly::word, verbly::word> difference::getOppositeIdentifiers() const |
| @@ -419,7 +270,7 @@ Magick::Image difference::composeImage( | |||
| 419 | return composite; | 270 | return composite; |
| 420 | } | 271 | } |
| 421 | 272 | ||
| 422 | std::string difference::generateTweetText( | 273 | std::string difference::generatePostText( |
| 423 | verbly::word word1, | 274 | verbly::word word1, |
| 424 | verbly::word word2) const | 275 | verbly::word word2) const |
| 425 | { | 276 | { |
| @@ -432,12 +283,44 @@ std::string difference::generateTweetText( | |||
| 432 | return result.compile(); | 283 | return result.compile(); |
| 433 | } | 284 | } |
| 434 | 285 | ||
| 435 | void difference::sendTweet(std::string text, Magick::Image image) const | 286 | void difference::sendPost(std::string text, Magick::Image image) const |
| 436 | { | 287 | { |
| 437 | Magick::Blob outputBlob; | ||
| 438 | image.magick("png"); | 288 | image.magick("png"); |
| 439 | image.write(&outputBlob); | 289 | image.write(tempfile_); |
| 440 | 290 | ||
| 441 | long media_id = client_->uploadMedia("image/png", (const char*) outputBlob.data(), outputBlob.length()); | 291 | #ifdef ENABLE_BOT |
| 442 | client_->updateStatus(std::move(text), {media_id}); | 292 | auto answer{connection_->post(mastodonpp::API::v1::media, |
| 293 | {{"file", std::string("@file:") + tempfile_}, {"description", text}})}; | ||
| 294 | if (!answer) | ||
| 295 | { | ||
| 296 | if (answer.curl_error_code == 0) | ||
| 297 | { | ||
| 298 | std::cout << "HTTP status: " << answer.http_status << std::endl; | ||
| 299 | } | ||
| 300 | else | ||
| 301 | { | ||
| 302 | std::cout << "libcurl error " << std::to_string(answer.curl_error_code) | ||
| 303 | << ": " << answer.error_message << std::endl; | ||
| 304 | } | ||
| 305 | return; | ||
| 306 | } | ||
| 307 | |||
| 308 | nlohmann::json response_json = nlohmann::json::parse(answer.body); | ||
| 309 | answer = connection_->post(mastodonpp::API::v1::statuses, | ||
| 310 | {{"status", ""}, {"media_ids", | ||
| 311 | std::vector<std::string_view>{response_json["id"].get<std::string>()}}}); | ||
| 312 | |||
| 313 | if (!answer) | ||
| 314 | { | ||
| 315 | if (answer.curl_error_code == 0) | ||
| 316 | { | ||
| 317 | std::cout << "HTTP status: " << answer.http_status << std::endl; | ||
| 318 | } | ||
| 319 | else | ||
| 320 | { | ||
| 321 | std::cout << "libcurl error " << std::to_string(answer.curl_error_code) | ||
| 322 | << ": " << answer.error_message << std::endl; | ||
| 323 | } | ||
| 324 | } | ||
| 325 | #endif | ||
| 443 | } | 326 | } |
