diff --git a/.env.sample b/.env.sample index 9878e58..c275e6f 100644 --- a/.env.sample +++ b/.env.sample @@ -7,12 +7,13 @@ DIALOGFLOW_PROJECT_ID= GITHUB_ACCESS_TOKEN= MAIN_GROUP_ID= NODE_ENV=development +PIPEDREAM_USERNAME= +PIPEDREAM_PASSWORD= SENTRY_DSN= +SUPERFEEDR_SECRET= TELEGRAM_BOT_TOKEN= TWITTER_CONSUMER_KEY= TWITTER_CONSUMER_SECRET= TWITTER_ACCESS_TOKEN= TWITTER_ACCESS_TOKEN_SECRET= TWITTER_WEBHOOK_ENV= -ZAPIER_BASIC_AUTH_USERNAME= -ZAPIER_BASIC_AUTH_PASSWORD= diff --git a/.eslintrc.js b/.eslintrc.js index cab34cf..549ddf1 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -15,6 +15,12 @@ module.exports = { node: true }, rules: { - "no-console": "off" + "no-console": "off", + "@typescript-eslint/ban-ts-comment": [ + 2, + { + "ts-ignore": "allow-with-description" + } + ] } }; diff --git a/api/_utils/zapier-auth.ts b/api/_utils/basic-auth.ts similarity index 55% rename from api/_utils/zapier-auth.ts rename to api/_utils/basic-auth.ts index 60e1064..c85edd2 100644 --- a/api/_utils/zapier-auth.ts +++ b/api/_utils/basic-auth.ts @@ -1,8 +1,10 @@ import { Buffer } from 'buffer'; -const { ZAPIER_BASIC_AUTH_USERNAME, ZAPIER_BASIC_AUTH_PASSWORD } = process.env; - -const isBasicAuthValid = (authorizationHeader = '') => { +const isBasicAuthValid = ( + authorizationHeader: string, + matchingUsername: string, + matchingPassword: string, +): boolean => { const token = authorizationHeader.split(/\s+/).pop() || ''; const auth = Buffer.from(token, 'base64').toString(); @@ -11,7 +13,9 @@ const isBasicAuthValid = (authorizationHeader = '') => { const username = parts[0]; const password = parts[1]; - return username === ZAPIER_BASIC_AUTH_USERNAME && password === ZAPIER_BASIC_AUTH_PASSWORD + return ( + username === matchingUsername && password === matchingPassword + ); }; export default isBasicAuthValid; diff --git a/api/_utils/blog/feed.ts b/api/_utils/blog/feed.ts new file mode 100644 index 0000000..4715a09 --- /dev/null +++ b/api/_utils/blog/feed.ts @@ -0,0 +1,38 @@ +import messages from '../messages'; +import { sendMessage } from '../telegram/bot-methods'; + +const { MAIN_GROUP_ID } = process.env; + +interface BlogFeed { + actor: { + displayName: string; + }; + permalinkUrl: string; + title: string; +} + +/** + * TODO: for the ngVenezuela blog to work we have to wait until this is + * closed: https://github.com/thepracticaldev/dev.to/issues/3363 + */ +const handleBlogFeed = async (feeds: BlogFeed[]) => { + if (MAIN_GROUP_ID) { + const promises: Promise[] = []; + + feeds.forEach(feed => { + const promise = sendMessage({ + chatId: Number(MAIN_GROUP_ID), + text: messages.newBlogPost + .replace('#{author}', feed.actor.displayName) + .replace('#{link}', feed.permalinkUrl) + .replace('#{title}', feed.title), + }); + + promises.push(promise); + }); + + await Promise.all(promises); + } +}; + +export default handleBlogFeed; diff --git a/api/_utils/config.ts b/api/_utils/config.ts index d989cc3..88c8c79 100644 --- a/api/_utils/config.ts +++ b/api/_utils/config.ts @@ -1,41 +1,63 @@ +/** + * About feeds: + * Superfeedr free plan only support up to 10 feeds + * in the free tier: https://superfeedr.com/subscriber#pricing + */ + const config = { whiteListedDomains: [ - "https://github.com", - "https://medium.com", - "https://twitter.com" + 'https://github.com', + 'https://dev.to', + 'https://twitter.com', ], githubFeeds: [ { - name: "angular", - repo: "angular/angular", + name: 'angular', + hasChangelog: true, + feed: 'https://github.com/angular/angular/releases.atom', + }, + { + name: 'ionic', hasChangelog: true, - feed: "https://github.com/angular/angular/releases.atom" + feed: 'https://github.com/driftyco/ionic/releases.atom', }, { - name: "ionic", - repo: "driftyco/ionic", + name: 'nativescript', hasChangelog: true, - feed: "https://github.com/driftyco/ionic/releases.atom" + feed: + 'https://github.com/NativeScript/NativeScript/releases.atom', }, { - name: "nativescript", - repo: "NativeScript/NativeScript", + name: 'wengy-ven', hasChangelog: true, - feed: "https://github.com/NativeScript/NativeScript/releases.atom" + feed: 'https://github.com/ngVenezuela/wengy-ven/releases.atom', }, { - name: "wengy-ven", - repo: "ngVenezuela/wengy-ven", + name: 'ngrx', hasChangelog: true, - feed: "https://github.com/ngVenezuela/wengy-ven/releases.atom" - } + feed: 'https://github.com/ngrx/platform/releases.atom', + }, + { + name: 'TypeScript', + hasChangelog: false, + feed: 'https://github.com/Microsoft/TypeScript/releases.atom', + }, + { + name: 'nx', + hasChangelog: false, + feed: 'https://github.com/nrwl/nx/releases.atom', + }, ], blogFeeds: [ { - name: "Blog oficial de ngvenezuela", - feed: "https://dev.to/feed/ngvenezuela" - } - ] + name: 'Blog oficial de ngvenezuela', + feed: 'https://dev.to/feed/ngvenezuela', + }, + { + name: 'Angular blog', + feed: 'https://blog.angular.io/feed', + }, + ], }; export default config; diff --git a/api/_utils/github/feed.ts b/api/_utils/github/feed.ts new file mode 100644 index 0000000..cd4adf0 --- /dev/null +++ b/api/_utils/github/feed.ts @@ -0,0 +1,40 @@ +import messages from '../messages'; +import { sendMessage } from '../telegram/bot-methods'; + +const { MAIN_GROUP_ID } = process.env; + +interface RepoConfig { + hasChangelog: boolean; + name: string; + feed: string; +} + +const handleGithubFeed = async ( + repoConfig: RepoConfig, + tags: string[], +) => { + if (MAIN_GROUP_ID) { + const promises: Promise[] = []; + + tags.forEach(tag => { + const promise = sendMessage({ + chatId: Number(MAIN_GROUP_ID), + text: messages.githubRelease + .replace('#{name}', repoConfig.name) + .replace('#{version}', tag) + .replace( + '#{url}', + repoConfig.hasChangelog + ? `https://github.com/${repoConfig.name}/blob/master/CHANGELOG.md` + : `https://github.com/${repoConfig.name}/releases/tag/${tag}`, + ), + }); + + promises.push(promise); + }); + + await Promise.all(promises); + } +}; + +export default handleGithubFeed; diff --git a/api/_utils/http.ts b/api/_utils/http.ts new file mode 100644 index 0000000..ee6ecfa --- /dev/null +++ b/api/_utils/http.ts @@ -0,0 +1,22 @@ +import { NowRequest } from '@vercel/node'; +import { Buffer } from 'buffer'; + +const getRawBody = async (readable: NowRequest): Promise => { + const chunks: any[] = []; + let bytes = 0; + + return new Promise((resolve, reject) => { + readable.on('error', reject); + + readable.on('data', chunk => { + chunks.push(chunk); + bytes += chunk.length; + }); + + readable.on('end', () => { + resolve(Buffer.concat(chunks, bytes)); + }); + }); +}; + +export default getRawBody; diff --git a/api/pipedream/morning-greeting.ts b/api/pipedream/morning-greeting.ts new file mode 100644 index 0000000..3b4bb74 --- /dev/null +++ b/api/pipedream/morning-greeting.ts @@ -0,0 +1,79 @@ +import * as Sentry from '@sentry/node'; +import { NowRequest, NowResponse } from '@vercel/node'; + +import messages from '../_utils/messages'; +import { sendMessage } from '../_utils/telegram/bot-methods'; +import isBasicAuthValid from '../_utils/basic-auth'; + +const { + MAIN_GROUP_ID, + SENTRY_DSN, + NODE_ENV, + PIPEDREAM_USERNAME, + PIPEDREAM_PASSWORD, +} = process.env; + +Sentry.init({ dsn: SENTRY_DSN }); + +// aparently when eslint is run, it always create a long func +// eslint-disable-next-line max-len +const generateRandomBetween = (min: number, max: number) => + Math.floor(Math.random() * (max - min + 1)) + min; + +const getMorningMessage = () => { + const { goodMornings } = messages; + + const now = new Date(); + const weekday = now.getDay(); + + const weekDays: { [index: number]: string } = { + 0: 'generic', + 1: 'mondays', + 2: 'generic', + 3: 'generic', + 4: 'generic', + 5: 'fridays', + 6: 'generic', + }; + + const randomIndex = generateRandomBetween( + 0, + goodMornings[weekDays[weekday]].length - 1, + ); + + return goodMornings[weekDays[weekday]][randomIndex]; +}; + +export default async (request: NowRequest, response: NowResponse) => { + try { + const matchingUsername = PIPEDREAM_USERNAME || ''; + const matchingPassword = PIPEDREAM_PASSWORD || ''; + const authorization = request.headers.authorization || ''; + + if ( + isBasicAuthValid( + authorization, + matchingUsername, + matchingPassword, + ) + ) { + const text = getMorningMessage(); + + if (MAIN_GROUP_ID) { + await sendMessage({ chatId: Number(MAIN_GROUP_ID), text }); + } + + response.status(200).send('ok'); + } else { + response.status(401).send('Unauthorized'); + } + } catch (error) { + if (NODE_ENV === 'development') { + console.error(error); + } else { + Sentry.captureException(error); + } + + response.status(400).send('not ok'); + } +}; diff --git a/api/superfeedr/index.ts b/api/superfeedr/index.ts new file mode 100644 index 0000000..39f3ee7 --- /dev/null +++ b/api/superfeedr/index.ts @@ -0,0 +1,93 @@ +import crypto from 'crypto'; +import { Buffer } from 'buffer'; +import * as Sentry from '@sentry/node'; +import { NowRequest, NowResponse } from '@vercel/node'; + +import config from '../_utils/config'; +import handleGithubFeed from '../_utils/github/feed'; +import handleBlogFeed from '../_utils/blog/feed'; +import getRawBody from '../_utils/http'; + +const { SENTRY_DSN, NODE_ENV, SUPERFEEDR_SECRET } = process.env; + +Sentry.init({ dsn: SENTRY_DSN }); + +// Taken from https://documentation.superfeedr.com/schema.html +interface Item { + title: string; +} + +/** + * @description Used to verify that the request comes from Superfeedr + * @see https://www.w3.org/TR/websub/#signature-validation + */ +const validateSignature = ( + headers: { [index: string]: any }, + body: Buffer, +) => { + const signatureHeaderName = 'x-hub-signature'; + + if (typeof headers[signatureHeaderName] === 'undefined') { + throw new TypeError( + `validateSignature: header ${signatureHeaderName} not found`, + ); + } + + const signature = `sha1=${crypto + .createHmac('sha1', SUPERFEEDR_SECRET ?? '') + .update(body) + .digest('hex')}`; + + return crypto.timingSafeEqual( + Buffer.from(headers[signatureHeaderName]), + Buffer.from(signature), + ); +}; + +export default async (request: NowRequest, response: NowResponse) => { + try { + if (request.method === 'POST') { + const rawBody = await getRawBody(request); + + if (validateSignature(request.headers, rawBody)) { + const updatedFeed = request.body.status.feed; + + if (updatedFeed.startsWith('https://github.com')) { + const repoConfig = config.githubFeeds.find( + ({ feed }) => feed === updatedFeed, + ); + const tags = request.body.items.map( + (item: Item) => item.title, + ); + + if (repoConfig) { + await handleGithubFeed(repoConfig, tags); + } + } else { + // If it isn't a github feed, it's a blog feed + const isBlogFeedListed = config.blogFeeds.find( + ({ feed }) => feed === updatedFeed, + ); + + if (isBlogFeedListed) { + await handleBlogFeed(request.body.items); + } + } + + response.status(200).send('ok'); + } else { + response.status(400).send('signature is not valid'); + } + } else { + response.status(401).send('Unauthorized'); + } + } catch (error) { + if (NODE_ENV === 'development') { + console.error(error); + } else { + Sentry.captureException(error); + } + + response.status(400).send('not ok'); + } +}; diff --git a/api/twitter/set.ts b/api/twitter/set.ts index 459c9e7..7d63306 100644 --- a/api/twitter/set.ts +++ b/api/twitter/set.ts @@ -1,8 +1,7 @@ -import * as Sentry from "@sentry/node"; -// twitter-autohook does not have types -// @ts-ignore -import { Autohook } from "twitter-autohook"; -import { NowRequest, NowResponse } from "@vercel/node"; +import * as Sentry from '@sentry/node'; +// @ts-ignore: twitter-autohook does not have types +import { Autohook } from 'twitter-autohook'; +import { NowRequest, NowResponse } from '@vercel/node'; const { TWITTER_ACCESS_TOKEN, @@ -12,7 +11,7 @@ const { TWITTER_WEBHOOK_ENV, SENTRY_DSN, APP_URL, - NODE_ENV + NODE_ENV, } = process.env; Sentry.init({ dsn: SENTRY_DSN }); @@ -26,27 +25,27 @@ export default async (request: NowRequest, response: NowResponse) => { token_secret: TWITTER_ACCESS_TOKEN_SECRET, consumer_key: TWITTER_CONSUMER_KEY, consumer_secret: TWITTER_CONSUMER_SECRET, - env: TWITTER_WEBHOOK_ENV + env: TWITTER_WEBHOOK_ENV, }); await webhook.removeWebhooks(); await webhook.start(`${APP_URL}/api/twitter/update`); await webhook.subscribe({ oauth_token: TWITTER_ACCESS_TOKEN, - oauth_token_secret: TWITTER_ACCESS_TOKEN_SECRET + oauth_token_secret: TWITTER_ACCESS_TOKEN_SECRET, }); - response.status(200).send("ok"); + response.status(200).send('ok'); } else { - response.status(401).send("Unauthorized"); + response.status(401).send('Unauthorized'); } } catch (error) { - if (NODE_ENV === "development") { + if (NODE_ENV === 'development') { console.error(error); } else { Sentry.captureException(error); } - response.status(400).send("not ok"); + response.status(400).send('not ok'); } }; diff --git a/api/twitter/update.ts b/api/twitter/update.ts index a718848..567ac2a 100644 --- a/api/twitter/update.ts +++ b/api/twitter/update.ts @@ -5,6 +5,7 @@ import { NowRequest, NowResponse } from '@vercel/node'; import { sendMessage } from '../_utils/telegram/bot-methods'; import messages from '../_utils/messages'; +import getRawBody from '../_utils/http'; const { TWITTER_CONSUMER_SECRET, @@ -24,6 +25,7 @@ interface TweetInterface { retweeted_status: { text: string; }; + is_quote_status: boolean; } Sentry.init({ dsn: SENTRY_DSN }); @@ -34,10 +36,17 @@ const isReply = (tweet: TweetInterface): boolean => const isRt = (tweet: TweetInterface): boolean => tweet.retweeted_status !== undefined; +const isQuote = (tweet: TweetInterface): boolean => + tweet.is_quote_status; + +const isNewTweet = (tweet: TweetInterface) => + !isReply(tweet) && !isRt(tweet) && !isQuote(tweet); + const handleTweets = async (tweets: TweetInterface[] = []) => { const promises: Promise[] = []; + tweets.forEach(tweet => { - if (MAIN_GROUP_ID && !isReply(tweet) && !isRt(tweet)) { + if (MAIN_GROUP_ID && isNewTweet(tweet)) { const tweetUrl = `https://twitter.com/${tweet.user.screen_name}/status/${tweet.id_str}`; const promise = sendMessage({ @@ -91,24 +100,6 @@ const getChallengeResponse = (crcToken: string) => .update(crcToken) .digest('base64'); -const getRawBody = (readable: NowRequest): Promise => { - const chunks: any[] = []; - let bytes = 0; - - return new Promise((resolve, reject) => { - readable.on('error', reject); - - readable.on('data', chunk => { - chunks.push(chunk); - bytes += chunk.length; - }); - - readable.on('end', () => { - resolve(Buffer.concat(chunks, bytes)); - }); - }); -}; - export default async (request: NowRequest, response: NowResponse) => { try { /* diff --git a/api/zapier/blog.ts b/api/zapier/blog.ts deleted file mode 100644 index 32e88ad..0000000 --- a/api/zapier/blog.ts +++ /dev/null @@ -1,63 +0,0 @@ -import * as Sentry from "@sentry/node"; -import { NowRequest, NowResponse } from "@vercel/node"; - -import config from "../_utils/config"; -import messages from "../_utils/messages"; -import isBasicAuthValid from "../_utils/zapier-auth"; -import { sendMessage } from "../_utils/telegram/bot-methods"; - -const { MAIN_GROUP_ID, SENTRY_DSN, NODE_ENV } = process.env; - -Sentry.init({ dsn: SENTRY_DSN }); - -interface BlogFeed { - actor: { - displayName: string; - }; - permalinkUrl: string; - title: string; -} - -const handleBlogFeed = async (feed: BlogFeed) => { - if (MAIN_GROUP_ID) { - await sendMessage({ - chatId: Number(MAIN_GROUP_ID), - text: messages.newBlogPost - .replace("#{author}", feed.actor.displayName) - .replace("#{link}", feed.permalinkUrl) - .replace("#{title}", feed.title) - }); - } -}; - -export default async (request: NowRequest, response: NowResponse) => { - try { - if (isBasicAuthValid(request.headers.authorization)) { - const updatedFeed = request.body.feed; - const isBlogFeedListed = config.blogFeeds.find( - ({ feed }) => feed === updatedFeed - ); - - if (isBlogFeedListed) { - /* - TODO: for the blog to work we have to wait until this is - closed: https://github.com/thepracticaldev/dev.to/issues/3363 - We might have to update the next method as well - */ - await handleBlogFeed(request.body); - } - - response.status(200).send("ok"); - } else { - response.status(401).send("Unauthorized"); - } - } catch (error) { - if (NODE_ENV === "development") { - console.error(error); - } else { - Sentry.captureException(error); - } - - response.status(400).send("not ok"); - } -}; diff --git a/api/zapier/github.ts b/api/zapier/github.ts deleted file mode 100644 index 0e902b9..0000000 --- a/api/zapier/github.ts +++ /dev/null @@ -1,73 +0,0 @@ -import * as Sentry from "@sentry/node"; -import { NowRequest, NowResponse } from "@vercel/node"; - -import config from "../_utils/config"; -import messages from "../_utils/messages"; -import isBasicAuthValid from "../_utils/zapier-auth"; -import { sendMessage } from "../_utils/telegram/bot-methods"; - -const { MAIN_GROUP_ID, SENTRY_DSN, NODE_ENV } = process.env; - -Sentry.init({ dsn: SENTRY_DSN }); - -interface RepoConfig { - repo: string; - hasChangelog: boolean; -} - -interface GithubFeed { - id: string; -} - -const handleGithubFeed = async ( - repoConfig: RepoConfig, - newFeed: GithubFeed -) => { - /* we get the reponame, and ignore the rest */ - const repoName = repoConfig.repo.match(/[\w.-]+$/gi)![0]; - - /* we get the tag version, and ignore the rest */ - const tag = newFeed.id.match(/[\w.-]+$/gi)![0]; - - if (MAIN_GROUP_ID) { - await sendMessage({ - chatId: Number(MAIN_GROUP_ID), - text: messages.githubRelease - .replace("#{name}", repoName) - .replace("#{version}", tag) - .replace( - "#{url}", - repoConfig.hasChangelog - ? `https://github.com/${repoConfig.repo}/blob/master/CHANGELOG.md` - : `https://github.com/${repoConfig.repo}/releases/tag/${tag}` - ) - }); - } -}; - -export default async (request: NowRequest, response: NowResponse) => { - try { - if (isBasicAuthValid(request.headers.authorization)) { - const updatedFeed = request.body.feed; - const isGithubFeedListed = config.githubFeeds.find( - ({ feed }) => feed === updatedFeed - ); - - if (isGithubFeedListed) { - await handleGithubFeed(isGithubFeedListed, request.body); - } - - response.status(200).send("ok"); - } else { - response.status(401).send("Unauthorized"); - } - } catch (error) { - if (NODE_ENV === "development") { - console.error(error); - } else { - Sentry.captureException(error); - } - - response.status(400).send("not ok"); - } -}; diff --git a/api/zapier/morning-greeting.ts b/api/zapier/morning-greeting.ts deleted file mode 100644 index 982d056..0000000 --- a/api/zapier/morning-greeting.ts +++ /dev/null @@ -1,63 +0,0 @@ -import * as Sentry from "@sentry/node"; -import { NowRequest, NowResponse } from "@vercel/node"; - -import messages from "../_utils/messages"; -import isBasicAuthValid from "../_utils/zapier-auth"; -import { sendMessage } from "../_utils/telegram/bot-methods"; - -const { MAIN_GROUP_ID, SENTRY_DSN, NODE_ENV } = process.env; - -Sentry.init({ dsn: SENTRY_DSN }); - -// aparently when eslint is run, it always create a long func -// eslint-disable-next-line max-len -const generateRandomBetween = (min: number, max: number) => - Math.floor(Math.random() * (max - min + 1)) + min; - -const getMorningMessage = () => { - const { goodMornings } = messages; - - const now = new Date(); - const weekday = now.getDay(); - - const weekDays: { [index: number]: string } = { - 0: "generic", - 1: "mondays", - 2: "generic", - 3: "generic", - 4: "generic", - 5: "fridays", - 6: "generic" - }; - - const randomIndex = generateRandomBetween( - 0, - goodMornings[weekDays[weekday]].length - 1 - ); - - return goodMornings[weekDays[weekday]][randomIndex]; -}; - -export default async (request: NowRequest, response: NowResponse) => { - try { - if (isBasicAuthValid(request.headers.authorization)) { - const text = getMorningMessage(); - - if (MAIN_GROUP_ID) { - await sendMessage({ chatId: Number(MAIN_GROUP_ID), text }); - } - - response.status(200).send("ok"); - } else { - response.status(401).send("Unauthorized"); - } - } catch (error) { - if (NODE_ENV === "development") { - console.error(error); - } else { - Sentry.captureException(error); - } - - response.status(400).send("not ok"); - } -}; diff --git a/package-lock.json b/package-lock.json index 327e2d3..14ca9c6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -813,12 +813,12 @@ "integrity": "sha512-I99sngh224D0M7XgW1s120zxCt3VYQ3IQsuw3P3jbq5GG4yc79+ZjyKznyOGIQrflfylLgcfekeZW/vk0yng6A==" }, "@typescript-eslint/eslint-plugin": { - "version": "3.7.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-3.7.1.tgz", - "integrity": "sha512-3DB9JDYkMrc8Au00rGFiJLK2Ja9CoMP6Ut0sHsXp3ZtSugjNxvSSHTnKLfo4o+QmjYBJqEznDqsG1zj4F2xnsg==", + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-3.8.0.tgz", + "integrity": "sha512-lFb4VCDleFSR+eo4Ew+HvrJ37ZH1Y9ZyE+qyP7EiwBpcCVxwmUc5PAqhShCQ8N8U5vqYydm74nss+a0wrrCErw==", "dev": true, "requires": { - "@typescript-eslint/experimental-utils": "3.7.1", + "@typescript-eslint/experimental-utils": "3.8.0", "debug": "^4.1.1", "functional-red-black-tree": "^1.0.1", "regexpp": "^3.0.0", @@ -835,45 +835,45 @@ } }, "@typescript-eslint/experimental-utils": { - "version": "3.7.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-3.7.1.tgz", - "integrity": "sha512-TqE97pv7HrqWcGJbLbZt1v59tcqsSVpWTOf1AqrWK7n8nok2sGgVtYRuGXeNeLw3wXlLEbY1MKP3saB2HsO/Ng==", + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-3.8.0.tgz", + "integrity": "sha512-o8T1blo1lAJE0QDsW7nSyvZHbiDzQDjINJKyB44Z3sSL39qBy5L10ScI/XwDtaiunoyKGLiY9bzRk4YjsUZl8w==", "dev": true, "requires": { "@types/json-schema": "^7.0.3", - "@typescript-eslint/types": "3.7.1", - "@typescript-eslint/typescript-estree": "3.7.1", + "@typescript-eslint/types": "3.8.0", + "@typescript-eslint/typescript-estree": "3.8.0", "eslint-scope": "^5.0.0", "eslint-utils": "^2.0.0" } }, "@typescript-eslint/parser": { - "version": "3.7.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-3.7.1.tgz", - "integrity": "sha512-W4QV/gXvfIsccN8225784LNOorcm7ch68Fi3V4Wg7gmkWSQRKevO4RrRqWo6N/Z/myK1QAiGgeaXN57m+R/8iQ==", + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-3.8.0.tgz", + "integrity": "sha512-u5vjOBaCsnMVQOvkKCXAmmOhyyMmFFf5dbkM3TIbg3MZ2pyv5peE4gj81UAbTHwTOXEwf7eCQTUMKrDl/+qGnA==", "dev": true, "requires": { "@types/eslint-visitor-keys": "^1.0.0", - "@typescript-eslint/experimental-utils": "3.7.1", - "@typescript-eslint/types": "3.7.1", - "@typescript-eslint/typescript-estree": "3.7.1", + "@typescript-eslint/experimental-utils": "3.8.0", + "@typescript-eslint/types": "3.8.0", + "@typescript-eslint/typescript-estree": "3.8.0", "eslint-visitor-keys": "^1.1.0" } }, "@typescript-eslint/types": { - "version": "3.7.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-3.7.1.tgz", - "integrity": "sha512-PZe8twm5Z4b61jt7GAQDor6KiMhgPgf4XmUb9zdrwTbgtC/Sj29gXP1dws9yEn4+aJeyXrjsD9XN7AWFhmnUfg==", + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-3.8.0.tgz", + "integrity": "sha512-8kROmEQkv6ss9kdQ44vCN1dTrgu4Qxrd2kXr10kz2NP5T8/7JnEfYNxCpPkArbLIhhkGLZV3aVMplH1RXQRF7Q==", "dev": true }, "@typescript-eslint/typescript-estree": { - "version": "3.7.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-3.7.1.tgz", - "integrity": "sha512-m97vNZkI08dunYOr2lVZOHoyfpqRs0KDpd6qkGaIcLGhQ2WPtgHOd/eVbsJZ0VYCQvupKrObAGTOvk3tfpybYA==", + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-3.8.0.tgz", + "integrity": "sha512-MTv9nPDhlKfclwnplRNDL44mP2SY96YmPGxmMbMy6x12I+pERcxpIUht7DXZaj4mOKKtet53wYYXU0ABaiXrLw==", "dev": true, "requires": { - "@typescript-eslint/types": "3.7.1", - "@typescript-eslint/visitor-keys": "3.7.1", + "@typescript-eslint/types": "3.8.0", + "@typescript-eslint/visitor-keys": "3.8.0", "debug": "^4.1.1", "glob": "^7.1.6", "is-glob": "^4.0.1", @@ -891,9 +891,9 @@ } }, "@typescript-eslint/visitor-keys": { - "version": "3.7.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-3.7.1.tgz", - "integrity": "sha512-xn22sQbEya+Utj2IqJHGLA3i1jDzR43RzWupxojbSWnj3nnPLavaQmWe5utw03CwYao3r00qzXfgJMGNkrzrAA==", + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-3.8.0.tgz", + "integrity": "sha512-gfqQWyVPpT9NpLREXNR820AYwgz+Kr1GuF3nf1wxpHD6hdxI62tq03ToomFnDxY0m3pUB39IF7sil7D5TQexLA==", "dev": true, "requires": { "eslint-visitor-keys": "^1.1.0" @@ -2280,9 +2280,9 @@ "dev": true }, "eslint": { - "version": "7.5.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-7.5.0.tgz", - "integrity": "sha512-vlUP10xse9sWt9SGRtcr1LAC67BENcQMFeV+w5EvLEoFe3xJ8cF1Skd0msziRx/VMC+72B4DxreCE+OR12OA6Q==", + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-7.6.0.tgz", + "integrity": "sha512-QlAManNtqr7sozWm5TF4wIH9gmUm2hE3vNRUvyoYAa4y1l5/jxD/PQStEjBMQtCqZmSep8UxrcecI60hOpe61w==", "dev": true, "requires": { "@babel/code-frame": "^7.0.0", @@ -9620,9 +9620,9 @@ "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=" }, "twitter-autohook": { - "version": "1.7.1", - "resolved": "https://registry.npmjs.org/twitter-autohook/-/twitter-autohook-1.7.1.tgz", - "integrity": "sha512-Vf3ZwgU2DrTWPVRmp3kBAWYpvkykThO9w06cQZBAyQDmg5Q32LsuRWEGU2zxOBFPIscr90Vo1xBq2X1A9o8NBw==", + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/twitter-autohook/-/twitter-autohook-1.7.2.tgz", + "integrity": "sha512-V8yDgnTxDIOxezwLSsbGc58kzA007yBr4ZwR451VXbBjrTY8j4DjW/rbn1uGXIbx08YlfXZo9EsXdpgh/hZGzw==", "requires": { "commander": "^2.20.0", "dotenv": "^8.0.0", diff --git a/package.json b/package.json index dfa0a45..38fed32 100644 --- a/package.json +++ b/package.json @@ -30,14 +30,14 @@ "dotenv": "^8.2.0", "ngrok": "^3.2.7", "node-fetch": "^2.6.0", - "twitter-autohook": "^1.7.1" + "twitter-autohook": "^1.7.2" }, "devDependencies": { "@types/node": "^14.0.27", "@types/node-fetch": "^2.5.7", - "@typescript-eslint/eslint-plugin": "^3.7.1", - "@typescript-eslint/parser": "^3.7.1", - "eslint": "^7.5.0", + "@typescript-eslint/eslint-plugin": "^3.8.0", + "@typescript-eslint/parser": "^3.8.0", + "eslint": "^7.6.0", "eslint-config-prettier": "^6.11.0", "eslint-plugin-prettier": "^3.1.4", "husky": "^3.1.0",