├── .github ├── images │ ├── header.png │ └── screenshot.png └── workflows │ └── node.yml ├── .gitignore ├── CODE_OF_CONDUCT.md ├── LICENSE ├── README.md ├── package-lock.json ├── package.json ├── src ├── app.ts ├── config.json ├── lib │ └── slack.ts └── parser │ ├── blogs.ts │ └── index.ts └── tsconfig.json /.github/images/header.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techinpark/today-devblog-bot/a583e968052e835276e835de7ba5524c04ffe7e7/.github/images/header.png -------------------------------------------------------------------------------- /.github/images/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techinpark/today-devblog-bot/a583e968052e835276e835de7ba5524c04ffe7e7/.github/images/screenshot.png -------------------------------------------------------------------------------- /.github/workflows/node.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: node.js CI 5 | 6 | on: 7 | 8 | schedule: 9 | # 매일 아침 8시에 블로그 글 발송 10 | - cron: "0 23 * * *" 11 | 12 | jobs: 13 | build: 14 | runs-on: ubuntu-latest 15 | strategy: 16 | matrix: 17 | node-version: [12.x] 18 | steps: 19 | - uses: actions/checkout@v2 20 | - name: Use Node.js ${{ matrix.node-version }} 21 | uses: actions/setup-node@v1 22 | with: 23 | node-version: ${{ matrix.node-version }} 24 | - run: npm ci 25 | - run: npm run build 26 | - run: npm start 27 | env: 28 | CI: true 29 | WEBHOOKS: ${{ secrets.WEBHOOKS }} 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .keys 2 | credentials.json 3 | .DS_STORE 4 | node_modules 5 | dist/ 6 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at fernando@kakao.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Fernando 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 | ![](./.github/images/header.png) 2 | # today-devblog-bot ![node.js CI](https://github.com/techinpark/today-devblog-bot/workflows/node.js%20CI/badge.svg?branch=master) ![techinpark/appstore-review-bot license](https://img.shields.io/github/license/techinpark/today-devblog-bot?color=blue) ![stars](https://img.shields.io/github/stars/techinpark/today-devblog-bot?color=yellow&style=social) 3 | 4 | 📨 매일 오전 8시 개발자님들의 블로그 글을 슬랙 채널로 편하게 보내드려요 5 | 6 | `Github Actions` 를 이용한 토이프로젝트 입니다. 7 | `Repository`를 `fork` 하신 후 아래와 같은 방법을 사용하시면 슬랙을 통해 매일 오전 메세지를 받으실 수 있습니다. 8 | 9 | # 사용법 10 | 11 | ![](./.github/images/screenshot.png) 12 | 13 | - 레포지토리를 `fork` 합니다. 14 | - `Settings` - `Secrets` - `Add a new secret` 메뉴로 들어갑니다 15 | - `WEBHOOKS` 라는 이름으로 슬랙의 `Incomming Webhook` 주소를 입력하여 저장합니다. 16 | - `src/config.json` 파일에서 원하는 태그만 남겨놓고 저장합니다 (default: 전체). 17 | - 매일 아침 8시 새로운 블로그글들이 슬랙으로 전송됩니다. 🎉 18 | 19 | 20 | # 레퍼런스 21 | - https://github.com/sarojaba/awesome-devblog 22 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "today-devblog-bot", 3 | "version": "1.0.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "@actions/core": { 8 | "version": "1.2.6", 9 | "resolved": "https://registry.npmjs.org/@actions/core/-/core-1.2.6.tgz", 10 | "integrity": "sha512-ZQYitnqiyBc3D+k7LsgSBmMDVkOVidaagDG7j3fOym77jNunWRuYx7VSHa9GNfFZh+zh61xsCjRj4JxMZlDqTA==" 11 | }, 12 | "@slack/types": { 13 | "version": "1.5.0", 14 | "resolved": "https://registry.npmjs.org/@slack/types/-/types-1.5.0.tgz", 15 | "integrity": "sha512-oCYgatJYxHf9wE3tKXzOLeeTsF0ghX1TIcguNfVmO2V6NDe+cHAzZRglEOmJLdRINDS5gscAgSkeZpDhpKBeUA==" 16 | }, 17 | "@slack/webhook": { 18 | "version": "5.0.3", 19 | "resolved": "https://registry.npmjs.org/@slack/webhook/-/webhook-5.0.3.tgz", 20 | "integrity": "sha512-51vnejJ2zABNumPVukOLyerpHQT39/Lt0TYFtOEz/N2X77bPofOgfPj2atB3etaM07mxWHLT9IRJ4Zuqx38DkQ==", 21 | "requires": { 22 | "@slack/types": "^1.2.1", 23 | "@types/node": ">=8.9.0", 24 | "axios": "^0.19.0" 25 | }, 26 | "dependencies": { 27 | "axios": { 28 | "version": "0.19.2", 29 | "resolved": "https://registry.npmjs.org/axios/-/axios-0.19.2.tgz", 30 | "integrity": "sha512-fjgm5MvRHLhx+osE2xoekY70AhARk3a6hkN+3Io1jc00jtquGvxYlKlsFUhmUET0V5te6CcZI7lcv2Ym61mjHA==", 31 | "requires": { 32 | "follow-redirects": "1.5.10" 33 | } 34 | }, 35 | "follow-redirects": { 36 | "version": "1.5.10", 37 | "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.5.10.tgz", 38 | "integrity": "sha512-0V5l4Cizzvqt5D44aTXbFZz+FtyXV1vrDN6qrelxtfYQKW0KO0W2T/hkE8xvGa/540LkZlkaUjO4ailYTFtHVQ==", 39 | "requires": { 40 | "debug": "=3.1.0" 41 | } 42 | } 43 | } 44 | }, 45 | "@types/axios": { 46 | "version": "0.14.0", 47 | "resolved": "https://registry.npmjs.org/@types/axios/-/axios-0.14.0.tgz", 48 | "integrity": "sha1-7CMA++fX3d1+udOr+HmZlkyvzkY=", 49 | "requires": { 50 | "axios": "*" 51 | } 52 | }, 53 | "@types/cheerio": { 54 | "version": "0.22.18", 55 | "resolved": "https://registry.npmjs.org/@types/cheerio/-/cheerio-0.22.18.tgz", 56 | "integrity": "sha512-Fq7R3fINAPSdUEhOyjG4iVxgHrOnqDJbY0/BUuiN0pvD/rfmZWekVZnv+vcs8TtpA2XF50uv50LaE4EnpEL/Hw==", 57 | "requires": { 58 | "@types/node": "*" 59 | } 60 | }, 61 | "@types/node": { 62 | "version": "13.13.2", 63 | "resolved": "https://registry.npmjs.org/@types/node/-/node-13.13.2.tgz", 64 | "integrity": "sha512-LB2R1Oyhpg8gu4SON/mfforE525+Hi/M1ineICEDftqNVTyFg1aRIeGuTvXAoWHc4nbrFncWtJgMmoyRvuGh7A==" 65 | }, 66 | "arg": { 67 | "version": "4.1.3", 68 | "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", 69 | "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==" 70 | }, 71 | "axios": { 72 | "version": "0.21.1", 73 | "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.1.tgz", 74 | "integrity": "sha512-dKQiRHxGD9PPRIUNIWvZhPTPpl1rf/OxTYKsqKUDjBwYylTvV7SjSHJb9ratfyzM6wCdLCOYLzs73qpg5c4iGA==", 75 | "requires": { 76 | "follow-redirects": "^1.10.0" 77 | } 78 | }, 79 | "boolbase": { 80 | "version": "1.0.0", 81 | "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", 82 | "integrity": "sha1-aN/1++YMUes3cl6p4+0xDcwed24=" 83 | }, 84 | "buffer-from": { 85 | "version": "1.1.1", 86 | "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", 87 | "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==" 88 | }, 89 | "cheerio": { 90 | "version": "1.0.0-rc.3", 91 | "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0-rc.3.tgz", 92 | "integrity": "sha512-0td5ijfUPuubwLUu0OBoe98gZj8C/AA+RW3v67GPlGOrvxWjZmBXiBCRU+I8VEiNyJzjth40POfHiz2RB3gImA==", 93 | "requires": { 94 | "css-select": "~1.2.0", 95 | "dom-serializer": "~0.1.1", 96 | "entities": "~1.1.1", 97 | "htmlparser2": "^3.9.1", 98 | "lodash": "^4.15.0", 99 | "parse5": "^3.0.1" 100 | } 101 | }, 102 | "css-select": { 103 | "version": "1.2.0", 104 | "resolved": "https://registry.npmjs.org/css-select/-/css-select-1.2.0.tgz", 105 | "integrity": "sha1-KzoRBTnFNV8c2NMUYj6HCxIeyFg=", 106 | "requires": { 107 | "boolbase": "~1.0.0", 108 | "css-what": "2.1", 109 | "domutils": "1.5.1", 110 | "nth-check": "~1.0.1" 111 | } 112 | }, 113 | "css-what": { 114 | "version": "2.1.3", 115 | "resolved": "https://registry.npmjs.org/css-what/-/css-what-2.1.3.tgz", 116 | "integrity": "sha512-a+EPoD+uZiNfh+5fxw2nO9QwFa6nJe2Or35fGY6Ipw1R3R4AGz1d1TEZrCegvw2YTmZ0jXirGYlzxxpYSHwpEg==" 117 | }, 118 | "debug": { 119 | "version": "3.1.0", 120 | "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", 121 | "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", 122 | "requires": { 123 | "ms": "2.0.0" 124 | } 125 | }, 126 | "diff": { 127 | "version": "4.0.2", 128 | "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", 129 | "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==" 130 | }, 131 | "dom-serializer": { 132 | "version": "0.1.1", 133 | "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.1.1.tgz", 134 | "integrity": "sha512-l0IU0pPzLWSHBcieZbpOKgkIn3ts3vAh7ZuFyXNwJxJXk/c4Gwj9xaTJwIDVQCXawWD0qb3IzMGH5rglQaO0XA==", 135 | "requires": { 136 | "domelementtype": "^1.3.0", 137 | "entities": "^1.1.1" 138 | } 139 | }, 140 | "domelementtype": { 141 | "version": "1.3.1", 142 | "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.3.1.tgz", 143 | "integrity": "sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w==" 144 | }, 145 | "domhandler": { 146 | "version": "2.4.2", 147 | "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-2.4.2.tgz", 148 | "integrity": "sha512-JiK04h0Ht5u/80fdLMCEmV4zkNh2BcoMFBmZ/91WtYZ8qVXSKjiw7fXMgFPnHcSZgOo3XdinHvmnDUeMf5R4wA==", 149 | "requires": { 150 | "domelementtype": "1" 151 | } 152 | }, 153 | "domutils": { 154 | "version": "1.5.1", 155 | "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.5.1.tgz", 156 | "integrity": "sha1-3NhIiib1Y9YQeeSMn3t+Mjc2gs8=", 157 | "requires": { 158 | "dom-serializer": "0", 159 | "domelementtype": "1" 160 | } 161 | }, 162 | "entities": { 163 | "version": "1.1.2", 164 | "resolved": "https://registry.npmjs.org/entities/-/entities-1.1.2.tgz", 165 | "integrity": "sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w==" 166 | }, 167 | "follow-redirects": { 168 | "version": "1.13.1", 169 | "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.13.1.tgz", 170 | "integrity": "sha512-SSG5xmZh1mkPGyKzjZP8zLjltIfpW32Y5QpdNJyjcfGxK3qo3NDDkZOZSFiGn1A6SclQxY9GzEwAHQ3dmYRWpg==" 171 | }, 172 | "htmlparser2": { 173 | "version": "3.10.1", 174 | "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.10.1.tgz", 175 | "integrity": "sha512-IgieNijUMbkDovyoKObU1DUhm1iwNYE/fuifEoEHfd1oZKZDaONBSkal7Y01shxsM49R4XaMdGez3WnF9UfiCQ==", 176 | "requires": { 177 | "domelementtype": "^1.3.1", 178 | "domhandler": "^2.3.0", 179 | "domutils": "^1.5.1", 180 | "entities": "^1.1.1", 181 | "inherits": "^2.0.1", 182 | "readable-stream": "^3.1.1" 183 | } 184 | }, 185 | "inherits": { 186 | "version": "2.0.4", 187 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", 188 | "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" 189 | }, 190 | "lodash": { 191 | "version": "4.17.19", 192 | "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.19.tgz", 193 | "integrity": "sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ==" 194 | }, 195 | "make-error": { 196 | "version": "1.3.6", 197 | "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", 198 | "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==" 199 | }, 200 | "moment": { 201 | "version": "2.24.0", 202 | "resolved": "https://registry.npmjs.org/moment/-/moment-2.24.0.tgz", 203 | "integrity": "sha512-bV7f+6l2QigeBBZSM/6yTNq4P2fNpSWj/0e7jQcy87A8e7o2nAfP/34/2ky5Vw4B9S446EtIhodAzkFCcR4dQg==" 204 | }, 205 | "ms": { 206 | "version": "2.0.0", 207 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", 208 | "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" 209 | }, 210 | "nth-check": { 211 | "version": "1.0.2", 212 | "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-1.0.2.tgz", 213 | "integrity": "sha512-WeBOdju8SnzPN5vTUJYxYUxLeXpCaVP5i5e0LF8fg7WORF2Wd7wFX/pk0tYZk7s8T+J7VLy0Da6J1+wCT0AtHg==", 214 | "requires": { 215 | "boolbase": "~1.0.0" 216 | } 217 | }, 218 | "parse5": { 219 | "version": "3.0.3", 220 | "resolved": "https://registry.npmjs.org/parse5/-/parse5-3.0.3.tgz", 221 | "integrity": "sha512-rgO9Zg5LLLkfJF9E6CCmXlSE4UVceloys8JrFqCcHloC3usd/kJCyPDwH2SOlzix2j3xaP9sUX3e8+kvkuleAA==", 222 | "requires": { 223 | "@types/node": "*" 224 | } 225 | }, 226 | "readable-stream": { 227 | "version": "3.6.0", 228 | "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", 229 | "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", 230 | "requires": { 231 | "inherits": "^2.0.3", 232 | "string_decoder": "^1.1.1", 233 | "util-deprecate": "^1.0.1" 234 | } 235 | }, 236 | "safe-buffer": { 237 | "version": "5.2.0", 238 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.0.tgz", 239 | "integrity": "sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg==" 240 | }, 241 | "source-map": { 242 | "version": "0.6.1", 243 | "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", 244 | "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" 245 | }, 246 | "source-map-support": { 247 | "version": "0.5.19", 248 | "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.19.tgz", 249 | "integrity": "sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw==", 250 | "requires": { 251 | "buffer-from": "^1.0.0", 252 | "source-map": "^0.6.0" 253 | } 254 | }, 255 | "string_decoder": { 256 | "version": "1.3.0", 257 | "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", 258 | "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", 259 | "requires": { 260 | "safe-buffer": "~5.2.0" 261 | } 262 | }, 263 | "ts-node": { 264 | "version": "8.9.0", 265 | "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-8.9.0.tgz", 266 | "integrity": "sha512-rwkXfOs9zmoHrV8xE++dmNd6ZIS+nmHHCxcV53ekGJrxFLMbp+pizpPS07ARvhwneCIECPppOwbZHvw9sQtU4w==", 267 | "requires": { 268 | "arg": "^4.1.0", 269 | "diff": "^4.0.1", 270 | "make-error": "^1.1.1", 271 | "source-map-support": "^0.5.17", 272 | "yn": "3.1.1" 273 | } 274 | }, 275 | "typescript": { 276 | "version": "3.8.3", 277 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.8.3.tgz", 278 | "integrity": "sha512-MYlEfn5VrLNsgudQTVJeNaQFUAI7DkhnOjdpAp4T+ku1TfQClewlbSuTVHiA+8skNBgaf02TL/kLOvig4y3G8w==" 279 | }, 280 | "util-deprecate": { 281 | "version": "1.0.2", 282 | "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", 283 | "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" 284 | }, 285 | "yn": { 286 | "version": "3.1.1", 287 | "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", 288 | "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==" 289 | } 290 | } 291 | } 292 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "today-devblog-bot", 3 | "version": "1.0.0", 4 | "description": "📨 매일마다 개발자들의 블로그 글을 슬랙 채널로 편하게 보내드려요", 5 | "main": "app.ts", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "build": "tsc", 9 | "start": "node dist/app.js" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "git+https://github.com/techinpark/today-devblog-bot.git" 14 | }, 15 | "author": "Fernando", 16 | "license": "MIT", 17 | "bugs": { 18 | "url": "https://github.com/techinpark/today-devblog-bot/issues" 19 | }, 20 | "homepage": "https://github.com/techinpark/today-devblog-bot#readme", 21 | "dependencies": { 22 | "@actions/core": "^1.2.6", 23 | "@slack/webhook": "^5.0.3", 24 | "@types/axios": "^0.14.0", 25 | "@types/cheerio": "^0.22.18", 26 | "@types/node": "^13.13.2", 27 | "axios": "^0.21.1", 28 | "cheerio": "^1.0.0-rc.3", 29 | "moment": "^2.24.0", 30 | "ts-node": "^8.9.0", 31 | "typescript": "^3.8.3" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/app.ts: -------------------------------------------------------------------------------- 1 | import * as core from '@actions/core' 2 | import parser from './parser' 3 | import slack from './lib/slack' 4 | 5 | (async () => { 6 | const WEBHOOKS = process.env.WEBHOOKS 7 | if(null == WEBHOOKS) throw new Error("⚠️ Github Secrets 에서 WEBHOOK 등록여부를 확인해주세요") 8 | 9 | const webhookList = WEBHOOKS.split(",") 10 | const parsed = await parser() 11 | 12 | webhookList.map(async url => { 13 | 14 | // Slack webhook 15 | if(url.includes('hooks.slack.com')) { 16 | await slack({ 17 | data: parsed.blog, 18 | url, 19 | }) 20 | } 21 | }) 22 | 23 | console.log("✅ 성공적으로 슬랙에 전송되었습니다.") 24 | })().catch(e => { 25 | console.log(e) 26 | core.setFailed(e) 27 | }) -------------------------------------------------------------------------------- /src/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "base_url": "https://awesome-devblog.now.sh/api/korean/people/feeds", 3 | "sort": "date.desc", 4 | "page": "1", 5 | "size": "300", 6 | "tags": [ 7 | "ASP.NET", 8 | "AWS", 9 | "Azure", 10 | "Cache", 11 | "Django", 12 | "Firebase", 13 | "GCP", 14 | "Hazelcast", 15 | "Hibernate", 16 | "Java", 17 | "JDK", 18 | "JPA", 19 | "JUnit", 20 | "Kafka", 21 | "Memcached", 22 | "Mybatis", 23 | "MySQL", 24 | "Nginx", 25 | "Node", 26 | "NoSQL", 27 | "Oracle", 28 | "RabbitMQ", 29 | "Reactor", 30 | "Redis", 31 | "REST", 32 | "Ruby on Rails", 33 | "Spring", 34 | "Spring Boot", 35 | "SQL", 36 | "Android", 37 | "Kotlin", 38 | "Swift", 39 | "Angular", 40 | "Chrome", 41 | "Firefox", 42 | "Flexbox", 43 | "HTML", 44 | "Javascript", 45 | "Jekyll", 46 | "PWA", 47 | "React.js", 48 | "Vue", 49 | "Webpack", 50 | "BERT", 51 | "Keras", 52 | "Python", 53 | "Spark", 54 | "TensorFlow", 55 | "Ansible", 56 | "BGP", 57 | "Cloud", 58 | "Docker", 59 | "Kubernetes", 60 | "Linux", 61 | "NETCONF", 62 | "Rust", 63 | "Ubuntu", 64 | "YANG", 65 | "zsh", 66 | "Assembly", 67 | "Code Review", 68 | "Blockchain", 69 | "Ethereum", 70 | "Git", 71 | "GoLang", 72 | "HTTPS", 73 | "HTTP/3", 74 | "Intellij", 75 | "Slack", 76 | "UML", 77 | "VR", 78 | "Windows" 79 | ] 80 | } -------------------------------------------------------------------------------- /src/lib/slack.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | 3 | interface Contents { 4 | _id: string 5 | title: string 6 | link: string 7 | date: Date 8 | description: string 9 | author: string 10 | imgUrl: string 11 | tags: [string] 12 | count: string 13 | markdown: string 14 | } 15 | 16 | interface slackArgs { 17 | data: Contents[], 18 | url: string 19 | } 20 | 21 | export default async({data, url}: slackArgs) => { 22 | const today = new Date().toLocaleDateString().replace(/\. /g, '-').replace('.', '') 23 | const count = data.length 24 | let markdown: string = "" 25 | 26 | let message: any = { 27 | attachments: [], 28 | } 29 | 30 | data.forEach(function(item){ 31 | markdown += item.markdown + "\n" 32 | }) 33 | 34 | if(count <= 0) { 35 | return 36 | } 37 | 38 | message.attachments.push({ 39 | pretext: `오늘 *${count}* 개의 새로운 블로그 글이 있어요`, 40 | 41 | fields: [ 42 | { 43 | type:'mrkdwn', 44 | title: '', 45 | value: markdown, 46 | }, 47 | ], 48 | footer: 'Github - today-devblog-bot' 49 | }) 50 | 51 | 52 | await axios.post(url, message) 53 | } -------------------------------------------------------------------------------- /src/parser/blogs.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | import cheerio from 'cheerio' 3 | import moment from 'moment' 4 | import config from '../config.json' 5 | 6 | 7 | export interface Contents { 8 | _id: string 9 | title: string 10 | link: string 11 | date: Date 12 | description: string 13 | author: string 14 | imgUrl: string 15 | tags: [string] 16 | count: string 17 | markdown: string 18 | }[]; 19 | 20 | export const parse = async () => { 21 | const baseURL = `${config.base_url}?sort=${config.sort}&page=${config.page}&size=${config.size}&tags=${encodeURI(config.tags.join(","))}` 22 | console.log(baseURL) 23 | const response = await axios.get(baseURL) 24 | const data = response.data 25 | const obj: [Contents] = response.data.data 26 | const yesterday = moment().subtract(1, 'day') 27 | type contentType = Contents 28 | const contents: contentType[] = [] 29 | 30 | console.log(yesterday.toString()) 31 | 32 | obj.forEach(function (item) { 33 | const item_date = moment(item.date); 34 | if (yesterday.isSame(item_date, 'day')) { 35 | item.markdown = "*" + " " + `<${item.link}|${item.title}>` + " by " + item.author 36 | contents.push(item) 37 | } 38 | }); 39 | 40 | console.log('✅ 블로그글 파싱 완료') 41 | return contents 42 | } -------------------------------------------------------------------------------- /src/parser/index.ts: -------------------------------------------------------------------------------- 1 | import * as blog from './blogs' 2 | 3 | const parser = async() => { 4 | const content = await blog.parse() 5 | return { 6 | blog: content 7 | } 8 | } 9 | 10 | export default parser -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Basic Options */ 4 | // "incremental": true, /* Enable incremental compilation */ 5 | "target": "es5", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */ 6 | "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */ 7 | // "lib": [], /* Specify library files to be included in the compilation. */ 8 | // "allowJs": true, /* Allow javascript files to be compiled. */ 9 | // "checkJs": true, /* Report errors in .js files. */ 10 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ 11 | // "declaration": true, /* Generates corresponding '.d.ts' file. */ 12 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ 13 | // "sourceMap": true, /* Generates corresponding '.map' file. */ 14 | // "outFile": "./", /* Concatenate and emit output to single file. */ 15 | "outDir": "./dist", /* Redirect output structure to the directory. */ 16 | // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 17 | // "composite": true, /* Enable project compilation */ 18 | // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ 19 | // "removeComments": true, /* Do not emit comments to output. */ 20 | // "noEmit": true, /* Do not emit outputs. */ 21 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 22 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 23 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 24 | 25 | /* Strict Type-Checking Options */ 26 | "strict": true, /* Enable all strict type-checking options. */ 27 | // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ 28 | // "strictNullChecks": true, /* Enable strict null checks. */ 29 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */ 30 | // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ 31 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ 32 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 33 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 34 | 35 | /* Additional Checks */ 36 | // "noUnusedLocals": true, /* Report errors on unused locals. */ 37 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 38 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 39 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 40 | 41 | /* Module Resolution Options */ 42 | // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 43 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 44 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 45 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 46 | // "typeRoots": [], /* List of folders to include type definitions from. */ 47 | // "types": [], /* Type declaration files to be included in compilation. */ 48 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 49 | "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 50 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 51 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 52 | 53 | /* Source Map Options */ 54 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 55 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 56 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 57 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 58 | 59 | /* Experimental Options */ 60 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 61 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 62 | 63 | /* Advanced Options */ 64 | "forceConsistentCasingInFileNames": true, /* Disallow inconsistently-cased references to the same file. */ 65 | 66 | "resolveJsonModule": true 67 | } 68 | } 69 | --------------------------------------------------------------------------------