refactor: use gts instead of xo

feat: add browser opening to test:notification
feat: add c8 and mocha for testing
feat: update Docker and ci
style: update editorconfig
This commit is contained in:
Jef LeCompte
2021-01-17 15:00:31 -05:00
parent dbde7814af
commit f87053cb02
184 changed files with 29898 additions and 32899 deletions
+15 -15
View File
@@ -7,20 +7,20 @@ import notifier from 'node-notifier';
const {desktop} = config.notifications;
export function sendDesktopNotification(link: Link, store: Store) {
if (desktop) {
logger.debug('↗ sending desktop notification');
(async () => {
notifier.notify({
icon: join(
__dirname,
'../../docs/assets/images/streetmerchant-logo.png'
),
message: link.cartUrl ? link.cartUrl : link.url,
open: link.cartUrl ? link.cartUrl : link.url,
title: Print.inStock(link, store)
});
if (desktop) {
logger.debug('↗ sending desktop notification');
(async () => {
notifier.notify({
icon: join(
__dirname,
'../../../docs/assets/images/streetmerchant-logo.png'
),
message: link.cartUrl ? link.cartUrl : link.url,
open: link.cartUrl ? link.cartUrl : link.url,
title: Print.inStock(link, store),
});
logger.info('✔ desktop notification sent');
})();
}
logger.info('✔ desktop notification sent');
})();
}
}
+57 -65
View File
@@ -6,84 +6,76 @@ import {logger} from '../logger';
const {notifyGroup, webhooks, notifyGroupSeries} = config.notifications.discord;
function getIdAndToken(webhook: string) {
const match = /.*\/webhooks\/(\d+)\/(.+)/.exec(webhook);
const match = /.*\/webhooks\/(\d+)\/(.+)/.exec(webhook);
if (!match) {
throw new Error('could not get discord webhook');
}
if (!match) {
throw new Error('could not get discord webhook');
}
return {
id: match[1],
token: match[2]
};
return {
id: match[1],
token: match[2],
};
}
export function sendDiscordMessage(link: Link, store: Store) {
if (webhooks.length > 0) {
logger.debug('↗ sending discord message');
if (webhooks.length > 0) {
logger.debug('↗ sending discord message');
(async () => {
try {
const embed = new Discord.MessageEmbed()
.setTitle('_**Stock alert!**_')
.setDescription(
'> provided by [streetmerchant](https://github.com/jef/streetmerchant) with :heart:'
)
.setThumbnail(
'https://raw.githubusercontent.com/jef/streetmerchant/main/docs/assets/images/streetmerchant-logo.png'
)
.setColor('#52b788')
.setTimestamp();
(async () => {
try {
const embed = new Discord.MessageEmbed()
.setTitle('_**Stock alert!**_')
.setDescription(
'> provided by [streetmerchant](https://github.com/jef/streetmerchant) with :heart:'
)
.setThumbnail(
'https://raw.githubusercontent.com/jef/streetmerchant/main/docs/assets/images/streetmerchant-logo.png'
)
.setColor('#52b788')
.setTimestamp();
embed.addField('Store', store.name, true);
if (link.price)
embed.addField(
'Price',
`${store.currency}${link.price}`,
true
);
embed.addField('Product Page', link.url);
if (link.cartUrl) embed.addField('Add to Cart', link.cartUrl);
embed.addField('Brand', link.brand, true);
embed.addField('Model', link.model, true);
embed.addField('Series', link.series, true);
embed.addField('Store', store.name, true);
if (link.price)
embed.addField('Price', `${store.currency}${link.price}`, true);
embed.addField('Product Page', link.url);
if (link.cartUrl) embed.addField('Add to Cart', link.cartUrl);
embed.addField('Brand', link.brand, true);
embed.addField('Model', link.model, true);
embed.addField('Series', link.series, true);
embed.setTimestamp();
embed.setTimestamp();
let notifyText: string[] = [];
let notifyText: string[] = [];
if (notifyGroup) {
notifyText = notifyText.concat(notifyGroup);
}
if (notifyGroup) {
notifyText = notifyText.concat(notifyGroup);
}
if (Object.keys(notifyGroupSeries).indexOf(link.series) !== 0) {
notifyText = notifyText.concat(
notifyGroupSeries[link.series]
);
}
if (Object.keys(notifyGroupSeries).indexOf(link.series) !== 0) {
notifyText = notifyText.concat(notifyGroupSeries[link.series]);
}
const promises = [];
for (const webhook of webhooks) {
const {id, token} = getIdAndToken(webhook);
const client = new Discord.WebhookClient(id, token);
const promises = [];
for (const webhook of webhooks) {
const {id, token} = getIdAndToken(webhook);
const client = new Discord.WebhookClient(id, token);
promises.push({
client,
message: client.send(notifyText.join(' '), {
embeds: [embed],
username: 'streetmerchant'
})
});
}
promises.push({
client,
message: client.send(notifyText.join(' '), {
embeds: [embed],
username: 'streetmerchant',
}),
});
}
(await Promise.all(promises)).forEach(({client}) =>
client.destroy()
);
(await Promise.all(promises)).forEach(({client}) => client.destroy());
logger.info('✔ discord message sent');
} catch (error: unknown) {
logger.error("✖ couldn't send discord message", error);
}
})();
}
logger.info('✔ discord message sent');
} catch (error: unknown) {
logger.error("✖ couldn't send discord message", error);
}
})();
}
}
+31 -31
View File
@@ -9,47 +9,47 @@ const {email} = config.notifications;
const transportOptions: any = {};
if (email.username && (email.password || email.smtpAddress)) {
transportOptions.auth = {};
transportOptions.auth.user = email.username;
transportOptions.auth.pass = email.password;
transportOptions.auth = {};
transportOptions.auth.user = email.username;
transportOptions.auth.pass = email.password;
}
if (email.smtpAddress) {
transportOptions.host = email.smtpAddress;
transportOptions.port = email.smtpPort;
transportOptions.host = email.smtpAddress;
transportOptions.port = email.smtpPort;
} else {
transportOptions.service = 'gmail';
transportOptions.service = 'gmail';
}
export const transporter = nodemailer.createTransport({
...transportOptions
...transportOptions,
});
export function sendEmail(link: Link, store: Store) {
if (email.username && (email.password || email.smtpAddress)) {
logger.debug('↗ sending email');
if (email.username && (email.password || email.smtpAddress)) {
logger.debug('↗ sending email');
const mailOptions: Mail.Options = {
attachments: link.screenshot
? [
{
filename: link.screenshot,
path: `./${link.screenshot}`
}
]
: undefined,
from: email.username,
subject: Print.inStock(link, store),
text: Print.productInStock(link),
to: email.to
};
const mailOptions: Mail.Options = {
attachments: link.screenshot
? [
{
filename: link.screenshot,
path: `./${link.screenshot}`,
},
]
: undefined,
from: email.username,
subject: Print.inStock(link, store),
text: Print.productInStock(link),
to: email.to,
};
transporter.sendMail(mailOptions, (error) => {
if (error) {
logger.error("✖ couldn't send email", error);
} else {
logger.info('✔ email sent');
}
});
}
transporter.sendMail(mailOptions, error => {
if (error) {
logger.error("✖ couldn't send email", error);
} else {
logger.info('✔ email sent');
}
});
}
}
+67 -67
View File
@@ -7,60 +7,60 @@ const {mqtt} = config.notifications;
let client: MqttClient.Client;
if (mqtt.broker) {
if (checkInsecureUsage(mqtt.password, mqtt.broker)) {
logger.warn(
'✖ Insecure transport of password - Only use credentials with MQTT brokers on private networks.'
);
} else {
const clientOptions: IClientOptions = {
clean: mqtt.clientId === '',
clientId: mqtt.clientId === '' ? undefined : mqtt.clientId,
password: mqtt.password === '' ? undefined : mqtt.password,
username: mqtt.username === '' ? undefined : mqtt.username
};
client = MqttClient.connect(
`mqtt://${mqtt.broker}:${mqtt.port}`,
clientOptions
);
}
if (checkInsecureUsage(mqtt.password, mqtt.broker)) {
logger.warn(
'✖ Insecure transport of password - Only use credentials with MQTT brokers on private networks.'
);
} else {
const clientOptions: IClientOptions = {
clean: mqtt.clientId === '',
clientId: mqtt.clientId === '' ? undefined : mqtt.clientId,
password: mqtt.password === '' ? undefined : mqtt.password,
username: mqtt.username === '' ? undefined : mqtt.username,
};
client = MqttClient.connect(
`mqtt://${mqtt.broker}:${mqtt.port}`,
clientOptions
);
}
}
export function sendMqttMessage(link: Link, store: Store) {
if (client) {
logger.debug('↗ sending mqtt message');
if (client) {
logger.debug('↗ sending mqtt message');
(async () => {
const givenUrl = link.cartUrl ? link.cartUrl : link.url;
const message = `{"msg":"${Print.inStock(
link,
store
)}", "url":"${givenUrl}"}`;
const topic = generateTopic(link, store, mqtt.topic);
const pubOptions: IClientPublishOptions = {
qos: mqtt.qos as 0 | 1 | 2,
retain: false
};
(async () => {
const givenUrl = link.cartUrl ? link.cartUrl : link.url;
const message = `{"msg":"${Print.inStock(
link,
store
)}", "url":"${givenUrl}"}`;
const topic = generateTopic(link, store, mqtt.topic);
const pubOptions: IClientPublishOptions = {
qos: mqtt.qos as 0 | 1 | 2,
retain: false,
};
try {
client.publish(topic, message, pubOptions);
logger.info('✔ mqtt message sent');
} catch (error: unknown) {
logger.error("✖ couldn't send mqtt message", error);
}
})();
}
try {
client.publish(topic, message, pubOptions);
logger.info('✔ mqtt message sent');
} catch (error: unknown) {
logger.error("✖ couldn't send mqtt message", error);
}
})();
}
}
function generateTopic(link: Link, store: Store, topic: string): string {
topic.trim();
topic = topic.replace(/^\//, '');
topic = topic
.replace(/%series%/g, link.series)
.replace(/%brand%/g, link.brand)
.replace(/%model%/g, link.model)
.replace(/%store%/g, store.name);
topic.trim();
topic = topic.replace(/^\//, '');
topic = topic
.replace(/%series%/g, link.series)
.replace(/%brand%/g, link.brand)
.replace(/%model%/g, link.model)
.replace(/%store%/g, store.name);
return topic;
return topic;
}
/**
@@ -72,43 +72,43 @@ function generateTopic(link: Link, store: Store, topic: string): string {
*
*/
function checkInsecureUsage(pass: string, address: string): boolean {
if (pass !== '') {
if (
isClassANet(address) ||
isClassBNet(address) ||
isClassCNet(address) ||
isLinkLocal(address)
) {
logger.debug(`MQTT using private network broker: ${address}`);
} else {
logger.debug(`MQTT using public network broker: ${address}`);
return true;
}
}
if (pass !== '') {
if (
isClassANet(address) ||
isClassBNet(address) ||
isClassCNet(address) ||
isLinkLocal(address)
) {
logger.debug(`MQTT using private network broker: ${address}`);
} else {
logger.debug(`MQTT using public network broker: ${address}`);
return true;
}
}
return false;
return false;
}
function isClassANet(address: string): boolean {
const classRegex = /^(10\.(\d|[1-9]\d|[12][0-5]{2})\.(\d|[1-9]\d|[12][0-5]{2})\.(\d|[1-9]\d|[12][0-5]{2}))$/;
const classRegex = /^(10\.(\d|[1-9]\d|[12][0-5]{2})\.(\d|[1-9]\d|[12][0-5]{2})\.(\d|[1-9]\d|[12][0-5]{2}))$/;
return Boolean(classRegex.exec(address));
return Boolean(classRegex.exec(address));
}
function isClassBNet(address: string): boolean {
const classRegex = /^(172\.(1[6-9]|2\d|3[01])\.(\d|[1-9]\d|[12][0-5]{2})\.(\d|[1-9]\d|[12][0-5]{2}))$/;
const classRegex = /^(172\.(1[6-9]|2\d|3[01])\.(\d|[1-9]\d|[12][0-5]{2})\.(\d|[1-9]\d|[12][0-5]{2}))$/;
return Boolean(classRegex.exec(address));
return Boolean(classRegex.exec(address));
}
function isClassCNet(address: string): boolean {
const classRegex = /^(192\.168\.(\d|[1-9]\d|[12][0-5]{2})\.(\d|[1-9]\d|[12][0-5]{2}))$/;
const classRegex = /^(192\.168\.(\d|[1-9]\d|[12][0-5]{2})\.(\d|[1-9]\d|[12][0-5]{2}))$/;
return Boolean(classRegex.exec(address));
return Boolean(classRegex.exec(address));
}
function isLinkLocal(address: string): boolean {
const linkLocal = /.+\.local$/;
const linkLocal = /.+\.local$/;
return Boolean(linkLocal.exec(address));
return Boolean(linkLocal.exec(address));
}
+18 -18
View File
@@ -17,22 +17,22 @@ import {sendTwitchMessage} from './twitch';
import {updateRedis} from './redis';
export function sendNotification(link: Link, store: Store) {
// Priority
playSound();
sendDiscordMessage(link, store);
sendDesktopNotification(link, store);
sendEmail(link, store);
sendSms(link, store);
// Non-priority
adjustPhilipsHueLights();
sendMqttMessage(link, store);
sendPagerDutyNotification(link, store);
sendPushbulletNotification(link, store);
sendPushoverNotification(link, store);
sendSlackMessage(link, store);
sendTelegramMessage(link, store);
sendTweet(link, store);
sendTwilioMessage(link, store);
sendTwitchMessage(link, store);
updateRedis(link, store);
// Priority
playSound();
sendDiscordMessage(link, store);
sendDesktopNotification(link, store);
sendEmail(link, store);
sendSms(link, store);
// Non-priority
adjustPhilipsHueLights();
sendMqttMessage(link, store);
sendPagerDutyNotification(link, store);
sendPushbulletNotification(link, store);
sendPushoverNotification(link, store);
sendSlackMessage(link, store);
sendTelegramMessage(link, store);
sendTweet(link, store);
sendTwilioMessage(link, store);
sendTwitchMessage(link, store);
updateRedis(link, store);
}
+21 -21
View File
@@ -6,26 +6,26 @@ import {config} from '../config';
const pd = new PDClient('');
export function sendPagerDutyNotification(link: Link, store: Store) {
if (config.notifications.pagerduty.integrationKey) {
logger.debug('↗ sending pagerduty message');
const links = [{href: link.url, text: 'Visit Store'}];
if (link.cartUrl) {
links.push({
href: link.cartUrl,
text: 'Add to Cart'
});
}
if (config.notifications.pagerduty.integrationKey) {
logger.debug('↗ sending pagerduty message');
const links = [{href: link.url, text: 'Visit Store'}];
if (link.cartUrl) {
links.push({
href: link.cartUrl,
text: 'Add to Cart',
});
}
pd.events.sendEvent({
dedup_key: link.url,
event_action: 'trigger',
payload: {
links,
severity: config.notifications.pagerduty.severity,
source: store.name,
summary: Print.inStock(link, store)
},
routing_key: config.notifications.pagerduty.integrationKey
});
}
pd.events.sendEvent({
dedup_key: link.url,
event_action: 'trigger',
payload: {
links,
severity: config.notifications.pagerduty.severity,
source: store.name,
summary: Print.inStock(link, store),
},
routing_key: config.notifications.pagerduty.integrationKey,
});
}
}
+108 -120
View File
@@ -5,135 +5,123 @@ import {logger} from '../logger';
const {LightState} = hueAPI.lightStates;
const {
apiKey,
bridgeIp,
lightIds,
lightColor,
lightPattern,
clientId,
clientSecret,
accessToken,
refreshToken,
remoteApiUsername
apiKey,
bridgeIp,
lightIds,
lightColor,
lightPattern,
clientId,
clientSecret,
accessToken,
refreshToken,
remoteApiUsername,
} = config.notifications.philips_hue;
// Default Light State
const lightState = new LightState()
.on(true)
.brightness(100)
.rgb(46.27, 72.55, 0);
.on(true)
.brightness(100)
.rgb(46.27, 72.55, 0);
const adjustLightsWithAPI = (hueBridge: Api) => {
logger.debug('Connected to Philips Hue bridge.');
// Set the custom light state (COLOR and METHOD here)
if (lightColor) {
const rgbArray = lightColor.split(',');
// If there's not three values, must not be RGB
if (rgbArray.length === 3) {
lightState.rgb(rgbArray[0], rgbArray[1], rgbArray[2]);
} else {
logger.debug('✖ Error assigning RGB Values');
}
}
logger.debug('Connected to Philips Hue bridge.');
// Set the custom light state (COLOR and METHOD here)
if (lightColor) {
const rgbArray = lightColor.split(',');
// If there's not three values, must not be RGB
if (rgbArray.length === 3) {
lightState.rgb(rgbArray[0], rgbArray[1], rgbArray[2]);
} else {
logger.debug('✖ Error assigning RGB Values');
}
}
// If blink is specified, then blink the lights
if (lightPattern === 'blink') {
lightState.alertLong();
}
// If blink is specified, then blink the lights
if (lightPattern === 'blink') {
lightState.alertLong();
}
// If we've been given light IDs, then only adjust those IDs
if (lightIds) {
const arrayOfIDs = lightIds.split(',');
arrayOfIDs.forEach((light) => {
logger.debug('adjusting specified lights');
(hueBridge.lights.setLightState(
light,
lightState
) as Promise<any>).catch((error: Error) => {
logger.error('Failed to adjust specified lights.');
logger.error(error);
throw error;
});
});
} else {
// Adjust all light IDs
hueBridge.lights
.getAll()
.then((allLights: any[]) => {
allLights.forEach((light: any) => {
logger.debug('adjusting all hue lights');
(hueBridge.lights.setLightState(
light,
lightState
) as Promise<any>).catch((error: Error) => {
logger.error('Failed to adjust all lights.');
logger.error(error);
throw error;
});
});
})
.catch((error: Error) => {
logger.error('Failed to get all lights.');
logger.error(error);
throw error;
});
}
// If we've been given light IDs, then only adjust those IDs
if (lightIds) {
const arrayOfIDs = lightIds.split(',');
arrayOfIDs.forEach(light => {
logger.debug('adjusting specified lights');
(hueBridge.lights.setLightState(light, lightState) as Promise<any>).catch(
(error: Error) => {
logger.error('Failed to adjust specified lights.');
logger.error(error);
throw error;
}
);
});
} else {
// Adjust all light IDs
hueBridge.lights
.getAll()
.then((allLights: any[]) => {
allLights.forEach((light: any) => {
logger.debug('adjusting all hue lights');
(hueBridge.lights.setLightState(
light,
lightState
) as Promise<any>).catch((error: Error) => {
logger.error('Failed to adjust all lights.');
logger.error(error);
throw error;
});
});
})
.catch((error: Error) => {
logger.error('Failed to get all lights.');
logger.error(error);
throw error;
});
}
};
export function adjustPhilipsHueLights() {
// Check if the required variables have been set
if (apiKey && bridgeIp) {
logger.info('↗ adjusting Philips Hue lights over LAN');
(async () => {
logger.debug(
'Attempting to connect to Philips Hue bridge at ' + bridgeIp
);
hueAPI.api
.createLocal(bridgeIp)
.connect(apiKey)
.then(
(hueBridge) => {
adjustLightsWithAPI(hueBridge);
logger.info('✔ adjusted Philips Hue lights over LAN');
},
(error: Error) => {
logger.error("✖ couldn't adjust hue lights.", error);
}
);
})();
} else if (apiKey && clientId && clientSecret) {
logger.info('↗ adjusting Philips Hue lights over cloud');
(async () => {
logger.debug(
'Attempting to connect to Philips Hue bridge over cloud'
);
const remoteBootstrap = hueAPI.api.createRemote(
clientId,
clientSecret
);
if (accessToken && refreshToken) {
remoteBootstrap
.connectWithTokens(
accessToken,
refreshToken,
remoteApiUsername
)
.then(
(hueBridge) => {
adjustLightsWithAPI(hueBridge);
logger.info(
'✔ adjusted Philips Hue lights over cloud'
);
},
(error: Error) => {
logger.error(
'Failed to get a remote Philips Hue connection using supplied tokens.'
);
logger.error(error);
throw error;
}
);
}
})();
}
// Check if the required variables have been set
if (apiKey && bridgeIp) {
logger.info('↗ adjusting Philips Hue lights over LAN');
(async () => {
logger.debug(
'Attempting to connect to Philips Hue bridge at ' + bridgeIp
);
hueAPI.api
.createLocal(bridgeIp)
.connect(apiKey)
.then(
hueBridge => {
adjustLightsWithAPI(hueBridge);
logger.info('✔ adjusted Philips Hue lights over LAN');
},
(error: Error) => {
logger.error("✖ couldn't adjust hue lights.", error);
}
);
})();
} else if (apiKey && clientId && clientSecret) {
logger.info('↗ adjusting Philips Hue lights over cloud');
(async () => {
logger.debug('Attempting to connect to Philips Hue bridge over cloud');
const remoteBootstrap = hueAPI.api.createRemote(clientId, clientSecret);
if (accessToken && refreshToken) {
remoteBootstrap
.connectWithTokens(accessToken, refreshToken, remoteApiUsername)
.then(
hueBridge => {
adjustLightsWithAPI(hueBridge);
logger.info('✔ adjusted Philips Hue lights over cloud');
},
(error: Error) => {
logger.error(
'Failed to get a remote Philips Hue connection using supplied tokens.'
);
logger.error(error);
throw error;
}
);
}
})();
}
}
+16 -16
View File
@@ -6,22 +6,22 @@ import {config} from '../config';
const {pushbullet} = config.notifications;
export function sendPushbulletNotification(link: Link, store: Store) {
if (pushbullet) {
logger.debug('↗ sending pushbullet message');
if (pushbullet) {
logger.debug('↗ sending pushbullet message');
const pusher = new PushBullet(pushbullet);
const pusher = new PushBullet(pushbullet);
pusher.note(
{},
Print.inStock(link, store),
link.cartUrl ? link.cartUrl : link.url,
(error: Error) => {
if (error) {
logger.error("✖ couldn't send pushbullet message", error);
} else {
logger.info('✔ pushbullet message sent');
}
}
);
}
pusher.note(
{},
Print.inStock(link, store),
link.cartUrl ? link.cartUrl : link.url,
(error: Error) => {
if (error) {
logger.error("✖ couldn't send pushbullet message", error);
} else {
logger.info('✔ pushbullet message sent');
}
}
);
}
}
+30 -30
View File
@@ -6,37 +6,37 @@ import {config} from '../config';
const {pushover} = config.notifications;
export function sendPushoverNotification(link: Link, store: Store) {
if (pushover.token && pushover.username) {
logger.debug('↗ sending pushover message');
if (pushover.token && pushover.username) {
logger.debug('↗ sending pushover message');
const push = new Push({
token: pushover.token,
user: pushover.username
});
const push = new Push({
token: pushover.token,
user: pushover.username,
});
const message: PushoverMessage =
pushover.priority < 2
? {
message: link.cartUrl ? link.cartUrl : link.url,
priority: pushover.priority,
title: Print.inStock(link, store),
...(link.screenshot && {file: `./${link.screenshot}`})
}
: {
expire: pushover.expire,
message: link.cartUrl ? link.cartUrl : link.url,
priority: pushover.priority,
retry: pushover.retry,
title: Print.inStock(link, store),
...(link.screenshot && {file: `./${link.screenshot}`})
};
const message: PushoverMessage =
pushover.priority < 2
? {
message: link.cartUrl ? link.cartUrl : link.url,
priority: pushover.priority,
title: Print.inStock(link, store),
...(link.screenshot && {file: `./${link.screenshot}`}),
}
: {
expire: pushover.expire,
message: link.cartUrl ? link.cartUrl : link.url,
priority: pushover.priority,
retry: pushover.retry,
title: Print.inStock(link, store),
...(link.screenshot && {file: `./${link.screenshot}`}),
};
push.send(message, (error: Error) => {
if (error) {
logger.error("✖ couldn't send pushover message", error);
} else {
logger.info('✔ pushover message sent');
}
});
}
push.send(message, (error: Error) => {
if (error) {
logger.error("✖ couldn't send pushover message", error);
} else {
logger.info('✔ pushover message sent');
}
});
}
}
+26 -26
View File
@@ -7,39 +7,39 @@ const {url} = config.notifications.redis;
let client: RedisClient;
function initRedis(): RedisClient | null {
if (url) {
client = redis.createClient({url});
}
if (url) {
client = redis.createClient({url});
}
return null;
return null;
}
export function updateRedis(link: Link, store: Store) {
try {
if (client) {
const key = `${store.name}:${link.brand}:${link.model}`
.split(' ')
.join('-');
try {
if (client) {
const key = `${store.name}:${link.brand}:${link.model}`
.split(' ')
.join('-');
const value = {
...link,
labels: store.labels,
links: store.links,
name: store.name,
updatedAt: new Date().toUTCString()
};
const value = {
...link,
labels: store.labels,
links: store.links,
name: store.name,
updatedAt: new Date().toUTCString(),
};
const redisUpdated = client.set(key, JSON.stringify(value));
const redisUpdated = client.set(key, JSON.stringify(value));
if (redisUpdated) {
logger.info('✔ redis updated');
} else {
logger.error(`✖ couldn't update redis for key (${key})`);
}
}
} catch (error: unknown) {
logger.error("✖ couldn't update redis", error);
}
if (redisUpdated) {
logger.info('✔ redis updated');
} else {
logger.error(`✖ couldn't update redis for key (${key})`);
}
}
} catch (error: unknown) {
logger.error("✖ couldn't update redis", error);
}
}
initRedis();
+19 -19
View File
@@ -7,27 +7,27 @@ const {channel, token} = config.notifications.slack;
const web = new WebClient(token);
export function sendSlackMessage(link: Link, store: Store) {
if (channel && token) {
logger.debug('↗ sending slack message');
if (channel && token) {
logger.debug('↗ sending slack message');
(async () => {
const givenUrl = link.cartUrl ? link.cartUrl : link.url;
(async () => {
const givenUrl = link.cartUrl ? link.cartUrl : link.url;
try {
const result = await web.chat.postMessage({
channel: channel.replace('#', ''),
text: `${Print.inStock(link, store)}\n${givenUrl}`
});
try {
const result = await web.chat.postMessage({
channel: channel.replace('#', ''),
text: `${Print.inStock(link, store)}\n${givenUrl}`,
});
if (!result.ok) {
logger.error("✖ couldn't send slack message", result);
return;
}
if (!result.ok) {
logger.error("✖ couldn't send slack message", result);
return;
}
logger.info('✔ slack message sent');
} catch (error: unknown) {
logger.error("✖ couldn't send slack message", error);
}
})();
}
logger.info('✔ slack message sent');
} catch (error: unknown) {
logger.error("✖ couldn't send slack message", error);
}
})();
}
}
+57 -55
View File
@@ -7,72 +7,74 @@ import {transporter} from './email';
const {email, phone} = config.notifications;
if (phone.number.length > 0 && (!email.username || !email.password)) {
logger.warn(
'✖ in order to receive sms alerts, email notifications must also be configured'
);
logger.warn(
'✖ in order to receive sms alerts, email notifications must also be configured'
);
}
if (phone.carrier.length !== phone.number.length) {
logger.warn(
'✖ the number of carriers must match the number of phone numbers',
{carrier: phone.carrier, number: phone.number}
);
logger.warn(
'✖ the number of carriers must match the number of phone numbers',
{carrier: phone.carrier, number: phone.number}
);
}
export function sendSms(link: Link, store: Store) {
for (
let i = 0;
i < Math.max(phone.number.length, phone.carrier.length);
i++
) {
const currentNumber = phone.number[i];
const currentCarrier = phone.carrier[i];
for (
let i = 0;
i < Math.max(phone.number.length, phone.carrier.length);
i++
) {
const currentNumber = phone.number[i];
const currentCarrier = phone.carrier[i];
if (!currentNumber) {
logger.error(`${currentCarrier} is not associated with a number`);
continue;
} else if (!currentCarrier) {
logger.error(`${currentNumber} is not associated with a carrier`);
continue;
}
if (!currentNumber) {
logger.error(`${currentCarrier} is not associated with a number`);
continue;
} else if (!currentCarrier) {
logger.error(`${currentNumber} is not associated with a carrier`);
continue;
}
if (!phone.availableCarriers.has(currentCarrier)) {
logger.error(`✖ unknown carrier ${currentCarrier}`);
continue;
}
if (!phone.availableCarriers.has(currentCarrier)) {
logger.error(`✖ unknown carrier ${currentCarrier}`);
continue;
}
logger.debug('↗ sending sms');
logger.debug('↗ sending sms');
const mailOptions: Mail.Options = {
attachments: link.screenshot
? [
{
filename: link.screenshot,
path: `./${link.screenshot}`
}
]
: undefined,
from: email.username,
subject: Print.inStock(link, store, false, true),
text: link.cartUrl ? link.cartUrl : link.url,
to: generateAddress(currentNumber, currentCarrier)
};
const mailOptions: Mail.Options = {
attachments: link.screenshot
? [
{
filename: link.screenshot,
path: `./${link.screenshot}`,
},
]
: undefined,
from: email.username,
subject: Print.inStock(link, store, false, true),
text: link.cartUrl ? link.cartUrl : link.url,
to: generateAddress(currentNumber, currentCarrier),
};
transporter.sendMail(mailOptions, (error) => {
if (error) {
logger.error(
`✖ couldn't send sms to ${currentNumber} for carrier ${currentCarrier}`,
error
);
} else {
logger.info('✔ sms sent');
}
});
}
transporter.sendMail(mailOptions, error => {
if (error) {
logger.error(
`✖ couldn't send sms to ${currentNumber} for carrier ${currentCarrier}`,
error
);
} else {
logger.info('✔ sms sent');
}
});
}
}
function generateAddress(number: string, carrier: string) {
if (carrier && phone.availableCarriers.has(carrier)) {
return [number, phone.availableCarriers.get(carrier)].join('@');
}
function generateAddress(number: string, carrier: string): string {
if (carrier && phone.availableCarriers.has(carrier)) {
return [number, phone.availableCarriers.get(carrier)].join('@');
}
return '';
}
+24 -30
View File
@@ -6,41 +6,35 @@ import {logger} from '../logger';
let player: PlaySound;
if (config.notifications.playSound) {
player = config.notifications.soundPlayer
? playerLib({players: [config.notifications.soundPlayer]})
: playerLib();
player = config.notifications.soundPlayer
? playerLib({players: [config.notifications.soundPlayer]})
: playerLib();
if (player.player === null) {
logger.warn("✖ couldn't find sound player");
} else {
const playerName = player.player;
logger.info(`✔ sound player found: ${playerName}`);
}
if (player.player === null) {
logger.warn("✖ couldn't find sound player");
} else {
const playerName = player.player;
logger.info(`✔ sound player found: ${playerName}`);
}
}
export function playSound() {
if (config.notifications.playSound && player.player !== null) {
logger.debug('↗ playing sound');
if (config.notifications.playSound && player.player !== null) {
logger.debug('↗ playing sound');
fs.access(
config.notifications.playSound,
fs.constants.F_OK,
(error) => {
if (error) {
logger.error(
`✖ error opening sound file: ${error.message}`
);
return;
}
fs.access(config.notifications.playSound, fs.constants.F_OK, error => {
if (error) {
logger.error(`✖ error opening sound file: ${error.message}`);
return;
}
player.play(config.notifications.playSound, (error: Error) => {
if (error) {
logger.error("✖ couldn't play sound", error);
}
player.play(config.notifications.playSound, (error: Error) => {
if (error) {
logger.error("✖ couldn't play sound", error);
}
logger.info('✔ played sound');
});
}
);
}
logger.info('✔ played sound');
});
});
}
}
+22 -22
View File
@@ -6,32 +6,32 @@ import {config} from '../config';
const {telegram} = config.notifications;
const client = new TelegramClient({
accessToken: telegram.accessToken
accessToken: telegram.accessToken,
});
export function sendTelegramMessage(link: Link, store: Store) {
if (telegram.accessToken && telegram.chatId) {
logger.debug('↗ sending telegram message');
if (telegram.accessToken && telegram.chatId) {
logger.debug('↗ sending telegram message');
(async () => {
const message = Print.productInStock(link);
const results = [];
(async () => {
const message = Print.productInStock(link);
const results = [];
for (const chatId of telegram.chatId) {
try {
results.push(
client.sendMessage(
chatId,
`${Print.inStock(link, store)}\n${message}`
)
);
logger.info('✔ telegram message sent');
} catch (error: unknown) {
logger.error("✖ couldn't send telegram message", error);
}
}
for (const chatId of telegram.chatId) {
try {
results.push(
client.sendMessage(
chatId,
`${Print.inStock(link, store)}\n${message}`
)
);
logger.info('✔ telegram message sent');
} catch (error: unknown) {
logger.error("✖ couldn't send telegram message", error);
}
}
await Promise.all(results);
})();
}
await Promise.all(results);
})();
}
}
+25 -25
View File
@@ -7,34 +7,34 @@ const {twilio} = config.notifications;
let client: Twilio;
if (twilio.accountSid && twilio.authToken) {
client = new Twilio(twilio.accountSid, twilio.authToken);
client = new Twilio(twilio.accountSid, twilio.authToken);
}
export function sendTwilioMessage(link: Link, store: Store) {
if (client) {
logger.debug('↗ sending twilio message');
if (client) {
logger.debug('↗ sending twilio message');
(async () => {
const givenUrl = link.cartUrl ? link.cartUrl : link.url;
const message = `${Print.inStock(link, store)}\n${givenUrl}`;
const numbers = twilio.to.split(',');
const results = [];
for (const number of numbers) {
try {
results.push(
client.messages.create({
body: message,
from: twilio.from,
to: number
})
);
logger.info('✔ twilio message sent');
} catch (error: unknown) {
logger.error("✖ couldn't send twilio message", error);
}
}
(async () => {
const givenUrl = link.cartUrl ? link.cartUrl : link.url;
const message = `${Print.inStock(link, store)}\n${givenUrl}`;
const numbers = twilio.to.split(',');
const results = [];
for (const number of numbers) {
try {
results.push(
client.messages.create({
body: message,
from: twilio.from,
to: number,
})
);
logger.info('✔ twilio message sent');
} catch (error: unknown) {
logger.error("✖ couldn't send twilio message", error);
}
}
await Promise.all(results);
})();
}
await Promise.all(results);
})();
}
}
+68 -72
View File
@@ -11,95 +11,91 @@ const messages: string[] = [];
let alreadySaying = false;
let tokenData = {
accessToken: twitch.accessToken,
expiryTimestamp: 0,
refreshToken: twitch.refreshToken
accessToken: twitch.accessToken,
expiryTimestamp: 0,
refreshToken: twitch.refreshToken,
};
if (existsSync('./twitch.json')) {
tokenData = {
...JSON.parse(readFileSync('./twitch.json', 'utf-8')),
...tokenData
};
tokenData = {
...JSON.parse(readFileSync('./twitch.json', 'utf-8')),
...tokenData,
};
}
const chatClient: ChatClient = new ChatClient(
new RefreshableAuthProvider(
new StaticAuthProvider(twitch.clientId, tokenData.accessToken),
{
clientSecret: twitch.clientSecret,
expiry:
tokenData.expiryTimestamp === null
? null
: new Date(tokenData.expiryTimestamp),
onRefresh: async ({accessToken, refreshToken, expiryDate}) => {
return promises.writeFile(
'./twitch.json',
JSON.stringify(
{
accessToken,
expiryTimestamp:
expiryDate === null
? null
: expiryDate.getTime(),
refreshToken
},
null,
4
),
'utf-8'
);
},
refreshToken: tokenData.refreshToken
}
),
{
channels: [twitch.channel]
}
new RefreshableAuthProvider(
new StaticAuthProvider(twitch.clientId, tokenData.accessToken),
{
clientSecret: twitch.clientSecret,
expiry:
tokenData.expiryTimestamp === null
? null
: new Date(tokenData.expiryTimestamp),
onRefresh: async ({accessToken, refreshToken, expiryDate}) => {
return promises.writeFile(
'./twitch.json',
JSON.stringify(
{
accessToken,
expiryTimestamp:
expiryDate === null ? null : expiryDate.getTime(),
refreshToken,
},
null,
4
),
'utf-8'
);
},
refreshToken: tokenData.refreshToken,
}
),
{
channels: [twitch.channel],
}
);
chatClient.onJoin((channel: string, user: string) => {
if (channel === `#${twitch.channel}` && user === chatClient.currentNick) {
while (messages.length) {
const message: string | undefined = messages.shift();
if (channel === `#${twitch.channel}` && user === chatClient.currentNick) {
while (messages.length) {
const message: string | undefined = messages.shift();
if (message !== undefined) {
try {
void chatClient.say(channel, message);
logger.info('✔ twitch message sent');
} catch (error: unknown) {
logger.error("✖ couldn't send twitch message", error);
}
}
}
}
if (message !== undefined) {
try {
void chatClient.say(channel, message);
logger.info('✔ twitch message sent');
} catch (error: unknown) {
logger.error("✖ couldn't send twitch message", error);
}
}
}
}
void chatClient.quit();
void chatClient.quit();
});
chatClient.onDisconnect(() => {
alreadySaying = false;
alreadySaying = false;
});
export function sendTwitchMessage(link: Link, store: Store) {
if (
tokenData.accessToken &&
twitch.channel &&
twitch.clientId &&
twitch.clientSecret &&
tokenData.refreshToken
) {
logger.debug('↗ sending twitch message');
if (
tokenData.accessToken &&
twitch.channel &&
twitch.clientId &&
twitch.clientSecret &&
tokenData.refreshToken
) {
logger.debug('↗ sending twitch message');
messages.push(
`${Print.inStock(link, store)}\n${
link.cartUrl ? link.cartUrl : link.url
}`
);
messages.push(
`${Print.inStock(link, store)}\n${link.cartUrl ? link.cartUrl : link.url}`
);
if (!alreadySaying) {
alreadySaying = true;
void chatClient.connect();
}
}
if (!alreadySaying) {
alreadySaying = true;
void chatClient.connect();
}
}
}
+25 -25
View File
@@ -6,35 +6,35 @@ import {config} from '../config';
const {twitter} = config.notifications;
const client = new Twitter({
access_token_key: twitter.accessTokenKey,
access_token_secret: twitter.accessTokenSecret,
consumer_key: twitter.consumerKey,
consumer_secret: twitter.consumerSecret
access_token_key: twitter.accessTokenKey,
access_token_secret: twitter.accessTokenSecret,
consumer_key: twitter.consumerKey,
consumer_secret: twitter.consumerSecret,
});
export function sendTweet(link: Link, store: Store) {
if (
twitter.accessTokenKey &&
twitter.accessTokenSecret &&
twitter.consumerKey &&
twitter.consumerSecret
) {
logger.debug('↗ sending twitter message');
if (
twitter.accessTokenKey &&
twitter.accessTokenSecret &&
twitter.consumerKey &&
twitter.consumerSecret
) {
logger.debug('↗ sending twitter message');
let status = `${Print.inStock(link, store)}\n${
link.cartUrl ? link.cartUrl : link.url
}`;
let status = `${Print.inStock(link, store)}\n${
link.cartUrl ? link.cartUrl : link.url
}`;
if (twitter.tweetTags) {
status += `\n\n${twitter.tweetTags}`;
}
if (twitter.tweetTags) {
status += `\n\n${twitter.tweetTags}`;
}
client.post('statuses/update', {status}, (error) => {
if (error) {
logger.error("✖ couldn't send twitter notification", error);
} else {
logger.info('✔ twitter notification sent');
}
});
}
client.post('statuses/update', {status}, error => {
if (error) {
logger.error("✖ couldn't send twitter notification", error);
} else {
logger.info('✔ twitter notification sent');
}
});
}
}