├── .gitignore ├── LICENSE ├── README.md ├── extension ├── engagespot-client.min.js ├── icon │ └── icon.png ├── logo │ ├── Loading_icon.gif │ └── logo.png ├── manifest.json ├── popup.css ├── popup.html ├── popup.js └── script.js └── server ├── .dockerignore ├── .env.example ├── Dockerfile ├── docker-compose.yml ├── package-lock.json ├── package.json ├── src ├── api.listener.ts ├── config.ts ├── engagespot.ts ├── logger.ts ├── processor.ts ├── queue.ts ├── redis.ts └── server.ts └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | .env 3 | node_modules 4 | .DS_Store 5 | extension.zip -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Permission is hereby granted, free of charge, to any person obtaining 2 | a copy of this software and associated documentation files (the 3 | "Software"), to deal in the Software without restriction, including 4 | without limitation the rights to use, copy, modify, merge, publish, 5 | distribute, sublicense, and/or sell copies of the Software, and to 6 | permit persons to whom the Software is furnished to do so, subject to 7 | the following conditions: 8 | 9 | The above copyright notice and this permission notice shall be 10 | included in all copies or substantial portions of the Software. 11 | 12 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 13 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 14 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 15 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 16 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 17 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 18 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # HN Notifier - A Hacker News Notifications Google Chrome Extension 2 | 3 | ![Hn Notifier Screenshot](https://cdn.engagespot.co/misc/hn_notifier.png) 4 | 5 | HN Notifier is a chrome extension that shows the number of unread comments to your HackerNews thread. One of the issues with HackerNews is that it is difficult to know when someone has replied to your post or comment, thus by missing an opporunity to continue the conversation. 6 | 7 | This extension makes it easy by showing you the latest replies to your thread while you were away! 8 | 9 | Download this extension from Google Chrome Webstore - 10 | 11 | [![download](https://engagespot-website.s3.us-west-2.amazonaws.com/public/chrome_web_store_cta_57346788d7.png?updated_at=2023-05-22T06:00:11.705Z)](https://chrome.google.com/webstore/detail/hn-notifier/cdfedlekfaealogpkppjhlfcijmdlaep) 12 | 13 | Follow us on twitter for more updates - 14 | https://twitter.com/anandrmedia 15 | https://twitter.com/engagespot 16 | 17 | # Technology 18 | 19 | This extension was built using Engagespot Notification Infrastructure API 20 | 21 | # Features 22 | 23 | ## In-App realtime bell and inbox 24 | 25 | This extension adds an in-app inbox feed to your HackerNews app, just like the one you have in Reddit. Whenever you visit Hackernews, you can simply click on the bell icon to see unread notifications about replied, comments etc! 26 | 27 | ![inbox](https://lh3.googleusercontent.com/mGVxga_McWASW1KBfVUSoNq-qACbe5eVSmuX6qbHjkR989pnP9QN2zRfB1B6_ToxrIG5ozL5CDREIe5nAmxhIp6I=w640-h400-e365-rj-sc0x00ffffff) 28 | 29 | ## Optional email notifications 30 | 31 | If you want to receive email notifications when someone replies to your comment on Hackernews, you can enable it by updating your email address in the extension popup! 32 | 33 | ![enabling email notification HackerNews](https://engagespot-website.s3.us-west-2.amazonaws.com/public/notifier_hackernews_email_694764bb04.png?updated_at=2023-05-22T05:37:37.388Z) 34 | 35 | ### Batched email notifications 36 | 37 | You'll get batched email notifications like in Facebook, instead of spamming you with every reply that you receive! 38 | 39 | ![batched notification](https://lh3.googleusercontent.com/jQeO_eoO5xShvHL4OpNf1hq95RE7TTajMQsCoog1Tu53uc29wcKJ5hVj61uMmTchCVb2FTUtBuiJITxabkiRnfZTCg=w640-h400-e365-rj-sc0x00ffffff) 40 | 41 | # Privacy 42 | 43 | This extension does not collect any of your private information such as email id, or cookies without your consent. The extension simply use your public Hacker News username and track replies to your posts / comments and notifies you via the bell, and optionally email. 44 | 45 | # Source Code 46 | 47 | This project is open source. It has two components 48 | 49 | 1. Backend Worker 50 | 2. Chrome Extension 51 | 52 | Backend Worker polls Hacker News APIs and figure out whom to be notified. It is written in `node`, `bull`. Feel free to submit PRs. 53 | -------------------------------------------------------------------------------- /extension/icon/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meanands/hn-notifier/abaac126eadd4934565dc29197cf69e1de881898/extension/icon/icon.png -------------------------------------------------------------------------------- /extension/logo/Loading_icon.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meanands/hn-notifier/abaac126eadd4934565dc29197cf69e1de881898/extension/logo/Loading_icon.gif -------------------------------------------------------------------------------- /extension/logo/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meanands/hn-notifier/abaac126eadd4934565dc29197cf69e1de881898/extension/logo/logo.png -------------------------------------------------------------------------------- /extension/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "HN Notifier - Hacker News Reply Notifications", 3 | "description": "Notifies you via in-app bell and email when you get a new reply on HackerNews", 4 | "version": "2.0", 5 | "manifest_version": 3, 6 | "content_scripts": [ 7 | { 8 | "run_at": "document_end", 9 | "matches": ["https://news.ycombinator.com/*"], 10 | "js": ["engagespot-client.min.js","script.js"] 11 | } 12 | ], 13 | "icons": { "128": "icon/icon.png" }, 14 | "action": { 15 | "default_popup": "popup.html" 16 | }, 17 | "permissions": [ 18 | "tabs" 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /extension/popup.css: -------------------------------------------------------------------------------- 1 | body { 2 | width: 20rem; 3 | font-family: 'Roboto', sans-serif; 4 | } 5 | 6 | ul { 7 | list-style-type: none; 8 | padding-inline-start: 0; 9 | margin: 1rem 0; 10 | } 11 | 12 | li { 13 | padding: 0.25rem; 14 | } 15 | li:nth-child(odd) { 16 | background: #80808030; 17 | } 18 | li:nth-child(even) { 19 | background: #ffffff; 20 | } 21 | 22 | h3, 23 | p { 24 | margin: 0; 25 | } 26 | 27 | 28 | .header{ 29 | box-sizing: border-box; 30 | padding: 10px 10px; 31 | width: 100%; 32 | background: #BE185D; 33 | border-bottom: 1px solid rgba(0, 0, 0, 0.2); 34 | border-radius: 5px 5px 0px 0px; 35 | } 36 | 37 | .header span{ 38 | /* Notification */ 39 | 40 | 41 | width: 103px; 42 | height: 20px; 43 | font-style: normal; 44 | font-size: 13.5px; 45 | line-height: 20px; 46 | /* identical to box height, or 125% */ 47 | 48 | 49 | color: #FFFFFF; 50 | 51 | 52 | /* Inside auto layout */ 53 | 54 | flex: none; 55 | order: 0; 56 | flex-grow: 0; 57 | } 58 | 59 | .popup_title{ 60 | padding: 10px 0px; 61 | font-size:12px; 62 | } 63 | 64 | .item_card{ 65 | box-sizing: border-box; 66 | 67 | /* Auto layout */ 68 | 69 | 70 | justify-content: center; 71 | align-items: center; 72 | padding: 15px 0px; 73 | 74 | width:100%; 75 | height: 100px; 76 | 77 | background: #FFFFFF; 78 | border-width: 1px 0px; 79 | border-style: solid; 80 | border-color: #D8D8D8; 81 | } 82 | 83 | .item_card_icon{ 84 | float: left; 85 | width: 40px; 86 | height: 40px; 87 | text-align: center; 88 | line-height: 40px; 89 | font-size: 25px; 90 | color: #fff; 91 | background: #BE185D; 92 | border-radius: 10px; 93 | } 94 | 95 | .text_input{ 96 | width: 200px; 97 | padding: 5px 2px; 98 | border:1px solid rgb(111, 111, 111); 99 | border-radius: 5px; 100 | } 101 | 102 | .btn{ 103 | width: 30px; 104 | height: 30px; 105 | border-radius: 5px; 106 | border: 1px solid #9d144d; 107 | } 108 | 109 | .item_card_btn{ 110 | width: 50px; 111 | padding-top: 10px; 112 | float: left; 113 | } 114 | .item_card_title{ 115 | padding: 0px 10px; 116 | float: left; 117 | width: 200px; 118 | height: 20px; 119 | 120 | font-style: normal; 121 | font-weight: 600; 122 | font-size: 12px; 123 | /* identical to box height */ 124 | 125 | 126 | color: #262525; 127 | 128 | 129 | /* Inside auto layout */ 130 | 131 | flex: none; 132 | order: 0; 133 | flex-grow: 0; 134 | } -------------------------------------------------------------------------------- /extension/popup.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /extension/popup.js: -------------------------------------------------------------------------------- 1 | const button = document.getElementById("saveEmail"); 2 | 3 | function validateEmail(email) { 4 | 5 | var validRegex = /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$/; 6 | 7 | if (!email.match(validRegex)){ 8 | return false; 9 | } 10 | 11 | return true; 12 | } 13 | 14 | button.addEventListener("click", async () => { 15 | const email = document.getElementById("email").value; 16 | 17 | if(email.trim() == ''){ 18 | alert('Please enter an email id'); 19 | return; 20 | } 21 | 22 | if(!validateEmail(email)){ 23 | alert('Invalid email id'); 24 | return; 25 | } 26 | 27 | button.disabled = true; 28 | button.innerText = "..." 29 | chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => { 30 | console.log("Send"); 31 | chrome.tabs.sendMessage(tabs[0].id, "message", (response) => { 32 | console.log("Recv response = " + response?.userId); 33 | 34 | if (!response?.userId) { 35 | alert( 36 | "You must be on the HackerNews website to update your notification preferences" 37 | ); 38 | 39 | button.disabled = false; 40 | button.innerText = "💾" 41 | 42 | return; 43 | } else { 44 | 45 | const userId = response.userId; 46 | console.log("Updating ",email,"to ",userId); 47 | fetch("https://api.engagespot.co/v3/profile/", { 48 | method: "PUT", 49 | headers: { 50 | Accept: "application/json", 51 | "Content-Type": "application/json", 52 | "X-ENGAGESPOT-API-KEY":"otktz21s4eccxe0fip3th", 53 | "X-ENGAGESPOT-USER-ID":userId 54 | }, 55 | body: JSON.stringify({ email: email }), 56 | }).then(res => { 57 | 58 | button.disabled = false; 59 | button.innerText = "💾" 60 | 61 | if (res.status === 200){ 62 | alert("✅ You'll now receive email notifications!"); 63 | }else{ 64 | alert("Oops! some error occured. please try again"); 65 | } 66 | }); 67 | } 68 | }); 69 | }); 70 | }); 71 | -------------------------------------------------------------------------------- /extension/script.js: -------------------------------------------------------------------------------- 1 | var pagetop = document.getElementsByClassName('pagetop')[0]; 2 | var bellIconHolder = document.createElement('div'); 3 | var myId = document.getElementById('me').innerText; 4 | 5 | bellIconHolder.id = 'esBellIcon'; 6 | bellIconHolder.style.float = 'right' 7 | 8 | pagetop.parentNode.insertBefore(bellIconHolder, pagetop.nextSibling); 9 | 10 | chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { 11 | const userId = document.getElementById("me")?.innerText; 12 | sendResponse({ userId: userId}); 13 | return true; 14 | }); 15 | 16 | //Register 17 | //prod https://hnnotifier.engagespot.co 18 | //loc http://localhost:3002 19 | fetch('https://hnnotifier.engagespot.co/register', { 20 | method: 'POST', 21 | headers: { 22 | 'Accept': 'application/json', 23 | 'Content-Type': 'application/json' 24 | }, 25 | body: JSON.stringify({username: myId}) 26 | }); 27 | 28 | Engagespot.render('#esBellIcon', { 29 | apiKey: 'otktz21s4eccxe0fip3th', 30 | userId: myId, 31 | theme:{ 32 | colors:{ 33 | brandingPrimary:"#ff6600" 34 | } 35 | } 36 | }); -------------------------------------------------------------------------------- /server/.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | dist 4 | data 5 | .git 6 | .gitignore -------------------------------------------------------------------------------- /server/.env.example: -------------------------------------------------------------------------------- 1 | DEBUG = true 2 | APP_PORT = 3002 3 | REDIS_HOST = 127.0.0.1 4 | REDIS_PORT = 6379 5 | REDIS_PASSWORD = '' 6 | ENGAGESPOT_API_KEY = '' 7 | ENGAGESPOT_API_SECRET = '' -------------------------------------------------------------------------------- /server/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:14-alpine 2 | 3 | #app directory inside the image 4 | WORKDIR /usr/src/app 5 | 6 | # Install app dependencies 7 | # A wildcard is used to ensure both package.json AND package-lock.json are copied 8 | # where available (npm@5+) 9 | 10 | COPY package*.json ./ 11 | 12 | RUN npm install 13 | 14 | COPY . . 15 | 16 | RUN npm run build 17 | 18 | EXPOSE 3000 19 | 20 | CMD npm run start -------------------------------------------------------------------------------- /server/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.1" 2 | services: 3 | notifier: 4 | build: 5 | context: . 6 | ports: 7 | - "3000:3000" 8 | links: 9 | - redis 10 | redis: 11 | image: redis:alpine 12 | container_name: redis 13 | ports: 14 | - "6379:6379" -------------------------------------------------------------------------------- /server/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hn-notify", 3 | "version": "1.0.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "@ioredis/commands": { 8 | "version": "1.2.0", 9 | "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.2.0.tgz", 10 | "integrity": "sha512-Sx1pU8EM64o2BrqNpEO1CNLtKQwyhuXuqyfH7oGKCk+1a33d2r5saW8zNwm3j6BTExtjrv2BxTgzzkMwts6vGg==" 11 | }, 12 | "@msgpackr-extract/msgpackr-extract-darwin-arm64": { 13 | "version": "2.1.2", 14 | "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-arm64/-/msgpackr-extract-darwin-arm64-2.1.2.tgz", 15 | "integrity": "sha512-TyVLn3S/+ikMDsh0gbKv2YydKClN8HaJDDpONlaZR+LVJmsxLFUgA+O7zu59h9+f9gX1aj/ahw9wqa6rosmrYQ==", 16 | "optional": true 17 | }, 18 | "@msgpackr-extract/msgpackr-extract-darwin-x64": { 19 | "version": "2.1.2", 20 | "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-x64/-/msgpackr-extract-darwin-x64-2.1.2.tgz", 21 | "integrity": "sha512-YPXtcVkhmVNoMGlqp81ZHW4dMxK09msWgnxtsDpSiZwTzUBG2N+No2bsr7WMtBKCVJMSD6mbAl7YhKUqkp/Few==", 22 | "optional": true 23 | }, 24 | "@msgpackr-extract/msgpackr-extract-linux-arm": { 25 | "version": "2.1.2", 26 | "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm/-/msgpackr-extract-linux-arm-2.1.2.tgz", 27 | "integrity": "sha512-42R4MAFeIeNn+L98qwxAt360bwzX2Kf0ZQkBBucJ2Ircza3asoY4CDbgiu9VWklq8gWJVSJSJBwDI+c/THiWkA==", 28 | "optional": true 29 | }, 30 | "@msgpackr-extract/msgpackr-extract-linux-arm64": { 31 | "version": "2.1.2", 32 | "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm64/-/msgpackr-extract-linux-arm64-2.1.2.tgz", 33 | "integrity": "sha512-vHZ2JiOWF2+DN9lzltGbhtQNzDo8fKFGrf37UJrgqxU0yvtERrzUugnfnX1wmVfFhSsF8OxrfqiNOUc5hko1Zg==", 34 | "optional": true 35 | }, 36 | "@msgpackr-extract/msgpackr-extract-linux-x64": { 37 | "version": "2.1.2", 38 | "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-x64/-/msgpackr-extract-linux-x64-2.1.2.tgz", 39 | "integrity": "sha512-RjRoRxg7Q3kPAdUSC5EUUPlwfMkIVhmaRTIe+cqHbKrGZ4M6TyCA/b5qMaukQ/1CHWrqYY2FbKOAU8Hg0pQFzg==", 40 | "optional": true 41 | }, 42 | "@msgpackr-extract/msgpackr-extract-win32-x64": { 43 | "version": "2.1.2", 44 | "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-win32-x64/-/msgpackr-extract-win32-x64-2.1.2.tgz", 45 | "integrity": "sha512-rIZVR48zA8hGkHIK7ED6+ZiXsjRCcAVBJbm8o89OKAMTmEAQ2QvoOxoiu3w2isAaWwzgtQIOFIqHwvZDyLKCvw==", 46 | "optional": true 47 | }, 48 | "@types/body-parser": { 49 | "version": "1.19.2", 50 | "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.2.tgz", 51 | "integrity": "sha512-ALYone6pm6QmwZoAgeyNksccT9Q4AWZQ6PvfwR37GT6r6FWUPguq6sUmNGSMV2Wr761oQoBxwGGa6DR5o1DC9g==", 52 | "dev": true, 53 | "requires": { 54 | "@types/connect": "*", 55 | "@types/node": "*" 56 | } 57 | }, 58 | "@types/bull": { 59 | "version": "3.15.9", 60 | "resolved": "https://registry.npmjs.org/@types/bull/-/bull-3.15.9.tgz", 61 | "integrity": "sha512-MPUcyPPQauAmynoO3ezHAmCOhbB0pWmYyijr/5ctaCqhbKWsjW0YCod38ZcLzUBprosfZ9dPqfYIcfdKjk7RNQ==", 62 | "dev": true, 63 | "requires": { 64 | "@types/ioredis": "*", 65 | "@types/redis": "^2.8.0" 66 | } 67 | }, 68 | "@types/connect": { 69 | "version": "3.4.35", 70 | "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.35.tgz", 71 | "integrity": "sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ==", 72 | "dev": true, 73 | "requires": { 74 | "@types/node": "*" 75 | } 76 | }, 77 | "@types/cors": { 78 | "version": "2.8.12", 79 | "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.12.tgz", 80 | "integrity": "sha512-vt+kDhq/M2ayberEtJcIN/hxXy1Pk+59g2FV/ZQceeaTyCtCucjL2Q7FXlFjtWn4n15KCr1NE2lNNFhp0lEThw==", 81 | "dev": true 82 | }, 83 | "@types/express": { 84 | "version": "4.17.14", 85 | "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.14.tgz", 86 | "integrity": "sha512-TEbt+vaPFQ+xpxFLFssxUDXj5cWCxZJjIcB7Yg0k0GMHGtgtQgpvx/MUQUeAkNbA9AAGrwkAsoeItdTgS7FMyg==", 87 | "dev": true, 88 | "requires": { 89 | "@types/body-parser": "*", 90 | "@types/express-serve-static-core": "^4.17.18", 91 | "@types/qs": "*", 92 | "@types/serve-static": "*" 93 | } 94 | }, 95 | "@types/express-serve-static-core": { 96 | "version": "4.17.31", 97 | "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.31.tgz", 98 | "integrity": "sha512-DxMhY+NAsTwMMFHBTtJFNp5qiHKJ7TeqOo23zVEM9alT1Ml27Q3xcTH0xwxn7Q0BbMcVEJOs/7aQtUWupUQN3Q==", 99 | "dev": true, 100 | "requires": { 101 | "@types/node": "*", 102 | "@types/qs": "*", 103 | "@types/range-parser": "*" 104 | } 105 | }, 106 | "@types/ioredis": { 107 | "version": "4.28.10", 108 | "resolved": "https://registry.npmjs.org/@types/ioredis/-/ioredis-4.28.10.tgz", 109 | "integrity": "sha512-69LyhUgrXdgcNDv7ogs1qXZomnfOEnSmrmMFqKgt1XMJxmoOSG/u3wYy13yACIfKuMJ8IhKgHafDO3sx19zVQQ==", 110 | "dev": true, 111 | "requires": { 112 | "@types/node": "*" 113 | } 114 | }, 115 | "@types/mime": { 116 | "version": "3.0.1", 117 | "resolved": "https://registry.npmjs.org/@types/mime/-/mime-3.0.1.tgz", 118 | "integrity": "sha512-Y4XFY5VJAuw0FgAqPNd6NNoV44jbq9Bz2L7Rh/J6jLTiHBSBJa9fxqQIvkIld4GsoDOcCbvzOUAbLPsSKKg+uA==", 119 | "dev": true 120 | }, 121 | "@types/node": { 122 | "version": "18.8.2", 123 | "resolved": "https://registry.npmjs.org/@types/node/-/node-18.8.2.tgz", 124 | "integrity": "sha512-cRMwIgdDN43GO4xMWAfJAecYn8wV4JbsOGHNfNUIDiuYkUYAR5ec4Rj7IO2SAhFPEfpPtLtUTbbny/TCT7aDwA==", 125 | "dev": true 126 | }, 127 | "@types/qs": { 128 | "version": "6.9.7", 129 | "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.7.tgz", 130 | "integrity": "sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw==", 131 | "dev": true 132 | }, 133 | "@types/range-parser": { 134 | "version": "1.2.4", 135 | "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.4.tgz", 136 | "integrity": "sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==", 137 | "dev": true 138 | }, 139 | "@types/redis": { 140 | "version": "2.8.32", 141 | "resolved": "https://registry.npmjs.org/@types/redis/-/redis-2.8.32.tgz", 142 | "integrity": "sha512-7jkMKxcGq9p242exlbsVzuJb57KqHRhNl4dHoQu2Y5v9bCAbtIXXH0R3HleSQW4CTOqpHIYUW3t6tpUj4BVQ+w==", 143 | "dev": true, 144 | "requires": { 145 | "@types/node": "*" 146 | } 147 | }, 148 | "@types/serve-static": { 149 | "version": "1.15.0", 150 | "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.0.tgz", 151 | "integrity": "sha512-z5xyF6uh8CbjAu9760KDKsH2FcDxZ2tFCsA4HIMWE6IkiYMXfVoa+4f9KX+FN0ZLsaMw1WNG2ETLA6N+/YA+cg==", 152 | "dev": true, 153 | "requires": { 154 | "@types/mime": "*", 155 | "@types/node": "*" 156 | } 157 | }, 158 | "abbrev": { 159 | "version": "1.1.1", 160 | "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", 161 | "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", 162 | "dev": true 163 | }, 164 | "accepts": { 165 | "version": "1.3.8", 166 | "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", 167 | "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", 168 | "requires": { 169 | "mime-types": "~2.1.34", 170 | "negotiator": "0.6.3" 171 | } 172 | }, 173 | "ansi-regex": { 174 | "version": "5.0.1", 175 | "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", 176 | "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", 177 | "dev": true 178 | }, 179 | "ansi-styles": { 180 | "version": "4.3.0", 181 | "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", 182 | "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", 183 | "dev": true, 184 | "requires": { 185 | "color-convert": "^2.0.1" 186 | } 187 | }, 188 | "anymatch": { 189 | "version": "3.1.2", 190 | "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz", 191 | "integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==", 192 | "dev": true, 193 | "requires": { 194 | "normalize-path": "^3.0.0", 195 | "picomatch": "^2.0.4" 196 | } 197 | }, 198 | "array-flatten": { 199 | "version": "1.1.1", 200 | "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", 201 | "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" 202 | }, 203 | "asynckit": { 204 | "version": "0.4.0", 205 | "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", 206 | "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" 207 | }, 208 | "axios": { 209 | "version": "1.0.0", 210 | "resolved": "https://registry.npmjs.org/axios/-/axios-1.0.0.tgz", 211 | "integrity": "sha512-SsHsGFN1qNPFT5QhSoSD37SHDfGyLSW5AESmyLk2JeCMHv5g0I9g0Hz/zQHx2KNe0jGXh2q2hAm7OdkXm360CA==", 212 | "requires": { 213 | "follow-redirects": "^1.15.0", 214 | "form-data": "^4.0.0", 215 | "proxy-from-env": "^1.1.0" 216 | } 217 | }, 218 | "balanced-match": { 219 | "version": "1.0.2", 220 | "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", 221 | "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" 222 | }, 223 | "binary-extensions": { 224 | "version": "2.2.0", 225 | "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", 226 | "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", 227 | "dev": true 228 | }, 229 | "body-parser": { 230 | "version": "1.20.0", 231 | "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.0.tgz", 232 | "integrity": "sha512-DfJ+q6EPcGKZD1QWUjSpqp+Q7bDQTsQIF4zfUAtZ6qk+H/3/QRhg9CEp39ss+/T2vw0+HaidC0ecJj/DRLIaKg==", 233 | "requires": { 234 | "bytes": "3.1.2", 235 | "content-type": "~1.0.4", 236 | "debug": "2.6.9", 237 | "depd": "2.0.0", 238 | "destroy": "1.2.0", 239 | "http-errors": "2.0.0", 240 | "iconv-lite": "0.4.24", 241 | "on-finished": "2.4.1", 242 | "qs": "6.10.3", 243 | "raw-body": "2.5.1", 244 | "type-is": "~1.6.18", 245 | "unpipe": "1.0.0" 246 | } 247 | }, 248 | "brace-expansion": { 249 | "version": "1.1.11", 250 | "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", 251 | "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", 252 | "dev": true, 253 | "requires": { 254 | "balanced-match": "^1.0.0", 255 | "concat-map": "0.0.1" 256 | } 257 | }, 258 | "braces": { 259 | "version": "3.0.2", 260 | "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", 261 | "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", 262 | "dev": true, 263 | "requires": { 264 | "fill-range": "^7.0.1" 265 | } 266 | }, 267 | "bull": { 268 | "version": "4.10.0", 269 | "resolved": "https://registry.npmjs.org/bull/-/bull-4.10.0.tgz", 270 | "integrity": "sha512-VJ9/uTrpv7Xf1Xy/JrezwvqNmkCoL5gL00kOvmygCN9Gqe7jtNwd6U8X/48zykIjhEq3K2v+MGZzLpJM72kdIg==", 271 | "requires": { 272 | "cron-parser": "^4.2.1", 273 | "debuglog": "^1.0.0", 274 | "get-port": "^5.1.1", 275 | "ioredis": "^4.28.5", 276 | "lodash": "^4.17.21", 277 | "msgpackr": "^1.5.2", 278 | "p-timeout": "^3.2.0", 279 | "semver": "^7.3.2", 280 | "uuid": "^8.3.0" 281 | }, 282 | "dependencies": { 283 | "debug": { 284 | "version": "4.3.4", 285 | "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", 286 | "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", 287 | "requires": { 288 | "ms": "2.1.2" 289 | } 290 | }, 291 | "denque": { 292 | "version": "1.5.1", 293 | "resolved": "https://registry.npmjs.org/denque/-/denque-1.5.1.tgz", 294 | "integrity": "sha512-XwE+iZ4D6ZUB7mfYRMb5wByE8L74HCn30FBN7sWnXksWc1LO1bPDl67pBR9o/kC4z/xSNAwkMYcGgqDV3BE3Hw==" 295 | }, 296 | "ioredis": { 297 | "version": "4.28.5", 298 | "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-4.28.5.tgz", 299 | "integrity": "sha512-3GYo0GJtLqgNXj4YhrisLaNNvWSNwSS2wS4OELGfGxH8I69+XfNdnmV1AyN+ZqMh0i7eX+SWjrwFKDBDgfBC1A==", 300 | "requires": { 301 | "cluster-key-slot": "^1.1.0", 302 | "debug": "^4.3.1", 303 | "denque": "^1.1.0", 304 | "lodash.defaults": "^4.2.0", 305 | "lodash.flatten": "^4.4.0", 306 | "lodash.isarguments": "^3.1.0", 307 | "p-map": "^2.1.0", 308 | "redis-commands": "1.7.0", 309 | "redis-errors": "^1.2.0", 310 | "redis-parser": "^3.0.0", 311 | "standard-as-callback": "^2.1.0" 312 | } 313 | }, 314 | "ms": { 315 | "version": "2.1.2", 316 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", 317 | "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" 318 | }, 319 | "semver": { 320 | "version": "7.3.8", 321 | "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", 322 | "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", 323 | "requires": { 324 | "lru-cache": "^6.0.0" 325 | } 326 | } 327 | } 328 | }, 329 | "bullmq": { 330 | "version": "2.1.3", 331 | "resolved": "https://registry.npmjs.org/bullmq/-/bullmq-2.1.3.tgz", 332 | "integrity": "sha512-sOuPHbCWegOnC0uKC1i8L6njmiDquenkStMXdpCzh+xrHEB4AxOFVhuM8SchmnjcJUBnW1lVe+SBc1vMNyR1ZQ==", 333 | "requires": { 334 | "cron-parser": "^4.6.0", 335 | "glob": "^8.0.3", 336 | "ioredis": "^5.2.2", 337 | "lodash": "^4.17.21", 338 | "msgpackr": "^1.6.2", 339 | "semver": "^7.3.7", 340 | "tslib": "^2.0.0", 341 | "uuid": "^9.0.0" 342 | }, 343 | "dependencies": { 344 | "debug": { 345 | "version": "4.3.4", 346 | "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", 347 | "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", 348 | "requires": { 349 | "ms": "2.1.2" 350 | } 351 | }, 352 | "denque": { 353 | "version": "2.1.0", 354 | "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", 355 | "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==" 356 | }, 357 | "ioredis": { 358 | "version": "5.2.3", 359 | "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.2.3.tgz", 360 | "integrity": "sha512-gQNcMF23/NpvjCaa1b5YycUyQJ9rBNH2xP94LWinNpodMWVUPP5Ai/xXANn/SM7gfIvI62B5CCvZxhg5pOgyMw==", 361 | "requires": { 362 | "@ioredis/commands": "^1.1.1", 363 | "cluster-key-slot": "^1.1.0", 364 | "debug": "^4.3.4", 365 | "denque": "^2.0.1", 366 | "lodash.defaults": "^4.2.0", 367 | "lodash.isarguments": "^3.1.0", 368 | "redis-errors": "^1.2.0", 369 | "redis-parser": "^3.0.0", 370 | "standard-as-callback": "^2.1.0" 371 | } 372 | }, 373 | "ms": { 374 | "version": "2.1.2", 375 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", 376 | "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" 377 | }, 378 | "semver": { 379 | "version": "7.3.8", 380 | "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", 381 | "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", 382 | "requires": { 383 | "lru-cache": "^6.0.0" 384 | } 385 | }, 386 | "uuid": { 387 | "version": "9.0.0", 388 | "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz", 389 | "integrity": "sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==" 390 | } 391 | } 392 | }, 393 | "bytes": { 394 | "version": "3.1.2", 395 | "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", 396 | "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==" 397 | }, 398 | "call-bind": { 399 | "version": "1.0.2", 400 | "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", 401 | "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", 402 | "requires": { 403 | "function-bind": "^1.1.1", 404 | "get-intrinsic": "^1.0.2" 405 | } 406 | }, 407 | "chalk": { 408 | "version": "4.1.2", 409 | "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", 410 | "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", 411 | "dev": true, 412 | "requires": { 413 | "ansi-styles": "^4.1.0", 414 | "supports-color": "^7.1.0" 415 | }, 416 | "dependencies": { 417 | "supports-color": { 418 | "version": "7.2.0", 419 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", 420 | "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", 421 | "dev": true, 422 | "requires": { 423 | "has-flag": "^4.0.0" 424 | } 425 | } 426 | } 427 | }, 428 | "chokidar": { 429 | "version": "3.5.3", 430 | "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", 431 | "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", 432 | "dev": true, 433 | "requires": { 434 | "anymatch": "~3.1.2", 435 | "braces": "~3.0.2", 436 | "fsevents": "~2.3.2", 437 | "glob-parent": "~5.1.2", 438 | "is-binary-path": "~2.1.0", 439 | "is-glob": "~4.0.1", 440 | "normalize-path": "~3.0.0", 441 | "readdirp": "~3.6.0" 442 | } 443 | }, 444 | "cliui": { 445 | "version": "8.0.1", 446 | "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", 447 | "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", 448 | "dev": true, 449 | "requires": { 450 | "string-width": "^4.2.0", 451 | "strip-ansi": "^6.0.1", 452 | "wrap-ansi": "^7.0.0" 453 | } 454 | }, 455 | "cluster-key-slot": { 456 | "version": "1.1.1", 457 | "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.1.tgz", 458 | "integrity": "sha512-rwHwUfXL40Chm1r08yrhU3qpUvdVlgkKNeyeGPOxnW8/SyVDvgRaed/Uz54AqWNaTCAThlj6QAs3TZcKI0xDEw==" 459 | }, 460 | "color-convert": { 461 | "version": "2.0.1", 462 | "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", 463 | "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", 464 | "dev": true, 465 | "requires": { 466 | "color-name": "~1.1.4" 467 | } 468 | }, 469 | "color-name": { 470 | "version": "1.1.4", 471 | "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", 472 | "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", 473 | "dev": true 474 | }, 475 | "combined-stream": { 476 | "version": "1.0.8", 477 | "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", 478 | "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", 479 | "requires": { 480 | "delayed-stream": "~1.0.0" 481 | } 482 | }, 483 | "concat-map": { 484 | "version": "0.0.1", 485 | "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", 486 | "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", 487 | "dev": true 488 | }, 489 | "concurrently": { 490 | "version": "7.4.0", 491 | "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-7.4.0.tgz", 492 | "integrity": "sha512-M6AfrueDt/GEna/Vg9BqQ+93yuvzkSKmoTixnwEJkH0LlcGrRC2eCmjeG1tLLHIYfpYJABokqSGyMcXjm96AFA==", 493 | "dev": true, 494 | "requires": { 495 | "chalk": "^4.1.0", 496 | "date-fns": "^2.29.1", 497 | "lodash": "^4.17.21", 498 | "rxjs": "^7.0.0", 499 | "shell-quote": "^1.7.3", 500 | "spawn-command": "^0.0.2-1", 501 | "supports-color": "^8.1.0", 502 | "tree-kill": "^1.2.2", 503 | "yargs": "^17.3.1" 504 | } 505 | }, 506 | "content-disposition": { 507 | "version": "0.5.4", 508 | "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", 509 | "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", 510 | "requires": { 511 | "safe-buffer": "5.2.1" 512 | } 513 | }, 514 | "content-type": { 515 | "version": "1.0.4", 516 | "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", 517 | "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==" 518 | }, 519 | "cookie": { 520 | "version": "0.5.0", 521 | "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", 522 | "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==" 523 | }, 524 | "cookie-signature": { 525 | "version": "1.0.6", 526 | "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", 527 | "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" 528 | }, 529 | "cors": { 530 | "version": "2.8.5", 531 | "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", 532 | "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", 533 | "requires": { 534 | "object-assign": "^4", 535 | "vary": "^1" 536 | } 537 | }, 538 | "cron-parser": { 539 | "version": "4.6.0", 540 | "resolved": "https://registry.npmjs.org/cron-parser/-/cron-parser-4.6.0.tgz", 541 | "integrity": "sha512-guZNLMGUgg6z4+eGhmHGw7ft+v6OQeuHzd1gcLxCo9Yg/qoxmG3nindp2/uwGCLizEisf2H0ptqeVXeoCpP6FA==", 542 | "requires": { 543 | "luxon": "^3.0.1" 544 | } 545 | }, 546 | "date-fns": { 547 | "version": "2.29.3", 548 | "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.29.3.tgz", 549 | "integrity": "sha512-dDCnyH2WnnKusqvZZ6+jA1O51Ibt8ZMRNkDZdyAyK4YfbDwa/cEmuztzG5pk6hqlp9aSBPYcjOlktquahGwGeA==", 550 | "dev": true 551 | }, 552 | "debug": { 553 | "version": "2.6.9", 554 | "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", 555 | "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", 556 | "requires": { 557 | "ms": "2.0.0" 558 | } 559 | }, 560 | "debuglog": { 561 | "version": "1.0.1", 562 | "resolved": "https://registry.npmjs.org/debuglog/-/debuglog-1.0.1.tgz", 563 | "integrity": "sha512-syBZ+rnAK3EgMsH2aYEOLUW7mZSY9Gb+0wUMCFsZvcmiz+HigA0LOcq/HoQqVuGG+EKykunc7QG2bzrponfaSw==" 564 | }, 565 | "delayed-stream": { 566 | "version": "1.0.0", 567 | "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", 568 | "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==" 569 | }, 570 | "denque": { 571 | "version": "2.1.0", 572 | "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", 573 | "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==" 574 | }, 575 | "depd": { 576 | "version": "2.0.0", 577 | "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", 578 | "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==" 579 | }, 580 | "destroy": { 581 | "version": "1.2.0", 582 | "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", 583 | "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==" 584 | }, 585 | "dotenv": { 586 | "version": "16.0.3", 587 | "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.0.3.tgz", 588 | "integrity": "sha512-7GO6HghkA5fYG9TYnNxi14/7K9f5occMlp3zXAuSxn7CKCxt9xbNWG7yF8hTCSUchlfWSe3uLmlPfigevRItzQ==" 589 | }, 590 | "ee-first": { 591 | "version": "1.1.1", 592 | "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", 593 | "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" 594 | }, 595 | "emoji-regex": { 596 | "version": "8.0.0", 597 | "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", 598 | "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", 599 | "dev": true 600 | }, 601 | "encodeurl": { 602 | "version": "1.0.2", 603 | "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", 604 | "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==" 605 | }, 606 | "escalade": { 607 | "version": "3.1.1", 608 | "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", 609 | "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", 610 | "dev": true 611 | }, 612 | "escape-html": { 613 | "version": "1.0.3", 614 | "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", 615 | "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" 616 | }, 617 | "etag": { 618 | "version": "1.8.1", 619 | "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", 620 | "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==" 621 | }, 622 | "express": { 623 | "version": "4.18.1", 624 | "resolved": "https://registry.npmjs.org/express/-/express-4.18.1.tgz", 625 | "integrity": "sha512-zZBcOX9TfehHQhtupq57OF8lFZ3UZi08Y97dwFCkD8p9d/d2Y3M+ykKcwaMDEL+4qyUolgBDX6AblpR3fL212Q==", 626 | "requires": { 627 | "accepts": "~1.3.8", 628 | "array-flatten": "1.1.1", 629 | "body-parser": "1.20.0", 630 | "content-disposition": "0.5.4", 631 | "content-type": "~1.0.4", 632 | "cookie": "0.5.0", 633 | "cookie-signature": "1.0.6", 634 | "debug": "2.6.9", 635 | "depd": "2.0.0", 636 | "encodeurl": "~1.0.2", 637 | "escape-html": "~1.0.3", 638 | "etag": "~1.8.1", 639 | "finalhandler": "1.2.0", 640 | "fresh": "0.5.2", 641 | "http-errors": "2.0.0", 642 | "merge-descriptors": "1.0.1", 643 | "methods": "~1.1.2", 644 | "on-finished": "2.4.1", 645 | "parseurl": "~1.3.3", 646 | "path-to-regexp": "0.1.7", 647 | "proxy-addr": "~2.0.7", 648 | "qs": "6.10.3", 649 | "range-parser": "~1.2.1", 650 | "safe-buffer": "5.2.1", 651 | "send": "0.18.0", 652 | "serve-static": "1.15.0", 653 | "setprototypeof": "1.2.0", 654 | "statuses": "2.0.1", 655 | "type-is": "~1.6.18", 656 | "utils-merge": "1.0.1", 657 | "vary": "~1.1.2" 658 | } 659 | }, 660 | "fill-range": { 661 | "version": "7.0.1", 662 | "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", 663 | "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", 664 | "dev": true, 665 | "requires": { 666 | "to-regex-range": "^5.0.1" 667 | } 668 | }, 669 | "finalhandler": { 670 | "version": "1.2.0", 671 | "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", 672 | "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", 673 | "requires": { 674 | "debug": "2.6.9", 675 | "encodeurl": "~1.0.2", 676 | "escape-html": "~1.0.3", 677 | "on-finished": "2.4.1", 678 | "parseurl": "~1.3.3", 679 | "statuses": "2.0.1", 680 | "unpipe": "~1.0.0" 681 | } 682 | }, 683 | "follow-redirects": { 684 | "version": "1.15.2", 685 | "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz", 686 | "integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==" 687 | }, 688 | "form-data": { 689 | "version": "4.0.0", 690 | "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", 691 | "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", 692 | "requires": { 693 | "asynckit": "^0.4.0", 694 | "combined-stream": "^1.0.8", 695 | "mime-types": "^2.1.12" 696 | } 697 | }, 698 | "forwarded": { 699 | "version": "0.2.0", 700 | "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", 701 | "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==" 702 | }, 703 | "fresh": { 704 | "version": "0.5.2", 705 | "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", 706 | "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==" 707 | }, 708 | "fs.realpath": { 709 | "version": "1.0.0", 710 | "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", 711 | "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" 712 | }, 713 | "fsevents": { 714 | "version": "2.3.2", 715 | "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", 716 | "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", 717 | "dev": true, 718 | "optional": true 719 | }, 720 | "function-bind": { 721 | "version": "1.1.1", 722 | "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", 723 | "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" 724 | }, 725 | "get-caller-file": { 726 | "version": "2.0.5", 727 | "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", 728 | "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", 729 | "dev": true 730 | }, 731 | "get-intrinsic": { 732 | "version": "1.1.3", 733 | "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.3.tgz", 734 | "integrity": "sha512-QJVz1Tj7MS099PevUG5jvnt9tSkXN8K14dxQlikJuPt4uD9hHAHjLyLBiLR5zELelBdD9QNRAXZzsJx0WaDL9A==", 735 | "requires": { 736 | "function-bind": "^1.1.1", 737 | "has": "^1.0.3", 738 | "has-symbols": "^1.0.3" 739 | } 740 | }, 741 | "get-port": { 742 | "version": "5.1.1", 743 | "resolved": "https://registry.npmjs.org/get-port/-/get-port-5.1.1.tgz", 744 | "integrity": "sha512-g/Q1aTSDOxFpchXC4i8ZWvxA1lnPqx/JHqcpIw0/LX9T8x/GBbi6YnlN5nhaKIFkT8oFsscUKgDJYxfwfS6QsQ==" 745 | }, 746 | "glob": { 747 | "version": "8.0.3", 748 | "resolved": "https://registry.npmjs.org/glob/-/glob-8.0.3.tgz", 749 | "integrity": "sha512-ull455NHSHI/Y1FqGaaYFaLGkNMMJbavMrEGFXG/PGrg6y7sutWHUHrz6gy6WEBH6akM1M414dWKCNs+IhKdiQ==", 750 | "requires": { 751 | "fs.realpath": "^1.0.0", 752 | "inflight": "^1.0.4", 753 | "inherits": "2", 754 | "minimatch": "^5.0.1", 755 | "once": "^1.3.0" 756 | }, 757 | "dependencies": { 758 | "brace-expansion": { 759 | "version": "2.0.1", 760 | "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", 761 | "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", 762 | "requires": { 763 | "balanced-match": "^1.0.0" 764 | } 765 | }, 766 | "minimatch": { 767 | "version": "5.1.0", 768 | "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.0.tgz", 769 | "integrity": "sha512-9TPBGGak4nHfGZsPBohm9AWg6NoT7QTCehS3BIJABslyZbzxfV78QM2Y6+i741OPZIafFAaiiEMh5OyIrJPgtg==", 770 | "requires": { 771 | "brace-expansion": "^2.0.1" 772 | } 773 | } 774 | } 775 | }, 776 | "glob-parent": { 777 | "version": "5.1.2", 778 | "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", 779 | "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", 780 | "dev": true, 781 | "requires": { 782 | "is-glob": "^4.0.1" 783 | } 784 | }, 785 | "has": { 786 | "version": "1.0.3", 787 | "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", 788 | "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", 789 | "requires": { 790 | "function-bind": "^1.1.1" 791 | } 792 | }, 793 | "has-flag": { 794 | "version": "4.0.0", 795 | "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", 796 | "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", 797 | "dev": true 798 | }, 799 | "has-symbols": { 800 | "version": "1.0.3", 801 | "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", 802 | "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==" 803 | }, 804 | "http-errors": { 805 | "version": "2.0.0", 806 | "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", 807 | "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", 808 | "requires": { 809 | "depd": "2.0.0", 810 | "inherits": "2.0.4", 811 | "setprototypeof": "1.2.0", 812 | "statuses": "2.0.1", 813 | "toidentifier": "1.0.1" 814 | } 815 | }, 816 | "iconv-lite": { 817 | "version": "0.4.24", 818 | "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", 819 | "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", 820 | "requires": { 821 | "safer-buffer": ">= 2.1.2 < 3" 822 | } 823 | }, 824 | "ignore-by-default": { 825 | "version": "1.0.1", 826 | "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", 827 | "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==", 828 | "dev": true 829 | }, 830 | "inflight": { 831 | "version": "1.0.6", 832 | "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", 833 | "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", 834 | "requires": { 835 | "once": "^1.3.0", 836 | "wrappy": "1" 837 | } 838 | }, 839 | "inherits": { 840 | "version": "2.0.4", 841 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", 842 | "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" 843 | }, 844 | "ioredis": { 845 | "version": "5.2.3", 846 | "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.2.3.tgz", 847 | "integrity": "sha512-gQNcMF23/NpvjCaa1b5YycUyQJ9rBNH2xP94LWinNpodMWVUPP5Ai/xXANn/SM7gfIvI62B5CCvZxhg5pOgyMw==", 848 | "requires": { 849 | "@ioredis/commands": "^1.1.1", 850 | "cluster-key-slot": "^1.1.0", 851 | "debug": "^4.3.4", 852 | "denque": "^2.0.1", 853 | "lodash.defaults": "^4.2.0", 854 | "lodash.isarguments": "^3.1.0", 855 | "redis-errors": "^1.2.0", 856 | "redis-parser": "^3.0.0", 857 | "standard-as-callback": "^2.1.0" 858 | }, 859 | "dependencies": { 860 | "debug": { 861 | "version": "4.3.4", 862 | "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", 863 | "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", 864 | "requires": { 865 | "ms": "2.1.2" 866 | } 867 | }, 868 | "ms": { 869 | "version": "2.1.2", 870 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", 871 | "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" 872 | } 873 | } 874 | }, 875 | "ipaddr.js": { 876 | "version": "1.9.1", 877 | "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", 878 | "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==" 879 | }, 880 | "is-binary-path": { 881 | "version": "2.1.0", 882 | "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", 883 | "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", 884 | "dev": true, 885 | "requires": { 886 | "binary-extensions": "^2.0.0" 887 | } 888 | }, 889 | "is-extglob": { 890 | "version": "2.1.1", 891 | "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", 892 | "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", 893 | "dev": true 894 | }, 895 | "is-fullwidth-code-point": { 896 | "version": "3.0.0", 897 | "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", 898 | "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", 899 | "dev": true 900 | }, 901 | "is-glob": { 902 | "version": "4.0.3", 903 | "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", 904 | "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", 905 | "dev": true, 906 | "requires": { 907 | "is-extglob": "^2.1.1" 908 | } 909 | }, 910 | "is-number": { 911 | "version": "7.0.0", 912 | "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", 913 | "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", 914 | "dev": true 915 | }, 916 | "lodash": { 917 | "version": "4.17.21", 918 | "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", 919 | "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" 920 | }, 921 | "lodash.defaults": { 922 | "version": "4.2.0", 923 | "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", 924 | "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==" 925 | }, 926 | "lodash.flatten": { 927 | "version": "4.4.0", 928 | "resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz", 929 | "integrity": "sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g==" 930 | }, 931 | "lodash.isarguments": { 932 | "version": "3.1.0", 933 | "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz", 934 | "integrity": "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==" 935 | }, 936 | "lru-cache": { 937 | "version": "6.0.0", 938 | "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", 939 | "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", 940 | "requires": { 941 | "yallist": "^4.0.0" 942 | } 943 | }, 944 | "luxon": { 945 | "version": "3.0.4", 946 | "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.0.4.tgz", 947 | "integrity": "sha512-aV48rGUwP/Vydn8HT+5cdr26YYQiUZ42NM6ToMoaGKwYfWbfLeRkEu1wXWMHBZT6+KyLfcbbtVcoQFCbbPjKlw==" 948 | }, 949 | "media-typer": { 950 | "version": "0.3.0", 951 | "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", 952 | "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==" 953 | }, 954 | "merge-descriptors": { 955 | "version": "1.0.1", 956 | "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", 957 | "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==" 958 | }, 959 | "methods": { 960 | "version": "1.1.2", 961 | "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", 962 | "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==" 963 | }, 964 | "mime": { 965 | "version": "1.6.0", 966 | "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", 967 | "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==" 968 | }, 969 | "mime-db": { 970 | "version": "1.52.0", 971 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", 972 | "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==" 973 | }, 974 | "mime-types": { 975 | "version": "2.1.35", 976 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", 977 | "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", 978 | "requires": { 979 | "mime-db": "1.52.0" 980 | } 981 | }, 982 | "minimatch": { 983 | "version": "3.1.2", 984 | "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", 985 | "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", 986 | "dev": true, 987 | "requires": { 988 | "brace-expansion": "^1.1.7" 989 | } 990 | }, 991 | "ms": { 992 | "version": "2.0.0", 993 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", 994 | "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" 995 | }, 996 | "msgpackr": { 997 | "version": "1.7.2", 998 | "resolved": "https://registry.npmjs.org/msgpackr/-/msgpackr-1.7.2.tgz", 999 | "integrity": "sha512-mWScyHTtG6TjivXX9vfIy2nBtRupaiAj0HQ2mtmpmYujAmqZmaaEVPaSZ1NKLMvicaMLFzEaMk0ManxMRg8rMQ==", 1000 | "requires": { 1001 | "msgpackr-extract": "^2.1.2" 1002 | } 1003 | }, 1004 | "msgpackr-extract": { 1005 | "version": "2.1.2", 1006 | "resolved": "https://registry.npmjs.org/msgpackr-extract/-/msgpackr-extract-2.1.2.tgz", 1007 | "integrity": "sha512-cmrmERQFb19NX2JABOGtrKdHMyI6RUyceaPBQ2iRz9GnDkjBWFjNJC0jyyoOfZl2U/LZE3tQCCQc4dlRyA8mcA==", 1008 | "optional": true, 1009 | "requires": { 1010 | "@msgpackr-extract/msgpackr-extract-darwin-arm64": "2.1.2", 1011 | "@msgpackr-extract/msgpackr-extract-darwin-x64": "2.1.2", 1012 | "@msgpackr-extract/msgpackr-extract-linux-arm": "2.1.2", 1013 | "@msgpackr-extract/msgpackr-extract-linux-arm64": "2.1.2", 1014 | "@msgpackr-extract/msgpackr-extract-linux-x64": "2.1.2", 1015 | "@msgpackr-extract/msgpackr-extract-win32-x64": "2.1.2", 1016 | "node-gyp-build-optional-packages": "5.0.3" 1017 | } 1018 | }, 1019 | "negotiator": { 1020 | "version": "0.6.3", 1021 | "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", 1022 | "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==" 1023 | }, 1024 | "node-gyp-build-optional-packages": { 1025 | "version": "5.0.3", 1026 | "resolved": "https://registry.npmjs.org/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.0.3.tgz", 1027 | "integrity": "sha512-k75jcVzk5wnnc/FMxsf4udAoTEUv2jY3ycfdSd3yWu6Cnd1oee6/CfZJApyscA4FJOmdoixWwiwOyf16RzD5JA==", 1028 | "optional": true 1029 | }, 1030 | "nodemon": { 1031 | "version": "2.0.20", 1032 | "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-2.0.20.tgz", 1033 | "integrity": "sha512-Km2mWHKKY5GzRg6i1j5OxOHQtuvVsgskLfigG25yTtbyfRGn/GNvIbRyOf1PSCKJ2aT/58TiuUsuOU5UToVViw==", 1034 | "dev": true, 1035 | "requires": { 1036 | "chokidar": "^3.5.2", 1037 | "debug": "^3.2.7", 1038 | "ignore-by-default": "^1.0.1", 1039 | "minimatch": "^3.1.2", 1040 | "pstree.remy": "^1.1.8", 1041 | "semver": "^5.7.1", 1042 | "simple-update-notifier": "^1.0.7", 1043 | "supports-color": "^5.5.0", 1044 | "touch": "^3.1.0", 1045 | "undefsafe": "^2.0.5" 1046 | }, 1047 | "dependencies": { 1048 | "debug": { 1049 | "version": "3.2.7", 1050 | "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", 1051 | "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", 1052 | "dev": true, 1053 | "requires": { 1054 | "ms": "^2.1.1" 1055 | } 1056 | }, 1057 | "has-flag": { 1058 | "version": "3.0.0", 1059 | "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", 1060 | "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", 1061 | "dev": true 1062 | }, 1063 | "ms": { 1064 | "version": "2.1.3", 1065 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", 1066 | "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", 1067 | "dev": true 1068 | }, 1069 | "supports-color": { 1070 | "version": "5.5.0", 1071 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", 1072 | "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", 1073 | "dev": true, 1074 | "requires": { 1075 | "has-flag": "^3.0.0" 1076 | } 1077 | } 1078 | } 1079 | }, 1080 | "nopt": { 1081 | "version": "1.0.10", 1082 | "resolved": "https://registry.npmjs.org/nopt/-/nopt-1.0.10.tgz", 1083 | "integrity": "sha512-NWmpvLSqUrgrAC9HCuxEvb+PSloHpqVu+FqcO4eeF2h5qYRhA7ev6KvelyQAKtegUbC6RypJnlEOhd8vloNKYg==", 1084 | "dev": true, 1085 | "requires": { 1086 | "abbrev": "1" 1087 | } 1088 | }, 1089 | "normalize-path": { 1090 | "version": "3.0.0", 1091 | "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", 1092 | "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", 1093 | "dev": true 1094 | }, 1095 | "object-assign": { 1096 | "version": "4.1.1", 1097 | "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", 1098 | "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==" 1099 | }, 1100 | "object-inspect": { 1101 | "version": "1.12.2", 1102 | "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.2.tgz", 1103 | "integrity": "sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ==" 1104 | }, 1105 | "on-finished": { 1106 | "version": "2.4.1", 1107 | "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", 1108 | "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", 1109 | "requires": { 1110 | "ee-first": "1.1.1" 1111 | } 1112 | }, 1113 | "once": { 1114 | "version": "1.4.0", 1115 | "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", 1116 | "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", 1117 | "requires": { 1118 | "wrappy": "1" 1119 | } 1120 | }, 1121 | "p-finally": { 1122 | "version": "1.0.0", 1123 | "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", 1124 | "integrity": "sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==" 1125 | }, 1126 | "p-map": { 1127 | "version": "2.1.0", 1128 | "resolved": "https://registry.npmjs.org/p-map/-/p-map-2.1.0.tgz", 1129 | "integrity": "sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw==" 1130 | }, 1131 | "p-timeout": { 1132 | "version": "3.2.0", 1133 | "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-3.2.0.tgz", 1134 | "integrity": "sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==", 1135 | "requires": { 1136 | "p-finally": "^1.0.0" 1137 | } 1138 | }, 1139 | "parseurl": { 1140 | "version": "1.3.3", 1141 | "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", 1142 | "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==" 1143 | }, 1144 | "path-to-regexp": { 1145 | "version": "0.1.7", 1146 | "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", 1147 | "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" 1148 | }, 1149 | "picomatch": { 1150 | "version": "2.3.1", 1151 | "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", 1152 | "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", 1153 | "dev": true 1154 | }, 1155 | "proxy-addr": { 1156 | "version": "2.0.7", 1157 | "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", 1158 | "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", 1159 | "requires": { 1160 | "forwarded": "0.2.0", 1161 | "ipaddr.js": "1.9.1" 1162 | } 1163 | }, 1164 | "proxy-from-env": { 1165 | "version": "1.1.0", 1166 | "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", 1167 | "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" 1168 | }, 1169 | "pstree.remy": { 1170 | "version": "1.1.8", 1171 | "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", 1172 | "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", 1173 | "dev": true 1174 | }, 1175 | "qs": { 1176 | "version": "6.10.3", 1177 | "resolved": "https://registry.npmjs.org/qs/-/qs-6.10.3.tgz", 1178 | "integrity": "sha512-wr7M2E0OFRfIfJZjKGieI8lBKb7fRCH4Fv5KNPEs7gJ8jadvotdsS08PzOKR7opXhZ/Xkjtt3WF9g38drmyRqQ==", 1179 | "requires": { 1180 | "side-channel": "^1.0.4" 1181 | } 1182 | }, 1183 | "range-parser": { 1184 | "version": "1.2.1", 1185 | "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", 1186 | "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==" 1187 | }, 1188 | "raw-body": { 1189 | "version": "2.5.1", 1190 | "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz", 1191 | "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==", 1192 | "requires": { 1193 | "bytes": "3.1.2", 1194 | "http-errors": "2.0.0", 1195 | "iconv-lite": "0.4.24", 1196 | "unpipe": "1.0.0" 1197 | } 1198 | }, 1199 | "readdirp": { 1200 | "version": "3.6.0", 1201 | "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", 1202 | "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", 1203 | "dev": true, 1204 | "requires": { 1205 | "picomatch": "^2.2.1" 1206 | } 1207 | }, 1208 | "redis-commands": { 1209 | "version": "1.7.0", 1210 | "resolved": "https://registry.npmjs.org/redis-commands/-/redis-commands-1.7.0.tgz", 1211 | "integrity": "sha512-nJWqw3bTFy21hX/CPKHth6sfhZbdiHP6bTawSgQBlKOVRG7EZkfHbbHwQJnrE4vsQf0CMNE+3gJ4Fmm16vdVlQ==" 1212 | }, 1213 | "redis-errors": { 1214 | "version": "1.2.0", 1215 | "resolved": "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz", 1216 | "integrity": "sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==" 1217 | }, 1218 | "redis-parser": { 1219 | "version": "3.0.0", 1220 | "resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-3.0.0.tgz", 1221 | "integrity": "sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==", 1222 | "requires": { 1223 | "redis-errors": "^1.0.0" 1224 | } 1225 | }, 1226 | "require-directory": { 1227 | "version": "2.1.1", 1228 | "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", 1229 | "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", 1230 | "dev": true 1231 | }, 1232 | "rxjs": { 1233 | "version": "7.5.7", 1234 | "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.5.7.tgz", 1235 | "integrity": "sha512-z9MzKh/UcOqB3i20H6rtrlaE/CgjLOvheWK/9ILrbhROGTweAi1BaFsTT9FbwZi5Trr1qNRs+MXkhmR06awzQA==", 1236 | "dev": true, 1237 | "requires": { 1238 | "tslib": "^2.1.0" 1239 | } 1240 | }, 1241 | "safe-buffer": { 1242 | "version": "5.2.1", 1243 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", 1244 | "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" 1245 | }, 1246 | "safer-buffer": { 1247 | "version": "2.1.2", 1248 | "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", 1249 | "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" 1250 | }, 1251 | "semver": { 1252 | "version": "5.7.1", 1253 | "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", 1254 | "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", 1255 | "dev": true 1256 | }, 1257 | "send": { 1258 | "version": "0.18.0", 1259 | "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", 1260 | "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", 1261 | "requires": { 1262 | "debug": "2.6.9", 1263 | "depd": "2.0.0", 1264 | "destroy": "1.2.0", 1265 | "encodeurl": "~1.0.2", 1266 | "escape-html": "~1.0.3", 1267 | "etag": "~1.8.1", 1268 | "fresh": "0.5.2", 1269 | "http-errors": "2.0.0", 1270 | "mime": "1.6.0", 1271 | "ms": "2.1.3", 1272 | "on-finished": "2.4.1", 1273 | "range-parser": "~1.2.1", 1274 | "statuses": "2.0.1" 1275 | }, 1276 | "dependencies": { 1277 | "ms": { 1278 | "version": "2.1.3", 1279 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", 1280 | "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" 1281 | } 1282 | } 1283 | }, 1284 | "serve-static": { 1285 | "version": "1.15.0", 1286 | "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", 1287 | "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", 1288 | "requires": { 1289 | "encodeurl": "~1.0.2", 1290 | "escape-html": "~1.0.3", 1291 | "parseurl": "~1.3.3", 1292 | "send": "0.18.0" 1293 | } 1294 | }, 1295 | "setprototypeof": { 1296 | "version": "1.2.0", 1297 | "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", 1298 | "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" 1299 | }, 1300 | "shell-quote": { 1301 | "version": "1.7.3", 1302 | "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.7.3.tgz", 1303 | "integrity": "sha512-Vpfqwm4EnqGdlsBFNmHhxhElJYrdfcxPThu+ryKS5J8L/fhAwLazFZtq+S+TWZ9ANj2piSQLGj6NQg+lKPmxrw==", 1304 | "dev": true 1305 | }, 1306 | "side-channel": { 1307 | "version": "1.0.4", 1308 | "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", 1309 | "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", 1310 | "requires": { 1311 | "call-bind": "^1.0.0", 1312 | "get-intrinsic": "^1.0.2", 1313 | "object-inspect": "^1.9.0" 1314 | } 1315 | }, 1316 | "simple-update-notifier": { 1317 | "version": "1.0.7", 1318 | "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-1.0.7.tgz", 1319 | "integrity": "sha512-BBKgR84BJQJm6WjWFMHgLVuo61FBDSj1z/xSFUIozqO6wO7ii0JxCqlIud7Enr/+LhlbNI0whErq96P2qHNWew==", 1320 | "dev": true, 1321 | "requires": { 1322 | "semver": "~7.0.0" 1323 | }, 1324 | "dependencies": { 1325 | "semver": { 1326 | "version": "7.0.0", 1327 | "resolved": "https://registry.npmjs.org/semver/-/semver-7.0.0.tgz", 1328 | "integrity": "sha512-+GB6zVA9LWh6zovYQLALHwv5rb2PHGlJi3lfiqIHxR0uuwCgefcOJc59v9fv1w8GbStwxuuqqAjI9NMAOOgq1A==", 1329 | "dev": true 1330 | } 1331 | } 1332 | }, 1333 | "spawn-command": { 1334 | "version": "0.0.2-1", 1335 | "resolved": "https://registry.npmjs.org/spawn-command/-/spawn-command-0.0.2-1.tgz", 1336 | "integrity": "sha512-n98l9E2RMSJ9ON1AKisHzz7V42VDiBQGY6PB1BwRglz99wpVsSuGzQ+jOi6lFXBGVTCrRpltvjm+/XA+tpeJrg==", 1337 | "dev": true 1338 | }, 1339 | "standard-as-callback": { 1340 | "version": "2.1.0", 1341 | "resolved": "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-2.1.0.tgz", 1342 | "integrity": "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==" 1343 | }, 1344 | "statuses": { 1345 | "version": "2.0.1", 1346 | "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", 1347 | "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==" 1348 | }, 1349 | "string-width": { 1350 | "version": "4.2.3", 1351 | "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", 1352 | "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", 1353 | "dev": true, 1354 | "requires": { 1355 | "emoji-regex": "^8.0.0", 1356 | "is-fullwidth-code-point": "^3.0.0", 1357 | "strip-ansi": "^6.0.1" 1358 | } 1359 | }, 1360 | "strip-ansi": { 1361 | "version": "6.0.1", 1362 | "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", 1363 | "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", 1364 | "dev": true, 1365 | "requires": { 1366 | "ansi-regex": "^5.0.1" 1367 | } 1368 | }, 1369 | "supports-color": { 1370 | "version": "8.1.1", 1371 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", 1372 | "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", 1373 | "dev": true, 1374 | "requires": { 1375 | "has-flag": "^4.0.0" 1376 | } 1377 | }, 1378 | "to-regex-range": { 1379 | "version": "5.0.1", 1380 | "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", 1381 | "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", 1382 | "dev": true, 1383 | "requires": { 1384 | "is-number": "^7.0.0" 1385 | } 1386 | }, 1387 | "toidentifier": { 1388 | "version": "1.0.1", 1389 | "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", 1390 | "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==" 1391 | }, 1392 | "touch": { 1393 | "version": "3.1.0", 1394 | "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.0.tgz", 1395 | "integrity": "sha512-WBx8Uy5TLtOSRtIq+M03/sKDrXCLHxwDcquSP2c43Le03/9serjQBIztjRz6FkJez9D/hleyAXTBGLwwZUw9lA==", 1396 | "dev": true, 1397 | "requires": { 1398 | "nopt": "~1.0.10" 1399 | } 1400 | }, 1401 | "tree-kill": { 1402 | "version": "1.2.2", 1403 | "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", 1404 | "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", 1405 | "dev": true 1406 | }, 1407 | "tslib": { 1408 | "version": "2.4.0", 1409 | "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", 1410 | "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==" 1411 | }, 1412 | "type-is": { 1413 | "version": "1.6.18", 1414 | "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", 1415 | "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", 1416 | "requires": { 1417 | "media-typer": "0.3.0", 1418 | "mime-types": "~2.1.24" 1419 | } 1420 | }, 1421 | "typescript": { 1422 | "version": "4.8.4", 1423 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.8.4.tgz", 1424 | "integrity": "sha512-QCh+85mCy+h0IGff8r5XWzOVSbBO+KfeYrMQh7NJ58QujwcE22u+NUSmUxqF+un70P9GXKxa2HCNiTTMJknyjQ==", 1425 | "dev": true 1426 | }, 1427 | "undefsafe": { 1428 | "version": "2.0.5", 1429 | "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", 1430 | "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", 1431 | "dev": true 1432 | }, 1433 | "unpipe": { 1434 | "version": "1.0.0", 1435 | "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", 1436 | "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==" 1437 | }, 1438 | "utils-merge": { 1439 | "version": "1.0.1", 1440 | "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", 1441 | "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==" 1442 | }, 1443 | "uuid": { 1444 | "version": "8.3.2", 1445 | "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", 1446 | "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==" 1447 | }, 1448 | "vary": { 1449 | "version": "1.1.2", 1450 | "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", 1451 | "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==" 1452 | }, 1453 | "wrap-ansi": { 1454 | "version": "7.0.0", 1455 | "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", 1456 | "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", 1457 | "dev": true, 1458 | "requires": { 1459 | "ansi-styles": "^4.0.0", 1460 | "string-width": "^4.1.0", 1461 | "strip-ansi": "^6.0.0" 1462 | } 1463 | }, 1464 | "wrappy": { 1465 | "version": "1.0.2", 1466 | "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", 1467 | "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" 1468 | }, 1469 | "y18n": { 1470 | "version": "5.0.8", 1471 | "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", 1472 | "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", 1473 | "dev": true 1474 | }, 1475 | "yallist": { 1476 | "version": "4.0.0", 1477 | "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", 1478 | "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" 1479 | }, 1480 | "yargs": { 1481 | "version": "17.6.0", 1482 | "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.6.0.tgz", 1483 | "integrity": "sha512-8H/wTDqlSwoSnScvV2N/JHfLWOKuh5MVla9hqLjK3nsfyy6Y4kDSYSvkU5YCUEPOSnRXfIyx3Sq+B/IWudTo4g==", 1484 | "dev": true, 1485 | "requires": { 1486 | "cliui": "^8.0.1", 1487 | "escalade": "^3.1.1", 1488 | "get-caller-file": "^2.0.5", 1489 | "require-directory": "^2.1.1", 1490 | "string-width": "^4.2.3", 1491 | "y18n": "^5.0.5", 1492 | "yargs-parser": "^21.0.0" 1493 | } 1494 | }, 1495 | "yargs-parser": { 1496 | "version": "21.1.1", 1497 | "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", 1498 | "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", 1499 | "dev": true 1500 | } 1501 | } 1502 | } 1503 | -------------------------------------------------------------------------------- /server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hn-notify", 3 | "version": "1.0.0", 4 | "description": "Notifies you via Push & Email whenever someone replies to you on HackerNews", 5 | "main": "dist/server.js", 6 | "scripts": { 7 | "build": "npx tsc", 8 | "start": "node dist/server.js", 9 | "dev": "concurrently \"npx tsc --watch\" \"nodemon -q dist/index.js\"" 10 | }, 11 | "author": "anandrmedia@gmail.com", 12 | "license": "MIT", 13 | "dependencies": { 14 | "axios": "^1.0.0", 15 | "bull": "^4.10.0", 16 | "bullmq": "^2.1.3", 17 | "cors": "^2.8.5", 18 | "dotenv": "^16.0.3", 19 | "express": "^4.18.1", 20 | "ioredis": "^5.2.3" 21 | }, 22 | "devDependencies": { 23 | "@types/bull": "^3.15.9", 24 | "@types/cors": "^2.8.12", 25 | "@types/express": "^4.17.14", 26 | "@types/node": "^18.8.2", 27 | "concurrently": "^7.4.0", 28 | "nodemon": "^2.0.20", 29 | "typescript": "^4.8.4" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /server/src/api.listener.ts: -------------------------------------------------------------------------------- 1 | import axios from "axios" 2 | import { AppConfig } from "./config" 3 | 4 | export const getMaxItemId = () => { 5 | return axios.get(AppConfig.HN_API_MAX_ITEM_URL).then((response) => { 6 | return response.data as number 7 | }).catch((error) => { 8 | throw error as Error 9 | }) 10 | } -------------------------------------------------------------------------------- /server/src/config.ts: -------------------------------------------------------------------------------- 1 | import dotenv from 'dotenv'; 2 | import path from 'path'; 3 | dotenv.config({ path: path.join(__dirname, '../.env') }); 4 | 5 | interface AppConfig{ 6 | DEBUG: number 7 | APP_PORT : number 8 | REDIS_HOST: string 9 | REDIS_PORT: number 10 | REDIS_PASSWORD: string 11 | HN_API_MAX_ITEM_URL: string 12 | HN_API_FETCH_ITEM_URL: string 13 | ENGAGESPOT_API_KEY: string 14 | ENGAGESPOT_API_SECRET: string 15 | } 16 | 17 | export const AppConfig: AppConfig = { 18 | DEBUG: process.env.DEBUG as unknown as number || 0, 19 | APP_PORT : process.env.APP_PORT as unknown as number || 3000, 20 | REDIS_HOST : process.env.REDIS_HOST || '127.0.0.1', 21 | REDIS_PORT : process.env.REDIS_PORT as unknown as number || 6379, 22 | REDIS_PASSWORD : process.env.REDIS_PASSWORD || '', 23 | HN_API_MAX_ITEM_URL : 'https://hacker-news.firebaseio.com/v0/maxitem.json', 24 | HN_API_FETCH_ITEM_URL : 'https://hacker-news.firebaseio.com/v0/item/', 25 | ENGAGESPOT_API_KEY: process.env.ENGAGESPOT_API_KEY as unknown as string, 26 | ENGAGESPOT_API_SECRET: process.env.ENGAGESPOT_API_SECRET as unknown as string 27 | } 28 | 29 | -------------------------------------------------------------------------------- /server/src/engagespot.ts: -------------------------------------------------------------------------------- 1 | import axios from "axios" 2 | import { AppConfig } from "./config" 3 | import { logger } from "./logger"; 4 | 5 | export const engagespot = { 6 | send: async (recipient: string, templateId: number, data: any) =>{ 7 | axios.post('https://api.engagespot.co/v3/notifications',{ 8 | notification: { 9 | templateId: templateId 10 | }, 11 | "recipients": [ 12 | recipient 13 | ], 14 | data 15 | },{ 16 | headers:{ 17 | 'X-ENGAGESPOT-API-KEY': AppConfig.ENGAGESPOT_API_KEY, 18 | 'X-ENGAGESPOT-API-SECRET': AppConfig.ENGAGESPOT_API_SECRET 19 | } 20 | }).then((response) => { 21 | return true; 22 | //logger(response.data) 23 | }).catch((error) => { 24 | logger(error.response); 25 | }) 26 | } 27 | } -------------------------------------------------------------------------------- /server/src/logger.ts: -------------------------------------------------------------------------------- 1 | import { AppConfig } from "./config" 2 | 3 | export const logger = (message: string) => { 4 | if(AppConfig.DEBUG == 1){ 5 | console.log(message) 6 | } 7 | } -------------------------------------------------------------------------------- /server/src/processor.ts: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | import { DoneCallback, Job } from "bull"; 3 | import { Processor } from "bullmq"; 4 | import { getMaxItemId } from "./api.listener"; 5 | import { AppConfig } from "./config"; 6 | import { engagespot } from "./engagespot"; 7 | import { logger } from "./logger"; 8 | import { queue } from "./queue"; 9 | import { redis } from "./redis"; 10 | 11 | export const processor = async (job: any) => { 12 | 13 | logger('processor started'); 14 | 15 | if(job.data.name === 'hn_api_listener'){ 16 | 17 | getMaxItemId().then(async (maxItemId) => { 18 | 19 | logger('latest maxItemId received as '+maxItemId); 20 | 21 | const lastMaxId = await redis.get('lastMaxId') as unknown as number ||(maxItemId-1); 22 | logger('looping back till '+lastMaxId); 23 | 24 | for(let i=maxItemId; i>lastMaxId; i--){ 25 | logger("Pushing to fetch_and_notify with id "+i); 26 | await queue.add('fetch_and_notify',{ 27 | name:'fetch_and_notify', 28 | id:i 29 | },{ 30 | removeOnComplete: true 31 | }); 32 | } 33 | 34 | await redis.set('lastMaxId',maxItemId); 35 | return Promise.resolve(true); 36 | }).catch((error) => { 37 | return Promise.reject(error); 38 | }) 39 | } 40 | 41 | if(job.data.name === 'fetch_and_notify'){ 42 | 43 | logger('fetch_and_notify job received for id '+job.data.id); 44 | axios.get(AppConfig.HN_API_FETCH_ITEM_URL+job.data.id+'.json').then((response) => { 45 | 46 | if(response.data.type != 'comment'){ 47 | return Promise.resolve(true); 48 | } 49 | 50 | const commentBy = response.data.by; 51 | const commentText = response.data.text; 52 | 53 | //Find it's parent 54 | axios.get(AppConfig.HN_API_FETCH_ITEM_URL+response.data.parent+'.json').then(async (response) => { 55 | const to = response.data.by; 56 | const id = response.data.id; 57 | const type = response.data.type; 58 | const title = response.data.title; 59 | 60 | let users: string[] | null = await redis.lrange('users',0,-1); 61 | 62 | //Send notification only if they're using our extension 63 | if(users.indexOf(to) > -1 && (to != commentBy)){ 64 | users = null; 65 | //Notify this user. 66 | const notificationTitle = ''+commentBy+' replied to your '+type 67 | const notificationBody = commentText; 68 | 69 | logger(notificationTitle); 70 | await engagespot.send(to,8520, { 71 | user: commentBy, 72 | comment: commentText, 73 | storyTitle: title.slice(0,15)+'...', 74 | url: 'https://news.ycombinator.com/item?id='+id 75 | }); 76 | return Promise.resolve(true); 77 | }else{ 78 | logger("user not in list "+to); 79 | return Promise.resolve(true); 80 | } 81 | 82 | }).catch((error) => { 83 | return Promise.reject(error); 84 | }); 85 | 86 | }).catch((error) => { 87 | return Promise.reject(error); 88 | }) 89 | 90 | } 91 | } -------------------------------------------------------------------------------- /server/src/queue.ts: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | import { Job, Queue, Worker } from "bullmq"; 3 | import path from "path"; 4 | import { getMaxItemId } from "./api.listener"; 5 | import { AppConfig } from "./config"; 6 | import { engagespot } from "./engagespot"; 7 | import { logger } from "./logger"; 8 | import { processor } from "./processor"; 9 | import { redis } from "./redis"; 10 | 11 | //Start the Queue 12 | export const queue = new Queue('hn_worker_queue', { 13 | connection:{ 14 | host: AppConfig.REDIS_HOST, 15 | port: AppConfig.REDIS_PORT 16 | } 17 | }); 18 | 19 | queue.drain(); 20 | 21 | export const worker = new Worker('hn_worker_queue',async (job: Job) => { 22 | 23 | logger('processor started'); 24 | 25 | if(job.data.name === 'hn_api_listener'){ 26 | 27 | getMaxItemId().then(async (maxItemId) => { 28 | 29 | logger('latest maxItemId received as '+maxItemId); 30 | 31 | const lastMaxId = await redis.get('lastMaxId') as unknown as number ||(maxItemId-1); 32 | logger('looping back till '+lastMaxId); 33 | 34 | for(let i=maxItemId; i>lastMaxId; i--){ 35 | logger("Pushing to fetch_and_notify with id "+i); 36 | await queue.add('fetch_and_notify',{ 37 | name:'fetch_and_notify', 38 | id:i 39 | },{ 40 | removeOnComplete: true 41 | }); 42 | } 43 | 44 | await redis.set('lastMaxId',maxItemId); 45 | return Promise.resolve(true); 46 | }).catch((error) => { 47 | return Promise.reject(error); 48 | }) 49 | } 50 | 51 | if(job.data.name === 'fetch_and_notify'){ 52 | 53 | logger('fetch_and_notify job received for id '+job.data.id); 54 | axios.get(AppConfig.HN_API_FETCH_ITEM_URL+job.data.id+'.json').then((response) => { 55 | 56 | if(response?.data?.type != 'comment'){ 57 | return Promise.resolve(true); 58 | } 59 | 60 | const commentBy = response.data.by; 61 | const commentText = response.data.text; 62 | 63 | //Find it's parent 64 | axios.get(AppConfig.HN_API_FETCH_ITEM_URL+response.data.parent+'.json').then(async (response) => { 65 | const to = response.data.by; 66 | const id = response.data.id; 67 | const type = response.data.type; 68 | const title = response.data.title; 69 | 70 | let users: string[] | null = await redis.lrange('users',0,-1); 71 | 72 | //Send notification only if they're using our extension 73 | if(users.indexOf(to) > -1 && (to != commentBy)){ 74 | users = null; 75 | //Notify this user. 76 | const notificationTitle = commentBy+' replied to your '+type 77 | const notificationBody = commentText; 78 | 79 | logger(notificationTitle); 80 | await engagespot.send(to,8520, { 81 | user: commentBy, 82 | comment: commentText, 83 | storyTitle: title.slice(0,15)+'...', 84 | url: 'https://news.ycombinator.com/item?id='+id 85 | }); 86 | return Promise.resolve(true); 87 | }else{ 88 | logger("user not in list "+to); 89 | return Promise.resolve(true); 90 | } 91 | 92 | }).catch((error) => { 93 | return Promise.reject(error); 94 | }); 95 | 96 | }).catch((error) => { 97 | return Promise.reject(error); 98 | }) 99 | 100 | } 101 | }, { 102 | connection:{ 103 | host: AppConfig.REDIS_HOST, 104 | port: AppConfig.REDIS_PORT 105 | } 106 | } ); -------------------------------------------------------------------------------- /server/src/redis.ts: -------------------------------------------------------------------------------- 1 | import Redis from "ioredis"; 2 | import { AppConfig } from "./config"; 3 | export const redis = new Redis(AppConfig.REDIS_PORT, AppConfig.REDIS_HOST) -------------------------------------------------------------------------------- /server/src/server.ts: -------------------------------------------------------------------------------- 1 | import express, { Express, Request, Response } from 'express'; 2 | import { AppConfig } from './config'; 3 | import {Queue, Worker} from "bullmq" 4 | import { logger } from './logger'; 5 | import path from 'path'; 6 | import { queue } from './queue'; 7 | import { redis } from './redis'; 8 | import cors from "cors" 9 | 10 | const app: Express = express(); 11 | const port = AppConfig.APP_PORT; 12 | 13 | //Add HN API Listener which executes every 5 seconds. 14 | logger('Adding hn_api_listener to queue'); 15 | queue.add('hn_api_listener',{ 16 | name:'hn_api_listener', 17 | },{ 18 | repeat:{ 19 | every: 5000 20 | } 21 | }); 22 | 23 | app.use(cors({ 24 | origin: '*' 25 | })); 26 | app.use(express.json()) 27 | 28 | //Initialize a default route for health checking 29 | app.get('/', (req: Request, res: Response) => { 30 | res.send('HN-Notify running!'); 31 | }); 32 | 33 | //Initialize a default route for health checking 34 | app.post('/register', async (req: Request, res: Response) => { 35 | const users = await redis.lrange('users',0,-1); 36 | 37 | console.log("Redis fetch succeeded. Now checking if user exists"); 38 | 39 | if(users.indexOf(req.body.username) != -1){ 40 | console.log("sending"); 41 | return res.status(200).send('ok'); 42 | }else{ 43 | await redis.rpush('users',req.body.username).catch((err: any) => console.log("Redis failed", err)); 44 | console.log("sending 2"); 45 | return res.status(200).send('ok'); 46 | } 47 | }); 48 | 49 | app.listen(port, () => { 50 | console.log(`⚡️[server]: Server is running at https://localhost:${port}`); 51 | }); 52 | -------------------------------------------------------------------------------- /server/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig to read more about this file */ 4 | 5 | /* Projects */ 6 | // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ 7 | // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ 8 | // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ 9 | // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ 10 | // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ 11 | // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ 12 | 13 | /* Language and Environment */ 14 | "target": "es2016", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ 15 | // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ 16 | // "jsx": "preserve", /* Specify what JSX code is generated. */ 17 | // "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */ 18 | // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ 19 | // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ 20 | // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ 21 | // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ 22 | // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ 23 | // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ 24 | // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ 25 | // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ 26 | 27 | /* Modules */ 28 | "module": "commonjs", /* Specify what module code is generated. */ 29 | "rootDir": "./src", /* Specify the root folder within your source files. */ 30 | // "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */ 31 | // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ 32 | // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ 33 | // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ 34 | // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ 35 | // "types": [], /* Specify type package names to be included without being referenced in a source file. */ 36 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 37 | // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ 38 | // "resolveJsonModule": true, /* Enable importing .json files. */ 39 | // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ 40 | 41 | /* JavaScript Support */ 42 | // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ 43 | // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ 44 | // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ 45 | 46 | /* Emit */ 47 | // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ 48 | // "declarationMap": true, /* Create sourcemaps for d.ts files. */ 49 | // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ 50 | // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ 51 | // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ 52 | "outDir": "./dist", /* Specify an output folder for all emitted files. */ 53 | // "removeComments": true, /* Disable emitting comments. */ 54 | // "noEmit": true, /* Disable emitting files from a compilation. */ 55 | // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ 56 | // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */ 57 | // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ 58 | // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ 59 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 60 | // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ 61 | // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ 62 | // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ 63 | // "newLine": "crlf", /* Set the newline character for emitting files. */ 64 | // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ 65 | // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ 66 | // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ 67 | // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ 68 | // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ 69 | // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ 70 | 71 | /* Interop Constraints */ 72 | // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ 73 | // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ 74 | "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */ 75 | // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ 76 | "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ 77 | 78 | /* Type Checking */ 79 | "strict": true, /* Enable all strict type-checking options. */ 80 | // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ 81 | // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ 82 | // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ 83 | // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ 84 | // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ 85 | // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ 86 | // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ 87 | // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ 88 | // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ 89 | // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ 90 | // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ 91 | // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ 92 | // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ 93 | // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ 94 | // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ 95 | // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ 96 | // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ 97 | // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ 98 | 99 | /* Completeness */ 100 | // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ 101 | "skipLibCheck": true /* Skip type checking all .d.ts files. */ 102 | } 103 | } 104 | --------------------------------------------------------------------------------