├── .babelrc ├── .circleci └── config.yml ├── .eslintignore ├── .eslintrc ├── .gitignore ├── .npmignore ├── .nvmrc ├── .prettierrc ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── fixtures ├── event.json └── scheduled.json ├── jest.config.js ├── package.json ├── serverless.yml ├── src ├── invoke.ts ├── types │ ├── @postlight │ │ └── mercury-parser │ │ │ └── index.d.ts │ ├── index.ts │ └── lambda.ts ├── uploadPost.ts └── utils │ ├── getSlackData.ts │ ├── getUrlContent.ts │ ├── index.ts │ ├── lambda-response.ts │ ├── run-warm.ts │ └── validateUrl.ts ├── tsconfig.json ├── tslint.json ├── webpack.config.js └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "@babel/preset-env", 5 | { 6 | "targets": { 7 | "node": "8.10" 8 | } 9 | } 10 | ] 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | jobs: 3 | build: 4 | docker: 5 | - image: circleci/node:8.10 6 | steps: 7 | - checkout 8 | - run: yarn install 9 | - run: yarn build 10 | test: 11 | docker: 12 | - image: circleci/node:8.10 13 | steps: 14 | - checkout 15 | - run: yarn install 16 | - run: yarn lint 17 | - run: yarn test 18 | - run: yarn serverless invoke local --function hello 19 | workflows: 20 | version: 2 21 | build_and_test: 22 | jobs: 23 | - build 24 | - test 25 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | src/**/*.ts 2 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "airbnb", 4 | "prettier" 5 | ], 6 | "globals": { 7 | "afterAll": true, 8 | "afterEach": true, 9 | "beforeAll": true, 10 | "beforeEach": true, 11 | "describe": true, 12 | "expect": true, 13 | "fit": true, 14 | "it": true, 15 | "jasmine": true, 16 | "xit": true, 17 | "jest": true 18 | }, 19 | "settings": { 20 | "plugins": [ 21 | "import" 22 | ], 23 | "rules": { 24 | "import/no-unresolved": "error" 25 | }, 26 | "import/resolver": { 27 | "typescript": {}, 28 | } 29 | } 30 | } 31 | 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .env 3 | .webpack 4 | .serverless 5 | .env* 6 | .build 7 | secrets.json 8 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # package directories 2 | node_modules 3 | jspm_packages 4 | 5 | # Serverless directories 6 | .serverless -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | v8.10.0 2 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | singleQuote: true, 3 | trailingComma: "es5" 4 | } 5 | -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2019 Postlight 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Postlight 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Parser 2 | 3 | This is a [serverless](https://serverless.com/) Slack Bot that uses Postlight's [mercury parser](https://github.com/postlight/mercury-parser) to display the parsed content directly in your slack channel! So instead of sending a link to a certain article that will open in a new browser window, you will be sending the article as a readable post directly. 4 | 5 | Once everything is set, use it as follows: 6 | 7 | ```bash 8 | /parser 9 | ``` 10 | 11 | ![bot-demo](https://user-images.githubusercontent.com/32297675/54197472-b740b880-44cc-11e9-9d5a-f413ca3cff52.gif) 12 | 13 | ## Development 14 | 15 | This repo is based on Postlight's [serverless-babel-starter](https://github.com/postlight/serverless-babel-starter). You can refer to it for serverless development documentation. 16 | 17 | ### Setting up the workspace 18 | 19 | clone this repo: 20 | 21 | ```bash 22 | git clone https://github.com/postlight/slash-mercury-parser.git 23 | ``` 24 | 25 | Install dependencies: 26 | 27 | ```bash 28 | yarn install 29 | ``` 30 | 31 | ### Creating your Slack Bot 32 | 33 | Create a new slack app [here](https://api.slack.com/apps?new_app=1) and link it to your development slack workspace. 34 | 35 | Under `Add feature and functionality`: 36 | 37 | - Add a bot: 38 | 39 | - Click on `Bots` 40 | - Click on `Add a bot User` 41 | - Enter a `Display name` and `Default username` 42 | - Click on `Add bot User` 43 |   44 | 45 | - Add a slash command 46 | - Click on `Slash Commands` 47 | - Click on `Create New Command` 48 | - Enter `/parser`, `https://example.com/invoke`, and `Renders the parsed content` for `Command`, `Request URL`, and `Short Description` 49 | - Click on `Save` 50 | 51 | **NOTE:** you will fill in a correct `Request URL` once you deploy your functions. You can leave it as is for now. 52 | 53 | ### Environment variables 54 | 55 | After you've installed the bot in your slack workspace, navigate to `Install App` under `Settings` and copy your `Bot User OAuth Access Token`. This token will be used to verify the slack API call. 56 | 57 | Create a `secrets.json` file in the project root and add the following: 58 | 59 | ```json 60 | { 61 | "SLACK_AUTH_TOKEN": "xxxx-YOUR-ACCESS-TOKEN" 62 | } 63 | ``` 64 | 65 | ### Deploying the functions 66 | 67 | ```bash 68 | yarn deploy:env 69 | ``` 70 | 71 | Windows users should modify the `deploy` scripts as follows: 72 | 73 | ```json 74 | { 75 | "deploy:dev": "sls deploy --stage dev", 76 | "deploy:stage": "sls deploy --stage stage", 77 | "deploy:production": "sls deploy --stage production" 78 | } 79 | ``` 80 | 81 | **NOTE:** save your `/post` URL when the deployment is complete. It looks like this: `https://xxxxxxx.execute-api.region.amazonaws.com/env/invoke` and replace your `Request URL` with it. 82 | 83 | ### Testing the bot 84 | 85 | Inside a `public` channel in your slack workspace, invoke the bot using: 86 | 87 | ```bash 88 | /parser 89 | ``` 90 | 91 | You should see a `Parsing your article ..` message which is only visible to you, followed by a bot response of the parsed content as a post. 92 | 93 | ## The logic behind a slack bot 94 | 95 | - Whenever the slash command is executed, slack makes a `POST` request to your app via the `Request URL` that was set. 96 | - Your logic will get executed and will invoke a slack API call. 97 | - Slack expects a response within 3 seconds 98 | - [Slack API Documentation](https://api.slack.com/web) 99 | 100 | ## The logic behind the parser bot 101 | 102 | Since Slack expects an `OK` response within 3 seconds, the first lambda function `./src/invoke.js` checks for any errors in the URL, fetches the content from mercury, and invokes the second function `./src/uploadPost.js`. The second function is responsible for hitting the slack API and sending the post. If an error occurs, the user will be notified. 103 | 104 | For example, `/parser blabla` will result in this error: 105 | 106 | ![image](https://user-images.githubusercontent.com/32297675/54199202-ff61da00-44d0-11e9-8161-288152b424c9.png) 107 | 108 | ## Contributing 109 | 110 | Unless it is explicitly stated otherwise, any contribution intentionally submitted for inclusion in the work, as defined in the Apache-2.0 license, shall be dual licensed as above without any additional terms or conditions. 111 | 112 | --- 113 | 114 | 🔬 A Labs project from your friends at [Postlight](https://postlight.com/labs) 115 | -------------------------------------------------------------------------------- /fixtures/event.json: -------------------------------------------------------------------------------- 1 | { 2 | "key3": "value3", 3 | "key2": "value2", 4 | "key1": "value1" 5 | } 6 | -------------------------------------------------------------------------------- /fixtures/scheduled.json: -------------------------------------------------------------------------------- 1 | { 2 | "source": "aws.events" 3 | } 4 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "roots": [ 3 | "/src" 4 | ], 5 | "testRegex": "(.*\\.test\\.(tsx?|jsx?))$", 6 | "transform": { 7 | "^.+\\.tsx?$": "ts-jest", 8 | "^.+\\.jsx?$": "babel-jest" 9 | }, 10 | "moduleFileExtensions": [ 11 | "ts", 12 | "tsx", 13 | "js", 14 | "jsx", 15 | "json", 16 | "node" 17 | ], 18 | } 19 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "slash-mercury-parser", 3 | "version": "1.0.1", 4 | "main": "src/handler.js", 5 | "license": "MIT", 6 | "scripts": { 7 | "deploy:env": "sls deploy --stage production", 8 | "deploy": "export NODE_ENV=dev && yarn deploy:env", 9 | "deploy:production": "export NODE_ENV=production && yarn deploy:env", 10 | "deploy:stage": "export NODE_ENV=stage && yarn deploy:env", 11 | "lint": "tslint -c tslint.json --fix './src/**/*.ts' && eslint ./src --fix", 12 | "precommit": "lint-staged", 13 | "serve": "serverless offline start", 14 | "serve:watch": "nodemon -e js,ts,jsx,tsx -x serverless offline start", 15 | "tail:parse": "serverless logs --function parse --tail", 16 | "test": "jest", 17 | "test:watch": "jest --watch", 18 | "build": "serverless webpack", 19 | "watch:parse": "serverless invoke local --watch --function parse --path fixtures/event.json", 20 | "watch:warm": "serverless invoke local --watch --function parse --path fixtures/scheduled.json" 21 | }, 22 | "devDependencies": { 23 | "@babel/core": "7.2.2", 24 | "@babel/preset-env": "7.3.1", 25 | "@types/jest": "^24.0.0", 26 | "babel-core": "^7.0.0-bridge.0", 27 | "babel-jest": "^23.4.2", 28 | "babel-loader": "^8.0.0", 29 | "eslint": "^5.4.0", 30 | "eslint-config-airbnb": "^17.1.0", 31 | "eslint-config-prettier": "^4.0.0", 32 | "eslint-import-resolver-typescript": "^1.1.1", 33 | "eslint-plugin-import": "^2.14.0", 34 | "eslint-plugin-jsx-a11y": "^6.1.1", 35 | "eslint-plugin-react": "^7.11.0", 36 | "husky": "^1.0.0", 37 | "jest": "^23.5.0", 38 | "lint-staged": "^8.0.0", 39 | "nodemon": "^1.18.9", 40 | "prettier": "^1.14.2", 41 | "serverless": "^3.27.0", 42 | "serverless-offline": "^4.8.1", 43 | "serverless-webpack": "^5.2.0", 44 | "ts-jest": "^24.0.0", 45 | "ts-loader": "^5.3.1", 46 | "tslint": "^5.11.0", 47 | "tslint-config-prettier": "^1.17.0", 48 | "tslint-react": "^3.6.0", 49 | "typescript": "^3.2.1", 50 | "webpack": "^4.17.1", 51 | "webpack-node-externals": "^1.7.2" 52 | }, 53 | "dependencies": { 54 | "@postlight/mercury-parser": "^2.0.0", 55 | "@types/aws-lambda": "^8.10.21", 56 | "@types/dotenv": "^6.1.0", 57 | "@types/qs": "^6.5.2", 58 | "@types/request": "^2.48.1", 59 | "@types/request-promise": "^4.1.42", 60 | "aws-lambda": "^0.1.2", 61 | "aws-sdk": "^2.417.0", 62 | "body-parser": "^1.18.3", 63 | "dotenv": "^6.2.0", 64 | "qs": "^6.6.0", 65 | "request": "^2.88.0", 66 | "request-promise": "^4.2.4" 67 | }, 68 | "lint-staged": { 69 | "src/**/*.js": [ 70 | "yarn lint", 71 | "prettier --write", 72 | "git add" 73 | ] 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /serverless.yml: -------------------------------------------------------------------------------- 1 | # Welcome to Serverless! 2 | # 3 | # This file is the main config file for your service. 4 | # It's very minimal at this point and uses default values. 5 | # You can always add more config options for more control. 6 | # We've included some commented out config examples here. 7 | # Just uncomment any of them to get that config option. 8 | # 9 | # For full config options, check the docs: 10 | # docs.serverless.com 11 | # 12 | # Happy Coding! 13 | 14 | service: slash-mercury-parser 15 | 16 | # You can pin your service to only deploy with a specific Serverless version 17 | # Check out our docs for more details 18 | # frameworkVersion: "=X.X.X" 19 | 20 | provider: 21 | name: aws 22 | runtime: 'nodejs14.x' 23 | # If you want to change to a different AWS profile 24 | # from ~/.aws/credentials, you can do so here 25 | profile: default 26 | # you can overwrite defaults here 27 | # stage: dev 28 | # region: us-east-1 29 | 30 | # Allow functions to invoke other functions 31 | iamRoleStatements: 32 | - Effect: Allow 33 | Action: 34 | - lambda:InvokeFunction 35 | - lambda:InvokeAsync 36 | - sts:AssumeRole 37 | Resource: '*' 38 | 39 | custom: 40 | webpackIncludeModules: true 41 | webpack: 42 | webpackConfig: ./webpack.config.js 43 | packager: 'yarn' # Packager that will be used to package your external modules 44 | secrets: ${file(secrets.json)} 45 | 46 | # you can add statements to the Lambda function's IAM Role here 47 | # iamRoleStatements: 48 | # - Effect: "Allow" 49 | # Action: 50 | # - "s3:ListBucket" 51 | # Resource: { "Fn::Join" : ["", ["arn:aws:s3:::", { "Ref" : "ServerlessDeploymentBucket" } ] ] } 52 | # - Effect: "Allow" 53 | # Action: 54 | # - "s3:PutObject" 55 | # Resource: 56 | # Fn::Join: 57 | # - "" 58 | # - - "arn:aws:s3:::" 59 | # - "Ref" : "ServerlessDeploymentBucket" 60 | 61 | # you can define service wide environment variables here 62 | # environment: 63 | # variable1: value1 64 | 65 | # you can add packaging information here 66 | #package: 67 | # include: 68 | # - include-me.js 69 | # - include-me-dir/** 70 | # exclude: 71 | # - exclude-me.js 72 | # - exclude-me-dir/** 73 | 74 | functions: 75 | invoke: 76 | handler: src/invoke.default 77 | environment: 78 | SLACK_AUTH_TOKEN: ${self:custom.secrets.SLACK_AUTH_TOKEN} 79 | events: 80 | - http: 81 | path: invoke 82 | method: post 83 | # Ping every 5 minutes to avoid cold starts 84 | - schedule: 85 | rate: rate(5 minutes) 86 | enabled: true 87 | uploadPost: 88 | handler: src/uploadPost.default 89 | events: 90 | - http: 91 | path: uploadPost 92 | method: post 93 | # Ping every 5 minutes to avoid cold starts 94 | - schedule: 95 | rate: rate(5 minutes) 96 | enabled: true 97 | 98 | # The following are a few example events you can configure 99 | # NOTE: Please make sure to change your handler code to work with those events 100 | # Check the event documentation for details 101 | # events: 102 | # - http: 103 | # path: users/create 104 | # method: get 105 | # - s3: ${env:BUCKET} 106 | # - schedule: rate(10 minutes) 107 | # - sns: greeter-topic 108 | # - stream: arn:aws:dynamodb:region:XXXXXX:table/foo/stream/1970-01-01T00:00:00.000 109 | 110 | # Define function environment variables here 111 | 112 | # you can add CloudFormation resource templates here 113 | # resources: 114 | # Resources: 115 | # NewResource: 116 | # Type: AWS::S3::Bucket 117 | # Properties: 118 | # BucketName: my-new-bucket 119 | # Outputs: 120 | # NewOutput: 121 | # Description: "Description for the output" 122 | # Value: "Some output value" 123 | 124 | plugins: 125 | - serverless-webpack 126 | - serverless-offline 127 | -------------------------------------------------------------------------------- /src/invoke.ts: -------------------------------------------------------------------------------- 1 | import AWS from 'aws-sdk'; 2 | import qs from 'qs'; 3 | 4 | import { APIGatewayProxyEvent } from 'aws-lambda'; 5 | import { getSlackData, validateUrl, getUrlContent, runWarm } from './utils'; 6 | 7 | const lambda = new AWS.Lambda(); 8 | 9 | const invokeLambda = async ({ body }: APIGatewayProxyEvent) => { 10 | const req = qs.parse(body || ''); 11 | const url = req.text; 12 | 13 | try { 14 | await validateUrl(url); 15 | const content = await getUrlContent(url); 16 | const slackData = await getSlackData(req, content); 17 | await lambda 18 | .invoke({ 19 | FunctionName: 'slash-mercury-parser-production-uploadPost', 20 | Payload: JSON.stringify(slackData), 21 | InvocationType: 'Event', 22 | }) 23 | .promise(); 24 | 25 | return { statusCode: 200, body: 'Parsing your article...' }; 26 | } catch (err) { 27 | console.log('ERROR', err.message); 28 | return { statusCode: 200, body: err.message }; 29 | } 30 | }; 31 | 32 | export default runWarm(invokeLambda); 33 | -------------------------------------------------------------------------------- /src/types/@postlight/mercury-parser/index.d.ts: -------------------------------------------------------------------------------- 1 | declare module '@postlight/mercury-parser'; 2 | -------------------------------------------------------------------------------- /src/types/index.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/postlight/slash-mercury-parser/2fd88806f9b5b4234ef4379f4a2397ffce0622e2/src/types/index.ts -------------------------------------------------------------------------------- /src/types/lambda.ts: -------------------------------------------------------------------------------- 1 | export interface IEvent { 2 | source: string; 3 | } 4 | 5 | interface IJSON { 6 | [key: string]: any; 7 | } 8 | 9 | export type ICallback = (param1: any | null, response: IJSON | string) => void; 10 | 11 | export type ILambdaFunc = ( 12 | event: IEvent, 13 | context: {}, 14 | callback: ICallback 15 | ) => void | ILambdaFunc; 16 | -------------------------------------------------------------------------------- /src/uploadPost.ts: -------------------------------------------------------------------------------- 1 | import rp from 'request-promise'; 2 | import { APIGatewayProxyEvent } from 'aws-lambda'; 3 | 4 | import { runWarm } from './utils'; 5 | 6 | const handler = async (event: APIGatewayProxyEvent) => { 7 | try { 8 | const slackData = event; 9 | await rp.post('https://slack.com/api/files.upload', slackData); 10 | return; 11 | } catch (err) { 12 | console.log('Error', err); 13 | } 14 | }; 15 | 16 | export default runWarm(handler); 17 | -------------------------------------------------------------------------------- /src/utils/getSlackData.ts: -------------------------------------------------------------------------------- 1 | import dotenv from 'dotenv'; 2 | 3 | dotenv.config(); 4 | 5 | const { SLACK_AUTH_TOKEN } = process.env; 6 | 7 | export async function getSlackData(req: any, content: any) { 8 | const { channel_id } = req; 9 | const { parsedContent, title } = content; 10 | 11 | return { 12 | form: { 13 | token: SLACK_AUTH_TOKEN, 14 | channels: channel_id, 15 | content: parsedContent, 16 | filetype: 'post', 17 | title, 18 | }, 19 | }; 20 | } 21 | -------------------------------------------------------------------------------- /src/utils/getUrlContent.ts: -------------------------------------------------------------------------------- 1 | import Mercury from '@postlight/mercury-parser'; 2 | 3 | export async function getUrlContent(url: string) { 4 | try { 5 | const result = await Mercury.parse(url, { 6 | contentType: 'markdown', 7 | }); 8 | 9 | if (result.error) { 10 | throw new Error(result.messages); 11 | } 12 | 13 | const { title, content } = result; 14 | 15 | return { 16 | title, 17 | parsedContent: `${url} ${content}`, 18 | }; 19 | } catch (err) { 20 | throw new Error(err.message); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/utils/index.ts: -------------------------------------------------------------------------------- 1 | export { default as runWarm } from './run-warm'; 2 | export * from './lambda-response'; 3 | export * from './validateUrl'; 4 | export * from './getUrlContent'; 5 | export * from './getSlackData'; 6 | -------------------------------------------------------------------------------- /src/utils/lambda-response.ts: -------------------------------------------------------------------------------- 1 | interface IJSON { 2 | [key: string]: any; 3 | } 4 | 5 | interface IResponseOptions { 6 | json: IJSON; 7 | statusCode: number; 8 | allowCORS?: boolean; 9 | } 10 | 11 | interface IResponse { 12 | statusCode: number; 13 | body: string; 14 | headers?: { 15 | [key: string]: any; 16 | }; 17 | } 18 | 19 | function lambdaResponse({ 20 | json, 21 | statusCode, 22 | allowCORS = false, 23 | }: IResponseOptions) { 24 | const response: IResponse = { 25 | statusCode, 26 | body: JSON.stringify(json), 27 | }; 28 | 29 | if (allowCORS) { 30 | response.headers = { 31 | 'Access-Control-Allow-Origin': '*', 32 | }; 33 | } 34 | 35 | return response; 36 | } 37 | 38 | export function errorResponse(json: IJSON) { 39 | return lambdaResponse({ 40 | json, 41 | statusCode: 500, 42 | }); 43 | } 44 | 45 | export function corsErrorResponse(json: IJSON) { 46 | return lambdaResponse({ 47 | json, 48 | statusCode: 500, 49 | allowCORS: true, 50 | }); 51 | } 52 | 53 | export function successResponse(json: IJSON) { 54 | return lambdaResponse({ 55 | json, 56 | statusCode: 200, 57 | }); 58 | } 59 | 60 | export function corsSuccessResponse(json: IJSON) { 61 | return lambdaResponse({ 62 | json, 63 | statusCode: 200, 64 | allowCORS: true, 65 | }); 66 | } 67 | -------------------------------------------------------------------------------- /src/utils/run-warm.ts: -------------------------------------------------------------------------------- 1 | const runWarm = (lambdaFunc: AWSLambda.Handler): AWSLambda.Handler => ( 2 | event, 3 | context, 4 | callback 5 | ) => { 6 | // Detect the keep-alive ping from CloudWatch and exit early. This keeps our 7 | // lambda function running hot. 8 | if (event.source === 'aws.events') { 9 | return callback(null, 'pinged'); 10 | } 11 | 12 | return lambdaFunc(event, context, callback); 13 | }; 14 | 15 | export default runWarm; 16 | -------------------------------------------------------------------------------- /src/utils/validateUrl.ts: -------------------------------------------------------------------------------- 1 | export async function validateUrl(url: string) { 2 | if (url) { 3 | const modifiedArgs = url.trim().split(/\s+/); 4 | if (modifiedArgs.length !== 1) { 5 | throw new Error('please provide only one URL argument: /parser '); 6 | } 7 | } else { 8 | throw new Error('please provide one URL argument: /parser '); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "allowJs": true, 5 | "skipLibCheck": false, 6 | "esModuleInterop": true, 7 | "allowSyntheticDefaultImports": true, 8 | "strict": true, 9 | "forceConsistentCasingInFileNames": true, 10 | "module": "esnext", 11 | "moduleResolution": "node", 12 | "resolveJsonModule": true, 13 | "noEmit": false, 14 | "jsx": "preserve", 15 | "noUnusedLocals": true, 16 | "noUnusedParameters": true 17 | }, 18 | "include": ["src"] 19 | } 20 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "defaultSeverity": "error", 3 | "extends": ["tslint:latest", "tslint-react", "tslint-config-prettier"], 4 | "jsRules": {}, 5 | "rules": { 6 | "quotemark": [true, "single", "jsx-double"], 7 | "jsx-no-multiline-js": false, 8 | "jsx-no-lambda": false, 9 | "ordered-imports": false, 10 | "no-console": { 11 | "severity": "warning" 12 | }, 13 | "object-literal-sort-keys": false, 14 | "interface-name": false, 15 | "variable-name": [true, "check-format", "allow-leading-underscore"] 16 | }, 17 | "rulesDirectory": [] 18 | } 19 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const nodeExternals = require('webpack-node-externals'); 2 | const slsw = require('serverless-webpack'); 3 | 4 | module.exports = { 5 | entry: slsw.lib.entries, 6 | target: 'node', 7 | mode: slsw.lib.webpack.isLocal ? 'development' : 'production', 8 | externals: [nodeExternals()], 9 | module: { 10 | rules: [ 11 | { 12 | test: /\.tsx?$/, 13 | use: 'ts-loader', 14 | exclude: /node_modules/ 15 | }, 16 | { 17 | test: /\.jsx?$/, 18 | include: __dirname, 19 | exclude: /node_modules/, 20 | use: { 21 | loader: 'babel-loader', 22 | }, 23 | }, 24 | ], 25 | }, 26 | resolve: { 27 | extensions: [ '.tsx', '.ts', '.js', '.jsx' ] 28 | }, 29 | 30 | }; 31 | --------------------------------------------------------------------------------