From bd8ff4083d41417f014241ddb94e43b39bee6080 Mon Sep 17 00:00:00 2001 From: Kelly Rauchenberger Date: Mon, 10 Apr 2017 11:35:55 -0400 Subject: Updated verbly (new API) Also updated libtwitter++, and blacklisted some problematic images. --- CMakeLists.txt | 9 +- difference.cpp | 663 +++++++++++++++++++++++++++++---------------------- difference.h | 58 +++++ main.cpp | 35 +++ vendor/libtwittercpp | 2 +- vendor/verbly | 2 +- 6 files changed, 477 insertions(+), 292 deletions(-) create mode 100644 difference.h create mode 100644 main.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index c7ae7a9..adb9ba1 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,18 +1,15 @@ cmake_minimum_required (VERSION 3.1) project (difference) -set(CMAKE_BUILD_TYPE Debug) - add_subdirectory(vendor/libtwittercpp) add_subdirectory(vendor/verbly) add_subdirectory(vendor/yaml-cpp EXCLUDE_FROM_ALL) find_package(PkgConfig) -pkg_check_modules(sqlite3 sqlite3 REQUIRED) pkg_check_modules(GraphicsMagick GraphicsMagick++ REQUIRED) -include_directories(vendor/libtwittercpp/src vendor/libtwittercpp/vendor/curlcpp/include ${sqlite3_INCLUDE_DIR} vendor/verbly/lib ${GraphicsMagick_INCLUDE_DIRS} vendor/yaml-cpp/include) -add_executable(difference difference.cpp) +include_directories(vendor/libtwittercpp/src vendor/libtwittercpp/vendor/curlcpp/include vendor/verbly/lib ${GraphicsMagick_INCLUDE_DIRS} vendor/yaml-cpp/include) +add_executable(difference difference.cpp main.cpp) set_property(TARGET difference PROPERTY CXX_STANDARD 11) set_property(TARGET difference PROPERTY CXX_STANDARD_REQUIRED ON) -target_link_libraries(difference verbly ${sqlite3_LIBRARIES} yaml-cpp twitter++ ${GraphicsMagick_LIBRARIES}) +target_link_libraries(difference verbly yaml-cpp twitter++ ${GraphicsMagick_LIBRARIES}) diff --git a/difference.cpp b/difference.cpp index b9290cb..123a040 100644 --- a/difference.cpp +++ b/difference.cpp @@ -1,344 +1,439 @@ -#include -#include +#include "difference.h" #include #include -#include -#include #include -#include -#include #include #include #include #include -#include +#include -std::string capitalize(std::string input) +difference::difference( + std::string configFile, + std::mt19937& rng) : + rng_(rng) { - std::string result; - bool capnext = true; - - for (auto ch : input) + // 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())); + + fontfile_ = "@" + config["font"].as(); +} + +void difference::run() const +{ + for (;;) { - if (capnext) + std::cout << "Generating tweet..." << std::endl; + + try { - result += toupper(ch); - capnext = false; - } else { - result += ch; + // 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 two pictures of that noun. + std::cout << "Finding an image..." << std::endl; + + Magick::Image image1; + Magick::Image image2; + std::tie(image1, image2) = getImagesForNoun(pictured); + + // Choose two opposite words. + std::cout << "Choosing two opposite words..." << std::endl; + + verbly::word word1; + verbly::word word2; + std::tie(word1, word2) = getOppositeIdentifiers(); + + // Compose the images and words. + std::cout << "Composing image..." << std::endl; + + Magick::Image image = composeImage( + std::move(image1), + word1, + std::move(image2), + word2); + + // Generate the tweet text. + std::cout << "Generating text..." << std::endl; + + std::string text = generateTweetText(std::move(word1), std::move(word2)); + + 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)); } - - if ((ch == ' ') || (ch == '-')) + + std::cout << std::endl; + } +} + +verbly::word difference::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 >= 2)); + + verbly::word pictured = pictureQuery.first(); + + return pictured; +} + +std::pair + difference::getImagesForNoun(verbly::word pictured) const +{ + 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) { - capnext = true; + throw could_not_get_images(); } + + std::cout << "Got URLs." << std::endl; + lstdata = lstbuf.str(); + } + + std::vector lstvec = verbly::split>(lstdata, "\r\n"); + if (lstvec.size() < 2) + { + throw could_not_get_images(); } - - return result; + + std::shuffle(std::begin(lstvec), std::end(lstvec), rng_); + + std::deque urls; + for (std::string& url : lstvec) + { + urls.push_back(url); + } + + Magick::Image image1; + bool success = false; + while (!urls.empty()) + { + std::string url = urls.front(); + urls.pop_front(); + + try + { + image1 = getImageAtUrl(url); + + success = true; + break; + } catch (const could_not_get_images& ex) + { + // Just try the next one. + } + } + + if (!success) + { + throw could_not_get_images(); + } + + Magick::Image image2; + success = false; + while (!urls.empty()) + { + std::string url = urls.front(); + urls.pop_front(); + + try + { + image2 = getImageAtUrl(url); + + success = true; + break; + } catch (const could_not_get_images& ex) + { + // Just try the next one. + } + } + + if (!success) + { + throw could_not_get_images(); + } + + return {std::move(image1), std::move(image2)}; } -bool downloadImage(std::string url, curl::curl_header headers, Magick::Blob& img, Magick::Image& pic) +Magick::Image difference::getImageAtUrl(std::string url) const { // willyfogg.com is a thumbnail generator known to return 200 even if the target image no longer exists if (url.find("willyfogg.com/thumb.php") != std::string::npos) { - return false; + throw could_not_get_images(); } - + + // 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); + 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 { + + try + { imghandle.perform(); } catch (curl::curl_easy_exception error) { error.print_traceback(); - - return false; + + throw could_not_get_images(); } if (imghandle.get_info().get() != 200) { - return false; + throw could_not_get_images(); } - + std::string content_type = imghandle.get_info().get(); if (content_type.substr(0, 6) != "image/") { - return false; + throw could_not_get_images(); } - + std::string imgstr = imgbuf.str(); - img = Magick::Blob(imgstr.c_str(), imgstr.length()); - pic.read(img); - if (pic.rows() == 0) + Magick::Blob img(imgstr.c_str(), imgstr.length()); + Magick::Image pic; + + try + { + pic.read(img); + + if ((pic.rows() > 0) && (pic.columns() >= 800)) + { + std::cout << url << std::endl; + } + } catch (const Magick::ErrorOption& e) { - return false; + // Occurs when the the data downloaded from the server is malformed + std::cout << "Magick: " << e.what() << std::endl; + + throw could_not_get_images(); } - - std::cout << url << std::endl; - - return true; + + return pic; } -int main(int argc, char** argv) +std::pair difference::getOppositeIdentifiers() const { - Magick::InitializeMagick(nullptr); + verbly::part_of_speech pos; - if (argc != 2) + if (std::bernoulli_distribution(1.0/2.0)(rng_)) { - std::cout << "usage: difference [configfile]" << std::endl; - return -1; + pos = verbly::part_of_speech::noun; + } else { + pos = verbly::part_of_speech::adjective; } - std::string configfile(argv[1]); - YAML::Node config = YAML::LoadFile(configfile); + verbly::word w1 = database_->words( + (verbly::notion::partOfSpeech == pos) + && (verbly::word::antonyms)).first(); - int delay = 60 * 60; - - 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()); - - twitter::client client(auth); - - std::random_device random_device; - std::mt19937 random_engine{random_device()}; - - std::string fontfile = "@" + config["font"].as(); - - verbly::data database(config["verbly_datafile"].as()); - - auto whitelist = database.nouns(); - whitelist.with_wnid(111530512); // Crops (plants) - whitelist.with_wnid(111536087); // Ornamental (plants) - whitelist.with_wnid(111536230); // Pot plants - whitelist.with_wnid(113083023); // Houseplants - whitelist.with_wnid(113083306); // Garden plants - whitelist.with_wnid(113083586); // Vascular plants (includes flowers) - whitelist.with_wnid(109287968); // Geological formations - whitelist.with_wnid(109208496); // Asterisms (collections of stars) - whitelist.with_wnid(109239740); // Celestial bodies - whitelist.with_wnid(109277686); // Exterrestrial objects (comets and meteroids) - whitelist.with_wnid(109403211); // Radiators (supposedly natural radiators but actually these are just pictures of radiators) - whitelist.with_wnid(109416076); // Rocks - whitelist.with_wnid(105442131); // Chromosomes - whitelist.with_wnid(100324978); // Tightrope walking - whitelist.with_wnid(100326094); // Rock climbing - whitelist.with_wnid(100433458); // Contact sports - whitelist.with_wnid(100433802); // Gymnastics - whitelist.with_wnid(100439826); // Track and field - whitelist.with_wnid(100440747); // Skiing - whitelist.with_wnid(100441824); // Water sport - whitelist.with_wnid(100445351); // Rowing - whitelist.with_wnid(100446980); // Archery - // TODO: add more sports - whitelist.with_wnid(100021939); // Artifacts - whitelist.with_wnid(101471682); // Vertebrates - - auto whitedata = whitelist.run(); - std::set wnids; - verbly::filter whitefilter; - whitefilter.set_orlogic(true); - for (auto wd : whitedata) + verbly::word w2 = database_->words( + (verbly::notion::partOfSpeech == pos) + && (verbly::word::antonyms %= w1)).first(); + + return {std::move(w1), std::move(w2)}; +} + +Magick::Image difference::composeImage( + Magick::Image image1, + verbly::word word1, + Magick::Image image2, + verbly::word word2) const +{ + if (image1.columns() < 320) { - if (wnids.count(wd.wnid()) == 0) - { - whitefilter << wd; - wnids.insert(wd.wnid()); - } + image1.zoom(Magick::Geometry(320, image1.rows() * 320 / image1.columns(), 0, 0)); } - - // 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); - - std::cout << "Started!" << std::endl; + + if (image2.columns() < 320) + { + image2.zoom(Magick::Geometry(320, image2.rows() * 320 / image2.columns(), 0, 0)); + } + + int width = std::min(image1.columns(), image2.columns()); + int height = std::min(image1.rows(), image2.rows()); + Magick::Geometry geo1(width, height, image1.columns()/2 - width/2, image1.rows()/2 - height/2); + Magick::Geometry geo2(width, height, image2.columns()/2 - width/2, image2.rows()/2 - height/2); + image1.crop(geo1); + image2.crop(geo2); + + Magick::Image composite(Magick::Geometry(width*2, height, 0, 0), "white"); + composite.draw(Magick::DrawableCompositeImage(0, 0, image1)); + composite.draw(Magick::DrawableCompositeImage(width, 0, image2)); + composite.font(fontfile_); + + std::string ant1 = word1.getBaseForm().getText(); + std::string ant2 = word2.getBaseForm().getText(); + + double fontsize = 72; for (;;) { - try - { - std::cout << "Generating noun..." << std::endl; - verbly::noun pictured = database.nouns().full_hyponym_of(whitefilter).at_least_n_images(2).random().limit(1).run().front(); - std::cout << "Noun: " << pictured.singular_form() << std::endl; - std::cout << "Getting URLs..." << std::endl; - std::ostringstream lstbuf; - curl::curl_ios lstios(lstbuf); - curl::curl_easy lsthandle(lstios); - std::string lsturl = pictured.imagenet_url(); - lsthandle.add(lsturl.c_str()); - lsthandle.perform(); - - if (lsthandle.get_info().get() != 200) - { - std::cout << "Could not get URLs" << std::endl; - continue; - } - - std::cout << "Got URLs." << std::endl; - std::string lstdata = lstbuf.str(); - auto lstlist = verbly::split>(lstdata, "\r\n"); - std::set lstset; - for (auto url : lstlist) - { - lstset.insert(url); - } - - if (lstset.size() < 2) - { - continue; - } - - std::vector lstvec; - for (auto url : lstset) - { - lstvec.push_back(url); - } - - std::shuffle(std::begin(lstvec), std::end(lstvec), random_engine); - - Magick::Blob img1; - Magick::Image pic1; - bool success = false; - int curind = 0; - for (; curind < lstvec.size(); curind++) - { - if (downloadImage(lstvec[curind], headers, img1, pic1)) - { - success = true; - break; - } - } - - if (!success) - { - continue; - } - - success = false; - Magick::Blob img2; - Magick::Image pic2; - for (curind++; curind < lstvec.size(); curind++) - { - if (downloadImage(lstvec[curind], headers, img2, pic2)) - { - success = true; - break; - } - } - - if (!success) - { - continue; - } - - std::string ant1, ant2; - std::uniform_int_distribution coinflip(0, 1); - if (coinflip(random_engine)==0) - { - verbly::noun n1 = database.nouns().has_antonyms().random().limit(1).run().front(); - verbly::noun n2 = n1.antonyms().random().limit(1).run().front(); - ant1 = n1.singular_form(); - ant2 = n2.singular_form(); - } else { - verbly::adjective a1 = database.adjectives().has_antonyms().random().limit(1).run().front(); - verbly::adjective a2 = a1.antonyms().random().limit(1).run().front(); - ant1 = a1.base_form(); - ant2 = a2.base_form(); - } - - if (pic1.columns() < 320) - { - pic1.zoom(Magick::Geometry(320, pic1.rows() * 320 / pic1.columns(), 0, 0)); - } - - if (pic2.columns() < 320) - { - pic2.zoom(Magick::Geometry(320, pic2.rows() * 320 / pic2.columns(), 0, 0)); - } - - int width = std::min(pic1.columns(), pic2.columns()); - int height = std::min(pic1.rows(), pic2.rows()); - Magick::Geometry geo1(width, height, pic1.columns()/2 - width/2, pic1.rows()/2 - height/2); - Magick::Geometry geo2(width, height, pic2.columns()/2 - width/2, pic2.rows()/2 - height/2); - pic1.crop(geo1); - pic2.crop(geo2); - - Magick::Image composite(Magick::Geometry(width*2, height, 0, 0), "white"); - composite.draw(Magick::DrawableCompositeImage(0, 0, pic1)); - composite.draw(Magick::DrawableCompositeImage(width, 0, pic2)); - composite.font(fontfile); - - double fontsize = 72; - for (;;) - { - composite.fontPointsize(fontsize); - - Magick::TypeMetric metric; - composite.fontTypeMetrics(ant1, &metric); - if (metric.textWidth() > 300) - { - fontsize -= 0.5; - continue; - } - - composite.fontTypeMetrics(ant2, &metric); - if (metric.textWidth() > 300) - { - fontsize -= 0.5; - continue; - } - - break; - } - - Magick::TypeMetric metric; - composite.fontTypeMetrics(ant1, &metric); - std::uniform_int_distribution rowdist(20, (int)(composite.rows() - 19 - metric.textHeight())); - int y = rowdist(random_engine); - y = composite.rows() - y; - int x1 = (width - 40 - metric.textWidth())/2+20; - composite.fontTypeMetrics(ant2, &metric); - int x2 = (width - 40 - metric.textWidth())/2+20+width; - composite.strokeColor("white"); - composite.strokeWidth(2); - composite.antiAlias(false); - composite.draw(Magick::DrawableText(x1, y, ant1)); - composite.draw(Magick::DrawableText(x2, y, ant2)); - - composite.magick("png"); - - Magick::Blob outputimg; - composite.write(&outputimg); - - std::cout << "Generated image!" << std::endl << "Tweeting..." << std::endl; - - std::stringstream msg; - msg << capitalize(ant1); - msg << " vs. "; - msg << capitalize(ant2); - - long media_id = client.uploadMedia("image/png", (const char*) outputimg.data(), outputimg.length()); - client.updateStatus(msg.str(), {media_id}); - - std::cout << "Done!" << std::endl << "Waiting..." << std::endl << std::endl; - } catch (const curl::curl_easy_exception& error) + composite.fontPointsize(fontsize); + + Magick::TypeMetric metric; + composite.fontTypeMetrics(ant1, &metric); + if (metric.textWidth() > 300) { - error.print_traceback(); - } catch (const twitter::twitter_error& e) + fontsize -= 0.5; + continue; + } + + composite.fontTypeMetrics(ant2, &metric); + if (metric.textWidth() > 300) { - std::cout << "Twitter error: " << e.what() << std::endl; + fontsize -= 0.5; + continue; } - - std::this_thread::sleep_for(std::chrono::seconds(delay)); + + break; } - - return 0; + + Magick::TypeMetric metric; + composite.fontTypeMetrics(ant1, &metric); + std::uniform_int_distribution rowdist(20, (int)(composite.rows() - 19 - metric.textHeight())); + int y = rowdist(rng_); + y = composite.rows() - y; + int x1 = (width - 40 - metric.textWidth())/2+20; + composite.fontTypeMetrics(ant2, &metric); + int x2 = (width - 40 - metric.textWidth())/2+20+width; + composite.strokeColor("white"); + composite.strokeWidth(2); + composite.antiAlias(false); + composite.draw(Magick::DrawableText(x1, y, ant1)); + composite.draw(Magick::DrawableText(x2, y, ant2)); + + composite.magick("png"); + + return composite; +} + +std::string difference::generateTweetText( + verbly::word word1, + verbly::word word2) const +{ + verbly::token result { + verbly::token::capitalize(verbly::token::casing::capitalize, word1), + "vs.", + verbly::token::capitalize(verbly::token::casing::capitalize, word2) + }; + + return result.compile(); +} + +void difference::sendTweet(std::string text, Magick::Image image) const +{ + Magick::Blob outputBlob; + image.magick("png"); + image.write(&outputBlob); + + long media_id = client_->uploadMedia("image/png", (const char*) outputBlob.data(), outputBlob.length()); + client_->updateStatus(std::move(text), {media_id}); } diff --git a/difference.h b/difference.h new file mode 100644 index 0000000..bccf9ab --- /dev/null +++ b/difference.h @@ -0,0 +1,58 @@ +#ifndef DIFFERENCE_H_081276A3 +#define DIFFERENCE_H_081276A3 + +#include +#include +#include +#include +#include +#include +#include +#include + +class difference { +public: + + difference( + std::string configFile, + std::mt19937& rng); + + void run() const; + +private: + + verbly::word getPicturedNoun() const; + + std::pair + getImagesForNoun(verbly::word pictured) const; + + Magick::Image getImageAtUrl(std::string url) const; + + std::pair getOppositeIdentifiers() const; + + Magick::Image composeImage( + Magick::Image image1, + verbly::word word1, + Magick::Image image2, + verbly::word word2) const; + + std::string generateTweetText(verbly::word word1, verbly::word word2) 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_; + std::string fontfile_; + +}; + +#endif /* end of include guard: DIFFERENCE_H_081276A3 */ diff --git a/main.cpp b/main.cpp new file mode 100644 index 0000000..1b49563 --- /dev/null +++ b/main.cpp @@ -0,0 +1,35 @@ +#include "difference.h" + +int main(int argc, char** argv) +{ + Magick::InitializeMagick(nullptr); + + std::random_device randomDevice; + std::mt19937 rng(randomDevice()); + + if (argc != 2) + { + std::cout << "usage: difference [configfile]" << std::endl; + return -1; + } + + std::string configfile(argv[1]); + + try + { + difference 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; + } + + return 0; +} diff --git a/vendor/libtwittercpp b/vendor/libtwittercpp index d90a1e7..df90612 160000 --- a/vendor/libtwittercpp +++ b/vendor/libtwittercpp @@ -1 +1 @@ -Subproject commit d90a1e74c77ba67f25a812609fd49d479bc464dd +Subproject commit df906121dd862c0f704e44f28ee079158c431c41 diff --git a/vendor/verbly b/vendor/verbly index 1f898f3..59eab84 160000 --- a/vendor/verbly +++ b/vendor/verbly @@ -1 +1 @@ -Subproject commit 1f898f3bd66c29672275c2c884b17ba662ced626 +Subproject commit 59eab842de02b2b2ba8bf53e2214b558457e6356 -- cgit 1.4.1