-
Notifications
You must be signed in to change notification settings - Fork 0
/
server.js
296 lines (252 loc) · 10.9 KB
/
server.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
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
// Get env variables up and running
require("dotenv").config();
// The built-in fetch seems to be having problems
// https://blog.logrocket.com/fetch-api-node-js/
// Temporarily using node-fetch@2.6.1
// Example:
// https://www.raymondcamden.com/2022/02/04/an-early-look-at-netlify-scheduled-functions
const fetch = require('node-fetch');
// Allow for scheduling tweets
const schedule = require("node-schedule");
// Prepare for automatic Netlify builds at the same time as tweeting
const rebuild_url = process.env.NETLIFY_REBUILD_URL
// Allow for image editing
const Jimp = require("jimp");
// Initiate Twit
const Twit = require("twit");
const T = new Twit({
consumer_key: process.env.TWITTER_API_KEY,
consumer_secret: process.env.TWITTER_API_SECRET_KEY,
access_token: process.env.TWITTER_ACCESS_TOKEN,
access_token_secret: process.env.TWITTER_ACCESS_SECRET
});
// Get Airtable going
const base = require("airtable").base(process.env.AIRTABLE_BASE_ID);
const table = "Main";
// Functions
// Function for tweeting any new ephemera
// Has parameter to control how many bits of ephemera at a time,
// with the default value set 6
function tweetLatestEphemera(itemLimit = 6) {
console.log("🎬 Checking Airtable for new ephemera")
// Prepare array
const notYetTweetedEphemera = []
base(table).select({
view: "Grid",
filterByFormula: "({hidden}= '')", // Only show items that are not hidden
sort: [{ field: "date", direction: "desc" }], // Search by newest to oldest to ensure newest items are picked up (reversed later)
}).eachPage(function page(records, fetchNextPage) {
// This function (`page`) will get called for each page of records.
records.forEach(function (record) {
// If this record does not have its 'tweeted' checkbox checked...
if (!record.fields.tweeted) {
console.log(`✨ New ephemera available for tweeting: ${record.fields.name}`)
// Prepare for updating Airtable
// Airtable's API wants these records formatted in a very particular way
const recordObject = {}
recordObject["id"] = record.id
recordObject["fields"] = record.fields
// Push to array used for both tweeting and later updating so it isn't tweeted again
notYetTweetedEphemera.push(recordObject)
}
});
fetchNextPage();
}, function done(err) {
if (err) throw err;
console.log("🔵 Finished checking Airtable")
// If there are new items...
if (notYetTweetedEphemera.length) {
// Reverse array so oldest items get tweeted first
oldestToNewest = notYetTweetedEphemera.reverse()
console.log(`🔪 Only tweeting out the oldest ${itemLimit} item(s) as per your request`)
// Trim array to limit
trimmedListTwoTweet = oldestToNewest.slice(0, itemLimit);
// Go through the list of items (with a gap of 20 seconds),
// ...kicking off a tweet for each
for (let i = 0; i < trimmedListTwoTweet.length; i++) {
(function (i) {
setTimeout(function () {
const recordObject = trimmedListTwoTweet[i]
// Kick off the tweet
kickOffTweet(recordObject, false)
console.log("🐦 Now initiating tweet for:", recordObject.fields.name)
}, 20000 * i);
})(i);
// Flick the 'tweeted' switch on to true now that tweet(s) have been kicked off
// Note that this assumes kickOffTweet() will succeed
trimmedListTwoTweet.map(i => {
i.fields.tweeted = true
})
// Update Airtable base records accordingly
base('Main').update(trimmedListTwoTweet, function (err, records) {
if (err) throw err;
});
}
} else {
console.log("✅ Now new items to tweet")
// Exit from Node with success code
// https://nodejs.dev/en/learn/how-to-exit-from-a-nodejs-program
// process.exit(0)
}
});
}
// Function for tweeting a random Throwback Thursday ephemera item
function tweetThursdayRandomEphemera() {
console.log("🎬 Scouring Airtable for a random piece of ephemera")
// Prepare array for all records
const allRecords = []
base(table).select({
view: "Grid",
filterByFormula: "({hidden}= '')", // Only show items that are not hidden
sort: [{ field: "date", direction: "desc" }], // Sort by newest-first just for debugging (has no effect on random)
}).eachPage(function page(records, fetchNextPage) {
// This function (`page`) will get called for each page of records.
records.forEach(function (record) {
// Push each record to the array
allRecords.push(record);
});
fetchNextPage();
}, function done(err) {
if (err) throw err;
console.log("✅ Finished checking Airtable")
// Select a random record for tweeting
const record = allRecords[Math.floor(Math.random() * allRecords.length)];
// Kick off the tweet
kickOffTweet(record, true)
});
}
async function deployNetlify() {
await fetch(rebuild_url, { method: 'POST' })
return {
statusCode: 200,
};
}
function kickOffTweet(record, isThrowback) {
// Deploy to Netlify via build hook
console.log("🌐 Now starting Netlify deploy of https://ephemera.fyi for:", record.fields.name)
deployNetlify()
// Then prepare tweet
// Prepare record text with throwback text true
const recordText = prepareText(record, isThrowback)
// Prepare image
prepareImage(record)
.then((value) => {
// Trim off extraneous bits that Jimp adds to base64
const recordImage = value.substring(23, value.length)
// Tweet it
tweetIt(recordText, recordImage)
})
}
// Function for sending out tweet
function tweetIt(tweetText, tweetImage) {
// Upload image
T.post('media/upload', { media_data: tweetImage }, uploaded);
// Once image is uploaded on Twitter
function uploaded(err, data, response) {
if (err) throw err;
// Prepare tweet content
const tweetContent = {
"status": tweetText,
// Put Twitter's image ID into an array
"media_ids": new Array(data.media_id_string),
// TODO: get alt_text to work
// Mention @get_altText on tweets to debug
// "media_id": `${data.media_id_string}`,
// "alt_text": { "text": "A scanned piece of physical ephemera" }
}
// Tweet it!
T.post('statuses/update', tweetContent, onTweeted);
}
// After the tweet has been sent...
function onTweeted(err, reply) {
if (err !== undefined) {
console.log(err)
// Exit from Node with error
// process.exit(1);
} else {
console.log('🟢 Successfully tweeted: ' + reply.text)
// Exit from Node with success code
// https://nodejs.dev/en/learn/how-to-exit-from-a-nodejs-program
// process.exit(0);
}
}
}
// Function for preparing tweet text
function prepareText(record, isThrowback) {
const recordName = record.fields.name;
const recordDate = record.fields.date;
// Format date so it can be programmatically changed a few lines below
// Pass through the year, month, and day
const recordDateStructured = new Date(recordDate.substring(0, 4), (recordDate.substring(5, 7) - 1), recordDate.substring(8, 10));
// Use this to create a readable date format
const recordDateHuman = recordDateStructured.toLocaleString("default", { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' })
const recordLocation = record.fields.location;
const recordCountry = record.fields.country;
const recordLocationAndCountry = `${recordLocation}, ${recordCountry}`
const recordTags = record.fields.tags;
// Remove dashes/hyphens from each tag array item
const recordTagsDashed = recordTags.map(i => i.replace(/-/g, ""));
// Add a hashtag to the start of each tag array item
const recordTagsHashed = recordTagsDashed.map(i => "#" + i);
// Format this array into one comma-separated string
const recordTagsFormatted = recordTagsHashed.join(", ");
const recordString =
`${recordName}. ${recordLocationAndCountry}. ${recordDateHuman}. Tagged with ${recordTagsFormatted}.`
const throwbackString =
`Throwback Thursday:\n\n${recordString}`
if (isThrowback) {
return throwbackString
} else {
return recordString
}
}
// Function for preparing tweet image
function prepareImage(record) {
const imageDirectory = "https://res.cloudinary.com/ephemera/image/upload/q_auto,f_auto,w_2048/"
const imageSlug = record.fields.imageSlug
const imageUrl = imageDirectory + imageSlug
console.log("🎟 Loading image from URL: ", imageUrl)
return new Promise((resolve, reject) => {
// Create the white frame
// Set to 2048x1024 to match Twitter's preferred 1024x512 ratio
new Jimp(2048, 1024, '#FFFFFF', (err, frame) => {
if (err) throw err;
// Then read the ephemera image
Jimp.read(imageUrl, (err, image) => {
if (err) throw err;
// Scale image to have horizontal and vertical padding
image.scaleToFit(2048 * 0.8, 1024 * 0.65);
// Calculate coordinates to center image
const x = Math.floor((frame.bitmap.width - image.bitmap.width) / 2);
const y = Math.floor((frame.bitmap.height - image.bitmap.height) / 2);
// Composite image onto the frame
frame.composite(image, x, y)
// Write the final image to file for debugging
.write("tweet-img.jpg")
// Convert image file to base64 for tweeting
.getBase64(Jimp.MIME_JPEG, (err, base64ImageString) => {
// Resolve it
resolve(base64ImageString)
})
});
});
})
}
// Call main functions
console.log("Starting...")
// Instant functions for debugging only
// tweetLatestEphemera(1)
// tweetThursdayRandomEphemera()
// Throwback Thursday
// Tweet every Thursday morning at 8AM GMT (6pm AEST, 7PM AEDT, 3AM EST, 12AM PST)
// schedule.scheduleJob("0 8 * * THU", function () {
// tweetThursdayRandomEphemera()
// })
// Latest ephemera
// Checks for and tweets new Airtable records once a day
// 8PM GMT (6AM AEST, 7AM AEDT, 3PM EST, 12PM PST)
// Post a maximum of one ephemera item
schedule.scheduleJob("0 20 * * *", function () {
console.log("Kicking off process...")
tweetLatestEphemera(1)
});