├── .github └── workflows │ └── main.yml ├── .gitignore ├── README.md ├── index.js ├── package-lock.json ├── package.json ├── rss2.xml ├── tests ├── testFeed.js └── testHeadlines.js └── utils ├── feed.js ├── getHeadlines.js └── issue.js /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: hackernews-daily 2 | 3 | on: 4 | workflow_dispatch: 5 | schedule: 6 | - cron: "0 6 * * *" 7 | 8 | env: 9 | BRANCH_NAME: ${{ github.head_ref || github.ref_name }} 10 | 11 | jobs: 12 | fetch-top-posts: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v4 16 | - name: Use Node.js 17 | uses: actions/setup-node@v4 18 | with: 19 | node-version: 22 20 | - name: npm install 21 | run: npm install --only=prod 22 | working-directory: . 23 | - name: fetch 24 | run: node index.js 25 | working-directory: . 26 | env: 27 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 28 | - name: commit 29 | run: | 30 | git config user.name github-actions 31 | git config user.email github-actions@github.com 32 | git add . 33 | git commit -m "feed: update" 34 | git push 35 | 36 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | 78 | # Next.js build output 79 | .next 80 | 81 | # Nuxt.js build / generate output 82 | .nuxt 83 | dist 84 | 85 | # Gatsby files 86 | .cache/ 87 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 88 | # https://nextjs.org/blog/next-9-1#public-directory-support 89 | # public 90 | 91 | # vuepress build output 92 | .vuepress/dist 93 | 94 | # Serverless directories 95 | .serverless/ 96 | 97 | # FuseBox cache 98 | .fusebox/ 99 | 100 | # DynamoDB Local files 101 | .dynamodb/ 102 | 103 | # TernJS port file 104 | .tern-port 105 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # RSS feed 2 | 3 | https://raw.githubusercontent.com/meixger/hackernews-daily/main/rss2.xml 4 | 5 | https://rsshub.app/github/issue/meixger/hackernews-daily 6 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | import * as core from '@actions/core'; 2 | import { getHeadlines } from './utils/getHeadlines.js'; 3 | import { lockIssue, openIssue } from './utils/issue.js'; 4 | import { updateFeed } from './utils/feed.js'; 5 | 6 | const date = new Date(); 7 | const takeHeadlines = 30; 8 | 9 | const contents = await getHeadlines(date, takeHeadlines); 10 | if (!contents) { 11 | core.warning("no content - skip issue creation"); 12 | process.exit(1); 13 | } 14 | core.info(contents); 15 | 16 | if (process.env.BRANCH_NAME === 'main') { 17 | const res = await openIssue({ 18 | owner: 'meixger', 19 | repo: 'hackernews-daily', 20 | title: `Hacker News Daily Top ${takeHeadlines} @${date.toISOString().slice(0, 10)}`, 21 | body: contents 22 | }); 23 | 24 | const issueNumber = res.data.number; 25 | core.info(`created issue ${issueNumber}`); 26 | 27 | await lockIssue({ 28 | owner: 'meixger', 29 | repo: 'hackernews-daily', 30 | issueNumber, 31 | }); 32 | } 33 | 34 | await updateFeed(); 35 | 36 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hackernews-daily", 3 | "version": "1.0.0", 4 | "lockfileVersion": 3, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "hackernews-daily", 9 | "version": "1.0.0", 10 | "license": "ISC", 11 | "dependencies": { 12 | "@actions/core": "^1.11.1", 13 | "@octokit/auth-action": "^5.1.1", 14 | "@octokit/auth-app": "^7.1.5", 15 | "@octokit/core": "^6.1.4", 16 | "dayjs": "^1.11.13", 17 | "feed": "^4.2.2", 18 | "marked": "^15.0.7", 19 | "timeago.js": "^4.0.2" 20 | } 21 | }, 22 | "node_modules/@actions/core": { 23 | "version": "1.11.1", 24 | "resolved": "https://registry.npmjs.org/@actions/core/-/core-1.11.1.tgz", 25 | "integrity": "sha512-hXJCSrkwfA46Vd9Z3q4cpEpHB1rL5NG04+/rbqW9d3+CSvtB1tYe8UTpAlixa1vj0m/ULglfEK2UKxMGxCxv5A==", 26 | "license": "MIT", 27 | "dependencies": { 28 | "@actions/exec": "^1.1.1", 29 | "@actions/http-client": "^2.0.1" 30 | } 31 | }, 32 | "node_modules/@actions/exec": { 33 | "version": "1.1.1", 34 | "resolved": "https://registry.npmjs.org/@actions/exec/-/exec-1.1.1.tgz", 35 | "integrity": "sha512-+sCcHHbVdk93a0XT19ECtO/gIXoxvdsgQLzb2fE2/5sIZmWQuluYyjPQtrtTHdU1YzTZ7bAPN4sITq2xi1679w==", 36 | "license": "MIT", 37 | "dependencies": { 38 | "@actions/io": "^1.0.1" 39 | } 40 | }, 41 | "node_modules/@actions/http-client": { 42 | "version": "2.1.0", 43 | "resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-2.1.0.tgz", 44 | "integrity": "sha512-BonhODnXr3amchh4qkmjPMUO8mFi/zLaaCeCAJZqch8iQqyDnVIkySjB38VHAC8IJ+bnlgfOqlhpyCUZHlQsqw==", 45 | "dependencies": { 46 | "tunnel": "^0.0.6" 47 | } 48 | }, 49 | "node_modules/@actions/io": { 50 | "version": "1.1.3", 51 | "resolved": "https://registry.npmjs.org/@actions/io/-/io-1.1.3.tgz", 52 | "integrity": "sha512-wi9JjgKLYS7U/z8PPbco+PvTb/nRWjeoFlJ1Qer83k/3C5PHQi28hiVdeE2kHXmIL99mQFawx8qt/JPjZilJ8Q==", 53 | "license": "MIT" 54 | }, 55 | "node_modules/@octokit/auth-action": { 56 | "version": "5.1.1", 57 | "resolved": "https://registry.npmjs.org/@octokit/auth-action/-/auth-action-5.1.1.tgz", 58 | "integrity": "sha512-JE2gbAZcwwVuww88YY7oB97P6eVAPgKZk2US9Uyz+ZUw5ubeRkZqog7G/gUEAjayIFt8s0UX3qNntP1agVcB0g==", 59 | "dependencies": { 60 | "@octokit/auth-token": "^5.0.0", 61 | "@octokit/types": "^13.0.0" 62 | }, 63 | "engines": { 64 | "node": ">= 18" 65 | } 66 | }, 67 | "node_modules/@octokit/auth-app": { 68 | "version": "7.1.5", 69 | "resolved": "https://registry.npmjs.org/@octokit/auth-app/-/auth-app-7.1.5.tgz", 70 | "integrity": "sha512-boklS4E6LpbA3nRx+SU2fRKRGZJdOGoSZne/i3Y0B5rfHOcGwFgcXrwDLdtbv4igfDSnAkZaoNBv1GYjPDKRNw==", 71 | "license": "MIT", 72 | "dependencies": { 73 | "@octokit/auth-oauth-app": "^8.1.3", 74 | "@octokit/auth-oauth-user": "^5.1.3", 75 | "@octokit/request": "^9.2.1", 76 | "@octokit/request-error": "^6.1.7", 77 | "@octokit/types": "^13.8.0", 78 | "toad-cache": "^3.7.0", 79 | "universal-github-app-jwt": "^2.2.0", 80 | "universal-user-agent": "^7.0.0" 81 | }, 82 | "engines": { 83 | "node": ">= 18" 84 | } 85 | }, 86 | "node_modules/@octokit/auth-oauth-app": { 87 | "version": "8.1.3", 88 | "resolved": "https://registry.npmjs.org/@octokit/auth-oauth-app/-/auth-oauth-app-8.1.3.tgz", 89 | "integrity": "sha512-4e6OjVe5rZ8yBe8w7byBjpKtSXFuro7gqeGAAZc7QYltOF8wB93rJl2FE0a4U1Mt88xxPv/mS+25/0DuLk0Ewg==", 90 | "license": "MIT", 91 | "dependencies": { 92 | "@octokit/auth-oauth-device": "^7.1.3", 93 | "@octokit/auth-oauth-user": "^5.1.3", 94 | "@octokit/request": "^9.2.1", 95 | "@octokit/types": "^13.6.2", 96 | "universal-user-agent": "^7.0.0" 97 | }, 98 | "engines": { 99 | "node": ">= 18" 100 | } 101 | }, 102 | "node_modules/@octokit/auth-oauth-device": { 103 | "version": "7.1.4", 104 | "resolved": "https://registry.npmjs.org/@octokit/auth-oauth-device/-/auth-oauth-device-7.1.4.tgz", 105 | "integrity": "sha512-yK35I9VGDGjYxu0NPZ9Rl+zXM/+DO/Hu1VR5FUNz+ZsU6i8B8oQ43TPwci9nuH8bAF6rQrKDNR9F0r0+kzYJhA==", 106 | "license": "MIT", 107 | "dependencies": { 108 | "@octokit/oauth-methods": "^5.1.4", 109 | "@octokit/request": "^9.2.1", 110 | "@octokit/types": "^13.6.2", 111 | "universal-user-agent": "^7.0.0" 112 | }, 113 | "engines": { 114 | "node": ">= 18" 115 | } 116 | }, 117 | "node_modules/@octokit/auth-oauth-user": { 118 | "version": "5.1.3", 119 | "resolved": "https://registry.npmjs.org/@octokit/auth-oauth-user/-/auth-oauth-user-5.1.3.tgz", 120 | "integrity": "sha512-zNPByPn9K7TC+OOHKGxU+MxrE9SZAN11UHYEFLsK2NRn3akJN2LHRl85q+Eypr3tuB2GrKx3rfj2phJdkYCvzw==", 121 | "license": "MIT", 122 | "dependencies": { 123 | "@octokit/auth-oauth-device": "^7.1.3", 124 | "@octokit/oauth-methods": "^5.1.3", 125 | "@octokit/request": "^9.2.1", 126 | "@octokit/types": "^13.6.2", 127 | "universal-user-agent": "^7.0.0" 128 | }, 129 | "engines": { 130 | "node": ">= 18" 131 | } 132 | }, 133 | "node_modules/@octokit/auth-token": { 134 | "version": "5.1.1", 135 | "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-5.1.1.tgz", 136 | "integrity": "sha512-rh3G3wDO8J9wSjfI436JUKzHIxq8NaiL0tVeB2aXmG6p/9859aUOAjA9pmSPNGGZxfwmaJ9ozOJImuNVJdpvbA==", 137 | "engines": { 138 | "node": ">= 18" 139 | } 140 | }, 141 | "node_modules/@octokit/core": { 142 | "version": "6.1.4", 143 | "resolved": "https://registry.npmjs.org/@octokit/core/-/core-6.1.4.tgz", 144 | "integrity": "sha512-lAS9k7d6I0MPN+gb9bKDt7X8SdxknYqAMh44S5L+lNqIN2NuV8nvv3g8rPp7MuRxcOpxpUIATWprO0C34a8Qmg==", 145 | "license": "MIT", 146 | "dependencies": { 147 | "@octokit/auth-token": "^5.0.0", 148 | "@octokit/graphql": "^8.1.2", 149 | "@octokit/request": "^9.2.1", 150 | "@octokit/request-error": "^6.1.7", 151 | "@octokit/types": "^13.6.2", 152 | "before-after-hook": "^3.0.2", 153 | "universal-user-agent": "^7.0.0" 154 | }, 155 | "engines": { 156 | "node": ">= 18" 157 | } 158 | }, 159 | "node_modules/@octokit/endpoint": { 160 | "version": "10.1.3", 161 | "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-10.1.3.tgz", 162 | "integrity": "sha512-nBRBMpKPhQUxCsQQeW+rCJ/OPSMcj3g0nfHn01zGYZXuNDvvXudF/TYY6APj5THlurerpFN4a/dQAIAaM6BYhA==", 163 | "license": "MIT", 164 | "dependencies": { 165 | "@octokit/types": "^13.6.2", 166 | "universal-user-agent": "^7.0.2" 167 | }, 168 | "engines": { 169 | "node": ">= 18" 170 | } 171 | }, 172 | "node_modules/@octokit/graphql": { 173 | "version": "8.2.1", 174 | "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-8.2.1.tgz", 175 | "integrity": "sha512-n57hXtOoHrhwTWdvhVkdJHdhTv0JstjDbDRhJfwIRNfFqmSo1DaK/mD2syoNUoLCyqSjBpGAKOG0BuwF392slw==", 176 | "license": "MIT", 177 | "dependencies": { 178 | "@octokit/request": "^9.2.2", 179 | "@octokit/types": "^13.8.0", 180 | "universal-user-agent": "^7.0.0" 181 | }, 182 | "engines": { 183 | "node": ">= 18" 184 | } 185 | }, 186 | "node_modules/@octokit/oauth-authorization-url": { 187 | "version": "7.1.1", 188 | "resolved": "https://registry.npmjs.org/@octokit/oauth-authorization-url/-/oauth-authorization-url-7.1.1.tgz", 189 | "integrity": "sha512-ooXV8GBSabSWyhLUowlMIVd9l1s2nsOGQdlP2SQ4LnkEsGXzeCvbSbCPdZThXhEFzleGPwbapT0Sb+YhXRyjCA==", 190 | "license": "MIT", 191 | "engines": { 192 | "node": ">= 18" 193 | } 194 | }, 195 | "node_modules/@octokit/oauth-methods": { 196 | "version": "5.1.4", 197 | "resolved": "https://registry.npmjs.org/@octokit/oauth-methods/-/oauth-methods-5.1.4.tgz", 198 | "integrity": "sha512-Jc/ycnePClOvO1WL7tlC+TRxOFtyJBGuTDsL4dzXNiVZvzZdrPuNw7zHI3qJSUX2n6RLXE5L0SkFmYyNaVUFoQ==", 199 | "license": "MIT", 200 | "dependencies": { 201 | "@octokit/oauth-authorization-url": "^7.0.0", 202 | "@octokit/request": "^9.2.1", 203 | "@octokit/request-error": "^6.1.7", 204 | "@octokit/types": "^13.6.2" 205 | }, 206 | "engines": { 207 | "node": ">= 18" 208 | } 209 | }, 210 | "node_modules/@octokit/openapi-types": { 211 | "version": "24.2.0", 212 | "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-24.2.0.tgz", 213 | "integrity": "sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg==", 214 | "license": "MIT" 215 | }, 216 | "node_modules/@octokit/request": { 217 | "version": "9.2.2", 218 | "resolved": "https://registry.npmjs.org/@octokit/request/-/request-9.2.2.tgz", 219 | "integrity": "sha512-dZl0ZHx6gOQGcffgm1/Sf6JfEpmh34v3Af2Uci02vzUYz6qEN6zepoRtmybWXIGXFIK8K9ylE3b+duCWqhArtg==", 220 | "license": "MIT", 221 | "dependencies": { 222 | "@octokit/endpoint": "^10.1.3", 223 | "@octokit/request-error": "^6.1.7", 224 | "@octokit/types": "^13.6.2", 225 | "fast-content-type-parse": "^2.0.0", 226 | "universal-user-agent": "^7.0.2" 227 | }, 228 | "engines": { 229 | "node": ">= 18" 230 | } 231 | }, 232 | "node_modules/@octokit/request-error": { 233 | "version": "6.1.7", 234 | "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-6.1.7.tgz", 235 | "integrity": "sha512-69NIppAwaauwZv6aOzb+VVLwt+0havz9GT5YplkeJv7fG7a40qpLt/yZKyiDxAhgz0EtgNdNcb96Z0u+Zyuy2g==", 236 | "license": "MIT", 237 | "dependencies": { 238 | "@octokit/types": "^13.6.2" 239 | }, 240 | "engines": { 241 | "node": ">= 18" 242 | } 243 | }, 244 | "node_modules/@octokit/types": { 245 | "version": "13.10.0", 246 | "resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.10.0.tgz", 247 | "integrity": "sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA==", 248 | "license": "MIT", 249 | "dependencies": { 250 | "@octokit/openapi-types": "^24.2.0" 251 | } 252 | }, 253 | "node_modules/before-after-hook": { 254 | "version": "3.0.2", 255 | "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-3.0.2.tgz", 256 | "integrity": "sha512-Nik3Sc0ncrMK4UUdXQmAnRtzmNQTAAXmXIopizwZ1W1t8QmfJj+zL4OA2I7XPTPW5z5TDqv4hRo/JzouDJnX3A==" 257 | }, 258 | "node_modules/dayjs": { 259 | "version": "1.11.13", 260 | "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz", 261 | "integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==", 262 | "license": "MIT" 263 | }, 264 | "node_modules/fast-content-type-parse": { 265 | "version": "2.0.1", 266 | "resolved": "https://registry.npmjs.org/fast-content-type-parse/-/fast-content-type-parse-2.0.1.tgz", 267 | "integrity": "sha512-nGqtvLrj5w0naR6tDPfB4cUmYCqouzyQiz6C5y/LtcDllJdrcc6WaWW6iXyIIOErTa/XRybj28aasdn4LkVk6Q==", 268 | "funding": [ 269 | { 270 | "type": "github", 271 | "url": "https://github.com/sponsors/fastify" 272 | }, 273 | { 274 | "type": "opencollective", 275 | "url": "https://opencollective.com/fastify" 276 | } 277 | ], 278 | "license": "MIT" 279 | }, 280 | "node_modules/feed": { 281 | "version": "4.2.2", 282 | "resolved": "https://registry.npmjs.org/feed/-/feed-4.2.2.tgz", 283 | "integrity": "sha512-u5/sxGfiMfZNtJ3OvQpXcvotFpYkL0n9u9mM2vkui2nGo8b4wvDkJ8gAkYqbA8QpGyFCv3RK0Z+Iv+9veCS9bQ==", 284 | "dependencies": { 285 | "xml-js": "^1.6.11" 286 | }, 287 | "engines": { 288 | "node": ">=0.4.0" 289 | } 290 | }, 291 | "node_modules/marked": { 292 | "version": "15.0.7", 293 | "resolved": "https://registry.npmjs.org/marked/-/marked-15.0.7.tgz", 294 | "integrity": "sha512-dgLIeKGLx5FwziAnsk4ONoGwHwGPJzselimvlVskE9XLN4Orv9u2VA3GWw/lYUqjfA0rUT/6fqKwfZJapP9BEg==", 295 | "license": "MIT", 296 | "bin": { 297 | "marked": "bin/marked.js" 298 | }, 299 | "engines": { 300 | "node": ">= 18" 301 | } 302 | }, 303 | "node_modules/sax": { 304 | "version": "1.4.1", 305 | "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.1.tgz", 306 | "integrity": "sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==" 307 | }, 308 | "node_modules/timeago.js": { 309 | "version": "4.0.2", 310 | "resolved": "https://registry.npmjs.org/timeago.js/-/timeago.js-4.0.2.tgz", 311 | "integrity": "sha512-a7wPxPdVlQL7lqvitHGGRsofhdwtkoSXPGATFuSOA2i1ZNQEPLrGnj68vOp2sOJTCFAQVXPeNMX/GctBaO9L2w==" 312 | }, 313 | "node_modules/toad-cache": { 314 | "version": "3.7.0", 315 | "resolved": "https://registry.npmjs.org/toad-cache/-/toad-cache-3.7.0.tgz", 316 | "integrity": "sha512-/m8M+2BJUpoJdgAHoG+baCwBT+tf2VraSfkBgl0Y00qIWt41DJ8R5B8nsEw0I58YwF5IZH6z24/2TobDKnqSWw==", 317 | "license": "MIT", 318 | "engines": { 319 | "node": ">=12" 320 | } 321 | }, 322 | "node_modules/tunnel": { 323 | "version": "0.0.6", 324 | "resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz", 325 | "integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==", 326 | "engines": { 327 | "node": ">=0.6.11 <=0.7.0 || >=0.7.3" 328 | } 329 | }, 330 | "node_modules/universal-github-app-jwt": { 331 | "version": "2.2.0", 332 | "resolved": "https://registry.npmjs.org/universal-github-app-jwt/-/universal-github-app-jwt-2.2.0.tgz", 333 | "integrity": "sha512-G5o6f95b5BggDGuUfKDApKaCgNYy2x7OdHY0zSMF081O0EJobw+1130VONhrA7ezGSV2FNOGyM+KQpQZAr9bIQ==" 334 | }, 335 | "node_modules/universal-user-agent": { 336 | "version": "7.0.2", 337 | "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-7.0.2.tgz", 338 | "integrity": "sha512-0JCqzSKnStlRRQfCdowvqy3cy0Dvtlb8xecj/H8JFZuCze4rwjPZQOgvFvn0Ws/usCHQFGpyr+pB9adaGwXn4Q==" 339 | }, 340 | "node_modules/xml-js": { 341 | "version": "1.6.11", 342 | "resolved": "https://registry.npmjs.org/xml-js/-/xml-js-1.6.11.tgz", 343 | "integrity": "sha512-7rVi2KMfwfWFl+GpPg6m80IVMWXLRjO+PxTq7V2CDhoGak0wzYzFgUY2m4XJ47OGdXd8eLE8EmwfAmdjw7lC1g==", 344 | "dependencies": { 345 | "sax": "^1.2.4" 346 | }, 347 | "bin": { 348 | "xml-js": "bin/cli.js" 349 | } 350 | } 351 | } 352 | } 353 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hackernews-daily", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "type": "module", 7 | "repository": { 8 | "type": "git", 9 | "url": "git+https://github.com/meixger/hackernews-daily.git" 10 | }, 11 | "author": "", 12 | "license": "ISC", 13 | "bugs": { 14 | "url": "https://github.com/meixger/hackernews-daily/issues" 15 | }, 16 | "homepage": "https://github.com/meixger/hackernews-daily#readme", 17 | "dependencies": { 18 | "@actions/core": "^1.11.1", 19 | "@octokit/auth-action": "^5.1.1", 20 | "@octokit/auth-app": "^7.1.5", 21 | "@octokit/core": "^6.1.4", 22 | "dayjs": "^1.11.13", 23 | "feed": "^4.2.2", 24 | "marked": "^15.0.7", 25 | "timeago.js": "^4.0.2" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /tests/testFeed.js: -------------------------------------------------------------------------------- 1 | import { updateFeed } from '../utils/feed.js'; 2 | 3 | await updateFeed(); 4 | -------------------------------------------------------------------------------- /tests/testHeadlines.js: -------------------------------------------------------------------------------- 1 | import { getHeadlines } from "../utils/getHeadlines.js"; 2 | 3 | const headlines = await getHeadlines(new Date(), 25); 4 | console.log(headlines); 5 | -------------------------------------------------------------------------------- /utils/feed.js: -------------------------------------------------------------------------------- 1 | import * as core from '@actions/core'; 2 | import * as fs from 'fs'; 3 | import { Feed } from "feed"; 4 | import { getIssues } from "../utils/issue.js"; 5 | import { marked } from "marked"; 6 | 7 | export const updateFeed = async () => { 8 | const issues = await getIssues({ 9 | owner: 'meixger', 10 | repo: 'hackernews-daily', 11 | take: 30 12 | }); 13 | 14 | const feed = new Feed({ 15 | title: `Hacker News Daily Top 30`, 16 | description: "Hacker News Daily Top 30", 17 | // id: "http://example.com/", 18 | link: "https://github.com/meixger/hackernews-daily/issues/", 19 | // language: "en", // optional, used only in RSS 2.0, possible values: http://www.w3.org/TR/REC-html40/struct/dirlang.html#langcodes 20 | // image: "http://example.com/image.png", 21 | // favicon: "http://example.com/favicon.ico", 22 | // copyright: "All rights reserved 2013, John Doe", 23 | // updated: new Date(2013, 6, 14), // optional, default = today 24 | // generator: "awesome", // optional, default = 'Feed for Node.js' 25 | // feedLinks: { 26 | // json: "https://example.com/json", 27 | // atom: "https://example.com/atom" 28 | // }, 29 | // author: { 30 | // name: "John Doe", 31 | // email: "johndoe@example.com", 32 | // link: "https://example.com/johndoe" 33 | // } 34 | }); 35 | 36 | issues.forEach(i => { 37 | feed.addItem({ 38 | title: i.title, 39 | id: i.number.toString(), 40 | link: i.html_url, 41 | // description: post.description, 42 | content: marked.parse(i.body), 43 | // author: [ 44 | // { 45 | // name: "Jane Doe", 46 | // email: "janedoe@example.com", 47 | // link: "https://example.com/janedoe" 48 | // } 49 | // ], 50 | // contributor: [ 51 | // { 52 | // name: "Shawn Kemp", 53 | // email: "shawnkemp@example.com", 54 | // link: "https://example.com/shawnkemp" 55 | // } 56 | // ], 57 | date: new Date(i.updated_at), 58 | // image: post.image 59 | }); 60 | }); 61 | 62 | fs.writeFile('rss2.xml', feed.rss2(), cb => { if (cb) core.error(cb); }) 63 | } 64 | -------------------------------------------------------------------------------- /utils/getHeadlines.js: -------------------------------------------------------------------------------- 1 | import core from "@actions/core"; 2 | import { EOL } from "os"; 3 | import { exit } from "process"; 4 | 5 | function githubReplaceRenovatebotRedirector(value) { 6 | // avoid creating a GitHub issue reference by using renovatebot redirector 7 | // ref: https://github.com/renovatebot/renovate/blob/main/lib/modules/platform/github/index.ts 8 | return value.replace(/https?:\/\/(www\.)?github.com\//g, 'https://togithub.com/'); 9 | } 10 | 11 | function escapeHTML(value) { 12 | return (value?.replace( 13 | /[&<>'"]/g, 14 | tag => ({ 15 | '&': '&', 16 | '<': '<', 17 | '>': '>', 18 | "'": ''', 19 | '"': '"' 20 | }[tag] || tag)) ?? "" 21 | ); 22 | } 23 | 24 | export const getHeadlines = async (date, take) => { 25 | try { 26 | // end of the date 27 | const endTime = Math.round(date.getTime() / 1000); 28 | // 1 hour before start of the date (save missed posts) 29 | const startTime = endTime - (25 * 60 * 60); 30 | core.notice(`date range from ${new Date(startTime * 1000)} to ${new Date(endTime * 1000)}`); 31 | const url = `https://hn.algolia.com/api/v1/search?hitsPerPage=${take}&numericFilters=created_at_i>${startTime},created_at_i<${endTime}`; 32 | let data; 33 | try { 34 | data = await fetch(url).then(res => res.json()); 35 | } catch (error) { 36 | core.info(url); 37 | core.error(`request failed: ${error.message}`); 38 | exit(1); 39 | } 40 | 41 | const count = data?.hits?.length; 42 | if (!(count > 0)) { 43 | core.info(url); 44 | core.error('no results from api'); 45 | exit(1); 46 | } 47 | 48 | const headlines = data.hits.slice(0, take); 49 | const contents = headlines 50 | .map((obj, i) => { 51 | let { title, url, points, objectID, num_comments } = obj; 52 | const ycombinatorUrl = `https://news.ycombinator.com/item?id=${objectID}`; 53 | if (!url) url = ycombinatorUrl; 54 | const domain = url ? `${new URL(url).hostname}` : ''; 55 | url = githubReplaceRenovatebotRedirector(url); 56 | const titleAndDomain = `[**${escapeHTML(title)}** ${domain}](${url})`; 57 | const commentsAndPoints = `[${num_comments} comments ${points} points](${ycombinatorUrl})`; 58 | return `${i + 1}. ${titleAndDomain} - ${commentsAndPoints}`; 59 | }) 60 | .join(EOL); 61 | 62 | return contents; 63 | } catch (error) { 64 | console.log(error); 65 | throw error 66 | } 67 | 68 | } 69 | -------------------------------------------------------------------------------- /utils/issue.js: -------------------------------------------------------------------------------- 1 | import { Octokit } from "@octokit/core"; 2 | import { createActionAuth } from "@octokit/auth-action"; 3 | 4 | export const openIssue = async ({ owner, repo, title, body }) => { 5 | const octokit = new Octokit({ 6 | authStrategy: createActionAuth 7 | }); 8 | try { 9 | console.log('opening issue'); 10 | const res = await octokit.request('POST /repos/{owner}/{repo}/issues', { 11 | owner, 12 | repo, 13 | title, 14 | body, 15 | }); 16 | console.log('opened'); 17 | return res; 18 | } catch (error) { 19 | console.log(error); 20 | throw error; 21 | } 22 | } 23 | 24 | export const getIssues = async ({ owner, repo, take }) => { 25 | const octokit = new Octokit(); 26 | const res = await octokit.request('GET /repos/{owner}/{repo}/issues', { 27 | owner, 28 | repo, 29 | per_page: take 30 | }); 31 | const issues = res.data; 32 | return issues 33 | } 34 | 35 | export const lockIssue = async ({owner, repo, issueNumber}) => { 36 | const octokit = new Octokit({ 37 | authStrategy: createActionAuth 38 | }); 39 | await octokit.request('PUT /repos/{owner}/{repo}/issues/{issue_number}/lock', { 40 | owner: owner, 41 | repo: repo, 42 | issue_number: issueNumber, 43 | lock_reason: 'resolved' 44 | }); 45 | } 46 | --------------------------------------------------------------------------------