├── .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?
--------------------------------------------------------------------------------