├── .dockerignore ├── .github ├── dependabot.yml └── workflows │ └── build.yml ├── .gitignore ├── Dockerfile ├── LICENSE.txt ├── README.md ├── SECURITY.md ├── img ├── CanvasIntegrationDetails.png ├── CanvasIntegrationNAT.png ├── CanvasIntegrationToken.png ├── IDProperty.png ├── IDPropertyHidden.png ├── NotionIntegration.gif ├── NotionPermissions.gif └── canvasNotionIntegration.png ├── package-lock.json ├── package.json ├── src ├── canvashelper.js ├── main.js ├── notionhelper.js └── util.js └── web └── index.js /.dockerignore: -------------------------------------------------------------------------------- 1 | # Include any files or directories that you don't want to be copied to your 2 | # container here (e.g., local build artifacts, temporary files, etc.). 3 | # 4 | # For more help, visit the .dockerignore file reference guide at 5 | # https://docs.docker.com/engine/reference/builder/#dockerignore-file 6 | 7 | **/.classpath 8 | **/.dockerignore 9 | **/.env 10 | **/.git 11 | **/.gitignore 12 | **/.project 13 | **/.settings 14 | **/.toolstarget 15 | **/.vs 16 | **/.vscode 17 | **/.next 18 | **/.cache 19 | **/*.*proj.user 20 | **/*.dbmdl 21 | **/*.jfm 22 | **/charts 23 | **/docker-compose* 24 | **/compose* 25 | **/Dockerfile* 26 | **/node_modules 27 | **/npm-debug.log 28 | **/obj 29 | **/secrets.dev.yaml 30 | **/values.dev.yaml 31 | **/build 32 | **/dist 33 | LICENSE.txt 34 | README.md 35 | SECURITY.md 36 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - 4 | package-ecosystem: "github-actions" 5 | directory: "/" 6 | schedule: 7 | interval: "daily" 8 | - 9 | package-ecosystem: "npm" 10 | directory: "/" 11 | schedule: 12 | interval: "daily" 13 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | 9 | env: 10 | REGISTRY: ghcr.io 11 | IMAGE_NAME: ${{ github.repository }} 12 | 13 | jobs: 14 | build: 15 | runs-on: ubuntu-latest 16 | timeout-minutes: 30 17 | if: github.ref == 'refs/heads/main' 18 | permissions: 19 | packages: write 20 | steps: 21 | - 22 | name: Checkout 23 | uses: actions/checkout@v4 24 | - 25 | name: Set up QEMU 26 | uses: docker/setup-qemu-action@v3 27 | - 28 | name: Set up Docker Buildx 29 | uses: docker/setup-buildx-action@v3 30 | - 31 | name: Login to GitHub Container Registry 32 | uses: docker/login-action@v3 33 | with: 34 | registry: ${{ env.REGISTRY }} 35 | username: ${{ github.actor }} 36 | password: ${{ secrets.GITHUB_TOKEN }} 37 | - 38 | name: Extract metadata for Docker 39 | id: meta 40 | uses: docker/metadata-action@v5 41 | with: 42 | images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} 43 | - 44 | name: Build and Push Docker Image 45 | uses: docker/build-push-action@v6 46 | with: 47 | context: . 48 | platforms: linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64 49 | push: true 50 | tags: ${{ steps.meta.outputs.tags }} 51 | labels: ${{ steps.meta.outputs.labels }} 52 | cache-from: type=gha 53 | cache-to: type=gha,mode=max 54 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # dotenv environment variables files 2 | .env 3 | .env.test 4 | /node_modules 5 | test.js 6 | /old -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # syntax=docker/dockerfile:1 2 | 3 | # Comments are provided throughout this file to help you get started. 4 | # If you need more help, visit the Dockerfile reference guide at 5 | # https://docs.docker.com/engine/reference/builder/ 6 | 7 | # Do not upgrade node past 18 8 | # Blocked by https://github.com/docker/build-push-action/issues/1071 9 | FROM node:18.19.1-alpine 10 | # Use production node environment by default. 11 | ENV NODE_ENV production 12 | 13 | WORKDIR /usr/src/app 14 | 15 | # Download dependencies as a separate step to take advantage of Docker's caching. 16 | # Leverage a cache mount to /root/.npm to speed up subsequent builds. 17 | # Leverage a bind mounts to package.json and package-lock.json to avoid having to copy them into 18 | # into this layer. 19 | RUN --mount=type=bind,source=package.json,target=package.json \ 20 | --mount=type=bind,source=package-lock.json,target=package-lock.json \ 21 | --mount=type=cache,target=/root/.npm \ 22 | npm ci --omit=dev 23 | 24 | # Run the application as a non-root user. 25 | USER node 26 | 27 | # Copy the rest of the source files into the image. 28 | COPY . . 29 | 30 | # Run the application. 31 | CMD node main.js 32 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Mari Garey 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 | # Canvas to Notion Integration 2 | View your Canvas assignments in Notion created by Mari Garey! 3 | 4 | 5 | 6 | ## Introduction 7 | 8 | Using this repository you will be able to export all of your assignments from Canvas to a Notion Database! 9 | Following the instructions below will help you set up the database! 10 | 11 | ### Support the Creator! 12 | 13 | * Give a ⭐️ to the repository please and thank you 🤗 14 | * Submit a PR for feedback or in the Discussion Tab 15 | * Watch the demo on YT and give it a 👍 16 | 17 | 18 | ## Using the Canvas to Notion Integration 19 | 20 | ### Video Tutorial 21 | 22 | *Coming Soon* 23 | 24 | ### 1. Project Setup 25 | 26 | ```zsh 27 | # Clone this repository to your computer 28 | git clone https://github.com/marigarey/canvas-notion-integration.git 29 | 30 | # Open this project 31 | cd canvas-notion-integration 32 | ``` 33 | 34 | #### Without Docker 35 | ```zsh 36 | # Install dependencies 37 | npm install 38 | ``` 39 | 40 | #### With Docker 41 | ```zsh 42 | # Build image 43 | docker -t canvas-notion-integration build . 44 | ``` 45 | 46 | > [!NOTE] 47 | > This step is not required on most architectures. GHCR should have built the latest version on the following architectures: 48 | > - `linux/amd64` 49 | > - `linux/arm/v6` 50 | > - `linux/arm/v7` 51 | > - `linux/arm64` 52 | 53 | ### 2. Canvas Token Access 54 | 55 | Go to your Canvas Profile Settings and scroll down to `Approved Integrations`. 56 | 57 | Click on `+ New Access Token` to create the token. 58 | 59 | 60 | Name your Token, and leave the date blank to have no expiration date. 61 | 62 | 63 | Once the Token is generated, copy the Token string. 64 | 65 | This string will be your **Canvas API Key** 66 | 67 | > [!WARNING] 68 | > Once you move away from that screen you will not be able to access the token string! 69 | > Make sure to save the Token string now! 70 | 71 | ### 3. Notion API Key Access[^1] 72 | 73 | Pull up the [Notion - My Integrations](https://www.notion.so/my-integrations) site and click `+ New Integration` 74 | 75 | Enter the name of the integration (ie Canvas Notion Integration) and what workspace the Integration will apply to. 76 | In the `Secrets` tab and copy the _Internal Integration Secret_ this will be your **Notion API Key**. 77 | 78 | 79 | 80 | ### 4. Create Integration within Notion 81 | 82 | Head to whatever Notion Page you want to put the database in and click on `...` in the top right. 83 | Scroll down to `+ Add Connections`. Find and select the integration. Make sure to click confirm. 84 | 85 | 86 | 87 | ### 5. Environment Variable `.env` file Setup 88 | Create a `.env` file and replace all the <> with your own information. Place the `.env` file in the `src` folder. 89 | *Keep the `NOTION_DATABASE` variable as is because it will be overwritten when you run the code* 90 | > [!NOTE] 91 | > How to Access the Key for the `NOTION_PAGE`: 92 | > 1. On the desired Notion page, click `Share` then `🔗 Copy link` 93 | > 2. Paste the link down, example url: notion.so/{name}/{page}-**0123456789abcdefghijklmnopqrstuv**?{otherstuff} 94 | > 3. Copy the string of 32 letter and number combination to the `.env` file 95 | 96 | ``` 97 | CANVAS_API_URL= 98 | CANVAS_API= 99 | NOTION_PAGE= 100 | NOTION_API= # filled by user 101 | NOTION_DATABASE='invalid' # filled by integration 102 | ``` 103 | 104 | ### 6. Run Code 105 | 106 | > [!IMPORTANT] 107 | > To update your database you will have to run the script every time there is a change in Canvas 108 | > It is recomended to rerun the code every semester or class/assignment changes 109 | 110 | #### Without Docker 111 | ```zsh 112 | cd src 113 | node main.js 114 | ``` 115 | 116 | #### With Docker 117 | ```zsh 118 | docker run --env-file ./.env canvas-notion-integration 119 | ``` 120 | 121 | > [!NOTE] 122 | > If you did not choose to build the image yourself, you can replace `canvas-notion-integration` with `ghcr.io/marigarey/canvas-notion-integration:main` 123 | 124 | ## Other Information 125 | 126 | In the future I do plan to add more to this, possibly blocks outside of the database. 127 | If you have any suggestions on what I should, please let me know! I want to hear your feedback and improve! 128 | 129 | > [!NOTE] 130 | > The ID Property is for internal use and you can hide it in your database 131 | > Hiding a Property: 132 | > 1. Go to `...` on the top right of your database 133 | > 2. Click on the `Properties` Tab 134 | > 3. Click the eye on the `ID` Property 135 | > 4. It should get crossed out and disapear from your database! 136 | 137 | Other: Docker addition doesn't run because the .env file is not set up 138 | 139 | [^1]: [Source of Gifs and for more information on Notion Integrations](https://developers.notion.com/docs/create-a-notion-integration) 140 | 141 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | Please be safe :D 3 | 4 | ## Supported Versions 5 | 6 | | Version | Supported | 7 | | ------- | ------------------ | 8 | | 2.2.15 | notionhq/client | 9 | | ^16.4.5 | dotenv | 10 | 11 | ## Reporting a Vulnerability 12 | 13 | Please open a PR and title it "VULNERABILITY:..." 14 | I will try to view PRs consistently and respond as soon as I can. 15 | There should not be anything that is super dangerous, so hopefully 16 | no vulnerabilities should happen :) 17 | -------------------------------------------------------------------------------- /img/CanvasIntegrationDetails.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marigarey/canvas-notion-integration/01cfd12a24f011af53f056cf92ef505888ac8cf9/img/CanvasIntegrationDetails.png -------------------------------------------------------------------------------- /img/CanvasIntegrationNAT.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marigarey/canvas-notion-integration/01cfd12a24f011af53f056cf92ef505888ac8cf9/img/CanvasIntegrationNAT.png -------------------------------------------------------------------------------- /img/CanvasIntegrationToken.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marigarey/canvas-notion-integration/01cfd12a24f011af53f056cf92ef505888ac8cf9/img/CanvasIntegrationToken.png -------------------------------------------------------------------------------- /img/IDProperty.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marigarey/canvas-notion-integration/01cfd12a24f011af53f056cf92ef505888ac8cf9/img/IDProperty.png -------------------------------------------------------------------------------- /img/IDPropertyHidden.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marigarey/canvas-notion-integration/01cfd12a24f011af53f056cf92ef505888ac8cf9/img/IDPropertyHidden.png -------------------------------------------------------------------------------- /img/NotionIntegration.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marigarey/canvas-notion-integration/01cfd12a24f011af53f056cf92ef505888ac8cf9/img/NotionIntegration.gif -------------------------------------------------------------------------------- /img/NotionPermissions.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marigarey/canvas-notion-integration/01cfd12a24f011af53f056cf92ef505888ac8cf9/img/NotionPermissions.gif -------------------------------------------------------------------------------- /img/canvasNotionIntegration.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marigarey/canvas-notion-integration/01cfd12a24f011af53f056cf92ef505888ac8cf9/img/canvasNotionIntegration.png -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nodemon", 3 | "lockfileVersion": 3, 4 | "requires": true, 5 | "packages": { 6 | "": { 7 | <<<<<<< HEAD 8 | "name": "nodemon", 9 | "license": "MIT", 10 | "dependencies": { 11 | "@notionhq/client": "^2.2.15", 12 | "dotenv": "^16.4.5", 13 | "html-to-notion": "^0.2.1" 14 | ======= 15 | "license": "MIT", 16 | "dependencies": { 17 | "@notionhq/client": "^2.2.17", 18 | "dotenv": "^16.4.7" 19 | >>>>>>> 6652d04b4df2a983a45f5c01ddf8b03b06b6ded1 20 | } 21 | }, 22 | "node_modules/@notionhq/client": { 23 | "version": "2.2.17", 24 | "resolved": "https://registry.npmjs.org/@notionhq/client/-/client-2.2.17.tgz", 25 | "integrity": "sha512-whkUc2RFAk7Vo93todfwsK6bxEHrBg4JSUHN+8cvopZGKsnU8aVL4JtJ6W2cexRz0Bp0AfznHsY7eD8/vNgMCw==", 26 | "license": "MIT", 27 | "dependencies": { 28 | "@types/node-fetch": "^2.5.10", 29 | "node-fetch": "^2.6.1" 30 | }, 31 | "engines": { 32 | "node": ">=12" 33 | } 34 | }, 35 | "node_modules/@types/node": { 36 | "version": "20.14.2", 37 | "resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.2.tgz", 38 | "integrity": "sha512-xyu6WAMVwv6AKFLB+e/7ySZVr/0zLCzOa7rSpq6jNwpqOrUbcACDWC+53d4n2QHOnDou0fbIsg8wZu/sxrnI4Q==", 39 | "dependencies": { 40 | "undici-types": "~5.26.4" 41 | } 42 | }, 43 | "node_modules/@types/node-fetch": { 44 | "version": "2.6.11", 45 | "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.11.tgz", 46 | "integrity": "sha512-24xFj9R5+rfQJLRyM56qh+wnVSYhyXC2tkoBndtY0U+vubqNsYXGjufB2nn8Q6gt0LrARwL6UBtMCSVCwl4B1g==", 47 | "license": "MIT", 48 | "dependencies": { 49 | "@types/node": "*", 50 | "form-data": "^4.0.0" 51 | } 52 | }, 53 | "node_modules/asynckit": { 54 | "version": "0.4.0", 55 | "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", 56 | "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", 57 | "license": "MIT" 58 | }, 59 | "node_modules/combined-stream": { 60 | "version": "1.0.8", 61 | "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", 62 | "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", 63 | "license": "MIT", 64 | "dependencies": { 65 | "delayed-stream": "~1.0.0" 66 | }, 67 | "engines": { 68 | "node": ">= 0.8" 69 | } 70 | }, 71 | "node_modules/delayed-stream": { 72 | "version": "1.0.0", 73 | "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", 74 | "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", 75 | "license": "MIT", 76 | "engines": { 77 | "node": ">=0.4.0" 78 | } 79 | }, 80 | "node_modules/dom-serializer": { 81 | "version": "1.4.1", 82 | "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz", 83 | "integrity": "sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==", 84 | "license": "MIT", 85 | "dependencies": { 86 | "domelementtype": "^2.0.1", 87 | "domhandler": "^4.2.0", 88 | "entities": "^2.0.0" 89 | }, 90 | "funding": { 91 | "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" 92 | } 93 | }, 94 | "node_modules/domelementtype": { 95 | "version": "2.3.0", 96 | "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", 97 | "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", 98 | "funding": [ 99 | { 100 | "type": "github", 101 | "url": "https://github.com/sponsors/fb55" 102 | } 103 | ], 104 | "license": "BSD-2-Clause" 105 | }, 106 | "node_modules/domhandler": { 107 | "version": "4.3.1", 108 | "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.3.1.tgz", 109 | "integrity": "sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==", 110 | "license": "BSD-2-Clause", 111 | "dependencies": { 112 | "domelementtype": "^2.2.0" 113 | }, 114 | "engines": { 115 | "node": ">= 4" 116 | }, 117 | "funding": { 118 | "url": "https://github.com/fb55/domhandler?sponsor=1" 119 | } 120 | }, 121 | "node_modules/domutils": { 122 | "version": "2.8.0", 123 | "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz", 124 | "integrity": "sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==", 125 | "license": "BSD-2-Clause", 126 | "dependencies": { 127 | "dom-serializer": "^1.0.1", 128 | "domelementtype": "^2.2.0", 129 | "domhandler": "^4.2.0" 130 | }, 131 | "funding": { 132 | "url": "https://github.com/fb55/domutils?sponsor=1" 133 | } 134 | }, 135 | "node_modules/dotenv": { 136 | "version": "16.4.7", 137 | "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz", 138 | "integrity": "sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==", 139 | "license": "BSD-2-Clause", 140 | "engines": { 141 | "node": ">=12" 142 | }, 143 | "funding": { 144 | "url": "https://dotenvx.com" 145 | } 146 | }, 147 | <<<<<<< HEAD 148 | "node_modules/entities": { 149 | "version": "2.2.0", 150 | "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", 151 | "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==", 152 | "license": "BSD-2-Clause", 153 | "funding": { 154 | "url": "https://github.com/fb55/entities?sponsor=1" 155 | } 156 | }, 157 | ======= 158 | >>>>>>> 6652d04b4df2a983a45f5c01ddf8b03b06b6ded1 159 | "node_modules/form-data": { 160 | "version": "4.0.0", 161 | "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", 162 | "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", 163 | "license": "MIT", 164 | "dependencies": { 165 | "asynckit": "^0.4.0", 166 | "combined-stream": "^1.0.8", 167 | "mime-types": "^2.1.12" 168 | }, 169 | "engines": { 170 | "node": ">= 6" 171 | } 172 | }, 173 | <<<<<<< HEAD 174 | "node_modules/fs": { 175 | "version": "0.0.1-security", 176 | "resolved": "https://registry.npmjs.org/fs/-/fs-0.0.1-security.tgz", 177 | "integrity": "sha512-3XY9e1pP0CVEUCdj5BmfIZxRBTSDycnbqhIOGec9QYtmVH2fbLpj86CFWkrNOkt/Fvty4KZG5lTglL9j/gJ87w==", 178 | "license": "ISC" 179 | }, 180 | "node_modules/html-to-notion": { 181 | "version": "0.2.1", 182 | "resolved": "https://registry.npmjs.org/html-to-notion/-/html-to-notion-0.2.1.tgz", 183 | "integrity": "sha512-6IgF8h7s5v/FFTQZevf42mLtYKHat2BrIjt0o7YvlQUWzMUGlGJCcJiUuuVjLjj+Ah1x0sGKzDylheNDfoZPeA==", 184 | "license": "MIT", 185 | "dependencies": { 186 | "@notionhq/client": "^0.2.2", 187 | "dotenv": "^10.0.0", 188 | "fs": "^0.0.1-security", 189 | "htmlparser2": "^6.1.0" 190 | }, 191 | "engines": { 192 | "node": ">=10" 193 | } 194 | }, 195 | "node_modules/html-to-notion/node_modules/@notionhq/client": { 196 | "version": "0.2.4", 197 | "resolved": "https://registry.npmjs.org/@notionhq/client/-/client-0.2.4.tgz", 198 | "integrity": "sha512-omokCm0TwRH0DTCkHGCX4V4jEd7XQoMvQ9d7bcLn+mZgnTW5uU50T+vqQudr6p3y3BMeCXROI/u+JmU2RJT/AQ==", 199 | "license": "MIT", 200 | "dependencies": { 201 | "@types/node-fetch": "^2.5.10", 202 | "node-fetch": "^2.6.1" 203 | }, 204 | "engines": { 205 | "node": ">=12" 206 | } 207 | }, 208 | "node_modules/html-to-notion/node_modules/dotenv": { 209 | "version": "10.0.0", 210 | "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-10.0.0.tgz", 211 | "integrity": "sha512-rlBi9d8jpv9Sf1klPjNfFAuWDjKLwTIJJ/VxtoTwIR6hnZxcEOQCZg2oIL3MWBYw5GpUDKOEnND7LXTbIpQ03Q==", 212 | "license": "BSD-2-Clause", 213 | "engines": { 214 | "node": ">=10" 215 | } 216 | }, 217 | "node_modules/htmlparser2": { 218 | "version": "6.1.0", 219 | "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-6.1.0.tgz", 220 | "integrity": "sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A==", 221 | "funding": [ 222 | "https://github.com/fb55/htmlparser2?sponsor=1", 223 | { 224 | "type": "github", 225 | "url": "https://github.com/sponsors/fb55" 226 | } 227 | ], 228 | "license": "MIT", 229 | "dependencies": { 230 | "domelementtype": "^2.0.1", 231 | "domhandler": "^4.0.0", 232 | "domutils": "^2.5.2", 233 | "entities": "^2.0.0" 234 | } 235 | }, 236 | ======= 237 | >>>>>>> 6652d04b4df2a983a45f5c01ddf8b03b06b6ded1 238 | "node_modules/mime-db": { 239 | "version": "1.52.0", 240 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", 241 | "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", 242 | "license": "MIT", 243 | "engines": { 244 | "node": ">= 0.6" 245 | } 246 | }, 247 | "node_modules/mime-types": { 248 | "version": "2.1.35", 249 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", 250 | "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", 251 | "license": "MIT", 252 | "dependencies": { 253 | "mime-db": "1.52.0" 254 | }, 255 | "engines": { 256 | "node": ">= 0.6" 257 | } 258 | }, 259 | "node_modules/node-fetch": { 260 | "version": "2.7.0", 261 | "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", 262 | "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", 263 | "license": "MIT", 264 | "dependencies": { 265 | "whatwg-url": "^5.0.0" 266 | }, 267 | "engines": { 268 | "node": "4.x || >=6.0.0" 269 | }, 270 | "peerDependencies": { 271 | "encoding": "^0.1.0" 272 | }, 273 | "peerDependenciesMeta": { 274 | "encoding": { 275 | "optional": true 276 | } 277 | } 278 | }, 279 | "node_modules/tr46": { 280 | "version": "0.0.3", 281 | "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", 282 | "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", 283 | "license": "MIT" 284 | }, 285 | "node_modules/undici-types": { 286 | "version": "5.26.5", 287 | "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", 288 | "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" 289 | }, 290 | "node_modules/webidl-conversions": { 291 | "version": "3.0.1", 292 | "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", 293 | "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", 294 | "license": "BSD-2-Clause" 295 | }, 296 | "node_modules/whatwg-url": { 297 | "version": "5.0.0", 298 | "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", 299 | "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", 300 | "license": "MIT", 301 | "dependencies": { 302 | "tr46": "~0.0.3", 303 | "webidl-conversions": "^3.0.0" 304 | } 305 | } 306 | } 307 | } 308 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | <<<<<<< HEAD 4 | "@notionhq/client": "^2.2.15", 5 | "dotenv": "^16.4.5", 6 | "html-to-notion": "^0.2.1" 7 | ======= 8 | "@notionhq/client": "^2.2.17", 9 | "dotenv": "^16.4.7" 10 | >>>>>>> 6652d04b4df2a983a45f5c01ddf8b03b06b6ded1 11 | }, 12 | "name": "nodemon", 13 | "homepage": "http://nodemon.io", 14 | "...": "... other standard package.json values", 15 | "nodemonConfig": { 16 | "ignore": [ 17 | "**/img/**" 18 | ], 19 | "delay": 2500 20 | }, 21 | "license": "MIT" 22 | } 23 | -------------------------------------------------------------------------------- /src/canvashelper.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config() 2 | 3 | /** 4 | * Assists with storing User's Canvas information 5 | * @author Mari Garey 6 | */ 7 | class CanvasHelper { 8 | 9 | url // canvas site url 10 | api // canvas api key 11 | user // user canvas id 12 | courses // list of current courses 13 | 14 | constructor() { 15 | this.url = process.env.CANVAS_API_URL 16 | this.api = process.env.CANVAS_API 17 | this.user = this.getUserId() 18 | this.courses = this.getCourses() 19 | } 20 | 21 | set url(url) { 22 | this.url = url 23 | } 24 | 25 | get url() { 26 | return this.url 27 | } 28 | 29 | set api(api) { 30 | this.api = api 31 | } 32 | 33 | get api() { 34 | return this.api 35 | } 36 | 37 | set user(user) { 38 | this.user = user 39 | } 40 | 41 | get user() { 42 | return this.user 43 | } 44 | 45 | set courses(courses) { 46 | this.courses = courses 47 | } 48 | 49 | get courses() { 50 | return this.courses 51 | } 52 | 53 | /** 54 | * Gets user id from internal CanvasAPI 55 | * @returns {number} 56 | */ 57 | async getUserId() { 58 | // Connect to CanvasAPI 59 | const domain = `${this.url}/api/v1/courses?access_token=${this.api}` 60 | console.log(domain) 61 | const response = await fetch(domain) 62 | const courses = await response.json() 63 | 64 | // Access first availible Course 65 | const course_option = await courses.filter(course => typeof course.name !== 'undefined') 66 | 67 | // returns the user id 68 | return await course_option[0]["enrollments"][0]["user_id"] 69 | } 70 | 71 | /** 72 | * Retrieves the user's courses 73 | * 74 | * @returns {Promise typeof course.name !== 'undefined' && course.end_at > new Date().toJSON()) 85 | .map(course => ({ 86 | id: course.id.toString(), 87 | name: course.name 88 | })) 89 | 90 | // list of the active courses 91 | return await courseList 92 | } 93 | 94 | /** 95 | * Retrieves the assignments from the Canvas API for a specific course. 96 | * 97 | * @param {string} courseID 98 | * @param {string} courseName 99 | * @returns {Promise>} 100 | */ 101 | async getCourseAssignments(courseID, courseName) { 102 | // Canvas API connection 103 | const url = `${this.url}/api/v1/users/${await this.user}/courses/${courseID}/assignments?access_token=${this.api}&per_page=100` 104 | const response = await fetch(url) 105 | const assignments = await response.json() 106 | //console.log(await assignments) 107 | 108 | // Convert each assignment for the API, only for assignments that are named 109 | const assignment_list = await assignments 110 | .filter(assignment => typeof assignment.name !== 'undefined') 111 | .map((assignment) => 112 | ({ 113 | "Assignment Name": { 114 | type: "title", 115 | title: [{ 116 | type: "text", 117 | text: { content: assignment.name } 118 | }] 119 | }, 120 | "Due Date": { 121 | type: "date", 122 | date: { start: assignment.due_at || '2020-09-10'} 123 | }, 124 | "Course": { 125 | select: { 126 | name: courseName 127 | } 128 | }, 129 | "URL": { 130 | type: "url", 131 | url: assignment.html_url 132 | }, 133 | "ID": { 134 | type: "number", 135 | number: assignment.id, 136 | }, 137 | } 138 | )) 139 | 140 | // list of assignments for the course 141 | return await assignment_list 142 | } 143 | 144 | /** 145 | * Retrieves the discussions from the Canvas API for a specific course. 146 | * 147 | * @param {string} courseID 148 | * @param {string} courseName 149 | * @returns {Promise>} 150 | */ 151 | async getCourseDiscussions(courseID, courseName) { 152 | const url = `${this.url}/api/v1/courses/${courseID}/discussion_topics?access_token=${this.api}` 153 | const response = await fetch(url) 154 | const discussion_topics = await response.json() 155 | 156 | // Convert each discussion for the API, only for assignments that are named 157 | const discussion_list = await discussion_topics 158 | .filter(discussion => typeof discussion.title !== 'undefined') 159 | .map((discussion) => 160 | ({ 161 | "Assignment Name": { 162 | type: "title", 163 | title: [{ 164 | type: "text", 165 | text: { content: discussion.title } 166 | }] 167 | }, 168 | "Due Date": { 169 | type: "date", 170 | date: { 171 | start: discussion.delayed_post_at || '2020-09-10', 172 | end: discussion.lock_at, 173 | } 174 | }, 175 | "Course": { 176 | select: { 177 | name: courseName 178 | } 179 | }, 180 | "URL": { 181 | type: "url", 182 | url: discussion.html_url 183 | }, 184 | "ID": { 185 | type: "number", 186 | number: discussion.id, 187 | }, 188 | } 189 | )) 190 | 191 | // list of dicussion for the course 192 | return await discussion_list 193 | } 194 | } 195 | 196 | module.exports = { CanvasHelper } 197 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | /** 2 | * TODO: 3 | * => change checkbox to status 4 | * => add description of assignments into each page 5 | */ 6 | 7 | require('dotenv').config() 8 | const { Client } = require("@notionhq/client") 9 | const { CanvasHelper } = require("./canvashelper") 10 | const { NotionHelper } = require("./notionhelper") 11 | const CanvasHelp = new CanvasHelper() 12 | const NotionHelp = new NotionHelper() 13 | const NotionClient = new Client({ auth: NotionHelp.api}) 14 | 15 | /** 16 | * Validates if there is a database present. 17 | * If so, update the database, else create a new database. 18 | */ 19 | async function checkDatabase() { 20 | const courses = await CanvasHelp.getCourses() 21 | try { 22 | const response = await NotionClient.databases.query({ 23 | database_id: NotionHelp.database 24 | }) 25 | console.log('FOUND: Database exists! Retrieving database data...') 26 | if (CanvasHelp.courses != courses) { 27 | await NotionHelp.updateNotionDatabase(courses) 28 | } 29 | 30 | } catch (error) { 31 | console.log('NOT FOUND: Database does not exist! Creating new database...') 32 | await NotionHelp.createNotionDatabase(courses) 33 | } 34 | } 35 | 36 | /** 37 | * Check whether current page exists withi the database. 38 | * If so, update page, otherwise create new page. 39 | * @param {Promise>} page 40 | */ 41 | async function checkPage(page, course) { 42 | try { 43 | if ((await course).includes(await page.ID.number) == true) { 44 | console.log(`FOUND: Assignment ${page.ID.number} exists!`) 45 | await NotionHelp.updateNotionPage(page) 46 | } 47 | else { 48 | console.log(`NOT FOUND: Assignment ${page.ID.number} does not exist in database!`) 49 | console.log("Creating new assignment...") 50 | await NotionHelp.createNotionPage(page) 51 | } 52 | } catch(error) { 53 | console.log(`ERROR: checkPage() did not run: ${error}`) 54 | } 55 | } 56 | 57 | /** 58 | * For each course, get each assignment to check if it exists. 59 | */ 60 | async function getCoursesPages() { 61 | try { 62 | const courses = await CanvasHelp.courses 63 | for (let i = 0; i < courses.length; i++) { 64 | const assignments = await CanvasHelp.getCourseAssignments(courses[i].id, courses[i].name) 65 | const pages = assignments.concat(await CanvasHelp.getCourseDiscussions(courses[i].id, courses[i].name)) 66 | const course_pages = await NotionHelp.getNotionPagesByCourse(courses[i]) 67 | for (let page of pages) { 68 | await checkPage(page, course_pages) 69 | } 70 | } 71 | console.log(`SUCCESS: all pages are updated or created!`) 72 | } catch(error) { 73 | console.log(`ERROR: getCoursesPages() did not run: ${error}`) 74 | } 75 | } 76 | 77 | /** 78 | * Runs the database and page function. 79 | */ 80 | async function run() { 81 | await checkDatabase() 82 | await getCoursesPages() 83 | } 84 | 85 | run() 86 | -------------------------------------------------------------------------------- /src/notionhelper.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config() 2 | 3 | const { Client } = require("@notionhq/client") 4 | const Notion = new Client({ auth: process.env.NOTION_API}) 5 | const { setEnvValue } = require("./util") 6 | 7 | /** 8 | * Class to help create/update the database in Notion, 9 | * as well as the pages in Notion database 10 | * @author Mari Garey 11 | */ 12 | class NotionHelper { 13 | 14 | api // notion api key 15 | page // notion parent page id 16 | database // notion database id 17 | 18 | constructor() { 19 | this.api = process.env.NOTION_API 20 | this.page = process.env.NOTION_PAGE 21 | this.database = process.env.NOTION_DATABASE 22 | } 23 | 24 | /** 25 | * Sets the local and the .env file 26 | */ 27 | set database(database) { 28 | this.database = database 29 | } 30 | 31 | get database() { 32 | return this.database 33 | } 34 | 35 | get pages() { 36 | return this.getNotionPages() 37 | } 38 | 39 | set pageId(pageId) { 40 | this.pageId = pageId 41 | } 42 | 43 | get pageId() { 44 | return this.pageId 45 | } 46 | 47 | set token(token) { 48 | this.token = token 49 | } 50 | 51 | get token() { 52 | return this.token 53 | } 54 | 55 | /** 56 | * IMPORTANT: only will get 100 pages max 57 | * 58 | * Accesses all the current pages in a Notion Database 59 | * @returns array of notion pages 60 | */ 61 | async getNotionPages() { 62 | try { 63 | const response = await Notion.databases.query({ 64 | database_id: this.database, 65 | }) 66 | const notion_pages = response.results.map( 67 | (page) => page.properties.ID.number 68 | ) 69 | 70 | return notion_pages 71 | } catch(error) { 72 | console.log(`ERROR: getNotionPages() did not run: ${error}`) 73 | } 74 | } 75 | 76 | /** 77 | * Accesses all the current pages for the 78 | * course property in the Notion Database 79 | * @param {Promise} course 80 | * @returns 81 | */ 82 | async getNotionPagesByCourse(course) { 83 | try { 84 | const response = await Notion.databases.query({ 85 | database_id: this.database, 86 | filter: { 87 | property: "Course", 88 | select: { 89 | equals: course.name 90 | } 91 | } 92 | }) 93 | 94 | const notion_pages = response.results.map( 95 | (page) => page.properties.ID.number 96 | ) 97 | return notion_pages 98 | } catch(error) { 99 | console.log(`ERROR: getNotionPagesByCourse() did not run: ${error}`) 100 | } 101 | } 102 | 103 | /** 104 | * Creates a new database in the Notion page 105 | * @param {Array} courses 106 | */ 107 | async createNotionDatabase(courses) { 108 | try { 109 | const newDatabase = await Notion.databases.create({ 110 | parent: { 111 | type: "page_id", 112 | page_id: this.page, 113 | }, 114 | title: [ 115 | { 116 | type: "text", 117 | text: { 118 | content: "Canvas Assignments", 119 | }, 120 | }, 121 | ], 122 | properties: { 123 | "Assignment Name": { 124 | type: "title", 125 | title: {}, 126 | }, 127 | "Due Date": { 128 | type: "date", 129 | date: {}, 130 | }, 131 | "Course": { 132 | select: { 133 | options: await courses, 134 | }, 135 | }, 136 | "Completion": { 137 | type: "checkbox", 138 | checkbox: {} 139 | }, 140 | "URL": { 141 | type: "url", 142 | url: {}, 143 | }, 144 | "ID": { 145 | type: "number", 146 | number: { 147 | format: "number" 148 | }, 149 | }, 150 | } 151 | }) 152 | console.log(`SUCCESS: Database has been created!`) 153 | this.database = newDatabase.id 154 | setEnvValue('NOTION_DATABASE', `'${this.database}'`) 155 | } catch (error) { 156 | console.log(`DATABASE ERROR: ${error}`) 157 | } 158 | } 159 | 160 | /** 161 | * Updates the Course Property in the Notion Database 162 | * @param {Array} updatedCourses 163 | */ 164 | async updateNotionDatabase(updatedCourses) { 165 | try { 166 | const response = await Notion.databases.update({ 167 | database_id: this.database, 168 | properties: { 169 | "Course": { 170 | select: { 171 | options: await updatedCourses, 172 | }, 173 | }, 174 | }, 175 | }) 176 | console.log(`SUCCESS: Database has been updated!`) 177 | } catch (error) { 178 | console.log(`ERROR: Database could not be update! ${error}`) 179 | } 180 | } 181 | 182 | /** 183 | * Creates a page in the Notion database with properties from page_properties 184 | * @param {Promise>} page_properties 185 | */ 186 | async createNotionPage(page_properties) { 187 | try { 188 | const newPage = await Notion.pages.create({ 189 | parent: { 190 | type: "database_id", 191 | database_id: this.database 192 | }, 193 | properties: await page_properties, 194 | }) 195 | console.log(`SUCCESS: new page ${page_properties.ID.number} has been created!`) 196 | } catch (error) { 197 | console.log(`ERROR: createNotionPage failed!\n${error}`) 198 | } 199 | } 200 | 201 | /** 202 | * Updates a page in the notion database with properties from page_properties 203 | * @param {Promise>} page_properties 204 | */ 205 | async updateNotionPage(page_properties) { 206 | try { 207 | // update properties 208 | const updatePage = await Notion.pages.update({ 209 | page_id: await this.getNotionPageID(page_properties), 210 | properties: page_properties, 211 | }) 212 | console.log(`SUCCESS: new page ${page_properties.ID.number} has been updated!`) 213 | } catch (error) { 214 | console.log(`ERROR: Could not update page ${page_properties.ID.number}`) 215 | } 216 | } 217 | 218 | /** 219 | * Returns the page id of the page associated with the page_properties 220 | * @param {Promise>} page_properties 221 | * @returns page id 222 | */ 223 | async getNotionPageID(page_properties) { 224 | try { 225 | const response = Notion.databases.query({ 226 | database_id: this.database, 227 | filter: { 228 | property: "ID", 229 | number: { 230 | equals: page_properties.ID.number 231 | } 232 | } 233 | }) 234 | return (await response).results[0].id 235 | } catch (error) { 236 | console.log(`ERROR: Could not locate page!!`) 237 | } 238 | } 239 | } 240 | 241 | module.exports = { NotionHelper} 242 | -------------------------------------------------------------------------------- /src/util.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs") 2 | const os = require("os") 3 | 4 | /** 5 | * Creates or updates /.env values. 6 | * 7 | * @source https://stackoverflow.com/questions/64996008/update-attributes-in-env-file-in-node-js 8 | * 9 | * @param {string} key 10 | * @param {string} value 11 | */ 12 | function setEnvValue(key, value) { 13 | 14 | // read file from hdd & split if from a linebreak to a array 15 | const ENV_VARS = fs.readFileSync("./.env", "utf8").split(os.EOL); 16 | 17 | // find the env we want based on the key 18 | const target = ENV_VARS.indexOf(ENV_VARS.find((line) => { 19 | return line.match(new RegExp(key)); 20 | })); 21 | 22 | // replace the key/value with the new value 23 | ENV_VARS.splice(target, 1, `${key}=${value}`); 24 | 25 | // write everything back to the file system 26 | fs.writeFileSync("./.env", ENV_VARS.join(os.EOL)); 27 | } 28 | 29 | /** 30 | * Potential function for future use. 31 | */ 32 | const htmlToNotion = () => { 33 | 34 | } 35 | 36 | module.exports = { setEnvValue } -------------------------------------------------------------------------------- /web/index.js: -------------------------------------------------------------------------------- 1 | // people fill out form 2 | 3 | // that information gets added to database 4 | 5 | // when first added, create integration 6 | 7 | // webserver to refresh every week? --------------------------------------------------------------------------------