summary refs log tree commit diff stats
diff options
context:
space:
mode:
-rw-r--r--.gitmodules9
-rw-r--r--CMakeLists.txt19
-rw-r--r--grunge.cpp355
-rw-r--r--grunge.h49
-rw-r--r--main.cpp33
-rw-r--r--palette.cpp119
-rw-r--r--palette.h30
m---------vendor/libtwittercpp0
m---------vendor/verbly0
m---------vendor/yaml-cpp0
10 files changed, 614 insertions, 0 deletions
diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..346e727 --- /dev/null +++ b/.gitmodules
@@ -0,0 +1,9 @@
1[submodule "vendor/verbly"]
2 path = vendor/verbly
3 url = git@github.com:hatkirby/verbly
4[submodule "vendor/libtwittercpp"]
5 path = vendor/libtwittercpp
6 url = git@github.com:hatkirby/libtwittercpp
7[submodule "vendor/yaml-cpp"]
8 path = vendor/yaml-cpp
9 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 @@
1cmake_minimum_required (VERSION 3.1)
2project (grunge)
3
4set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
5
6add_subdirectory(vendor/libtwittercpp)
7add_subdirectory(vendor/verbly)
8add_subdirectory(vendor/yaml-cpp EXCLUDE_FROM_ALL)
9
10find_package(PkgConfig)
11pkg_check_modules(GraphicsMagick GraphicsMagick++ REQUIRED)
12
13include_directories(${GraphicsMagick_INCLUDE_DIRS} vendor/verbly/lib vendor/libtwittercpp/src vendor/yaml-cpp/include vendor/libtwittercpp/vendor/curlcpp/include)
14link_directories(${GraphicsMagick_LIBRARY_DIRS})
15add_executable(grunge main.cpp grunge.cpp palette.cpp)
16set_property(TARGET grunge PROPERTY CXX_STANDARD 11)
17set_property(TARGET grunge PROPERTY CXX_STANDARD_REQUIRED ON)
18target_link_libraries(grunge ${GraphicsMagick_LIBRARIES} verbly yaml-cpp twitter++)
19
diff --git a/grunge.cpp b/grunge.cpp new file mode 100644 index 0000000..de9061e --- /dev/null +++ b/grunge.cpp
@@ -0,0 +1,355 @@
1#include "grunge.h"
2#include <yaml-cpp/yaml.h>
3#include <iostream>
4#include <thread>
5#include <chrono>
6#include <curl_easy.h>
7#include <curl_header.h>
8#include <deque>
9#include "palette.h"
10
11grunge::grunge(
12 std::string configFile,
13 std::mt19937& rng) :
14 rng_(rng)
15{
16 // Load the config file.
17 YAML::Node config = YAML::LoadFile(configFile);
18
19 // Set up the Twitter client.
20 twitter::auth auth;
21 auth.setConsumerKey(config["consumer_key"].as<std::string>());
22 auth.setConsumerSecret(config["consumer_secret"].as<std::string>());
23 auth.setAccessKey(config["access_key"].as<std::string>());
24 auth.setAccessSecret(config["access_secret"].as<std::string>());
25
26 client_ = std::unique_ptr<twitter::client>(new twitter::client(auth));
27
28 // Set up the verbly database.
29 database_ = std::unique_ptr<verbly::database>(
30 new verbly::database(config["verbly_datafile"].as<std::string>()));
31}
32
33void grunge::run() const
34{
35 for (;;)
36 {
37 std::cout << "Generating tweet..." << std::endl;
38
39 try
40 {
41 // Find a noun to use as the pictured item.
42 std::cout << "Choosing pictured noun..." << std::endl;
43
44 verbly::word pictured = getPicturedNoun();
45
46 std::cout << "Noun: " << pictured.getBaseForm().getText() << std::endl;
47
48 // Choose a picture of that noun.
49 std::cout << "Finding an image..." << std::endl;
50
51 Magick::Image image = getImageForNoun(pictured);
52
53 // Pixelate the image.
54 std::cout << "Pixelating image..." << std::endl;
55
56 image = pixelateImage(std::move(image));
57
58 // Pastelize the image.
59 std::cout << "Pastelizing image..." << std::endl;
60
61 image = pastelizeImage(std::move(image));
62
63 // Generate the tweet text.
64 std::cout << "Generating text..." << std::endl;
65
66 std::string text = generateTweetText(pictured);
67
68 std::cout << "Tweet text: " << text << std::endl;
69
70 // Send the tweet.
71 std::cout << "Sending tweet..." << std::endl;
72
73 sendTweet(std::move(text), std::move(image));
74
75 std::cout << "Tweeted!" << std::endl;
76
77 // Wait.
78 std::this_thread::sleep_for(std::chrono::hours(1));
79 } catch (const could_not_get_images& ex)
80 {
81 std::cout << ex.what() << std::endl;
82 } catch (const Magick::ErrorImage& ex)
83 {
84 std::cout << "Image error: " << ex.what() << std::endl;
85 } catch (const Magick::ErrorCorruptImage& ex)
86 {
87 std::cout << "Corrupt image: " << ex.what() << std::endl;
88 } catch (const twitter::twitter_error& ex)
89 {
90 std::cout << "Twitter error: " << ex.what() << std::endl;
91
92 std::this_thread::sleep_for(std::chrono::hours(1));
93 }
94
95 std::cout << std::endl;
96 }
97}
98
99verbly::word grunge::getPicturedNoun() const
100{
101 verbly::filter whitelist =
102 (verbly::notion::wnid == 109287968) // Geological formations
103 || (verbly::notion::wnid == 109208496) // Asterisms (collections of stars)
104 || (verbly::notion::wnid == 109239740) // Celestial bodies
105 || (verbly::notion::wnid == 109277686) // Exterrestrial objects (comets and meteroids)
106 || (verbly::notion::wnid == 109403211) // Radiators (supposedly natural radiators but actually these are just pictures of radiators)
107 || (verbly::notion::wnid == 109416076) // Rocks
108 || (verbly::notion::wnid == 105442131) // Chromosomes
109 || (verbly::notion::wnid == 100324978) // Tightrope walking
110 || (verbly::notion::wnid == 100326094) // Rock climbing
111 || (verbly::notion::wnid == 100433458) // Contact sports
112 || (verbly::notion::wnid == 100433802) // Gymnastics
113 || (verbly::notion::wnid == 100439826) // Track and field
114 || (verbly::notion::wnid == 100440747) // Skiing
115 || (verbly::notion::wnid == 100441824) // Water sport
116 || (verbly::notion::wnid == 100445351) // Rowing
117 || (verbly::notion::wnid == 100446980) // Archery
118 // TODO: add more sports
119 || (verbly::notion::wnid == 100021939) // Artifacts
120 || (verbly::notion::wnid == 101471682) // Vertebrates
121 ;
122
123 verbly::filter blacklist =
124 (verbly::notion::wnid == 106883725) // swastika
125 || (verbly::notion::wnid == 104416901) // tetraskele
126 || (verbly::notion::wnid == 102512053) // fish
127 || (verbly::notion::wnid == 103575691) // instrument of execution
128 || (verbly::notion::wnid == 103829563) // noose
129 ;
130
131 verbly::query<verbly::word> pictureQuery = database_->words(
132 (verbly::notion::fullHypernyms %= whitelist)
133 && !(verbly::notion::fullHypernyms %= blacklist)
134 && (verbly::notion::partOfSpeech == verbly::part_of_speech::noun)
135 && (verbly::notion::numOfImages >= 1));
136
137 verbly::word pictured = pictureQuery.first();
138
139 return pictured;
140}
141
142Magick::Image grunge::getImageForNoun(verbly::word pictured) const
143{
144 // Accept string from Google Chrome
145 std::string accept = "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8";
146 curl::curl_header headers;
147 headers.add(accept);
148
149 int backoff = 0;
150
151 std::cout << "Getting URLs..." << std::endl;
152
153 std::string lstdata;
154 while (lstdata.empty())
155 {
156 std::ostringstream lstbuf;
157 curl::curl_ios<std::ostringstream> lstios(lstbuf);
158 curl::curl_easy lsthandle(lstios);
159 std::string lsturl = pictured.getNotion().getImageNetUrl();
160 lsthandle.add<CURLOPT_URL>(lsturl.c_str());
161
162 try
163 {
164 lsthandle.perform();
165 } catch (const curl::curl_easy_exception& e)
166 {
167 e.print_traceback();
168
169 backoff++;
170 std::cout << "Waiting for " << backoff << " seconds..." << std::endl;
171
172 std::this_thread::sleep_for(std::chrono::seconds(backoff));
173
174 continue;
175 }
176
177 backoff = 0;
178
179 if (lsthandle.get_info<CURLINFO_RESPONSE_CODE>().get() != 200)
180 {
181 throw could_not_get_images();
182 }
183
184 std::cout << "Got URLs." << std::endl;
185 lstdata = lstbuf.str();
186 }
187
188 std::vector<std::string> lstvec = verbly::split<std::vector<std::string>>(lstdata, "\r\n");
189 if (lstvec.empty())
190 {
191 throw could_not_get_images();
192 }
193
194 std::shuffle(std::begin(lstvec), std::end(lstvec), rng_);
195
196 std::deque<std::string> urls;
197 for (std::string& url : lstvec)
198 {
199 urls.push_back(url);
200 }
201
202 bool found = false;
203 Magick::Blob img;
204 Magick::Image pic;
205
206 while (!found && !urls.empty())
207 {
208 std::string url = urls.front();
209 urls.pop_front();
210
211 std::ostringstream imgbuf;
212 curl::curl_ios<std::ostringstream> imgios(imgbuf);
213 curl::curl_easy imghandle(imgios);
214
215 imghandle.add<CURLOPT_HTTPHEADER>(headers.get());
216 imghandle.add<CURLOPT_URL>(url.c_str());
217 imghandle.add<CURLOPT_CONNECTTIMEOUT>(30);
218
219 try
220 {
221 imghandle.perform();
222 } catch (curl::curl_easy_exception error) {
223 error.print_traceback();
224
225 continue;
226 }
227
228 if (imghandle.get_info<CURLINFO_RESPONSE_CODE>().get() != 200)
229 {
230 continue;
231 }
232
233 std::string content_type = imghandle.get_info<CURLINFO_CONTENT_TYPE>().get();
234 if (content_type.substr(0, 6) != "image/")
235 {
236 continue;
237 }
238
239 std::string imgstr = imgbuf.str();
240 img = Magick::Blob(imgstr.c_str(), imgstr.length());
241
242 try
243 {
244 pic.read(img);
245
246 if ((pic.rows() > 0) && (pic.columns() >= 800))
247 {
248 std::cout << url << std::endl;
249 found = true;
250 }
251 } catch (const Magick::ErrorOption& e)
252 {
253 // Occurs when the the data downloaded from the server is malformed
254 std::cout << "Magick: " << e.what() << std::endl;
255 }
256 }
257
258 if (!found)
259 {
260 throw could_not_get_images();
261 }
262
263 return pic;
264}
265
266Magick::Image grunge::pixelateImage(Magick::Image image) const
267{
268 // Check that the image dimensions are a multiple of four.
269 if ((image.rows() % 4 != 0) || (image.columns() % 4 != 0))
270 {
271 Magick::Geometry cropped(
272 image.columns() - (image.columns() % 4),
273 image.rows() - (image.rows() % 4));
274
275 image.crop(cropped);
276 }
277
278 // Downscale the image.
279 Magick::Geometry originalSize = image.size();
280 Magick::Geometry pixelatedSize(
281 originalSize.width() / 4,
282 originalSize.height() / 4);
283
284 image.scale(pixelatedSize);
285
286 // Scale the image back up.
287 image.scale(originalSize);
288
289 return image;
290}
291
292Magick::Image grunge::pastelizeImage(Magick::Image input) const
293{
294 input.quantizeColorSpace(Magick::GRAYColorspace);
295 input.quantizeColors(256);
296 input.quantize();
297
298 palette pastelPalette = palette::randomPalette(rng_);
299 Magick::Geometry size = input.size();
300 Magick::Image pastelized(size, "white");
301
302 for (int y=0; y<size.height(); y++)
303 {
304 for (int x=0; x<size.width(); x++)
305 {
306 Magick::ColorGray grade =
307 static_cast<Magick::ColorGray>(input.pixelColor(x, y));
308 Magick::Color mapped = pastelPalette.getColor(grade.shade());
309
310 pastelized.pixelColor(x, y, mapped);
311 }
312 }
313
314 return pastelized;
315}
316
317std::string grunge::generateTweetText(verbly::word pictured) const
318{
319 verbly::word simpler = database_->words(
320 (verbly::notion::partOfSpeech == verbly::part_of_speech::noun)
321 && (verbly::notion::fullHyponyms %= pictured)
322 && (verbly::form::proper == false)).first();
323
324 std::vector<std::string> symbols = {"☯","✡","☨","✞","✝","☮","☥","☦","☪","✌"};
325 std::string prefix;
326 std::string suffix;
327 int length = std::geometric_distribution<int>(0.5)(rng_) + 1;
328 for (int i=0; i<length; i++)
329 {
330 std::string choice = symbols[
331 std::uniform_int_distribution<int>(0, symbols.size()-1)(rng_)];
332
333 prefix += choice;
334 suffix = choice + suffix;
335 }
336
337 verbly::token action = {
338 prefix,
339 "follow for more soft grunge",
340 simpler,
341 suffix
342 };
343
344 return action.compile();
345}
346
347void grunge::sendTweet(std::string text, Magick::Image image) const
348{
349 Magick::Blob outputBlob;
350 image.magick("jpg");
351 image.write(&outputBlob);
352
353 long media_id = client_->uploadMedia("image/jpeg", (const char*) outputBlob.data(), outputBlob.length());
354 client_->updateStatus(std::move(text), {media_id});
355}
diff --git a/grunge.h b/grunge.h new file mode 100644 index 0000000..5f95442 --- /dev/null +++ b/grunge.h
@@ -0,0 +1,49 @@
1#ifndef GRUNGE_H
2#define GRUNGE_H
3
4#include <random>
5#include <twitter.h>
6#include <verbly.h>
7#include <string>
8#include <memory>
9#include <Magick++.h>
10#include <stdexcept>
11
12class grunge {
13public:
14
15 grunge(
16 std::string configFile,
17 std::mt19937& rng);
18
19 void run() const;
20
21private:
22
23 verbly::word getPicturedNoun() const;
24
25 Magick::Image getImageForNoun(verbly::word pictured) const;
26
27 Magick::Image pixelateImage(Magick::Image image) const;
28
29 Magick::Image pastelizeImage(Magick::Image image) const;
30
31 std::string generateTweetText(verbly::word pictured) const;
32
33 void sendTweet(std::string text, Magick::Image image) const;
34
35 class could_not_get_images : public std::runtime_error {
36 public:
37
38 could_not_get_images() : std::runtime_error("Could not get images for noun")
39 {
40 }
41 };
42
43 std::mt19937& rng_;
44 std::unique_ptr<verbly::database> database_;
45 std::unique_ptr<twitter::client> client_;
46
47};
48
49#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 @@
1#include "grunge.h"
2
3int main(int argc, char** argv)
4{
5 Magick::InitializeMagick(nullptr);
6
7 std::random_device randomDevice;
8 std::mt19937 rng(randomDevice());
9
10 if (argc != 2)
11 {
12 std::cout << "usage: grunge [configfile]" << std::endl;
13 return -1;
14 }
15
16 std::string configfile(argv[1]);
17
18 try
19 {
20 grunge bot(configfile, rng);
21
22 try
23 {
24 bot.run();
25 } catch (const std::exception& ex)
26 {
27 std::cout << "Error running bot: " << ex.what() << std::endl;
28 }
29 } catch (const std::exception& ex)
30 {
31 std::cout << "Error initializing bot: " << ex.what() << std::endl;
32 }
33}
diff --git a/palette.cpp b/palette.cpp new file mode 100644 index 0000000..4135756 --- /dev/null +++ b/palette.cpp
@@ -0,0 +1,119 @@
1#include "palette.h"
2#include <algorithm>
3
4const std::vector<Magick::ColorRGB> palette::reds = {
5 {"#ff4848"}, {"#ff7575"}, {"#ff8a8a"}, {"#ff9797"}, {"#ffa8a8"}, {"#ffbbbb"}
6};
7
8const std::vector<Magick::ColorRGB> palette::oranges = {
9 {"#ffbf85"}, {"#ffcfa4"}, {"#ffbd82"}, {"#ffc48e"}
10};
11
12const std::vector<Magick::ColorRGB> palette::yellows = {
13 {"#fdfda5"}, {"#eff0ac"}, {"#fef495"}
14};
15
16const std::vector<Magick::ColorRGB> palette::greens = {
17 {"#c6fcb4"}, {"#d4ffa2"}, {"#93eeaa"}
18};
19
20const std::vector<Magick::ColorRGB> palette::blues = {
21 {"#62d0ff"}, {"#62a9ff"}, {"#63e9fc"}, {"#7bcae1"}, {"#92fef9"}
22};
23
24const std::vector<Magick::ColorRGB> palette::purples = {
25 {"#dfb0fe"}, {"#b0a7f1"}, {"#ff86ff"}, {"#ffacec"}, {"#ff86c2"}, {"#ea8dfe"}
26};
27
28palette::palette(std::vector<Magick::Color> foci)
29{
30 if (foci.size() < 1)
31 {
32 throw std::invalid_argument("Must have at least one focus");
33 }
34
35 if (foci.size() < 2)
36 {
37 // Degenerate scenario, but deal with it gracefully.
38 foci.push_back(foci.front());
39 }
40
41 int sections = foci.size() - 1;
42 double sectionSize = 256.0 / static_cast<double>(sections);
43 for (int i=0; i<256; i++)
44 {
45 int section = std::floor(static_cast<double>(i) / sectionSize);
46 double interpolation = (static_cast<double>(i) / sectionSize) - section;
47
48 Magick::ColorHSL interpLeft = foci[section];
49 Magick::ColorHSL interpRight = foci[section+1];
50
51 double newHue;
52 double diff = interpRight.hue() - interpLeft.hue();
53 if (diff < 0)
54 {
55 std::swap(interpLeft, interpRight);
56
57 diff = -diff;
58 interpolation = 1 - interpolation;
59 }
60
61 if (diff > 0.5)
62 {
63 newHue = 1.0 + interpLeft.hue()
64 * (interpolation * (interpRight.hue() - interpLeft.hue() - 1.0));
65
66 if (newHue > 1.0)
67 {
68 newHue -= 1.0;
69 }
70 } else {
71 newHue = interpLeft.hue() + interpolation * diff;
72 }
73
74 Magick::ColorHSL interpolated(
75 newHue,
76 ((1.0 - interpolation) * interpLeft.saturation())
77 + (interpolation * interpRight.saturation()),
78 ((1.0 - interpolation) * interpLeft.luminosity())
79 + (interpolation * interpRight.luminosity()));
80
81 gradient_.push_back(interpolated);
82 }
83}
84
85palette palette::randomPalette(std::mt19937& rng)
86{
87 std::vector<Magick::Color> foci;
88 foci.push_back(reds[
89 std::uniform_int_distribution<int>(0, reds.size()-1)(rng)]);
90 foci.push_back(oranges[
91 std::uniform_int_distribution<int>(0, oranges.size()-1)(rng)]);
92 foci.push_back(yellows[
93 std::uniform_int_distribution<int>(0, yellows.size()-1)(rng)]);
94 foci.push_back(greens[
95 std::uniform_int_distribution<int>(0, greens.size()-1)(rng)]);
96 foci.push_back(blues[
97 std::uniform_int_distribution<int>(0, blues.size()-1)(rng)]);
98 foci.push_back(purples[
99 std::uniform_int_distribution<int>(0, purples.size()-1)(rng)]);
100
101 std::shuffle(std::begin(foci), std::end(foci), rng);
102
103 int origNum = foci.size();
104 int numOfFoci = std::uniform_int_distribution<int>(4, origNum)(rng);
105 for (int i=0; i<(origNum-numOfFoci); i++)
106 {
107 foci.pop_back();
108 }
109
110 foci.push_back(foci[0]);
111
112 return palette(std::move(foci));
113}
114
115Magick::Color palette::getColor(double shade) const
116{
117 int index = std::min((int)floor(shade * 256.0), 255);
118 return gradient_[index];
119}
diff --git a/palette.h b/palette.h new file mode 100644 index 0000000..3a5cfb3 --- /dev/null +++ b/palette.h
@@ -0,0 +1,30 @@
1#ifndef PALETTE_H
2#define PALETTE_H
3
4#include <Magick++.h>
5#include <random>
6#include <vector>
7
8class palette {
9public:
10
11 palette(std::vector<Magick::Color> foci);
12
13 static palette randomPalette(std::mt19937& rng);
14
15 Magick::Color getColor(double shade) const;
16
17private:
18
19 static const std::vector<Magick::ColorRGB> reds;
20 static const std::vector<Magick::ColorRGB> oranges;
21 static const std::vector<Magick::ColorRGB> yellows;
22 static const std::vector<Magick::ColorRGB> greens;
23 static const std::vector<Magick::ColorRGB> blues;
24 static const std::vector<Magick::ColorRGB> purples;
25
26 std::vector<Magick::Color> gradient_;
27
28};
29
30#endif
diff --git a/vendor/libtwittercpp b/vendor/libtwittercpp new file mode 160000
Subproject df906121dd862c0f704e44f28ee079158c431c4
diff --git a/vendor/verbly b/vendor/verbly new file mode 160000
Subproject 59eab842de02b2b2ba8bf53e2214b558457e635
diff --git a/vendor/yaml-cpp b/vendor/yaml-cpp new file mode 160000
Subproject bedb28fdb4fd52d97e02f6cb946cae631037089