From d2988f06cea584007cac66ffc4ea54a575e45037 Mon Sep 17 00:00:00 2001 From: Matt Isenhower Date: Wed, 20 Sep 2017 12:04:21 -0700 Subject: [PATCH] Enable automatic Twitter posts for map schedules --- .env.example | 6 ++ .gitignore | 1 + package.json | 1 + src/updater/cron.js | 6 +- src/updater/twitter.js | 134 ++++++++++++++++++++++++++++++++++++++ yarn.lock | 142 +++++++++++++++++++++++++++++++++++++++-- 6 files changed, 281 insertions(+), 9 deletions(-) create mode 100644 src/updater/twitter.js diff --git a/.env.example b/.env.example index a55b15d..8b0db19 100644 --- a/.env.example +++ b/.env.example @@ -10,3 +10,9 @@ GOOGLE_ANALYTICS_ID= # (Optional) Sentry error reporting (https://sentry.io) SENTRY_DSN= + +# (Optional) Twitter API parameters +TWITTER_CONSUMER_KEY= +TWITTER_CONSUMER_SECRET= +TWITTER_ACCESS_TOKEN_KEY= +TWITTER_ACCESS_TOKEN_SECRET= diff --git a/.gitignore b/.gitignore index 4a39888..b46989a 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ node_modules public/* manual-festivals.json manual-salmonrun.json +twitter-lastTweetTimes.json diff --git a/package.json b/package.json index 864566f..b5a426a 100644 --- a/package.json +++ b/package.json @@ -39,6 +39,7 @@ "raven": "^2.1.1", "sass-loader": "^6.0.6", "style-loader": "^0.18.2", + "twitter": "^1.7.1", "v-click-outside": "^0.0.8", "vue": "^2.4.2", "vue-loader": "^13.0.2", diff --git a/src/updater/cron.js b/src/updater/cron.js index 10ff2cd..31d0575 100644 --- a/src/updater/cron.js +++ b/src/updater/cron.js @@ -1,10 +1,12 @@ require('./bootstrap'); const CronJob = require('cron').CronJob; const update = require('./update'); +const twitter = require('./twitter'); console.info('Starting periodic tasks...'); // Run every hour at 30sec after the hour -new CronJob('30 0 * * * *', () => { - update.updateAll(); +new CronJob('30 0 * * * *', async () => { + await update.updateAll(); + await twitter.maybePostTweets(); }, null, true); diff --git a/src/updater/twitter.js b/src/updater/twitter.js new file mode 100644 index 0000000..76e8ef5 --- /dev/null +++ b/src/updater/twitter.js @@ -0,0 +1,134 @@ +require('./bootstrap'); +const screenshots = require('./screenshots'); +const Twitter = require('twitter'); +const path = require('path'); +const fs = require('fs'); + +const dataPath = path.resolve('public/data'); +const lastTweetTimesPath = path.resolve('twitter-lastTweetTimes.json'); + +// Twitter API parameters +const consumer_key = process.env.TWITTER_CONSUMER_KEY; +const consumer_secret = process.env.TWITTER_CONSUMER_SECRET; +const access_token_key = process.env.TWITTER_ACCESS_TOKEN_KEY; +const access_token_secret = process.env.TWITTER_ACCESS_TOKEN_SECRET; + +// Twitter API client +const client = new Twitter({ consumer_key, consumer_secret, access_token_key, access_token_secret }); + +/** + * Utilities + */ + +function canTweet() { + return consumer_key && consumer_secret && access_token_key && access_token_secret; +} + +function getTopOfCurrentHour() { + let date = new Date; + date.setUTCMinutes(0); + date.setUTCSeconds(0); + return Math.floor(date.getTime() / 1000); +} + +function getLastTweetTimes() { + if (fs.existsSync(lastTweetTimesPath)) + return JSON.parse(fs.readFileSync(lastTweetTimesPath)); + return {}; +} + +function getLastTweetTime(key) { + return getLastTweetTimes()[key] || 0; +} + +function shouldTweet(key, time) { + return getLastTweetTime(key) < time; +} + +function updateLastTweetTime(key, time) { + let lastTweetTimes = getLastTweetTimes(); + lastTweetTimes[key] = time; + + fs.writeFileSync(lastTweetTimesPath, JSON.stringify(lastTweetTimes)); +} + +/** + * Twitter posts + */ + +async function maybePostTweets() { + if (!canTweet()) { + console.warn('Twitter API parameters not specified'); + return; + } + + await maybePostScheduleTweet(); +} + +/** + * Main schedule tweets + */ + +async function maybePostScheduleTweet() { + const key = 'schedule'; + + // What time are we posting the schedule for? + let time = getTopOfCurrentHour(); + + // Have we already posted for this time? + if (!shouldTweet(key, time)) { + console.info('Twitter: Schedule: Already posted for this hour'); + return; + } + + // Get the schedules + let schedulesPath = `${dataPath}/schedules.json`; + if (!fs.existsSync(schedulesPath)) { + console.warn('Twitter: Schedule: schedules.json does not exist'); + return; + } + let schedules = JSON.parse(fs.readFileSync(schedulesPath)); + + // Do we have a schedule for the specified time? + if (!schedules.regular.find(s => s.start_time == time)) { + console.info('Twitter: Schedule: No schedule for this hour') + return; + } + + // Everything looks good, let's post a tweet + await postScheduleTweet(time); + + // Update the last tweet time + updateLastTweetTime(key, time); +} + +function postScheduleTweet(startTime) { + return new Promise(async (resolve, reject) => { + // Generate the image + let imageData = await screenshots.captureScheduleScreenshot(startTime); + + // Upload the image + client.post('media/upload', { media: imageData }, (error, media, response) => { + if (error) + return reject(error); + + let status = { + status: 'Current Splatoon 2 map rotation, via https://splatoon2.ink #splatoon2', + media_ids: media.media_id_string, + }; + + client.post('statuses/update', status, (error, tweet, response) => { + if (error) + return reject(error); + + resolve(tweet); + }); + }); + }); +} + +module.exports = { + maybePostTweets, +} + +require('make-runnable/custom')({ printOutputFrame: false }); diff --git a/yarn.lock b/yarn.lock index cfd0bd4..bd2d5d4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -44,7 +44,7 @@ ajv@^4.11.2, ajv@^4.9.1: co "^4.6.0" json-stable-stringify "^1.0.1" -ajv@^5.0.0, ajv@^5.1.5: +ajv@^5.0.0, ajv@^5.1.0, ajv@^5.1.5: version "5.2.2" resolved "https://registry.yarnpkg.com/ajv/-/ajv-5.2.2.tgz#47c68d69e86f5d953103b0074a9430dc63da5e39" dependencies: @@ -237,7 +237,11 @@ aws-sign2@~0.6.0: version "0.6.0" resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.6.0.tgz#14342dd38dbcc94d0e5b87d763cd63612c0e794f" -aws4@^1.2.1: +aws-sign2@~0.7.0: + version "0.7.0" + resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8" + +aws4@^1.2.1, aws4@^1.6.0: version "1.6.0" resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.6.0.tgz#83ef5ca860b2b32e4a0deedee8c771b9db57471e" @@ -881,6 +885,18 @@ boom@2.x.x: dependencies: hoek "2.x.x" +boom@4.x.x: + version "4.3.1" + resolved "https://registry.yarnpkg.com/boom/-/boom-4.3.1.tgz#4f8a3005cb4a7e3889f749030fd25b96e01d2e31" + dependencies: + hoek "4.x.x" + +boom@5.x.x: + version "5.2.0" + resolved "https://registry.yarnpkg.com/boom/-/boom-5.2.0.tgz#5dd9da6ee3a5f302077436290cb717d3f4a54e02" + dependencies: + hoek "4.x.x" + brace-expansion@^1.1.7: version "1.1.8" resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.8.tgz#c07b211c7c952ec1f8efd51a77ef0d1d3990a292" @@ -1386,6 +1402,12 @@ cryptiles@2.x.x: dependencies: boom "2.x.x" +cryptiles@3.x.x: + version "3.1.2" + resolved "https://registry.yarnpkg.com/cryptiles/-/cryptiles-3.1.2.tgz#a89fbb220f5ce25ec56e8c4aa8a4fd7b5b0d29fe" + dependencies: + boom "5.x.x" + crypto-browserify@^3.11.0: version "3.11.1" resolved "https://registry.yarnpkg.com/crypto-browserify/-/crypto-browserify-3.11.1.tgz#948945efc6757a400d6e5e5af47194d10064279f" @@ -1555,6 +1577,10 @@ deep-equal@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-1.0.1.tgz#f5d260292b660e084eff4cdbc9f08ad3247448b5" +deep-extend@^0.5.0: + version "0.5.0" + resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.5.0.tgz#6ef4a09b05f98b0e358d6d93d4ca3caec6672803" + deep-extend@~0.4.0: version "0.4.2" resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.4.2.tgz#48b699c27e334bf89f10892be432f6e4c7d34a7f" @@ -1955,7 +1981,7 @@ express@^4.13.3: utils-merge "1.0.0" vary "~1.1.1" -extend@~3.0.0: +extend@~3.0.0, extend@~3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.1.tgz#a755ea7bc1adfcc5a31ce7e762dbaadc5e636444" @@ -2108,6 +2134,14 @@ form-data@~2.1.1: combined-stream "^1.0.5" mime-types "^2.1.12" +form-data@~2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.1.tgz#6fb94fbd71885306d73d15cc497fe4cc4ecd44bf" + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.5" + mime-types "^2.1.12" + forwarded@~0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.1.0.tgz#19ef9874c4ae1c297bcf078fde63a09b66a84363" @@ -2270,6 +2304,10 @@ har-schema@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-1.0.5.tgz#d263135f43307c02c602afc8fe95970c0151369e" +har-schema@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-2.0.0.tgz#a94c2224ebcac04782a0d9035521f24735b7ec92" + har-validator@~4.2.1: version "4.2.1" resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-4.2.1.tgz#33481d0f1bbff600dd203d75812a6a5fba002e2a" @@ -2277,6 +2315,13 @@ har-validator@~4.2.1: ajv "^4.9.1" har-schema "^1.0.5" +har-validator@~5.0.3: + version "5.0.3" + resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-5.0.3.tgz#ba402c266194f15956ef15e0fcf242993f6a7dfd" + dependencies: + ajv "^5.1.0" + har-schema "^2.0.0" + has-ansi@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/has-ansi/-/has-ansi-2.0.0.tgz#34f5049ce1ecdf2b0649af3ef24e45ed35416d91" @@ -2327,6 +2372,15 @@ hawk@~3.1.3: hoek "2.x.x" sntp "1.x.x" +hawk@~6.0.2: + version "6.0.2" + resolved "https://registry.yarnpkg.com/hawk/-/hawk-6.0.2.tgz#af4d914eb065f9b5ce4d9d11c1cb2126eecc3038" + dependencies: + boom "4.x.x" + cryptiles "3.x.x" + hoek "4.x.x" + sntp "2.x.x" + he@1.1.x, he@^1.1.0: version "1.1.1" resolved "https://registry.yarnpkg.com/he/-/he-1.1.1.tgz#93410fd21b009735151f8868c2f271f3427e23fd" @@ -2343,6 +2397,10 @@ hoek@2.x.x: version "2.16.3" resolved "https://registry.yarnpkg.com/hoek/-/hoek-2.16.3.tgz#20bb7403d3cea398e91dc4710a8ff1b8274a25ed" +hoek@4.x.x: + version "4.2.0" + resolved "https://registry.yarnpkg.com/hoek/-/hoek-4.2.0.tgz#72d9d0754f7fe25ca2d01ad8f8f9a9449a89526d" + home-or-tmp@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/home-or-tmp/-/home-or-tmp-2.0.0.tgz#e36c3f2d2cae7d746a857e38d18d5f32a7882db8" @@ -2451,6 +2509,14 @@ http-signature@~1.1.0: jsprim "^1.2.2" sshpk "^1.7.0" +http-signature@~1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.2.0.tgz#9aecd925114772f3d95b65a60abb8f7c18fbace1" + dependencies: + assert-plus "^1.0.0" + jsprim "^1.2.2" + sshpk "^1.7.0" + https-browserify@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/https-browserify/-/https-browserify-0.0.1.tgz#3f91365cabe60b77ed0ebba24b454e3e09d95a82" @@ -3062,12 +3128,22 @@ miller-rabin@^4.0.0: version "1.29.0" resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.29.0.tgz#48d26d235589651704ac5916ca06001914266878" +mime-db@~1.30.0: + version "1.30.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.30.0.tgz#74c643da2dd9d6a45399963465b26d5ca7d71f01" + mime-types@^2.1.12, mime-types@~2.1.11, mime-types@~2.1.15, mime-types@~2.1.7: version "2.1.16" resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.16.tgz#2b858a52e5ecd516db897ac2be87487830698e23" dependencies: mime-db "~1.29.0" +mime-types@~2.1.17: + version "2.1.17" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.17.tgz#09d7a393f03e995a79f8af857b70a9e0ab16557a" + dependencies: + mime-db "~1.30.0" + mime@1.3.4: version "1.3.4" resolved "https://registry.yarnpkg.com/mime/-/mime-1.3.4.tgz#115f9e3b6b3daf2959983cb38f149a2d40eb5d53" @@ -3339,7 +3415,7 @@ number-is-nan@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/number-is-nan/-/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d" -oauth-sign@~0.8.1: +oauth-sign@~0.8.1, oauth-sign@~0.8.2: version "0.8.2" resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.8.2.tgz#46a6ab7f0aead8deae9ec0565780b7d4efeb9d43" @@ -3547,6 +3623,10 @@ performance-now@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-0.2.0.tgz#33ef30c5c77d4ea21c5a53869d91b56d8f2555e5" +performance-now@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b" + pify@^2.0.0, pify@^2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c" @@ -3975,6 +4055,10 @@ qs@~6.4.0: version "6.4.0" resolved "https://registry.yarnpkg.com/qs/-/qs-6.4.0.tgz#13e26d28ad6b0ffaa91312cd3bf708ed351e7233" +qs@~6.5.1: + version "6.5.1" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.1.tgz#349cdf6eef89ec45c12d7d5eb3fc0c870343a6d8" + query-string@^4.1.0: version "4.3.4" resolved "https://registry.yarnpkg.com/query-string/-/query-string-4.3.4.tgz#bbb693b9ca915c232515b228b1a02b609043dbeb" @@ -4233,6 +4317,33 @@ request@2, request@^2.79.0, request@^2.81.0: tunnel-agent "^0.6.0" uuid "^3.0.0" +request@^2.72.0: + version "2.82.0" + resolved "https://registry.yarnpkg.com/request/-/request-2.82.0.tgz#2ba8a92cd7ac45660ea2b10a53ae67cd247516ea" + dependencies: + aws-sign2 "~0.7.0" + aws4 "^1.6.0" + caseless "~0.12.0" + combined-stream "~1.0.5" + extend "~3.0.1" + forever-agent "~0.6.1" + form-data "~2.3.1" + har-validator "~5.0.3" + hawk "~6.0.2" + http-signature "~1.2.0" + is-typedarray "~1.0.0" + isstream "~0.1.2" + json-stringify-safe "~5.0.1" + mime-types "~2.1.17" + oauth-sign "~0.8.2" + performance-now "^2.1.0" + qs "~6.5.1" + safe-buffer "^5.1.1" + stringstream "~0.0.5" + tough-cookie "~2.3.2" + tunnel-agent "^0.6.0" + uuid "^3.1.0" + require-directory@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" @@ -4291,7 +4402,7 @@ ripemd160@^2.0.0, ripemd160@^2.0.1: hash-base "^2.0.0" inherits "^2.0.1" -safe-buffer@5.1.1, safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@~5.1.0, safe-buffer@~5.1.1: +safe-buffer@5.1.1, safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@~5.1.0, safe-buffer@~5.1.1: version "5.1.1" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.1.tgz#893312af69b2123def71f57889001671eeb2c853" @@ -4447,6 +4558,12 @@ sntp@1.x.x: dependencies: hoek "2.x.x" +sntp@2.x.x: + version "2.0.2" + resolved "https://registry.yarnpkg.com/sntp/-/sntp-2.0.2.tgz#5064110f0af85f7cfdb7d6b67a40028ce52b4b2b" + dependencies: + hoek "4.x.x" + sockjs-client@1.1.4: version "1.1.4" resolved "https://registry.yarnpkg.com/sockjs-client/-/sockjs-client-1.1.4.tgz#5babe386b775e4cf14e7520911452654016c8b12" @@ -4629,7 +4746,7 @@ string_decoder@~1.0.3: dependencies: safe-buffer "~5.1.0" -stringstream@~0.0.4: +stringstream@~0.0.4, stringstream@~0.0.5: version "0.0.5" resolved "https://registry.yarnpkg.com/stringstream/-/stringstream-0.0.5.tgz#4e484cd4de5a0bbbee18e46307710a8a81621878" @@ -4764,7 +4881,7 @@ toposort@^1.0.0: version "1.0.3" resolved "https://registry.yarnpkg.com/toposort/-/toposort-1.0.3.tgz#f02cd8a74bd8be2fc0e98611c3bacb95a171869c" -tough-cookie@~2.3.0: +tough-cookie@~2.3.0, tough-cookie@~2.3.2: version "2.3.2" resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.3.2.tgz#f081f76e4c85720e6c37a5faced737150d84072a" dependencies: @@ -4792,6 +4909,13 @@ tweetnacl@^0.14.3, tweetnacl@~0.14.0: version "0.14.5" resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64" +twitter@^1.7.1: + version "1.7.1" + resolved "https://registry.yarnpkg.com/twitter/-/twitter-1.7.1.tgz#0762378f1dc1c050e48f666aca904e24b1a962f4" + dependencies: + deep-extend "^0.5.0" + request "^2.72.0" + type-is@~1.6.15: version "1.6.15" resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.15.tgz#cab10fb4909e441c82842eafe1ad646c81804410" @@ -4916,6 +5040,10 @@ uuid@^2.0.2: version "2.0.3" resolved "https://registry.yarnpkg.com/uuid/-/uuid-2.0.3.tgz#67e2e863797215530dff318e5bf9dcebfd47b21a" +uuid@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.1.0.tgz#3dd3d3e790abc24d7b0d3a034ffababe28ebbc04" + v-click-outside@^0.0.8: version "0.0.8" resolved "https://registry.yarnpkg.com/v-click-outside/-/v-click-outside-0.0.8.tgz#1f3033b8c4ace431c3233a4080127fc088d52093"