From 0bb6d64c929b237278abf2565b6eb9f5f85fcd37 Mon Sep 17 00:00:00 2001 From: Kelly Rauchenberger Date: Sun, 29 Oct 2017 09:55:03 -0400 Subject: Created bot --- .gitignore | 5 + .gitmodules | 9 ++ CMakeLists.txt | 16 +++ capital.cpp | 292 +++++++++++++++++++++++++++++++++++++++++++++++++++ capital.h | 45 ++++++++ main.cpp | 33 ++++++ vendor/libtwittercpp | 1 + vendor/verbly | 1 + vendor/yaml-cpp | 1 + 9 files changed, 403 insertions(+) create mode 100644 .gitignore create mode 100644 .gitmodules create mode 100644 CMakeLists.txt create mode 100644 capital.cpp create mode 100644 capital.h create mode 100644 main.cpp create mode 160000 vendor/libtwittercpp create mode 160000 vendor/verbly create mode 160000 vendor/yaml-cpp diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2e89877 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +CMakeCache.txt +CMakeFiles +Makefile +cmake_install.cmake +yaml-cpp.pc \ No newline at end of file diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..b8daec7 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,9 @@ +[submodule "vendor/verbly"] + path = vendor/verbly + url = git@github.com:hatkirby/verbly +[submodule "vendor/libtwittercpp"] + path = vendor/libtwittercpp + url = git@github.com:hatkirby/libtwittercpp.git +[submodule "vendor/yaml-cpp"] + path = vendor/yaml-cpp + url = git@github.com:jbeder/yaml-cpp.git diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..0462950 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,16 @@ +cmake_minimum_required (VERSION 3.1) +project (capital) + +add_subdirectory(vendor/libtwittercpp) +add_subdirectory(vendor/verbly) +add_subdirectory(vendor/yaml-cpp EXCLUDE_FROM_ALL) + +find_package(PkgConfig) +pkg_check_modules(GraphicsMagick GraphicsMagick++ REQUIRED) + +include_directories(${GraphicsMagick_INCLUDE_DIRS} vendor/verbly/lib vendor/libtwittercpp/src vendor/yaml-cpp/include vendor/libtwittercpp/vendor/curlcpp/include) +link_directories(${GraphicsMagick_LIBRARY_DIRS}) +add_executable(capital capital.cpp main.cpp) +set_property(TARGET capital PROPERTY CXX_STANDARD 11) +set_property(TARGET capital PROPERTY CXX_STANDARD_REQUIRED ON) +target_link_libraries(capital ${GraphicsMagick_LIBRARIES} verbly yaml-cpp twitter++) diff --git a/capital.cpp b/capital.cpp new file mode 100644 index 0000000..526e655 --- /dev/null +++ b/capital.cpp @@ -0,0 +1,292 @@ +#include "capital.h" +#include +#include +#include +#include +#include +#include + +capital::capital( + std::string configFile, + std::mt19937& rng) : + rng_(rng) +{ + // Load the config file. + YAML::Node config = YAML::LoadFile(configFile); + + // Set up the Twitter client. + twitter::auth auth; + auth.setConsumerKey(config["consumer_key"].as()); + auth.setConsumerSecret(config["consumer_secret"].as()); + auth.setAccessKey(config["access_key"].as()); + auth.setAccessSecret(config["access_secret"].as()); + + client_ = std::unique_ptr(new twitter::client(auth)); + + // Set up the verbly database. + database_ = std::unique_ptr( + new verbly::database(config["verbly_datafile"].as())); +} + +void capital::run() const +{ + for (;;) + { + std::cout << "Generating tweet..." << std::endl; + + try + { + // Find a noun to use as the pictured item. + std::cout << "Choosing pictured noun..." << std::endl; + + verbly::word pictured = getPicturedNoun(); + + std::cout << "Noun: " << pictured.getBaseForm().getText() << std::endl; + + // Choose a picture of that noun. + std::cout << "Finding an image..." << std::endl; + + Magick::Image image = getImageForNoun(pictured); + + // Generate the tweet text. + std::cout << "Generating text..." << std::endl; + + std::string text = generateTweetText(pictured); + + std::cout << "Tweet text: " << text << std::endl; + + // Send the tweet. + std::cout << "Sending tweet..." << std::endl; + + sendTweet(std::move(text), std::move(image)); + + std::cout << "Tweeted!" << std::endl; + + // Wait. + std::this_thread::sleep_for(std::chrono::hours(1)); + } catch (const could_not_get_image& ex) + { + std::cout << ex.what() << std::endl; + } catch (const Magick::ErrorImage& ex) + { + std::cout << "Image error: " << ex.what() << std::endl; + } catch (const Magick::ErrorCorruptImage& ex) + { + std::cout << "Corrupt image: " << ex.what() << std::endl; + } catch (const twitter::twitter_error& ex) + { + std::cout << "Twitter error: " << ex.what() << std::endl; + + std::this_thread::sleep_for(std::chrono::hours(1)); + } + + std::cout << std::endl; + } +} + +verbly::word capital::getPicturedNoun() const +{ + verbly::filter whitelist = (verbly::notion::wnid == 100021939); // Artifacts + + verbly::filter blacklist = + (verbly::notion::wnid == 106883725) // swastika + || (verbly::notion::wnid == 104416901) // tetraskele + || (verbly::notion::wnid == 103575691) // instrument of execution + || (verbly::notion::wnid == 103829563) // noose + ; + + verbly::query pictureQuery = database_->words( + (verbly::notion::fullHypernyms %= whitelist) + && !(verbly::notion::fullHypernyms %= blacklist) + && (verbly::notion::partOfSpeech == verbly::part_of_speech::noun) + && (verbly::notion::numOfImages >= 1) + // Blacklist ethnic slurs + && !(verbly::word::usageDomains %= (verbly::notion::wnid == 106718862)) + ); + + verbly::word pictured = pictureQuery.first(); + + return pictured; +} + +Magick::Image capital::getImageForNoun(verbly::word pictured) const +{ + // Accept string from Google Chrome + std::string accept = "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8"; + curl::curl_header headers; + headers.add(accept); + + int backoff = 0; + + std::cout << "Getting URLs..." << std::endl; + + std::string lstdata; + while (lstdata.empty()) + { + std::ostringstream lstbuf; + curl::curl_ios lstios(lstbuf); + curl::curl_easy lsthandle(lstios); + std::string lsturl = pictured.getNotion().getImageNetUrl(); + lsthandle.add(lsturl.c_str()); + + try + { + lsthandle.perform(); + } catch (const curl::curl_easy_exception& e) + { + e.print_traceback(); + + backoff++; + std::cout << "Waiting for " << backoff << " seconds..." << std::endl; + + std::this_thread::sleep_for(std::chrono::seconds(backoff)); + + continue; + } + + backoff = 0; + + if (lsthandle.get_info().get() != 200) + { + throw could_not_get_image(); + } + + std::cout << "Got URLs." << std::endl; + lstdata = lstbuf.str(); + } + + std::vector lstvec = + verbly::split>(lstdata, "\r\n"); + if (lstvec.empty()) + { + throw could_not_get_image(); + } + + std::shuffle(std::begin(lstvec), std::end(lstvec), rng_); + + std::deque urls; + for (std::string& url : lstvec) + { + urls.push_back(url); + } + + bool found = false; + Magick::Blob img; + Magick::Image pic; + + while (!found && !urls.empty()) + { + std::string url = urls.front(); + urls.pop_front(); + + std::ostringstream imgbuf; + curl::curl_ios imgios(imgbuf); + curl::curl_easy imghandle(imgios); + + imghandle.add(headers.get()); + imghandle.add(url.c_str()); + imghandle.add(30); + + try + { + imghandle.perform(); + } catch (curl::curl_easy_exception error) { + error.print_traceback(); + + continue; + } + + if (imghandle.get_info().get() != 200) + { + continue; + } + + std::string content_type = + imghandle.get_info().get(); + if (content_type.substr(0, 6) != "image/") + { + continue; + } + + std::string imgstr = imgbuf.str(); + img = Magick::Blob(imgstr.c_str(), imgstr.length()); + + try + { + pic.read(img); + + if (pic.rows() > 0) + { + std::cout << url << std::endl; + found = true; + } + } catch (const Magick::ErrorOption& e) + { + // Occurs when the the data downloaded from the server is malformed + std::cout << "Magick: " << e.what() << std::endl; + } + } + + if (!found) + { + throw could_not_get_image(); + } + + return pic; +} + +std::string capital::generateTweetText(verbly::word pictured) const +{ + int msd = std::uniform_int_distribution(1, 9)(rng_); + int mag = std::uniform_int_distribution(2, 9)(rng_); + + std::string money; + for (int i=0; i 3)) + { + money.insert(0, ","); + } + } + + money.insert(0, std::to_string(msd)); + money.insert(0, "$"); + + verbly::token nounTok = verbly::token::capitalize( + verbly::token::casing::title_case, + pictured); + + int aci = std::uniform_int_distribution(0, 7)(rng_); + verbly::token action; + + switch (aci) + { + case 0: action = { "No One Will Buy This", money, nounTok }; break; + case 1: action = { "This", nounTok, "Is Not Worth", money }; break; + case 2: action = { "We Can't Get Rid Of This", money, nounTok }; break; + case 3: action = { "Millenials Will No Longer Spend", money, "For", + verbly::token::definiteArticle(nounTok) }; break; + case 4: action = { "Why Does This", money, nounTok, "Exist?" }; break; + case 5: action = { "Someone Spent", money, "Making This", nounTok, + "That No One Will Buy" }; break; + case 6: action = { "What A Waste: This", nounTok, "Will Rot Because", + "No One Can Afford Its", money, "Price Tag" }; break; + case 7: action = { "This", money, nounTok, "Was A Mistake" }; break; + } + + return action.compile(); +} + +void capital::sendTweet(std::string text, Magick::Image image) const +{ + Magick::Blob outputBlob; + image.magick("jpg"); + image.write(&outputBlob); + + long media_id = client_->uploadMedia("image/jpeg", + (const char*) outputBlob.data(), outputBlob.length()); + + client_->updateStatus(std::move(text), {media_id}); +} diff --git a/capital.h b/capital.h new file mode 100644 index 0000000..1f763db --- /dev/null +++ b/capital.h @@ -0,0 +1,45 @@ +#ifndef CAPITAL_H_09912EAE +#define CAPITAL_H_09912EAE + +#include +#include +#include +#include +#include +#include +#include + +class capital { +public: + + capital( + std::string configFile, + std::mt19937& rng); + + void run() const; + +private: + + verbly::word getPicturedNoun() const; + + Magick::Image getImageForNoun(verbly::word pictured) const; + + std::string generateTweetText(verbly::word pictured) const; + + void sendTweet(std::string text, Magick::Image image) const; + + class could_not_get_image : public std::runtime_error { + public: + + could_not_get_image() : std::runtime_error("Could not get image for noun") + { + } + }; + + std::mt19937& rng_; + std::unique_ptr database_; + std::unique_ptr client_; + +}; + +#endif /* end of include guard: CAPITAL_H_09912EAE */ diff --git a/main.cpp b/main.cpp new file mode 100644 index 0000000..ffcd4a2 --- /dev/null +++ b/main.cpp @@ -0,0 +1,33 @@ +#include "capital.h" + +int main(int argc, char** argv) +{ + Magick::InitializeMagick(nullptr); + + std::random_device randomDevice; + std::mt19937 rng(randomDevice()); + + if (argc != 2) + { + std::cout << "usage: capital [configfile]" << std::endl; + return -1; + } + + std::string configfile(argv[1]); + + try + { + capital bot(configfile, rng); + + try + { + bot.run(); + } catch (const std::exception& ex) + { + std::cout << "Error running bot: " << ex.what() << std::endl; + } + } catch (const std::exception& ex) + { + std::cout << "Error initializing bot: " << ex.what() << std::endl; + } +} \ No newline at end of file diff --git a/vendor/libtwittercpp b/vendor/libtwittercpp new file mode 160000 index 0000000..df90612 --- /dev/null +++ b/vendor/libtwittercpp @@ -0,0 +1 @@ +Subproject commit df906121dd862c0f704e44f28ee079158c431c41 diff --git a/vendor/verbly b/vendor/verbly new file mode 160000 index 0000000..bea545d --- /dev/null +++ b/vendor/verbly @@ -0,0 +1 @@ +Subproject commit bea545d62732fe048ca9cec7f82e4be1185cb87b diff --git a/vendor/yaml-cpp b/vendor/yaml-cpp new file mode 160000 index 0000000..beb44b8 --- /dev/null +++ b/vendor/yaml-cpp @@ -0,0 +1 @@ +Subproject commit beb44b872c07c74556314e730c6f20a00b32e8e5 -- cgit 1.4.1