├── .babelrc ├── .editorconfig ├── .env.example ├── .eslintignore ├── .eslintrc.js ├── .github └── workflows │ ├── main.yml │ └── release.yml ├── .gitignore ├── .nvmrc ├── .prettierignore ├── .prettierrc.js ├── .vscode ├── launch.json └── settings.json ├── LICENSE ├── README.md ├── jsconfig.json ├── package-lock.json ├── package.json ├── screenshots ├── image-1.png ├── image-2.png ├── image-3.png ├── image-4.png ├── image-5.png └── working.gif ├── src ├── feed.js ├── helpers.js ├── index.js ├── notion.js └── parser.js └── webpack.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/preset-env"] 3 | } 4 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [{package.json,*.yml}] 12 | indent_style = space 13 | indent_size = 2 14 | 15 | [*.md] 16 | trim_trailing_whitespace = false -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | NOTION_API_TOKEN= 2 | NOTION_READER_DATABASE_ID= 3 | NOTION_FEEDS_DATABASE_ID= 4 | RUN_FREQUENCY=86400 # in seconds 5 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | dist 2 | node_modules 3 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: false, 4 | es2021: true, 5 | node: true, 6 | }, 7 | extends: ['airbnb-base', 'prettier'], 8 | parserOptions: { 9 | ecmaVersion: 12, 10 | sourceType: 'module', 11 | }, 12 | plugins: ['prettier'], 13 | rules: { 14 | 'prettier/prettier': 'error', 15 | 'no-unused-vars': ['off'], 16 | 'no-plusplus': ['off'], 17 | 'no-await-in-loop': ['off'], 18 | 'no-console': 'off', 19 | }, 20 | }; 21 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Get Feed 2 | 3 | on: 4 | schedule: 5 | - cron: '30 12 * * *' # 6pm IST 6 | workflow_dispatch: 7 | 8 | jobs: 9 | get-feed: 10 | runs-on: ubuntu-latest 11 | env: 12 | NOTION_API_TOKEN: ${{ secrets.NOTION_API_TOKEN }} 13 | NOTION_READER_DATABASE_ID: ${{ secrets.NOTION_READER_DATABASE_ID }} 14 | NOTION_FEEDS_DATABASE_ID: ${{ secrets.NOTION_FEEDS_DATABASE_ID }} 15 | RUN_FREQUENCY: 86400 # in seconds 16 | steps: 17 | - name: Setup Node 18 | uses: actions/setup-node@v2 19 | with: 20 | node-version: 14 21 | 22 | - name: Update Notion Feed 23 | run: | 24 | curl -o index.js https://raw.githubusercontent.com/ravgeetdhillon/notion-feeder/build/dist/index.js 25 | node index.js 26 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Set up Repository 13 | uses: actions/checkout@v2 14 | with: 15 | ref: master 16 | 17 | - name: Set up Node 18 | uses: actions/setup-node@v2 19 | with: 20 | node-version: 18 21 | 22 | - name: Cache NPM Dependencies 23 | id: cache-npm-dependencies 24 | uses: actions/cache@v2 25 | with: 26 | path: node_modules 27 | key: ${{ runner.os }}-node_modules-${{ hashFiles('**/package-lock.json') }} 28 | restore-keys: | 29 | ${{ runner.os }}-node_modules- 30 | 31 | - name: Install NPM Dependencies if not cached 32 | if: steps.cache-npm-dependencies.outputs.cache-hit != 'true' 33 | run: | 34 | npm install 35 | 36 | - name: Build Project 37 | run: | 38 | npm run build-prod 39 | 40 | - name: Upload Artifacts 41 | uses: actions/upload-artifact@v2 42 | with: 43 | name: dist 44 | path: dist 45 | 46 | commit-build: 47 | needs: build 48 | runs-on: ubuntu-latest 49 | steps: 50 | - name: Set up Repository 51 | uses: actions/checkout@v2 52 | with: 53 | ref: build 54 | 55 | - name: Download Build 56 | uses: actions/download-artifact@v2 57 | with: 58 | name: dist 59 | path: dist 60 | 61 | - name: Commit and Push 62 | run: | 63 | if [ $(git status dist --porcelain=v1 2>/dev/null | wc -l) != "0" ] ; then 64 | git config user.name "GitHub Actions" 65 | git config user.email noreply@github.com 66 | git add dist 67 | git commit -m "chore: updated build" 68 | git push origin HEAD --force 69 | fi 70 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.toptal.com/developers/gitignore/api/node 3 | # Edit at https://www.toptal.com/developers/gitignore?templates=node 4 | 5 | ### Node ### 6 | # Logs 7 | logs 8 | *.log 9 | npm-debug.log* 10 | yarn-debug.log* 11 | yarn-error.log* 12 | lerna-debug.log* 13 | .pnpm-debug.log* 14 | 15 | # Diagnostic reports (https://nodejs.org/api/report.html) 16 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 17 | 18 | # Runtime data 19 | pids 20 | *.pid 21 | *.seed 22 | *.pid.lock 23 | 24 | # Directory for instrumented libs generated by jscoverage/JSCover 25 | lib-cov 26 | 27 | # Coverage directory used by tools like istanbul 28 | coverage 29 | *.lcov 30 | 31 | # nyc test coverage 32 | .nyc_output 33 | 34 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 35 | .grunt 36 | 37 | # Bower dependency directory (https://bower.io/) 38 | bower_components 39 | 40 | # node-waf configuration 41 | .lock-wscript 42 | 43 | # Compiled binary addons (https://nodejs.org/api/addons.html) 44 | build/Release 45 | 46 | # Dependency directories 47 | node_modules/ 48 | jspm_packages/ 49 | 50 | # Snowpack dependency directory (https://snowpack.dev/) 51 | web_modules/ 52 | 53 | # TypeScript cache 54 | *.tsbuildinfo 55 | 56 | # Optional npm cache directory 57 | .npm 58 | 59 | # Optional eslint cache 60 | .eslintcache 61 | 62 | # Microbundle cache 63 | .rpt2_cache/ 64 | .rts2_cache_cjs/ 65 | .rts2_cache_es/ 66 | .rts2_cache_umd/ 67 | 68 | # Optional REPL history 69 | .node_repl_history 70 | 71 | # Output of 'npm pack' 72 | *.tgz 73 | 74 | # Yarn Integrity file 75 | .yarn-integrity 76 | 77 | # dotenv environment variables file 78 | .env 79 | .env.test 80 | .env.production 81 | 82 | # parcel-bundler cache (https://parceljs.org/) 83 | .cache 84 | .parcel-cache 85 | 86 | # Next.js build output 87 | .next 88 | out 89 | 90 | # Nuxt.js build / generate output 91 | .nuxt 92 | dist 93 | 94 | # Gatsby files 95 | .cache/ 96 | # Comment in the public line in if your project uses Gatsby and not Next.js 97 | # https://nextjs.org/blog/next-9-1#public-directory-support 98 | # public 99 | 100 | # vuepress build output 101 | .vuepress/dist 102 | 103 | # Serverless directories 104 | .serverless/ 105 | 106 | # FuseBox cache 107 | .fusebox/ 108 | 109 | # DynamoDB Local files 110 | .dynamodb/ 111 | 112 | # TernJS port file 113 | .tern-port 114 | 115 | # Stores VSCode versions used for testing VSCode extensions 116 | .vscode-test 117 | 118 | # yarn v2 119 | .yarn/cache 120 | .yarn/unplugged 121 | .yarn/build-state.yml 122 | .yarn/install-state.gz 123 | .pnp.* 124 | 125 | ### Node Patch ### 126 | # Serverless Webpack directories 127 | .webpack/ 128 | 129 | # End of https://www.toptal.com/developers/gitignore/api/node -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | v18 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | dist 2 | node_modules 3 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | trailingComma: 'es5', 3 | tabWidth: 2, 4 | semi: true, 5 | singleQuote: true, 6 | }; 7 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "pwa-node", 9 | "request": "launch", 10 | "name": "Launch Program", 11 | "skipFiles": ["/**"], 12 | "program": "${workspaceFolder}/dist/index.js" 13 | } 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "[javascript]": { 3 | "editor.codeActionsOnSave": { 4 | "source.organizeImports": false 5 | } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Ravgeet Dhillon 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 | [![Release](https://github.com/ravgeetdhillon/notion-feeder/actions/workflows/release.yml/badge.svg)](https://github.com/ravgeetdhillon/notion-feeder/actions/workflows/release.yml) 2 | [![Get Feed](https://github.com/ravgeetdhillon/notion-feeder/actions/workflows/main.yml/badge.svg)](https://github.com/ravgeetdhillon/notion-feeder/actions/workflows/main.yml) 3 | [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) 4 | 5 | Notion Feeder - Convert Notion to a Feed Reader | Product Hunt 6 | 7 | --- 8 | 9 | If you love this product and value my time, consider [sending some love](https://paypal.me/ravgeetdhillon) to me. This will enable me to work on more projects like these in the future. 10 | 11 | [![PayPal Donate Button](https://images.squarespace-cdn.com/content/v1/55f62c4ce4b02545cc6ee94f/1558204823259-CQ0YNKZEHP7W5LO64PBU/paypal-donate-button-1.PNG?format=300w)](https://paypal.me/ravgeetdhillon) 12 | 13 | --- 14 | 15 | # Notion Feeder 16 | 17 | A Node.js app for creating a Feed Reader in [Notion](https://notion.so). 18 | 19 | ![](/screenshots/working.gif) 20 | 21 | ## Features 22 | 23 | Separate database for your feed sources and feed items. 24 | 25 | ![](/screenshots/image-1.png) 26 | 27 | Add, enable and disable your feed sources. 28 | 29 | ![](/screenshots/image-2.png) 30 | 31 | Feeds are sourced daily and stored in the **Reader** database. New feed items are marked with 🔥. 32 | 33 | ![](/screenshots/image-3.png) 34 | 35 | Read a feed directly in Notion Page View. 36 | 37 | ![](/screenshots/image-4.png) 38 | 39 | Different views of accessing Unread, Starred feed items. 40 | 41 | ![](/screenshots/image-5.png) 42 | 43 | ## Setup 44 | 45 | 1. Create a new [Notion Integration](https://www.notion.so/my-integrations) and copy the secret code which you'll use as `NOTION_API_TOKEN` in Step 4. 46 | 47 | 2. Duplicate this [template](https://ravsamhq.notion.site/Feeder-fa2aa54827fa42c2af1eb25c7a45a408) to your Notion workspace. 48 | 49 | 3. Once the template is available on your Notion Workspace, open the **Reader** database. Click the three-button page menu in the top right corner **...** _> Add connections_ and search the Notion integration you created in Step 1 and Click **Invite**. Do the same for the **Feeds** database. 50 | 51 | 4. Fork this GitHub repository and once forking is complete, go to your forked GitHub repository. 52 | 53 | 5. Enable the GitHub Actions by visiting the **Actions** tab and click "I understand my workflows, enable them". 54 | 55 | 6. Click on the **Get Feed** action in the left panel and then click "Enable workflow". 56 | 57 | 7. Go to _Settings > Secrets_. Add the following three secrets along with their values as **Repository secrets**. 58 | 59 | ``` 60 | NOTION_API_TOKEN 61 | NOTION_READER_DATABASE_ID 62 | NOTION_FEEDS_DATABASE_ID 63 | ``` 64 | 65 | > To find your database id, visit your database on Notion. You'll get a URL like this: https://www.notion.so/{workspace_name}/{database_id}?v={view_id}. For example, if your URL looks like this: https://www.notion.so/abc/xyz?v=123, then `xyz` is your database ID. 66 | 67 | 8. Delete the [release workflow file](.github/workflows/release.yml) as it is only required in the original repository. 68 | 69 | 9. That's it. Now every day, your feed will be updated at 12:30 UTC. 70 | 71 | **Note**: You can change the time at which the script runs from [here](.github/workflows/main.yml#L5) and the frequency of running from [here](.github/workflows/main.yml#L15). 72 | 73 | ## Development 74 | 75 | You are more than welcome to contribute to this project. 76 | 77 | ### Prerequisites 78 | 79 | These things are required before setting up the project. 80 | 81 | - Git 82 | - Ubuntu 18.04 or 20.04 83 | - Node.js [Read Guide](https://www.digitalocean.com/community/tutorials/how-to-install-node-js-on-ubuntu-20-04) 84 | 85 | ### Setup 86 | 87 | Follow these instructions to get the project up and running. 88 | 89 | ```bash 90 | # clone the repo 91 | $ git clone https://github.com/ravgeetdhillon/notion-feeder.git 92 | 93 | # change directory 94 | $ cd notion-feeder 95 | 96 | # install dependencies 97 | $ npm install 98 | 99 | # enable webpack bundling 100 | $ npm run watch 101 | ``` 102 | 103 | ## Tech Stack 104 | 105 | - [Node](https://nodejs.org/) 106 | - [Notion API](https://developers.notion.com) 107 | 108 | ## Contributors 109 | 110 | - [Ravgeet Dhillon](https://github.com/ravgeetdhillon) 111 | 112 | ## Extra 113 | 114 | - You can request features and file bugs [here](https://github.com/ravgeetdhillon/notion-feeder/issues). 115 | - In case you get stuck somewhere, feel free to contact me at my [email](mailto:ravgeetdhillon@gmail.com). 116 | -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "paths": { 5 | "~/*": ["./*"], 6 | "@/*": ["./*"], 7 | "~~/*": ["./*"], 8 | "@@/*": ["./*"] 9 | } 10 | }, 11 | "exclude": ["node_modules", "dist"] 12 | } 13 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "notion-feed-reader", 3 | "version": "1.0.0", 4 | "private": true, 5 | "description": "A Node app for creating a Feed Reader in Notion.", 6 | "keywords": [ 7 | "notion", 8 | "notion-api", 9 | "notion-feed-reader", 10 | "feed-reader", 11 | "node", 12 | "javascript" 13 | ], 14 | "scripts": { 15 | "develop": "webpack --watch", 16 | "build": "webpack", 17 | "build-prod": "webpack --env mode='production'", 18 | "feed": "node dist/index.js" 19 | }, 20 | "author": "Ravgeet Dhillon ", 21 | "license": "MIT", 22 | "dependencies": { 23 | "@notionhq/client": "^0.4.9", 24 | "@tryfabric/martian": "^1.1.1", 25 | "dotenv": "^10.0.0", 26 | "rss-parser": "^3.12.0", 27 | "turndown": "^7.1.1" 28 | }, 29 | "devDependencies": { 30 | "@babel/core": "^7.17.8", 31 | "@babel/preset-env": "^7.16.4", 32 | "babel-loader": "^8.2.3", 33 | "eslint": "^7.32.0", 34 | "eslint-config-airbnb": "^18.2.1", 35 | "eslint-config-airbnb-base": "^14.2.1", 36 | "eslint-config-prettier": "^8.5.0", 37 | "eslint-plugin-import": "^2.25.4", 38 | "eslint-plugin-prettier": "^4.0.0", 39 | "eslint-webpack-plugin": "^3.1.1", 40 | "prettier": "^2.6.0", 41 | "webpack": "^5.70.0", 42 | "webpack-cli": "^4.9.2" 43 | }, 44 | "engines": { 45 | "node": ">=10.16.0 <=14.x.x", 46 | "npm": ">=6.0.0" 47 | }, 48 | "repository": { 49 | "type": "git", 50 | "url": "git+https://github.com/ravgeetdhillon/notion-feed-reader" 51 | }, 52 | "bugs": { 53 | "url": "https://github.com/ravgeetdhillon/notion-feed-reader/issues" 54 | }, 55 | "homepage": "https://github.com/ravgeetdhillon/notion-feed-reader" 56 | } 57 | -------------------------------------------------------------------------------- /screenshots/image-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ravgeetdhillon/notion-feeder/ff700f65518ec28c78600608047efab73578155b/screenshots/image-1.png -------------------------------------------------------------------------------- /screenshots/image-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ravgeetdhillon/notion-feeder/ff700f65518ec28c78600608047efab73578155b/screenshots/image-2.png -------------------------------------------------------------------------------- /screenshots/image-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ravgeetdhillon/notion-feeder/ff700f65518ec28c78600608047efab73578155b/screenshots/image-3.png -------------------------------------------------------------------------------- /screenshots/image-4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ravgeetdhillon/notion-feeder/ff700f65518ec28c78600608047efab73578155b/screenshots/image-4.png -------------------------------------------------------------------------------- /screenshots/image-5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ravgeetdhillon/notion-feeder/ff700f65518ec28c78600608047efab73578155b/screenshots/image-5.png -------------------------------------------------------------------------------- /screenshots/working.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ravgeetdhillon/notion-feeder/ff700f65518ec28c78600608047efab73578155b/screenshots/working.gif -------------------------------------------------------------------------------- /src/feed.js: -------------------------------------------------------------------------------- 1 | import Parser from 'rss-parser'; 2 | import dotenv from 'dotenv'; 3 | import timeDifference from './helpers'; 4 | import { getFeedUrlsFromNotion } from './notion'; 5 | 6 | dotenv.config(); 7 | 8 | const { RUN_FREQUENCY } = process.env; 9 | 10 | async function getNewFeedItemsFrom(feedUrl) { 11 | const parser = new Parser(); 12 | let rss; 13 | try { 14 | rss = await parser.parseURL(feedUrl); 15 | } catch (error) { 16 | console.error(error); 17 | return []; 18 | } 19 | const currentTime = new Date().getTime() / 1000; 20 | 21 | // Filter out items that fall in the run frequency range 22 | return rss.items.filter((item) => { 23 | const blogPublishedTime = new Date(item.pubDate).getTime() / 1000; 24 | const { diffInSeconds } = timeDifference(currentTime, blogPublishedTime); 25 | return diffInSeconds < RUN_FREQUENCY; 26 | }); 27 | } 28 | 29 | export default async function getNewFeedItems() { 30 | let allNewFeedItems = []; 31 | 32 | const feeds = await getFeedUrlsFromNotion(); 33 | 34 | for (let i = 0; i < feeds.length; i++) { 35 | const { feedUrl } = feeds[i]; 36 | const feedItems = await getNewFeedItemsFrom(feedUrl); 37 | allNewFeedItems = [...allNewFeedItems, ...feedItems]; 38 | } 39 | 40 | // sort feed items by published date 41 | allNewFeedItems.sort((a, b) => new Date(a.pubDate) - new Date(b.pubDate)); 42 | 43 | return allNewFeedItems; 44 | } 45 | -------------------------------------------------------------------------------- /src/helpers.js: -------------------------------------------------------------------------------- 1 | export default function timeDifference(date1, date2) { 2 | const difference = Math.floor(date1) - Math.floor(date2); 3 | 4 | const diffInDays = Math.floor(difference / 60 / 60 / 24); 5 | const diffInHours = Math.floor(difference / 60 / 60); 6 | const diffInMinutes = Math.floor(difference / 60); 7 | const diffInSeconds = Math.floor(difference); 8 | 9 | return { 10 | date1, 11 | date2, 12 | diffInDays, 13 | diffInHours, 14 | diffInMinutes, 15 | diffInSeconds, 16 | }; 17 | } 18 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import getNewFeedItems from './feed'; 2 | import { 3 | addFeedItemToNotion, 4 | deleteOldUnreadFeedItemsFromNotion, 5 | } from './notion'; 6 | import htmlToNotionBlocks from './parser'; 7 | 8 | async function index() { 9 | const feedItems = await getNewFeedItems(); 10 | 11 | for (let i = 0; i < feedItems.length; i++) { 12 | const item = feedItems[i]; 13 | const notionItem = { 14 | title: item.title, 15 | link: item.link, 16 | content: htmlToNotionBlocks(item.content), 17 | }; 18 | await addFeedItemToNotion(notionItem); 19 | } 20 | 21 | await deleteOldUnreadFeedItemsFromNotion(); 22 | } 23 | 24 | index(); 25 | -------------------------------------------------------------------------------- /src/notion.js: -------------------------------------------------------------------------------- 1 | import dotenv from 'dotenv'; 2 | import { Client, LogLevel } from '@notionhq/client'; 3 | 4 | dotenv.config(); 5 | 6 | const { 7 | NOTION_API_TOKEN, 8 | NOTION_READER_DATABASE_ID, 9 | NOTION_FEEDS_DATABASE_ID, 10 | CI, 11 | } = process.env; 12 | 13 | const logLevel = CI ? LogLevel.INFO : LogLevel.DEBUG; 14 | 15 | export async function getFeedUrlsFromNotion() { 16 | const notion = new Client({ 17 | auth: NOTION_API_TOKEN, 18 | logLevel, 19 | }); 20 | 21 | let response; 22 | try { 23 | response = await notion.databases.query({ 24 | database_id: NOTION_FEEDS_DATABASE_ID, 25 | filter: { 26 | or: [ 27 | { 28 | property: 'Enabled', 29 | checkbox: { 30 | equals: true, 31 | }, 32 | }, 33 | ], 34 | }, 35 | }); 36 | } catch (err) { 37 | console.error(err); 38 | return []; 39 | } 40 | 41 | const feeds = response.results.map((item) => ({ 42 | title: item.properties.Title.title[0].plain_text, 43 | feedUrl: item.properties.Link.url, 44 | })); 45 | 46 | return feeds; 47 | } 48 | 49 | export async function addFeedItemToNotion(notionItem) { 50 | const { title, link, content } = notionItem; 51 | 52 | const notion = new Client({ 53 | auth: NOTION_API_TOKEN, 54 | logLevel, 55 | }); 56 | 57 | try { 58 | await notion.pages.create({ 59 | parent: { 60 | database_id: NOTION_READER_DATABASE_ID, 61 | }, 62 | properties: { 63 | Title: { 64 | title: [ 65 | { 66 | text: { 67 | content: title, 68 | }, 69 | }, 70 | ], 71 | }, 72 | Link: { 73 | url: link, 74 | }, 75 | }, 76 | children: content, 77 | }); 78 | } catch (err) { 79 | console.error(err); 80 | } 81 | } 82 | 83 | export async function deleteOldUnreadFeedItemsFromNotion() { 84 | const notion = new Client({ 85 | auth: NOTION_API_TOKEN, 86 | logLevel, 87 | }); 88 | 89 | // Create a datetime which is 30 days earlier than the current time 90 | const fetchBeforeDate = new Date(); 91 | fetchBeforeDate.setDate(fetchBeforeDate.getDate() - 30); 92 | 93 | // Query the feed reader database 94 | // and fetch only those items that are unread or created before last 30 days 95 | let response; 96 | try { 97 | response = await notion.databases.query({ 98 | database_id: NOTION_READER_DATABASE_ID, 99 | filter: { 100 | and: [ 101 | { 102 | property: 'Created At', 103 | date: { 104 | on_or_before: fetchBeforeDate.toJSON(), 105 | }, 106 | }, 107 | { 108 | property: 'Read', 109 | checkbox: { 110 | equals: false, 111 | }, 112 | }, 113 | ], 114 | }, 115 | }); 116 | } catch (err) { 117 | console.error(err); 118 | return; 119 | } 120 | 121 | // Get the page IDs from the response 122 | const feedItemsIds = response.results.map((item) => item.id); 123 | 124 | for (let i = 0; i < feedItemsIds.length; i++) { 125 | const id = feedItemsIds[i]; 126 | try { 127 | await notion.pages.update({ 128 | page_id: id, 129 | archived: true, 130 | }); 131 | } catch (err) { 132 | console.error(err); 133 | } 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /src/parser.js: -------------------------------------------------------------------------------- 1 | import { markdownToBlocks } from '@tryfabric/martian'; 2 | import TurndownService from 'turndown'; 3 | 4 | function htmlToMarkdownJSON(htmlContent) { 5 | try { 6 | const turndownService = new TurndownService(); 7 | return turndownService.turndown(htmlContent); 8 | } catch (error) { 9 | console.error(error); 10 | return {}; 11 | } 12 | } 13 | 14 | function jsonToNotionBlocks(markdownContent) { 15 | return markdownToBlocks(markdownContent); 16 | } 17 | 18 | export default function htmlToNotionBlocks(htmlContent) { 19 | const markdownJson = htmlToMarkdownJSON(htmlContent); 20 | return jsonToNotionBlocks(markdownJson); 21 | } 22 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const ESLintPlugin = require('eslint-webpack-plugin'); 3 | const webpack = require('webpack'); 4 | 5 | const getSrcPath = (filePath) => { 6 | const src = path.resolve(__dirname, 'src'); 7 | return path.posix.join(src.replace(/\\/g, '/'), filePath); 8 | }; 9 | 10 | module.exports = (env) => { 11 | const isProductionMode = env.mode === 'production'; 12 | 13 | return { 14 | target: 'node', 15 | mode: isProductionMode ? 'production' : 'development', 16 | context: __dirname, 17 | entry: getSrcPath('/index.js'), 18 | stats: { errorDetails: !isProductionMode }, 19 | output: { 20 | filename: 'index.js', 21 | path: path.resolve(__dirname, 'dist'), 22 | clean: true, 23 | }, 24 | resolve: { 25 | alias: { 26 | src: path.resolve(__dirname, 'src'), 27 | }, 28 | extensions: ['.js'], 29 | }, 30 | optimization: { 31 | minimize: false, 32 | }, 33 | performance: { 34 | hints: false, 35 | }, 36 | watchOptions: { 37 | ignored: ['**/dist', '**/node_modules'], 38 | }, 39 | module: { 40 | rules: [ 41 | { 42 | test: /\.js$/, 43 | exclude: /node_modules/, 44 | use: { 45 | loader: 'babel-loader', 46 | options: { 47 | presets: [ 48 | ['@babel/preset-env', { targets: { node: 'current' } }], 49 | ], 50 | plugins: [ 51 | [ 52 | '@babel/plugin-proposal-object-rest-spread', 53 | { loose: true, useBuiltIns: true }, 54 | ], 55 | ], 56 | }, 57 | }, 58 | }, 59 | ], 60 | }, 61 | plugins: [new ESLintPlugin(), new webpack.ProgressPlugin()], 62 | }; 63 | }; 64 | --------------------------------------------------------------------------------