Soon
diff --git a/src/updater/twitter.js b/src/updater/twitter.js
index 8d48169..a919a0f 100644
--- a/src/updater/twitter.js
+++ b/src/updater/twitter.js
@@ -1,82 +1,16 @@
require('./bootstrap');
-const screenshots = require('./screenshots');
-const Twitter = require('twitter');
-const path = require('path');
-const fs = require('fs');
+const { canTweet } = require('./twitter/client');
+const ScheduleTweet = require('./twitter/ScheduleTweet');
+const GearTweet = require('./twitter/GearTweet');
+const SalmonRunTweet = require('./twitter/SalmonRunTweet');
+const NewWeaponTweet = require('./twitter/NewWeaponTweet');
-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));
-}
-
-function postMediaTweet(imageData, statusText) {
- return new Promise((resolve, reject) => {
- // Upload the image
- client.post('media/upload', { media: imageData }, (error, media, response) => {
- if (error)
- return reject(error);
-
- let status = {
- status: statusText,
- media_ids: media.media_id_string,
- };
-
- client.post('statuses/update', status, (error, tweet, response) => {
- if (error)
- return reject(error);
-
- resolve(tweet);
- });
- });
- });
-}
-
-/**
- * Twitter posts
- */
+let tweets = [
+ new ScheduleTweet,
+ new GearTweet,
+ new SalmonRunTweet,
+ new NewWeaponTweet,
+];
async function maybePostTweets() {
if (!canTweet()) {
@@ -84,301 +18,18 @@ async function maybePostTweets() {
return;
}
- await maybePostScheduleTweet();
- await maybePostGearTweet();
- await maybePostSalmonRunTweet();
- await maybePostNewWeaponTweet();
+ for (let tweet of tweets)
+ await tweet.maybePostTweet();
}
async function testScreenshots() {
- await testScheduleScreenshot();
- await testGearScreenshot();
- await testSalmonRunScreenshot();
- await testNewWeaponScreenshot();
-}
-
-/**
- * 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);
- console.info('Twitter: Schedule: Posted latest map schedule');
-
- // Update the last tweet time
- updateLastTweetTime(key, time);
-}
-
-async function testScheduleScreenshot() {
- let schedulesPath = `${dataPath}/schedules.json`;
- let schedules = JSON.parse(fs.readFileSync(schedulesPath));
- let imageData = await screenshots.captureScheduleScreenshot(schedules.regular[0].start_time);
- fs.writeFileSync('test-screenshot-schedules.png', imageData);
- console.info('Saved schedule screenshot');
-}
-
-async function postScheduleTweet(startTime) {
- // Generate the image
- let imageData = await screenshots.captureScheduleScreenshot(startTime);
-
- // Post the tweet
- return await postMediaTweet(imageData, 'Current Splatoon 2 map rotation');
-}
-
-/**
- * Gear tweets
- */
-
-function getMerchandises() {
- let merchandisesPath = `${dataPath}/merchandises.json`;
- if (!fs.existsSync(merchandisesPath)) {
- console.warn('Twitter: Gear: merchandises.json does not exist');
- return [];
- }
- return JSON.parse(fs.readFileSync(merchandisesPath)).merchandises;
-}
-
-async function maybePostGearTweet() {
- const key = 'gear';
-
- // What time are we posting the schedule for?
- let time = getTopOfCurrentHour();
-
- // Make sure we have merchandises
- let merchandises = getMerchandises();
- if (!merchandises.length) {
- console.info('Twitter: Gear: No merchandises');
- return;
- }
-
- // This one is a little different: we only have end_times for merchandise items, so we
- // need to track the latest end_time we've posted
- let endTimes = merchandises.map(m => m.end_time);
- let lastEndTime = Math.max(...endTimes);
-
- // Have we already posted for this time?
- if (!shouldTweet(key, lastEndTime)) {
- console.info('Twitter: Gear: Already posted for this hour');
- return;
- }
-
- // Everything looks good, let's post a tweet
- await postGearTweet(time, lastEndTime);
- console.info('Twitter: Gear: Posted latest SplatNet gear');
-
- // Update the last tweet time
- updateLastTweetTime(key, lastEndTime);
-}
-
-async function testGearScreenshot() {
- let merchandises = getMerchandises();
- let imageData = await screenshots.captureGearScreenshot(getTopOfCurrentHour(), merchandises[merchandises.length - 1].end_time);
- fs.writeFileSync('test-screenshot-gear.png', imageData);
- console.info('Saved gear screenshot');
-}
-
-async function postGearTweet(startTime, endTime) {
- // Generate the image
- let imageData = await screenshots.captureGearScreenshot(startTime, endTime);
-
- // Generate the text
- let gearText;
- let merchandise = getMerchandises().find(g => g.end_time == endTime);
- if (!merchandise)
- return;
- gearText = `Up now on SplatNet: ${merchandise.gear.name} with ${merchandise.skill.name} #splatnet2`;
-
- // Post the tweet
- return await postMediaTweet(imageData, gearText);
-}
-
-/**
- * Salmon Run tweets
- */
-
-function getSalmonRunSchedules() {
- let timelinePath = `${dataPath}/timeline.json`;
- let calendarPath = `${dataPath}/salmonruncalendar.json`;
- let schedules = {};
-
- if (fs.existsSync(timelinePath)) {
- let timeline = JSON.parse(fs.readFileSync(timelinePath)).coop;
- if (timeline)
- schedules[timeline.start_time] = timeline;
- }
-
- if (fs.existsSync(calendarPath)) {
- let calendar = JSON.parse(fs.readFileSync(calendarPath)).schedules;
- if (calendar) {
- for (schedule of calendar)
- schedules[schedule.start_time] = schedule;
- }
- }
-
- return Object.values(schedules);
-}
-
-async function maybePostSalmonRunTweet() {
- const key = 'salmonrun';
-
- // 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: Salmon Run: Already posted for this hour');
- return;
- }
-
- // Get the schedules
- let schedules = getSalmonRunSchedules();
- if (!schedules.length) {
- console.warn('Twitter: Salmon Run: no schedules found');
- return;
- }
-
- // Do we have a schedule for the specified time?
- if (!schedules.find(s => s.start_time == time)) {
- console.info('Twitter: Salmon Run: No schedule for this hour');
- return;
- }
-
- // Everything looks good, let's post a tweet
- await postSalmonRunTweet(time);
- console.info('Twitter: Salmon Run: Posted Salmon Run schedule');
-
- // Update the last tweet time
- updateLastTweetTime(key, time);
-}
-
-async function testSalmonRunScreenshot() {
- let schedules = getSalmonRunSchedules();
- let imageData = await screenshots.captureSalmonRunScreenshot(schedules[0].start_time);
- fs.writeFileSync('test-screenshot-salmonrun.png', imageData);
- console.info('Saved Salmon Run screenshot');
-}
-
-async function postSalmonRunTweet(startTime) {
- // Generate the image
- let imageData = await screenshots.captureSalmonRunScreenshot(startTime);
-
- // Post the tweet
- return await postMediaTweet(imageData, 'Salmon Run is now open! #salmonrun');
-}
-
-/**
- * New Weapon tweets
- */
-
-function getNewWeaponAvailabilities(releaseTime = null) {
- let timelinePath = `${dataPath}/timeline.json`;
- if (!fs.existsSync(timelinePath)) {
- console.warn('Twitter: Gear: timeline.json does not exist');
- return;
- }
- let weaponAvailability = JSON.parse(fs.readFileSync(timelinePath)).weapon_availability;
- if (weaponAvailability && weaponAvailability.availabilities) {
- if (releaseTime === null)
- return weaponAvailability.availabilities;
-
- return weaponAvailability.availabilities.filter(a => a.release_time == releaseTime);
- }
-}
-
-async function maybePostNewWeaponTweet() {
- const key = 'weapon';
-
- // What time are we posting the schedule for?
- let time = getTopOfCurrentHour();
-
- // Do we have a weapon?
- let weaponAvailabilities = getNewWeaponAvailabilities(time);
- if (!weaponAvailabilities || weaponAvailabilities.length == 0) {
- console.info('Twitter: No new weapon for this time');
- return;
- }
-
- // Have we already posted for this time?
- if (!shouldTweet(key, time)) {
- console.info('Twitter: Weapons: Already posted for this hour');
- return;
- }
-
- // Everything looks good, let's post a tweet
- await postNewWeaponTweet(time);
- console.info('Twitter: Weapons: Posted new weapon');
-
- // Update the last tweet time
- updateLastTweetTime(key, time);
-}
-
-async function testNewWeaponScreenshot() {
- let availabilities = getNewWeaponAvailabilities();
- let availability = availabilities[0];
- if (!availability) {
- console.info('No new weapon');
- return;
- }
-
- let imageData = await screenshots.captureNewWeaponScreenshot(availability.release_time);
- fs.writeFileSync('test-screenshot-weapon.png', imageData);
- console.info('Saved new weapon screenshot');
-}
-
-async function postNewWeaponTweet(releaseTime) {
- // Generate the image
- let imageData = await screenshots.captureNewWeaponScreenshot(releaseTime);
-
- // Generate the text
- let availabilities = getNewWeaponAvailabilities(releaseTime);
- let tweetText;
- if (availabilities.length == 1) {
- tweetText = `New weapon, now available: ${availabilities[0].weapon.name} #splatoon2`;
- }
- else {
- tweetText = `New weapons, now available:`;
- for (availability of availabilities)
- tweetText += `\n- ${availability.weapon.name}`;
- tweetText += '\n#splatoon2';
- }
-
- // Post the tweet
- return await postMediaTweet(imageData, tweetText);
+ for (let tweet of tweets)
+ await tweet.saveTestScreenshot();
}
module.exports = {
maybePostTweets,
testScreenshots,
- postScheduleTweet,
- postGearTweet,
- postSalmonRunTweet,
- postNewWeaponTweet,
}
require('make-runnable/custom')({ printOutputFrame: false });
diff --git a/src/updater/twitter/GearTweet.js b/src/updater/twitter/GearTweet.js
new file mode 100644
index 0000000..399a0f9
--- /dev/null
+++ b/src/updater/twitter/GearTweet.js
@@ -0,0 +1,40 @@
+const TwitterPostBase = require('./TwitterPostBase');
+const { captureGearScreenshot } = require('../screenshots');
+const { readData, getTopOfCurrentHour } = require('./utilities');
+
+class GearTweet extends TwitterPostBase {
+ getKey() { return 'gear'; }
+ getName() { return 'Gear'; }
+
+ getMerchandises() {
+ return readData('merchandises.json').merchandises;
+ }
+
+ getDataTime() {
+ // We only have end_times for merchandise items, so we need to track the latest end_time
+ let endTimes = this.getMerchandises().map(m => m.end_time);
+ let lastEndTime = Math.max(...endTimes);
+ return lastEndTime;
+ }
+
+ getData() {
+ return this.getMerchandises().find(m => m.end_time == this.getDataTime());
+ }
+
+ getTestData() {
+ let merchandises = this.getMerchandises();
+ return merchandises[merchandises.length - 1];
+ }
+
+ getImage(data) {
+ let startTime = getTopOfCurrentHour();
+ let endTime = this.getDataTime();
+ return captureGearScreenshot(startTime, endTime);
+ }
+
+ getText(data) {
+ return `Up now on SplatNet: ${data.gear.name} with ${data.skill.name} #splatnet2`;
+ }
+}
+
+module.exports = GearTweet;
diff --git a/src/updater/twitter/NewWeaponTweet.js b/src/updater/twitter/NewWeaponTweet.js
new file mode 100644
index 0000000..9d1d45e
--- /dev/null
+++ b/src/updater/twitter/NewWeaponTweet.js
@@ -0,0 +1,50 @@
+const TwitterPostBase = require('./TwitterPostBase');
+const { captureNewWeaponScreenshot } = require('../screenshots');
+const { readData } = require('./utilities');
+
+class NewWeaponTweet extends TwitterPostBase {
+ getKey() { return 'weapon'; }
+ getName() { return 'New Weapon'; }
+
+ getNewWeaponAvailabilities() {
+ let weaponAvailability = readData('timeline.json').weapon_availability;
+ if (weaponAvailability)
+ return weaponAvailability.availabilities;
+ return [];
+ }
+
+ getData() {
+ let availabilities = this.getNewWeaponAvailabilities().filter(a => a.release_time == this.getDataTime());
+
+ // Only return the array if it contains availabilities.
+ // Otherwise return false (i.e., there's no data, so don't post a Tweet).
+ if (!availabilities.length)
+ return false;
+
+ return availabilities;
+ }
+
+ getTestData() {
+ let availabilities = this.getNewWeaponAvailabilities()
+ if (availabilities.length)
+ return availabilities;
+ }
+
+ getImage(data) {
+ return captureNewWeaponScreenshot(data[0].release_time);
+ }
+
+ getText(data) {
+ if (data.length == 1)
+ return `New weapon now available: ${data[0].weapon.name} #splatoon2`;
+
+ let text = `New weapons now available:`;
+ for (let availability of data)
+ text += `\n- ${availability.weapon.name}`;
+ text += '\n#splatoon2';
+
+ return text;
+ }
+}
+
+module.exports = NewWeaponTweet;
diff --git a/src/updater/twitter/SalmonRunTweet.js b/src/updater/twitter/SalmonRunTweet.js
new file mode 100644
index 0000000..ef3d02d
--- /dev/null
+++ b/src/updater/twitter/SalmonRunTweet.js
@@ -0,0 +1,42 @@
+const TwitterPostBase = require('./TwitterPostBase');
+const { captureSalmonRunScreenshot } = require('../screenshots');
+const { readData } = require('./utilities');
+
+class SalmonRunTweet extends TwitterPostBase {
+ getKey() { return 'salmonrun'; }
+ getName() { return 'Salmon Run'; }
+
+ getSalmonRunSchedules() {
+ let schedules = {};
+
+ let timeline = readData('timeline.json').coop;
+ if (timeline)
+ schedules[timeline.start_time] = timeline;
+
+ let calendar = readData('salmonruncalendar.json').schedules;
+ if (calendar) {
+ for (let schedule of calendar)
+ schedules[schedule.start_time] = schedule;
+ }
+
+ return Object.values(schedules);
+ }
+
+ getData() {
+ return this.getSalmonRunSchedules().find(s => s.start_time == this.getDataTime());
+ }
+
+ getTestData() {
+ return this.getSalmonRunSchedules()[0];
+ }
+
+ getImage(data) {
+ return captureSalmonRunScreenshot(data.start_time);
+ }
+
+ getText(data) {
+ return 'Salmon Run is now open! #salmonrun #splatoon2';
+ }
+}
+
+module.exports = SalmonRunTweet;
diff --git a/src/updater/twitter/ScheduleTweet.js b/src/updater/twitter/ScheduleTweet.js
new file mode 100644
index 0000000..b6291b3
--- /dev/null
+++ b/src/updater/twitter/ScheduleTweet.js
@@ -0,0 +1,30 @@
+const TwitterPostBase = require('./TwitterPostBase');
+const { captureScheduleScreenshot } = require('../screenshots');
+const { readData } = require('./utilities');
+
+class ScheduleTweet extends TwitterPostBase {
+ getKey() { return 'schedule'; }
+ getName() { return 'Schedule'; }
+
+ getSchedules() {
+ return readData('schedules.json');
+ }
+
+ getData() {
+ return this.getSchedules().regular.find(s => s.start_time == this.getDataTime());
+ }
+
+ getTestData() {
+ return this.getSchedules().regular[0];
+ }
+
+ getImage(data) {
+ return captureScheduleScreenshot(data.start_time);
+ }
+
+ getText(data) {
+ return 'Current Splatoon 2 map rotation';
+ }
+}
+
+module.exports = ScheduleTweet;
diff --git a/src/updater/twitter/TwitterPostBase.js b/src/updater/twitter/TwitterPostBase.js
new file mode 100644
index 0000000..6a8b2cb
--- /dev/null
+++ b/src/updater/twitter/TwitterPostBase.js
@@ -0,0 +1,154 @@
+const path = require('path');
+const fs = require('fs');
+const { postMediaTweet } = require('./client');
+const { getTopOfCurrentHour, readJson } = require('./utilities');
+
+const lastTweetTimesPath = path.resolve('twitter-lastTweetTimes.json');
+
+class TwitterPostBase {
+ maybePostTweet() {
+ // Make sure we have data to post
+ if (!this.getData()) {
+ this.info('No data to post');
+ return false;
+ }
+
+ if (!this.shouldPostForCurrentTime()) {
+ this.info('Already posted for this time');
+ return false;
+ }
+
+ return this.postTweet();
+ }
+
+ async postTweet() {
+ try {
+ // Get the Tweet's text and image
+ let data = this.getData();
+ let text = await this.getText(data);
+ let image = await this.getImage(data);
+
+ // Post to Twitter
+ let tweet = await postMediaTweet(text, image);
+
+ // Update the last post time
+ this.updateLastTweetTime();
+
+ this.info('Posted Tweet');
+ }
+ catch (e) {
+ this.error('Couldn\'t post Tweet');
+ console.error(e);
+ }
+ }
+
+ async saveTestScreenshot() {
+ try {
+ let data = this.getTestData();
+ if (!data) {
+ this.info('No data available');
+ return;
+ }
+
+ let filename = this.getTestScreenshotFilename();
+ let image = await this.getImage(data);
+
+ fs.writeFileSync(filename, image);
+ this.info('Saved screenshot')
+ }
+ catch (e) {
+ this.error('Couldn\'t save screenshot');
+ console.error(e);
+ }
+ }
+
+ /**
+ * Post time helpers
+ */
+
+ getLastTweetTimes() {
+ if (fs.existsSync(lastTweetTimesPath))
+ return readJson(lastTweetTimesPath);
+ return {};
+ }
+
+ getLastTweetTime() {
+ let key = this.getKey();
+ return this.getLastTweetTimes()[key] || 0;
+ }
+
+ updateLastTweetTime() {
+ let key = this.getKey();
+ let time = this.getDataTime();
+ let lastTweetTimes = this.getLastTweetTimes();
+
+ lastTweetTimes[key] = time;
+
+ fs.writeFileSync(lastTweetTimesPath, JSON.stringify(lastTweetTimes));
+ }
+
+ shouldPostForCurrentTime() {
+ // Check whether the current data time has already been posted
+ let time = this.getDataTime();
+ let lastTweetTime = this.getLastTweetTime();
+ return lastTweetTime < time;
+ }
+
+ /**
+ * Log helpers
+ */
+
+ formatLogMessage(message) {
+ let name = this.getName();
+ return `[Twitter] [${name}] ${message}`;
+ }
+
+ log(message) {
+ console.log(this.formatLogMessage(message));
+ }
+
+ info(message) {
+ console.info(this.formatLogMessage(message));
+ }
+
+ error(message) {
+ console.error(this.formatLogMessage(message));
+ }
+
+ /**
+ * Overridable methods
+ */
+
+ // The unique key for this Tweet (used for storing the last time this Tweet was posted)
+ getKey() { }
+
+ // The friendly name for this Tweet (used for console log messages)
+ getName() { }
+
+ // The time which the current Tweet is based off of (usually the top of the current hour)
+ getDataTime() {
+ return getTopOfCurrentHour();
+ }
+
+ // The current data item the Tweet is based on (used by getImage and getText)
+ getData() { }
+
+ // Data for test screenshots
+ getTestData() {
+ return this.getData();
+ }
+
+ // The image data to be posted with the Tweet
+ getImage(data) { }
+
+ // The text body of the Tweet
+ getText(data) { }
+
+ // The filename for test screenshots
+ getTestScreenshotFilename() {
+ let key = this.getKey();
+ return `test-screenshot-${key}.png`;
+ }
+}
+
+module.exports = TwitterPostBase;
diff --git a/src/updater/twitter/client.js b/src/updater/twitter/client.js
new file mode 100644
index 0000000..30c55bb
--- /dev/null
+++ b/src/updater/twitter/client.js
@@ -0,0 +1,40 @@
+require('../bootstrap');
+const Twitter = require('twitter');
+
+// 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 });
+module.exports.client = client;
+
+function canTweet() {
+ return consumer_key && consumer_secret && access_token_key && access_token_secret;
+}
+module.exports.canTweet = canTweet;
+
+function postMediaTweet(text, image) {
+ return new Promise((resolve, reject) => {
+ // Upload the image
+ client.post('media/upload', { media: image }, (error, media, response) => {
+ if (error)
+ return reject(error);
+
+ let status = {
+ status: text,
+ media_ids: media.media_id_string,
+ };
+
+ client.post('statuses/update', status, (error, tweet, response) => {
+ if (error)
+ return reject(error);
+
+ resolve(tweet);
+ });
+ });
+ });
+}
+module.exports.postMediaTweet = postMediaTweet;
diff --git a/src/updater/twitter/utilities.js b/src/updater/twitter/utilities.js
new file mode 100644
index 0000000..efd4c9c
--- /dev/null
+++ b/src/updater/twitter/utilities.js
@@ -0,0 +1,22 @@
+const fs = require('fs');
+const path = require('path');
+
+const dataPath = path.resolve('public/data');
+
+function getTopOfCurrentHour() {
+ let date = new Date;
+ date.setUTCMinutes(0);
+ date.setUTCSeconds(0);
+ return Math.floor(date.getTime() / 1000);
+}
+module.exports.getTopOfCurrentHour = getTopOfCurrentHour;
+
+function readJson(filename) {
+ return JSON.parse(fs.readFileSync(filename));
+}
+module.exports.readJson = readJson;
+
+function readData(filename) {
+ return readJson(`${dataPath}/${filename}`);
+}
+module.exports.readData = readData;