summary refs log tree commit diff stats
path: root/advice.cpp
diff options
context:
space:
mode:
authorKelly Rauchenberger <fefferburbia@gmail.com>2017-02-03 13:56:19 -0500
committerKelly Rauchenberger <fefferburbia@gmail.com>2017-02-03 13:56:19 -0500
commit18742d79e1de863889521c492e938491489316fe (patch)
tree9930ef168c81c01e1c947b47d697d494aa7ca0f2 /advice.cpp
downloadadvice-18742d79e1de863889521c492e938491489316fe.tar.gz
advice-18742d79e1de863889521c492e938491489316fe.tar.bz2
advice-18742d79e1de863889521c492e938491489316fe.zip
Created bot
Diffstat (limited to 'advice.cpp')
-rw-r--r--advice.cpp359
1 files changed, 359 insertions, 0 deletions
diff --git a/advice.cpp b/advice.cpp new file mode 100644 index 0000000..320f719 --- /dev/null +++ b/advice.cpp
@@ -0,0 +1,359 @@
1#include "advice.h"
2#include <algorithm>
3#include <iostream>
4#include <vector>
5#include <deque>
6#include <curl_easy.h>
7#include <curl_header.h>
8#include <sstream>
9#include <list>
10#include <chrono>
11#include <thread>
12#include <yaml-cpp/yaml.h>
13
14advice::advice(
15 std::string configFile,
16 std::mt19937& rng) :
17 rng_(rng)
18{
19 // Load the config file.
20 YAML::Node config = YAML::LoadFile(configFile);
21
22 // Set up the Twitter client.
23 twitter::auth auth;
24 auth.setConsumerKey(config["consumer_key"].as<std::string>());
25 auth.setConsumerSecret(config["consumer_secret"].as<std::string>());
26 auth.setAccessKey(config["access_key"].as<std::string>());
27 auth.setAccessSecret(config["access_secret"].as<std::string>());
28
29 client_ = std::unique_ptr<twitter::client>(new twitter::client(auth));
30
31 // Set up the verbly database.
32 database_ = std::unique_ptr<verbly::database>(new verbly::database(config["verbly_datafile"].as<std::string>()));
33
34 // Set up the sentence generator.
35 generator_ = std::unique_ptr<sentence>(new sentence(*database_, rng_));
36}
37
38verbly::word advice::generateImageNoun() const
39{
40 verbly::filter whitelist =
41 (verbly::notion::wnid == 109287968) // Geological formations
42 || (verbly::notion::wnid == 109208496) // Asterisms (collections of stars)
43 || (verbly::notion::wnid == 109239740) // Celestial bodies
44 || (verbly::notion::wnid == 109277686) // Exterrestrial objects (comets and meteroids)
45 || (verbly::notion::wnid == 109403211) // Radiators (supposedly natural radiators but actually these are just pictures of radiators)
46 || (verbly::notion::wnid == 109416076) // Rocks
47 || (verbly::notion::wnid == 105442131) // Chromosomes
48 || (verbly::notion::wnid == 100324978) // Tightrope walking
49 || (verbly::notion::wnid == 100326094) // Rock climbing
50 || (verbly::notion::wnid == 100433458) // Contact sports
51 || (verbly::notion::wnid == 100433802) // Gymnastics
52 || (verbly::notion::wnid == 100439826) // Track and field
53 || (verbly::notion::wnid == 100440747) // Skiing
54 || (verbly::notion::wnid == 100441824) // Water sport
55 || (verbly::notion::wnid == 100445351) // Rowing
56 || (verbly::notion::wnid == 100446980) // Archery
57 // TODO: add more sports
58 || (verbly::notion::wnid == 100021939) // Artifacts
59 || (verbly::notion::wnid == 101471682) // Vertebrates
60 ;
61
62 verbly::filter blacklist =
63 (verbly::notion::wnid == 106883725) // swastika
64 || (verbly::notion::wnid == 104416901) // tetraskele
65 || (verbly::notion::wnid == 102512053) // fish
66 || (verbly::notion::wnid == 103575691) // instrument of execution
67 ;
68
69 verbly::query<verbly::word> pictureQuery = database_->words(
70 (verbly::notion::fullHypernyms %= whitelist)
71 && !(verbly::notion::fullHypernyms %= blacklist)
72 && (verbly::notion::partOfSpeech == verbly::part_of_speech::noun)
73 && (verbly::notion::numOfImages >= 1));
74
75 return pictureQuery.first();
76}
77
78Magick::Image advice::getImageForNoun(verbly::word pictured) const
79{
80 // Accept string from Google Chrome
81 std::string accept = "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8";
82 curl::curl_header headers;
83 headers.add(accept);
84
85 int backoff = 0;
86
87 std::cout << "Generating noun..." << std::endl;
88 std::cout << "Noun: " << pictured.getBaseForm() << std::endl;
89 std::cout << "Getting URLs..." << std::endl;
90
91 std::string lstdata;
92 while (lstdata.empty())
93 {
94 std::ostringstream lstbuf;
95 curl::curl_ios<std::ostringstream> lstios(lstbuf);
96 curl::curl_easy lsthandle(lstios);
97 std::string lsturl = pictured.getNotion().getImageNetUrl();
98 lsthandle.add<CURLOPT_URL>(lsturl.c_str());
99
100 try
101 {
102 lsthandle.perform();
103 } catch (const curl::curl_easy_exception& e)
104 {
105 e.print_traceback();
106
107 backoff++;
108 std::cout << "Waiting for " << backoff << " seconds..." << std::endl;
109
110 std::this_thread::sleep_for(std::chrono::seconds(backoff));
111
112 continue;
113 }
114
115 backoff = 0;
116
117 if (lsthandle.get_info<CURLINFO_RESPONSE_CODE>().get() != 200)
118 {
119 throw could_not_get_images();
120 }
121
122 std::cout << "Got URLs." << std::endl;
123 lstdata = lstbuf.str();
124 }
125
126 std::vector<std::string> lstvec = verbly::split<std::vector<std::string>>(lstdata, "\r\n");
127 if (lstvec.empty())
128 {
129 throw could_not_get_images();
130 }
131
132 std::shuffle(std::begin(lstvec), std::end(lstvec), rng_);
133
134 std::deque<std::string> urls;
135 for (std::string& url : lstvec)
136 {
137 urls.push_back(url);
138 }
139
140 bool found = false;
141 Magick::Blob img;
142 Magick::Image pic;
143
144 while (!found && !urls.empty())
145 {
146 std::string url = urls.front();
147 urls.pop_front();
148
149 std::ostringstream imgbuf;
150 curl::curl_ios<std::ostringstream> imgios(imgbuf);
151 curl::curl_easy imghandle(imgios);
152
153 imghandle.add<CURLOPT_HTTPHEADER>(headers.get());
154 imghandle.add<CURLOPT_URL>(url.c_str());
155 imghandle.add<CURLOPT_CONNECTTIMEOUT>(30);
156
157 try
158 {
159 imghandle.perform();
160 } catch (curl::curl_easy_exception error) {
161 error.print_traceback();
162
163 continue;
164 }
165
166 if (imghandle.get_info<CURLINFO_RESPONSE_CODE>().get() != 200)
167 {
168 continue;
169 }
170
171 std::string content_type = imghandle.get_info<CURLINFO_CONTENT_TYPE>().get();
172 if (content_type.substr(0, 6) != "image/")
173 {
174 continue;
175 }
176
177 std::string imgstr = imgbuf.str();
178 img = Magick::Blob(imgstr.c_str(), imgstr.length());
179 pic.read(img);
180 if (pic.rows() == 0)
181 {
182 continue;
183 }
184
185 // Too small!
186 if (pic.columns() < 400)
187 {
188 continue;
189 }
190
191 std::cout << url << std::endl;
192 found = true;
193 }
194
195 if (!found)
196 {
197 throw could_not_get_images();
198 }
199
200 return pic;
201}
202
203Magick::Image advice::layoutImage(Magick::Image pic, std::string title) const
204{
205 // Want a 16:9 aspect
206 int idealwidth = pic.rows()*(16.0/9.0);
207 if (idealwidth > pic.columns())
208 {
209 // If the image is narrower than the ideal width, use full width.
210 int newheight = pic.columns()*(9.0/16.0);
211
212 // Just take a slice out of the middle of the image.
213 int cropy = ((double)(pic.rows() - newheight))/2.0;
214
215 pic.crop(Magick::Geometry(pic.columns(), newheight, 0, cropy));
216 } else {
217 // If the image is wider than the ideal width, use full height.
218 // Just take a slice out of the middle of the image.
219 int cropx = ((double)(pic.columns() - idealwidth))/2.0;
220
221 pic.crop(Magick::Geometry(idealwidth, pic.rows(), cropx, 0));
222 }
223
224 pic.zoom(Magick::Geometry(400, 225));
225
226 // Layout the text.
227 std::list<std::string> words = verbly::split<std::list<std::string>>(title, " ");
228 std::vector<std::string> lines;
229 std::list<std::string> cur;
230 Magick::TypeMetric metric;
231 pic.fontPointsize(20);
232 pic.font("@coolvetica.ttf");
233
234 while (!words.empty())
235 {
236 cur.push_back(words.front());
237
238 std::string prefixText = verbly::implode(std::begin(cur), std::end(cur), " ");
239 pic.fontTypeMetrics(prefixText, &metric);
240
241 if (metric.textWidth() > 380)
242 {
243 if (cur.size() == 1)
244 {
245 words.pop_front();
246 } else {
247 cur.pop_back();
248 }
249
250 prefixText = verbly::implode(std::begin(cur), std::end(cur), " ");
251 lines.push_back(prefixText);
252 cur.clear();
253 } else {
254 words.pop_front();
255 }
256 }
257
258 if (!cur.empty())
259 {
260 std::string prefixText = verbly::implode(std::begin(cur), std::end(cur), " ");
261 lines.push_back(prefixText);
262 }
263
264 int lineHeight = metric.textHeight()-2;
265 int blockHeight = lineHeight * lines.size() + 18;
266 std::cout << "line " << lineHeight << "; block " << blockHeight << std::endl;
267
268 std::list<Magick::Drawable> drawList;
269 drawList.push_back(Magick::DrawableFillColor("black"));
270 drawList.push_back(Magick::DrawableFillOpacity(0.5));
271 drawList.push_back(Magick::DrawableStrokeColor("transparent"));
272 drawList.push_back(Magick::DrawableRectangle(0, 225-blockHeight-20, 400, 255)); // 0, 225-60, 400, 255
273 pic.draw(drawList);
274
275 drawList.clear();
276 drawList.push_back(Magick::DrawableFont("@coolvetica.ttf"));
277 drawList.push_back(Magick::DrawableFillColor("white"));
278 drawList.push_back(Magick::DrawablePointSize(14));
279 drawList.push_back(Magick::DrawableText(10, 225-blockHeight+4, "How to")); // 10, 255-62-4
280 pic.draw(drawList);
281
282 for (int i=0; i<lines.size(); i++)
283 {
284 drawList.clear();
285 drawList.push_back(Magick::DrawableFont("@coolvetica.ttf"));
286 drawList.push_back(Magick::DrawableFillColor("white"));
287 drawList.push_back(Magick::DrawablePointSize(20));
288 drawList.push_back(Magick::DrawableText(10, 255-blockHeight+(i*lineHeight)-4, lines[i])); // 10, 255-20-25
289 pic.draw(drawList);
290 }
291
292 return pic;
293}
294
295void advice::sendTweet(Magick::Image pic, std::string title) const
296{
297 Magick::Blob outputimg;
298
299 try
300 {
301 pic.magick("png");
302 pic.write(&outputimg);
303 } catch (const Magick::WarningCoder& e)
304 {
305 // Ignore
306 }
307
308 std::cout << "Generated image!" << std::endl << "Tweeting..." << std::endl;
309
310 std::string tweetText;
311 size_t tweetLim = 140 - client_->getConfiguration().getCharactersReservedPerMedia();
312 if (title.length() > tweetLim)
313 {
314 tweetText = title.substr(0, tweetLim - 1) + "…";
315 } else {
316 tweetText = title;
317 }
318
319 long media_id = client_->uploadMedia("image/png", (const char*) outputimg.data(), outputimg.length());
320 client_->updateStatus(tweetText, {media_id});
321}
322
323void advice::run() const
324{
325 for (;;)
326 {
327 try
328 {
329 // Pick a noun to use for the picture.
330 verbly::word pictured = generateImageNoun();
331
332 // Find an image of the picked noun.
333 Magick::Image pic = getImageForNoun(pictured);
334
335 // Generate the image text.
336 std::string title = generator_->generate();
337
338 // Layout the image.
339 Magick::Image output = layoutImage(std::move(pic), title);
340
341 // Tweet the image.
342 sendTweet(std::move(output), title);
343
344 std::cout << "Done!" << std::endl << "Waiting..." << std::endl << std::endl;
345
346 // Wait.
347 std::this_thread::sleep_for(std::chrono::hours(1));
348 } catch (const could_not_get_images& ex)
349 {
350 std::cout << ex.what() << std::endl;
351 } catch (const Magick::ErrorImage& ex)
352 {
353 std::cout << "Image error: " << ex.what() << std::endl;
354 } catch (const twitter::twitter_error& ex)
355 {
356 std::cout << "Twitter error: " << ex.what() << std::endl;
357 }
358 }
359}