Skip to content

Commit

Permalink
feat: separate notifier for cycles. closes #507. closes #506
Browse files Browse the repository at this point in the history
  • Loading branch information
TobiTenno committed Nov 22, 2021
1 parent 774ded8 commit 977d8b1
Show file tree
Hide file tree
Showing 7 changed files with 270 additions and 147 deletions.
1 change: 1 addition & 0 deletions src/CommonFunctions.js
Original file line number Diff line number Diff line change
Expand Up @@ -1227,6 +1227,7 @@ const createSelectionCollector = async (interaction, pages, ctx) => {
* @returns {Promise<void>}
*/
const createPagedInteractionCollector = async (interaction, pages, ctx) => {
if (!interaction.deferred) await interaction.deferReply({ ephemeral: ctx.ephemerate });
let page = 1;
if (pages.length === 1) {
const payload = { embeds: [pages[0]], ephemeral: ctx.ephemerate };
Expand Down
12 changes: 1 addition & 11 deletions src/embeds/SyndicateEmbed.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,17 +45,7 @@ const makeMissionValue = (mission, syndMissions) => {
return value;
};

/**
* Generates syndicate embeds
*/
class SyndicateEmbed extends BaseEmbed {
/**
* @param {Genesis} bot - An instance of Genesis
* @param {Array.<SyndicateMission>} missions - The missions to be included in the embed
* @param {string} syndicate - The syndicate to display the missions for
* @param {string} platform - Platform
* @param {boolean} skipCheck - True if skipping syndicate validity check.
*/
constructor(bot, missions, syndicate, platform, skipCheck) {
super(bot);

Expand Down Expand Up @@ -92,7 +82,7 @@ class SyndicateEmbed extends BaseEmbed {

if (missionValue.length < 2000) {
this.description = missionValue;
this.fields = undefined;
this.fields = null;
} else {
this.fields = missionValue.split('\n\n').map(spv => ({
name: '\u200B',
Expand Down
24 changes: 19 additions & 5 deletions src/interactions/tracking/Tracking.js
Original file line number Diff line number Diff line change
Expand Up @@ -58,15 +58,19 @@ module.exports = class Settings extends require('../../models/Interaction') {
name: 'channel',
type: Types.CHANNEL,
description: 'Channel (text-based) that this should apply to.',
}, {
name: 'clear-prepend',
type: Types.BOOLEAN,
description: 'Clear prepend for specified "remove" trackables. Won\'t remove them from tracking.',
}],
}],
}

static async commandHandler(interaction, ctx) {
await interaction.deferReply({ ephemeral: ctx.ephemerate });
const { options } = interaction;
const action = options?.getSubcommand();
if (action === 'manage') {
await interaction.deferReply();
const original = Object.freeze({
items: await ctx.settings.getTrackedItems(interaction.channel),
events: await ctx.settings.getTrackedEventTypes(interaction.channel),
Expand Down Expand Up @@ -388,21 +392,31 @@ module.exports = class Settings extends require('../../models/Interaction') {
.map(a => a?.trim())
.filter(Boolean));
const prepend = options.getString('prepend');
const clear = options.getBoolean('clear-prepend');
const channel = options?.getChannel('channel')?.type === 'GUILD_TEXT'
? options.getChannel('channel')
: interaction.channel;

if (clear && remove?.length) {
for (const unping of remove) {
await ctx.settings.removePing(interaction.guild, unping);
}
return interaction.reply({ content: ctx.i18n`Removed pings for ${remove.length} trackables.`, ephemeral: ctx.ephemerate });
}
if (clear && !remove?.length) {
return interaction.reply({ content: ctx.i18n`Specify trackables to remove the prepend for.`, ephemeral: ctx.ephemerate });
}
if (add?.events?.length) await ctx.settings.trackEventTypes(channel, add.events);
if (add?.items?.length) await ctx.settings.trackItems(channel, add.items);
const addString = `Added ${add?.events?.length || 0} events, ${add?.items?.length || 0} items`;
const addString = ctx.i18n`Added ${add?.events?.length || 0} events, ${add?.items?.length || 0} items`;
if (remove?.events?.length) await ctx.settings.untrackEventTypes(channel, remove.events);
if (remove?.items?.length) await ctx.settings.untrackItems(channel, remove.items);
const removeString = `Removed ${remove?.events?.length} events, ${remove?.items?.length} items`;
if (remove?.items?.length && !clear) await ctx.settings.untrackItems(channel, remove.items);
const removeString = ctx.i18n`Removed ${remove?.events?.length} events, ${remove?.items?.length} items`;
await interaction.editReply({ content: `${addString}\n${removeString}`, ephemeral: ctx.ephemerate });

if (prepend && (add.items.length || add.events.length)) {
await ctx.settings.addPings(interaction.guild, add, prepend);
const pingsString = `Adding \`${
const pingsString = ctx.i18n`Adding \`${
Discord.Util.escapeMarkdown(Discord.Util.removeMentions(prepend))
}\` for ${add?.events?.length || 0} events, ${add?.items?.length || 0} items`;
await interaction.editReply({
Expand Down
63 changes: 62 additions & 1 deletion src/notifications/NotifierUtils.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
'use strict';

/* eslint-disable global-require */
const util = require('util');
const exists = util.promisify(require('url-exists'));

const embeds = {
Alert: require('../embeds/AlertEmbed'),
Arbitration: require('../embeds/ArbitrationEmbed'),
Expand All @@ -27,9 +29,68 @@ const embeds = {
};

const logger = require('../Logger');
const fetch = require('../resources/Fetcher');
const { apiBase, apiCdnBase } = require('../CommonFunctions');

const syndicates = require('../resources/syndicates.json');
const I18n = require('../settings/I18n');

const i18ns = {};
require('../resources/locales.json').forEach((locale) => {
i18ns[locale] = I18n.use(locale);
});

const between = (activation, platform, refreshRate, beats) => {
const activationTs = new Date(activation).getTime();
const leeway = 9 * (refreshRate / 10);
const isBeforeCurr = activationTs < (beats[platform].currCycleStart);
const isAfterLast = activationTs > (beats[platform].lastUpdate - (leeway));
return isBeforeCurr && isAfterLast;
};

const getThumbnailForItem = async (query, fWiki) => {
if (query && !fWiki) {
const fq = query
.replace(/\d*\s*((?:\w|\s)*)\s*(?:blueprint|receiver|stock|barrel|blade|gauntlet|upper limb|lower limb|string|guard|neuroptics|systems|chassis|link)?/ig, '$1')
.trim().toLowerCase();
const results = await fetch(`${apiBase}/items/search/${encodeURIComponent(fq)}`);
if (results.length) {
const url = `${apiCdnBase}img/${results[0].imageName}`;
if (await exists(url)) {
return url;
}
}
}
return '';
};

const asId = (event, label) => {
const uppedTime = new Date(event.expiry);
uppedTime.setMilliseconds(0);
uppedTime.setSeconds(0);

return `${label}:${uppedTime.getTime()}`;
};

/**
* Returns the number of milliseconds between now and a given date
* @param {string} d The date from which the current time will be subtracted
* @param {function} [now] A function that returns the current UNIX time in milliseconds
* @returns {number}
*/
function fromNow(d, now = Date.now) {
return new Date(d).getTime() - now();
}

module.exports = {
embeds,
logger,
platforms: process.env.PLATFORMS,
between,
getThumbnailForItem,
syndicates,
I18n,
i18ns,
asId,
fromNow,
};
5 changes: 4 additions & 1 deletion src/notifications/Worker.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ const flatCache = require('flat-cache');
const Job = require('cron').CronJob;
require('colors');

const Notifier = require('./Notifier');
const Notifier = require('./worldstate/Notifier');
const CycleNotifier = require('./worldstate/CycleNotifier');
const FeedsNotifier = require('./FeedsNotifier');
const TwitchNotifier = require('./twitch/TwitchNotifier');

Expand Down Expand Up @@ -136,6 +137,7 @@ class Worker {

if (games.includes('WARFRAME')) {
this.notifier = new Notifier(deps);
this.cycleNotifier = new CycleNotifier(deps);
}

if (games.includes('RSS')) {
Expand All @@ -149,6 +151,7 @@ class Worker {
}

await this.notifier.start();
await this.cycleNotifier.start();

if (logger.isLoggable('DEBUG')) {
rest.controlMessage({
Expand Down
168 changes: 168 additions & 0 deletions src/notifications/worldstate/CycleNotifier.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
'use strict';

const { embeds, between, fromNow } = require('../NotifierUtils');
const Broadcaster = require('../Broadcaster');
const logger = require('../../Logger');
const { platforms } = require('../../CommonFunctions');

const beats = {};
let refreshRate = (process.env.WORLDSTATE_TIMEOUT || 60000) / 3;

function buildNotifiableData(newData, platform) {
const data = {
/* Cycles data */
cetusCycleChange: between(newData.cetusCycle.activation, platform, refreshRate, beats),
earthCycleChange: between(newData.earthCycle.activation, platform, refreshRate, beats),
vallisCycleChange: between(newData.vallisCycle.activation, platform, refreshRate, beats),
cambionCycleChange: between(newData.cambionCycle.activation, platform, refreshRate, beats),
cambionCycle: newData.cambionCycle,
cetusCycle: newData.cetusCycle,
earthCycle: newData.earthCycle,
vallisCycle: newData.vallisCycle,
};

const ostron = newData.syndicateMissions.filter(mission => mission.syndicate === 'Ostrons')[0];
if (ostron) {
data.cetusCycle.bountyExpiry = ostron.expiry;
}

return data;
}

module.exports = class CyclesNotifier {
constructor({
settings, client, messageManager, worldStates, timeout, workerCache,
}) {
this.settings = settings;
this.client = client;
this.worldStates = worldStates;
this.broadcaster = new Broadcaster({
client,
settings: this.settings,
messageManager,
workerCache,
});
logger.info('Ready', 'CY');

platforms.forEach((p) => {
beats[p] = {
lastUpdate: Date.now(),
currCycleStart: null,
};
});

this.updating = false;
refreshRate = timeout;
this.updating = [];
}

/** Start the notifier */
async start() {
Object.entries(this.worldStates).forEach(([, ws]) => {
ws.on('newData', async (platform, newData) => {
await this.onNewData(platform, newData);
});
});
}

/**
* Send notifications on new data from worldstate
* @param {string} platform Platform to be updated
* @param {json} newData Updated data from the worldstate
*/
async onNewData(platform, newData) {
// don't wait for the previous to finish, this creates a giant backup,
// adding 4 new entries every few seconds
if (this.updating.includes(platform)) return;

beats[platform].currCycleStart = Date.now();
if (!(newData && newData.timestamp)) return;

const notifiedIds = await this.settings.getNotifiedIds(`${platform}:cycles`);

// Set up data to notify
this.updating.push(platform);

await this.sendNew(platform, newData, notifiedIds,
buildNotifiableData(newData, platform));

this.updating.splice(this.updating.indexOf(platform), 1);
}

async sendNew(platform, rawData, notifiedIds, {
cetusCycle, earthCycle, cetusCycleChange, earthCycleChange, vallisCycleChange,
cambionCycle, cambionCycleChange, vallisCycle,
}) {
// Send all notifications
const cycleIds = [];
try {
logger.silly(`sending new data on ${platform}...`);
cycleIds.push(
await this.sendCetusCycle(cetusCycle, platform, cetusCycleChange, notifiedIds),
);
cycleIds.push(
await this.sendEarthCycle(earthCycle, platform, earthCycleChange, notifiedIds),
);
cycleIds.push(
await this.sendVallisCycle(vallisCycle, platform, vallisCycleChange, notifiedIds),
);
cycleIds.push(
await this.sendCambionCycle(cambionCycle, platform, cambionCycleChange, notifiedIds),
);
} catch (e) {
logger.error(e);
} finally {
beats[platform].lastUpdate = Date.now();
}

const alreadyNotified = [
...cycleIds,
].filter(a => a);

await this.settings.setNotifiedIds(`${platform}:cycles`, alreadyNotified);
logger.silly(`completed sending notifications for ${platform}`);
}

async sendCambionCycle(newCycle, platform, cycleChange, notifiedIds) {
const minutesRemaining = cycleChange ? '' : `.${Math.round(fromNow(newCycle.expiry) / 60000)}`;
const type = `cambion.${newCycle.active}${minutesRemaining}`;
if (!notifiedIds.includes(type)) {
await this.broadcaster.broadcast(
new embeds.Cambion({ logger }, newCycle), platform, type,
);
}
return type;
}

async sendCetusCycle(newCycle, platform, cycleChange, notifiedIds) {
const minutesRemaining = cycleChange ? '' : `.${Math.round(fromNow(newCycle.expiry) / 60000)}`;
const type = `cetus.${newCycle.isDay ? 'day' : 'night'}${minutesRemaining}`;
const embed = new embeds.Cycle({ logger }, newCycle);
if (!notifiedIds.includes(type)) {
await this.broadcaster.broadcast(embed, platform, type);
}
return type;
}

async sendEarthCycle(newCycle, platform, cycleChange, notifiedIds) {
const minutesRemaining = cycleChange ? '' : `.${Math.round(fromNow(newCycle.expiry) / 60000)}`;
const type = `earth.${newCycle.isDay ? 'day' : 'night'}${minutesRemaining}`;
if (!notifiedIds.includes(type)) {
await this.broadcaster.broadcast(
new embeds.Cycle({ logger }, newCycle), platform, type,
);
}
return type;
}

async sendVallisCycle(newCycle, platform, cycleChange, notifiedIds) {
const minutesRemaining = cycleChange ? '' : `.${Math.round(fromNow(newCycle.expiry) / 60000)}`;
const type = `solaris.${newCycle.isWarm ? 'warm' : 'cold'}${minutesRemaining}`;
if (!notifiedIds.includes(type)) {
await this.broadcaster.broadcast(
new embeds.Solaris({ logger }, newCycle), platform, type,
);
}
return type;
}
};

0 comments on commit 977d8b1

Please sign in to comment.