Skip to content

Commit

Permalink
Added interactive captcha handler with Slack and Discord
Browse files Browse the repository at this point in the history
  • Loading branch information
klords committed Apr 19, 2021
1 parent 4eb702b commit fa6f73a
Show file tree
Hide file tree
Showing 33 changed files with 483 additions and 131 deletions.
5 changes: 5 additions & 0 deletions dotenv-example
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,11 @@ APNS_PRODUCTION=
APNS_TEAMID=
AUTO_ADD_TO_CART=
BROWSER_TRUSTED=
CAPTCHA_HANDLER_POLL_INTERVAL=
CAPTCHA_HANDLER_RESPONSE_TIMEOUT=
CAPTCHA_HANDLER_SERVICE=
CAPTCHA_HANDLER_TOKEN=
CAPTCHA_HANDLER_USER_ID=
DESKTOP_NOTIFICATIONS=
DISCORD_NOTIFY_GROUP=
DISCORD_NOTIFY_GROUP_3060=
Expand Down
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@
"start:production": "node build/src/index.js",
"test": "c8 mocha 'build/test/**/test-*.js' --exclude 'build/test/functional/**/test-*.js'",
"test:notification": "npm run compile && node build/test/functional/test-notification.js",
"test:notification:production": "node build/test/functional/test-notification.js"
"test:notification:production": "node build/test/functional/test-notification.js",
"test:captcha": "npm run compile && node build/test/functional/test-captcha.js",
"test:captcha:production": "node build/test/functional/test-captcha.js"
},
"engines": {
"node": ">=12.0.0"
Expand Down
14 changes: 14 additions & 0 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ if (process.env.npm_config_conf) {
}
} else if (existsSync(path.resolve(__dirname, '../../dotenv'))) {
dotenv.config({path: path.resolve(__dirname, '../../dotenv')});
} else if (existsSync(path.resolve(__dirname, '../dotenv'))) {
dotenv.config({path: path.resolve(__dirname, '../dotenv')});
} else {
dotenv.config({path: path.resolve(__dirname, '../../.env')});
}
Expand Down Expand Up @@ -199,6 +201,17 @@ const browser = {
userAgent: '',
};

const captchaHandler = {
pollInterval: envOrNumber(process.env.CAPTCHA_HANDLER_POLL_INTERVAL, 5000),
responseTimeout: envOrNumber(
process.env.CAPTCHA_HANDLER_RESPONSE_TIMEOUT,
300000
),
service: envOrString(process.env.CAPTCHA_HANDLER_SERVICE),
token: envOrString(process.env.CAPTCHA_HANDLER_TOKEN),
userId: envOrString(process.env.CAPTCHA_HANDLER_USER_ID),
};

const docker = envOrBoolean(process.env.DOCKER, false);

const logLevel = envOrString(process.env.LOG_LEVEL, 'info');
Expand Down Expand Up @@ -475,6 +488,7 @@ export const defaultStoreData = {

export const config = {
browser,
captchaHandler,
docker,
logLevel,
notifications,
Expand Down
File renamed without changes.
19 changes: 19 additions & 0 deletions src/messaging/captcha.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import {config} from '../config';
import {getDiscordCaptchaInputAsync} from './discord';
import {getSlackCaptchaInputAsync} from './slack';

const {service} = config.captchaHandler;

export async function getCaptchaInputAsync(
payload: string,
timeout?: number
): Promise<string> {
switch (service) {
case 'discord':
return await getDiscordCaptchaInputAsync(payload, timeout);
case 'slack':
return await getSlackCaptchaInputAsync(payload, timeout);
default:
return '';
}
}
File renamed without changes.
205 changes: 205 additions & 0 deletions src/messaging/discord.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
import {Link, Store} from '../store/model';
import Discord from 'discord.js';
import {config} from '../config';
import {logger} from '../logger';

const {notifyGroup, webhooks, notifyGroupSeries} = config.notifications.discord;
const {pollInterval, responseTimeout, token, userId} = config.captchaHandler;

let clientInstance: Discord.Client | undefined;
let dmChannelInstance: Discord.DMChannel | undefined;

function getIdAndToken(webhook: string) {
const match = /.*\/webhooks\/(\d+)\/(.+)/.exec(webhook);

if (!match) {
throw new Error('could not get discord webhook');
}

return {
id: match[1],
token: match[2],
};
}

export function sendDiscordMessage(link: Link, store: Store) {
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();

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();

let notifyText: string[] = [];

if (notifyGroup) {
notifyText = notifyText.concat(notifyGroup);
}

const notifyKeys = Object.keys(notifyGroupSeries);
const notifyIndex = notifyKeys.indexOf(link.series);
if (notifyIndex !== -1) {
notifyText = notifyText.concat(
Object.values(notifyGroupSeries)[notifyIndex]
);
}

const promises = [];
for (const webhook of webhooks) {
const {id, token} = getIdAndToken(webhook);
const client = new Discord.WebhookClient(id, token);

promises.push(
new Promise((resolve, reject) => {
client
.send(notifyText.join(' '), {
embeds: [embed],
username: 'streetmerchant',
})
.then(resp => {
logger.info('✔ discord message sent resp.id: ' + resp.id);
resolve(resp);
})
.catch(err => reject(err))
.finally(() => client.destroy());
})
);
}

await Promise.all(promises).catch(err =>
logger.error("✖ couldn't send discord message", err)
);
} catch (error: unknown) {
logger.error("✖ couldn't send discord message", error);
}
})();
}
}

export async function sendDMAsync(
payload: string
): Promise<Discord.Message | undefined> {
if (userId && token) {
logger.debug('↗ sending discord DM');
try {
const client = await getDiscordClientAsync();
const dmChannel = await getDMChannelAsync(client);
if (!dmChannel) {
logger.error('unable to get discord DM channel');
return;
}
const result = await dmChannel.send(payload);
logger.info('✔ discord DM sent');
return result;
} catch (error: unknown) {
logger.error("✖ couldn't send discord DM", error);
}
}
return;
}

export async function getDMResponseAsync(
botMessage: Discord.Message | undefined,
timeout: number
): Promise<string> {
const iterations = Math.max(timeout / pollInterval, 1);
let iteration = 0;
const client = await getDiscordClientAsync();
const dmChannel = await getDMChannelAsync(client);
if (!dmChannel) {
logger.error('unable to get discord DM channel');
return '';
}
return new Promise(resolve => {
let response = '';
const intervalId = setInterval(async () => {
const finish = (result: string) => {
clearInterval(intervalId);
resolve(result);
};
try {
iteration++;
const messages = await dmChannel.messages.fetch({
after: botMessage?.id,
});
const lastUserMessage = messages
.filter(message => message.reference?.messageID === botMessage?.id)
.last();
if (!lastUserMessage) {
if (iteration >= iterations) {
await dmChannel.send('Timed out waiting for response... 😿');
logger.error('✖ no response from user');
return finish(response);
}
} else {
response = lastUserMessage.cleanContent;
lastUserMessage.react('✅');
logger.info(`✔ got captcha response: ${response}`);
return finish(response);
}
} catch (error: unknown) {
logger.error("✖ couldn't get captcha response", error);
return finish(response);
}
}, pollInterval);
});
}

export async function getDiscordCaptchaInputAsync(
payload: string,
timeout?: number
): Promise<string> {
const message = await sendDMAsync(payload);
const response = await getDMResponseAsync(
message,
timeout || responseTimeout
);
closeClient();
return response;
}

function closeClient() {
if (clientInstance) {
clientInstance.destroy();
clientInstance = undefined;
dmChannelInstance = undefined;
}
}

async function getDiscordClientAsync() {
if (!clientInstance && token) {
clientInstance = new Discord.Client();
await clientInstance.login(token);
}
return clientInstance;
}

async function getDMChannelAsync(client?: Discord.Client) {
if (!dmChannelInstance && userId && !!client) {
const user = await new Discord.User(client, {
id: userId,
}).fetch();
dmChannelInstance = await user.createDM();
}
return dmChannelInstance;
}
File renamed without changes.
File renamed without changes.
1 change: 1 addition & 0 deletions src/notification/index.ts → src/messaging/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export * from './captcha';
export * from './notification';
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
Loading

0 comments on commit fa6f73a

Please sign in to comment.