-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathindex.js
executable file
·239 lines (211 loc) · 8.62 KB
/
index.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
#!/usr/bin/env node
'use strict';
// Replace Math.random() with MT-based substitute:
Math.random = require('./lib/mt-rng');
// General requirements
const _ = require('lodash');
const P = require('bluebird');
const os = require('os');
const path = require('path');
const tls = require('tls');
const qs = require('querystring');
const cheerio = require('cheerio');
// Promisfy some imports for convenience sake
const fs = P.promisifyAll(require('fs'));
const { curly } = require('node-libcurl');
const exec = require('child-process-promise').exec;
// Read API keys etc
const creds = require('./credentials');
// Twitter interface
const Twit = require('./lib/twithelper');
const T = new Twit(creds.live);
// Masto interface
const Mastodon = require('mastodon-api');
const M = new Mastodon(creds.live.masto);
const TMP_OUT = path.join(os.tmpdir(), 'artcoma.jpg');
const TMP_IN = path.join(os.tmpdir(), 'artpiece.jpg');
// To help curly
const certFilePath = path.join(__dirname, 'cert.pem');
// Interpret a year as a year, or assume anything with letters is a century:
function parseStartDate(d) {
return d.match(/\D/) ? _.parseInt(d.replace(/\D/g,'')) * 100 : _.parseInt(d);
}
function parseEndDate(d) {
return d.match(/\D/) ? _.parseInt(d.replace(/\D/g,'')) * 100 - 99 : _.parseInt(d);
}
// this function is hell on earth, never unfold it
function parseDate(date) {
if (_.isUndefined(date) || _.isNull(date)) {
return date;
}
const rangeIndicators = /\ ?[\-\–\—]\ ?|\ or\ |\ to\ /gi;
let start, end;
date = date.split(',')[0]; // do not fuck with commas
if (date.match(rangeIndicators)) {
[start, end] = date.split(rangeIndicators);
if (end.match(/b\.c\./gi)) {
if (date.match(/centur/gi)) {
start = parseStartDate(start) * -1;
end = parseEndDate(end) * -1;
} else {
start = _.parseInt(start.replace(/\D/g, '')) * -1;
end = _.parseInt(end.replace(/\D/g, '')) * -1;
}
}
else {
if (start.match(/b\.c\./gi) ) {
start = _.parseInt(start.replace(/\D/g, '')) * -1;
} else {
start = _.parseInt(start.replace(/\D/g, ''));
}
end = _.parseInt(end.replace(/\D/g, ''));
if (end < start) {
let [startString,endString] = [start,end].map(_.method('toString'));
let lenDiff = startString.length - endString.length;
end = _.parseInt(_.take(startString,lenDiff).concat(endString.split('')).join(''));
}
}
}
else if (date.match(/centur/gi)) {
if (date.match(/b\.c\./gi)) {
start = parseStartDate(date) * -1;
end = start + 99;
} else {
start = parseStartDate(date) - 100;
end = start + 99;
}
}
else {
end = start = _.parseInt(date.replace(/\D/g, ''));
}
return {start: start, end: end};
}
// this one isn't great either...
function dateStringFromRange(range) {
if (_.isNull(range) || _.isUndefined(range)) {
return range;
}
let [start, end] = [range.start, range.end];
let dateString;
if (start == end) {
if (start < 0) {
dateString = `${start * -1} B.C.`;
} else {
dateString = `A.D. ${start}`;
}
}
else {
if (start < 0 && end < 0) {
dateString = `${start * -1}–${end * -1} B.C.`;
}
else if (start < 0) {
dateString = `${start * -1} B.C.–A.D. ${end}`;
}
else {
dateString = `A.D. ${start}–${end}`;
}
}
return dateString;
}
// const TARGET_ERA = _.sample(['1000+B.C.-A.D.+1','2000-1000+B.C.']);
// const MATERIAL = _.sample(["Ceramics"]);
const PER_PAGE = 12;
const MAX_PAGE = 550; // rough limiter for all objects on display, with images.
// const ENDPOINT = `https://www.mfa.org/collections/search?f[0]=field_onview%3A1&f[1]=field_checkbox%3A1&page=${_.random(MAX_PAGE)}`;
const BASE_URL = `https://collections.mfa.org`;
const ENDPOINT = `${BASE_URL}/search/Objects/onview%3Atrue%3BimageExistence%3Atrue/*/images?page=${_.random(MAX_PAGE)}`;
// If/when we want to be more specific about page limit, parse it from the Cheerio model of search result page
async function getMaxPageNumber($) {
return _.parseInt($('span.maxPages').text().replace(/\D/g,''));
}
function parsePieceSummary(gridItem) {
const $ = cheerio.load(gridItem);
let pieceObj = {};
pieceObj.artist = $(gridItem).find('.primaryMaker').text().trim().split("\n")[0];
pieceObj.img = BASE_URL + $(gridItem).find('img').get(0).attribs.src;
pieceObj.href = BASE_URL + $(gridItem).find('a').get(0).attribs.href;
pieceObj.date = $(gridItem).find('.displayDate').text().trim().split("\n")[0].replace(/Date:\s*/,'');
pieceObj.dateRange = parseDate(pieceObj.date);
pieceObj.dateString = dateStringFromRange(pieceObj.dateRange);
// No title or no date? No return.
if (pieceObj.title && pieceObj.date) {
return null;
}
return pieceObj;
}
function filterByText(context, el, regex) {
return context(el).text().match(regex);
}
async function getPieceDetails(piece) {
let pieceDetails = {};
let {statusCode, data: res, headers } = await curly.get(piece.href, {
httpHeader: [ 'User-Agent: Etruscan Ceramic / Twitter bot / ART PROJECT'],
caInfo: certFilePath,
});
let $ = cheerio.load(res);
pieceDetails.title = $('.titleField > h2').text().trim().split("\n")[0];
pieceDetails.date = $('.displayDateField').text().trim().split("\n")[0];
pieceDetails.culture = $('.cultureField').text().trim().split("\n")[0];
// if (pieceDetails.culture.match(/\d\d+/)) { pieceDetails.culture=''; }
pieceDetails.medium = $('.mediumField > .detailFieldValue').text().trim().split("\n")[0];
pieceDetails.gallery = $('.onviewField > .locationLink').text().trim().split("\n")[0];
return pieceDetails;
}
async function saveBinary(uri, destination) {
let {statusCode, data: res, headers } = await curly.get(uri, {
httpHeader: [ 'User-Agent: Etruscan Ceramic / Twitter bot / ART PROJECT'],
caInfo: certFilePath,
curlyResponseBodyParser: false,
});
let written = await fs.writeFileAsync(destination, res, 'binary');
return res;
}
async function makeToot(status, mediaPath=null, altText="", client) {
let uploadParams = {}, uploadResponse, mediaIdStr, postParams={}, tootResponse;
if (!_.isEmpty(mediaPath)) {
uploadParams.file = fs.createReadStream(mediaPath);
if (!_.isEmpty(altText)) { uploadParams.description = altText; }
uploadResponse = await client.post('media', uploadParams);
}
mediaIdStr = uploadResponse ? uploadResponse.data.id : null;
postParams.status = status;
postParams.media_ids = [mediaIdStr];
return tootResponse = client.post('statuses', postParams);
}
async function main(endpoint) {
let { statusCode, data: res, headers } = await curly.get(endpoint, {
httpHeader: [ 'User-Agent: Etruscan Ceramic / Twitter bot / ART PROJECT'],
caInfo: certFilePath,
});
let $ = cheerio.load(res);
let objects = $('.result.item');
let pieces = _.map(objects, parsePieceSummary);
// Filter pieces here? For range of dates/specific wordcount/etc?
let piece = _.sample(pieces);
let pieceDetails = await getPieceDetails(piece);
piece = Object.assign(piece, pieceDetails); // {title, date, href, img, dateRange, dateString, culture, medium, gallery}
// console.dir(piece);
let imgBody = await saveBinary(piece.img, TMP_IN)
// Strings for image generation:
let thisYear = new Date().getFullYear();
let comaLength;
if (piece.dateRange.start == piece.dateRange.end) {
comaLength = `${thisYear - piece.dateRange.start} years`;
} else {
comaLength = `${thisYear - piece.dateRange.end}-${thisYear - piece.dateRange.start} years`;
}
let pieceLabel = `${piece.title} ${piece.dateString} ${piece.culture} ${piece.medium}`;
let reply = `you're in luck because ${pieceLabel} can be found in the Boston Museum of Fine Arts' ${piece.gallery}`;
// Now let's make that image:
let magickBin = process.env.IMAGEMAGICK_BINARY || 'convert';
let magickArgs = `./assets/base.png -gravity Northwest -pointsize 64 -annotate +25+55 "${comaLength}" -pointsize 40 -size 925x200 -background none caption:"${pieceLabel}" -trim -geometry +142+218 -composite -pointsize 40 -size 670x200 -background none caption:"${pieceLabel}" -trim -geometry +5+972 -composite -draw "image over 0,1110 685,600 '${TMP_IN}'" -pointsize 24 -size 760x140 caption:"${reply}" -trim -geometry +316+835 -composite ${TMP_OUT}`;
let imCall = `"${magickBin}" ${magickArgs}`;
let callRes = await exec(imCall);
// console.log(imCall);
let status = pieceLabel;
let altText = reply;
let mastoRes = await makeToot(status, TMP_OUT, altText, M);
return T.makeTweet(status, TMP_OUT, altText);
}
// then run
main(ENDPOINT).then(res=>console.log(`ARTCOMA twote: ${res.data.id_str}`)).catch(console.error);