From 7ba70196fd05a893a851f24a07868e3a1e9a7818 Mon Sep 17 00:00:00 2001 From: Kelly Rauchenberger Date: Fri, 3 Mar 2017 18:17:37 -0500 Subject: Created bot --- .gitmodules | 9 ++ CMakeLists.txt | 19 +++ grunge.cpp | 355 +++++++++++++++++++++++++++++++++++++++++++++++++++ grunge.h | 49 +++++++ main.cpp | 33 +++++ palette.cpp | 119 +++++++++++++++++ palette.h | 30 +++++ vendor/libtwittercpp | 1 + vendor/verbly | 1 + vendor/yaml-cpp | 1 + 10 files changed, 617 insertions(+) create mode 100644 .gitmodules create mode 100644 CMakeLists.txt create mode 100644 grunge.cpp create mode 100644 grunge.h create mode 100644 main.cpp create mode 100644 palette.cpp create mode 100644 palette.h create mode 160000 vendor/libtwittercpp create mode 160000 vendor/verbly create mode 160000 vendor/yaml-cpp diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..346e727 --- /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 +[submodule "vendor/yaml-cpp"] + path = vendor/yaml-cpp + url = git@github.com:jbeder/yaml-cpp diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..d9b21f5 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,19 @@ +cmake_minimum_required (VERSION 3.1) +project (grunge) + +set(CMAKE_EXPORT_COMPILE_COMMANDS ON) + +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(grunge main.cpp grunge.cpp palette.cpp) +set_property(TARGET grunge PROPERTY CXX_STANDARD 11) +set_property(TARGET grunge PROPERTY CXX_STANDARD_REQUIRED ON) +target_link_libraries(grunge ${GraphicsMagick_LIBRARIES} verbly yaml-cpp twitter++) + diff --git a/grunge.cpp b/grunge.cpp new file mode 100644 index 0000000..de9061e --- /dev/null +++ b/grunge.cpp @@ -0,0 +1,355 @@ +#include "grunge.h" +#include +#include +#include +#include +#include +#include +#include +#include "palette.h" + +grunge::grunge( + 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 grunge::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); + + // Pixelate the image. + std::cout << "Pixelating image..." << std::endl; + + image = pixelateImage(std::move(image)); + + // Pastelize the image. + std::cout << "Pastelizing image..." << std::endl; + + image = pastelizeImage(std::move(image)); + + // 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_images& 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 grunge::getPicturedNoun() const +{ + verbly::filter whitelist = + (verbly::notion::wnid == 109287968) // Geological formations + || (verbly::notion::wnid == 109208496) // Asterisms (collections of stars) + || (verbly::notion::wnid == 109239740) // Celestial bodies + || (verbly::notion::wnid == 109277686) // Exterrestrial objects (comets and meteroids) + || (verbly::notion::wnid == 109403211) // Radiators (supposedly natural radiators but actually these are just pictures of radiators) + || (verbly::notion::wnid == 109416076) // Rocks + || (verbly::notion::wnid == 105442131) // Chromosomes + || (verbly::notion::wnid == 100324978) // Tightrope walking + || (verbly::notion::wnid == 100326094) // Rock climbing + || (verbly::notion::wnid == 100433458) // Contact sports + || (verbly::notion::wnid == 100433802) // Gymnastics + || (verbly::notion::wnid == 100439826) // Track and field + || (verbly::notion::wnid == 100440747) // Skiing + || (verbly::notion::wnid == 100441824) // Water sport + || (verbly::notion::wnid == 100445351) // Rowing + || (verbly::notion::wnid == 100446980) // Archery + // TODO: add more sports + || (verbly::notion::wnid == 100021939) // Artifacts + || (verbly::notion::wnid == 101471682) // Vertebrates + ; + + verbly::filter blacklist = + (verbly::notion::wnid == 106883725) // swastika + || (verbly::notion::wnid == 104416901) // tetraskele + || (verbly::notion::wnid == 102512053) // fish + || (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)); + + verbly::word pictured = pictureQuery.first(); + + return pictured; +} + +Magick::Image grunge::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_images(); + } + + std::cout << "Got URLs." << std::endl; + lstdata = lstbuf.str(); + } + + std::vector lstvec = verbly::split>(lstdata, "\r\n"); + if (lstvec.empty()) + { + throw could_not_get_images(); + } + + 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) && (pic.columns() >= 800)) + { + 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_images(); + } + + return pic; +} + +Magick::Image grunge::pixelateImage(Magick::Image image) const +{ + // Check that the image dimensions are a multiple of four. + if ((image.rows() % 4 != 0) || (image.columns() % 4 != 0)) + { + Magick::Geometry cropped( + image.columns() - (image.columns() % 4), + image.rows() - (image.rows() % 4)); + + image.crop(cropped); + } + + // Downscale the image. + Magick::Geometry originalSize = image.size(); + Magick::Geometry pixelatedSize( + originalSize.width() / 4, + originalSize.height() / 4); + + image.scale(pixelatedSize); + + // Scale the image back up. + image.scale(originalSize); + + return image; +} + +Magick::Image grunge::pastelizeImage(Magick::Image input) const +{ + input.quantizeColorSpace(Magick::GRAYColorspace); + input.quantizeColors(256); + input.quantize(); + + palette pastelPalette = palette::randomPalette(rng_); + Magick::Geometry size = input.size(); + Magick::Image pastelized(size, "white"); + + for (int y=0; y(input.pixelColor(x, y)); + Magick::Color mapped = pastelPalette.getColor(grade.shade()); + + pastelized.pixelColor(x, y, mapped); + } + } + + return pastelized; +} + +std::string grunge::generateTweetText(verbly::word pictured) const +{ + verbly::word simpler = database_->words( + (verbly::notion::partOfSpeech == verbly::part_of_speech::noun) + && (verbly::notion::fullHyponyms %= pictured) + && (verbly::form::proper == false)).first(); + + std::vector symbols = {"☯","✡","☨","✞","✝","☮","☥","☦","☪","✌"}; + std::string prefix; + std::string suffix; + int length = std::geometric_distribution(0.5)(rng_) + 1; + for (int i=0; i(0, symbols.size()-1)(rng_)]; + + prefix += choice; + suffix = choice + suffix; + } + + verbly::token action = { + prefix, + "follow for more soft grunge", + simpler, + suffix + }; + + return action.compile(); +} + +void grunge::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/grunge.h b/grunge.h new file mode 100644 index 0000000..5f95442 --- /dev/null +++ b/grunge.h @@ -0,0 +1,49 @@ +#ifndef GRUNGE_H +#define GRUNGE_H + +#include +#include +#include +#include +#include +#include +#include + +class grunge { +public: + + grunge( + std::string configFile, + std::mt19937& rng); + + void run() const; + +private: + + verbly::word getPicturedNoun() const; + + Magick::Image getImageForNoun(verbly::word pictured) const; + + Magick::Image pixelateImage(Magick::Image image) const; + + Magick::Image pastelizeImage(Magick::Image image) const; + + std::string generateTweetText(verbly::word pictured) const; + + void sendTweet(std::string text, Magick::Image image) const; + + class could_not_get_images : public std::runtime_error { + public: + + could_not_get_images() : std::runtime_error("Could not get images for noun") + { + } + }; + + std::mt19937& rng_; + std::unique_ptr database_; + std::unique_ptr client_; + +}; + +#endif diff --git a/main.cpp b/main.cpp new file mode 100644 index 0000000..ae49523 --- /dev/null +++ b/main.cpp @@ -0,0 +1,33 @@ +#include "grunge.h" + +int main(int argc, char** argv) +{ + Magick::InitializeMagick(nullptr); + + std::random_device randomDevice; + std::mt19937 rng(randomDevice()); + + if (argc != 2) + { + std::cout << "usage: grunge [configfile]" << std::endl; + return -1; + } + + std::string configfile(argv[1]); + + try + { + grunge 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; + } +} diff --git a/palette.cpp b/palette.cpp new file mode 100644 index 0000000..4135756 --- /dev/null +++ b/palette.cpp @@ -0,0 +1,119 @@ +#include "palette.h" +#include + +const std::vector palette::reds = { + {"#ff4848"}, {"#ff7575"}, {"#ff8a8a"}, {"#ff9797"}, {"#ffa8a8"}, {"#ffbbbb"} +}; + +const std::vector palette::oranges = { + {"#ffbf85"}, {"#ffcfa4"}, {"#ffbd82"}, {"#ffc48e"} +}; + +const std::vector palette::yellows = { + {"#fdfda5"}, {"#eff0ac"}, {"#fef495"} +}; + +const std::vector palette::greens = { + {"#c6fcb4"}, {"#d4ffa2"}, {"#93eeaa"} +}; + +const std::vector palette::blues = { + {"#62d0ff"}, {"#62a9ff"}, {"#63e9fc"}, {"#7bcae1"}, {"#92fef9"} +}; + +const std::vector palette::purples = { + {"#dfb0fe"}, {"#b0a7f1"}, {"#ff86ff"}, {"#ffacec"}, {"#ff86c2"}, {"#ea8dfe"} +}; + +palette::palette(std::vector foci) +{ + if (foci.size() < 1) + { + throw std::invalid_argument("Must have at least one focus"); + } + + if (foci.size() < 2) + { + // Degenerate scenario, but deal with it gracefully. + foci.push_back(foci.front()); + } + + int sections = foci.size() - 1; + double sectionSize = 256.0 / static_cast(sections); + for (int i=0; i<256; i++) + { + int section = std::floor(static_cast(i) / sectionSize); + double interpolation = (static_cast(i) / sectionSize) - section; + + Magick::ColorHSL interpLeft = foci[section]; + Magick::ColorHSL interpRight = foci[section+1]; + + double newHue; + double diff = interpRight.hue() - interpLeft.hue(); + if (diff < 0) + { + std::swap(interpLeft, interpRight); + + diff = -diff; + interpolation = 1 - interpolation; + } + + if (diff > 0.5) + { + newHue = 1.0 + interpLeft.hue() + * (interpolation * (interpRight.hue() - interpLeft.hue() - 1.0)); + + if (newHue > 1.0) + { + newHue -= 1.0; + } + } else { + newHue = interpLeft.hue() + interpolation * diff; + } + + Magick::ColorHSL interpolated( + newHue, + ((1.0 - interpolation) * interpLeft.saturation()) + + (interpolation * interpRight.saturation()), + ((1.0 - interpolation) * interpLeft.luminosity()) + + (interpolation * interpRight.luminosity())); + + gradient_.push_back(interpolated); + } +} + +palette palette::randomPalette(std::mt19937& rng) +{ + std::vector foci; + foci.push_back(reds[ + std::uniform_int_distribution(0, reds.size()-1)(rng)]); + foci.push_back(oranges[ + std::uniform_int_distribution(0, oranges.size()-1)(rng)]); + foci.push_back(yellows[ + std::uniform_int_distribution(0, yellows.size()-1)(rng)]); + foci.push_back(greens[ + std::uniform_int_distribution(0, greens.size()-1)(rng)]); + foci.push_back(blues[ + std::uniform_int_distribution(0, blues.size()-1)(rng)]); + foci.push_back(purples[ + std::uniform_int_distribution(0, purples.size()-1)(rng)]); + + std::shuffle(std::begin(foci), std::end(foci), rng); + + int origNum = foci.size(); + int numOfFoci = std::uniform_int_distribution(4, origNum)(rng); + for (int i=0; i<(origNum-numOfFoci); i++) + { + foci.pop_back(); + } + + foci.push_back(foci[0]); + + return palette(std::move(foci)); +} + +Magick::Color palette::getColor(double shade) const +{ + int index = std::min((int)floor(shade * 256.0), 255); + return gradient_[index]; +} diff --git a/palette.h b/palette.h new file mode 100644 index 0000000..3a5cfb3 --- /dev/null +++ b/palette.h @@ -0,0 +1,30 @@ +#ifndef PALETTE_H +#define PALETTE_H + +#include +#include +#include + +class palette { +public: + + palette(std::vector foci); + + static palette randomPalette(std::mt19937& rng); + + Magick::Color getColor(double shade) const; + +private: + + static const std::vector reds; + static const std::vector oranges; + static const std::vector yellows; + static const std::vector greens; + static const std::vector blues; + static const std::vector purples; + + std::vector gradient_; + +}; + +#endif 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..59eab84 --- /dev/null +++ b/vendor/verbly @@ -0,0 +1 @@ +Subproject commit 59eab842de02b2b2ba8bf53e2214b558457e6356 diff --git a/vendor/yaml-cpp b/vendor/yaml-cpp new file mode 160000 index 0000000..bedb28f --- /dev/null +++ b/vendor/yaml-cpp @@ -0,0 +1 @@ +Subproject commit bedb28fdb4fd52d97e02f6cb946cae631037089e -- cgit 1.4.1