├── .github └── workflows │ ├── docs.yml │ ├── format.yml │ └── test.yml ├── .gitignore ├── .npmignore ├── .npmrc ├── .prettierignore ├── .prettierrc ├── License ├── Readme.md ├── __tests__ ├── __snapshots__ │ └── index.test.js.snap └── index.test.js ├── dist ├── webuntis.d.ts ├── webuntis.js ├── webuntis.js.map ├── webuntis.mjs └── webuntis.mjs.map ├── docs ├── .nojekyll ├── CNAME ├── assets │ ├── highlight.css │ ├── main.js │ ├── navigation.js │ ├── search.js │ └── style.css ├── classes │ ├── InternalWebuntisSecretLogin.html │ ├── WebUntis.html │ ├── WebUntisAnonymousAuth.html │ ├── WebUntisQR.html │ └── WebUntisSecretAuth.html ├── enums │ ├── WebUntisDay.html │ └── WebUntisElementType.html ├── hierarchy.html ├── index.html ├── interfaces │ ├── Absence.html │ ├── Absences.html │ ├── CodesEntity.html │ ├── ColorEntity.html │ ├── Department.html │ ├── Exam.html │ ├── Excuse.html │ ├── Holiday.html │ ├── Homework.html │ ├── Inbox.html │ ├── Inboxmessage.html │ ├── Klasse.html │ ├── Lesson.html │ ├── LsEntity.html │ ├── MessagesOfDay.html │ ├── Messagesender.html │ ├── NewsWidget.html │ ├── Room.html │ ├── SchoolYear.html │ ├── ShortData.html │ ├── StatusData.html │ ├── Student.html │ ├── Subject.html │ ├── Teacher.html │ ├── TimeUnit.html │ ├── Timegrid.html │ ├── WebAPITimetable.html │ ├── WebElement.html │ └── WebElementData.html ├── modules.html └── types │ ├── Authenticator.html │ └── URLClass.html ├── jest.config.js ├── package.json ├── rollup.config.mjs ├── src ├── anonymous.ts ├── base-64.ts ├── base.ts ├── cookie.ts ├── index.ts ├── internal.ts ├── qr.ts ├── secret.ts └── types.ts ├── test.js ├── tsconfig.json ├── typedoc.json └── yarn.lock /.github/workflows/docs.yml: -------------------------------------------------------------------------------- 1 | name: Ensure documentation is up-to-date 2 | on: 3 | push: 4 | paths-ignore: 5 | - 'docs/**' 6 | - 'dist/**' 7 | 8 | concurrency: ci-${{ github.ref }} 9 | 10 | jobs: 11 | build: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v3 15 | with: 16 | fetch-depth: 2 17 | 18 | - run: git pull --rebase 19 | 20 | - name: Install node_modules 21 | run: yarn 22 | 23 | - name: Generate docs 24 | run: yarn generate-doc 25 | 26 | - name: Commit changes 27 | uses: EndBug/add-and-commit@v9 28 | if: ${{ github.ref == 'refs/heads/master' }} 29 | with: 30 | author_name: high5-bot 31 | author_email: me+high5@dunklestoast.de 32 | message: 'docs: update documentation' 33 | add: '.' 34 | push: true 35 | -------------------------------------------------------------------------------- /.github/workflows/format.yml: -------------------------------------------------------------------------------- 1 | name: Ensure code is formatted 2 | on: 3 | push: 4 | paths-ignore: 5 | - 'docs/**' 6 | - 'dist/**' 7 | 8 | concurrency: ci-${{ github.ref }} 9 | 10 | jobs: 11 | build: 12 | runs-on: ubuntu-latest 13 | if: "!startsWith(github.event.head_commit.message, 'chore(release): ')" 14 | steps: 15 | - uses: actions/checkout@v3 16 | with: 17 | fetch-depth: 2 18 | 19 | - run: git pull --rebase 20 | 21 | - name: Install node_modules 22 | run: yarn 23 | 24 | - name: Format 25 | run: yarn format 26 | 27 | - name: Commit changes 28 | uses: EndBug/add-and-commit@v9 29 | if: ${{ github.ref == 'refs/heads/master' }} 30 | with: 31 | author_name: high5-bot 32 | author_email: me+high5@dunklestoast.de 33 | message: 'style: format code with prettier' 34 | add: '.' 35 | push: true 36 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | on: [push, pull_request] 3 | jobs: 4 | build: 5 | runs-on: ubuntu-latest 6 | steps: 7 | - name: Checkout repo 8 | uses: actions/checkout@v2 9 | 10 | - name: Install node_modules 11 | run: yarn 12 | 13 | - name: Run tests 14 | run: yarn test 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (http://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # Typescript v1 declaration files 40 | typings/ 41 | .idea/ 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | .idea/runConfigurations/**/* 61 | 62 | .DS_Store 63 | .DS_store 64 | .env 65 | test2.js 66 | 67 | .vscode 68 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | Readme.md 2 | test.mjs 3 | .gitignore 4 | .git 5 | .idea/ 6 | .DS_Store 7 | .DS_store 8 | .env 9 | test2.js 10 | .vscode 11 | typedoc.json 12 | test.js 13 | jest.config.js 14 | __tests__ 15 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | message="chore(release): %s" -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | docs 3 | dist 4 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 4, 3 | "singleQuote": true, 4 | "printWidth": 120 5 | } 6 | -------------------------------------------------------------------------------- /License: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Nils Bergmann 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 | # WebUntis API 2 | 3 | This is a NodeJS Wrapper for the JSON RPC WebUntis API. 4 | 5 | The Documentation is available at [https://webuntis.noim.me/](https://webuntis.noim.me/) 6 | 7 | In case you need the Untis API Spec (pdf), you need to email Untis directly and ask. I am (legally) not allowed to publish it. 8 | 9 | ## Note: 10 | 11 | As I have not been a student for a long time, I currently have no access to any Untis services. If you want to share your login details with me for testing purposes, contact me via [Telegram](t.me/TheNoim) or other means ([Homepage](noim.io)). 12 | 13 | ## Examples 14 | 15 | ### User/Password Login 16 | 17 | ```javascript 18 | import { WebUntis } from 'webuntis'; 19 | 20 | const untis = new WebUntis('school', 'username', 'password', 'xyz.webuntis.com'); 21 | 22 | await untis.login(); 23 | const timetable = await untis.getOwnTimetableForToday(); 24 | 25 | // profit 26 | ``` 27 | 28 | ### QR Code Login 29 | 30 | ```javascript 31 | import { WebUntisQR } from 'webuntis'; 32 | import { URL } from 'url'; 33 | import { authenticator as Authenticator } from 'otplib'; 34 | 35 | // The result of the scanned QR Code 36 | const QRCodeData = 'untis://setschool?url=[...]&school=[...]&user=[...]&key=[...]&schoolNumber=[...]'; 37 | 38 | const untis = new WebUntisQR(QRCodeData, 'custom-identity', Authenticator, URL); 39 | 40 | await untis.login(); 41 | const timetable = await untis.getOwnTimetableForToday(); 42 | 43 | // profit 44 | ``` 45 | 46 | ### User/Secret Login 47 | 48 | ```javascript 49 | import { WebUntisSecretAuth } from 'webuntis'; 50 | import { authenticator as Authenticator } from 'otplib'; 51 | 52 | const secret = 'NL04FGY4FSY5'; 53 | 54 | const untis = new WebUntisSecretAuth('school', 'username', secret, 'xyz.webuntis.com', 'custom-identity', Authenticator); 55 | 56 | await untis.login(); 57 | const timetable = await untis.getOwnTimetableForToday(); 58 | 59 | // profit 60 | ``` 61 | 62 | ### Anonymous Login 63 | 64 | Only if your school supports public access. 65 | 66 | ```javascript 67 | import { WebUntisAnonymousAuth, WebUntisElementType } from 'webuntis'; 68 | 69 | const untis = new WebUntisAnonymousAuth('school', 'xyz.webuntis.com'); 70 | 71 | await untis.login(); 72 | const classes = await untis.getClasses(); 73 | const timetable = await untis.getTimetableForToday(classes[0].id, WebUntisElementType.CLASS); 74 | 75 | // profit 76 | ``` 77 | 78 | ### Installation 79 | 80 | This package is compatible with CJS and ESM. *Note:* This package primary target is nodejs. It may also work with runtimes like react-native, but it will probably not work in the browser. 81 | 82 | ```bash 83 | yarn add webuntis 84 | # Or 85 | npm i webuntis --save 86 | # Or 87 | pnpm i webuntis 88 | ``` 89 | 90 | ### ESM note: 91 | 92 | If you use the esm version of this package, you need to provide `Authenticator` and `URL` if necessary. For more information, look at the `User/Secret Login` or `QR Code Login` example. This is not needed for `username/password` or `anonymous` login. 93 | 94 | ### Notice 95 | 96 | I am not affiliated with Untis GmbH. Use this at your own risk. 97 | -------------------------------------------------------------------------------- /__tests__/__snapshots__/index.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`should getHomeWorkAndLessons catch error invalidate 1`] = `"Server returned invalid data."`; 4 | 5 | exports[`should getHomeWorkAndLessons catch error invalidate with not object 1`] = `"Server returned invalid data."`; 6 | 7 | exports[`should getHomeWorkAndLessons catch error invalidate without homeworks 1`] = `"Data object doesn't contains homeworks object."`; 8 | 9 | exports[`should getHomeWorkAndLessons catch error validate 1`] = `"Current Session is not valid"`; 10 | 11 | exports[`should getHomeWorkAndLessons catch error validate with not object 1`] = `"Server returned invalid data."`; 12 | 13 | exports[`should getHomeWorkAndLessons catch error validate without homeworks 1`] = `"Data object doesn't contains homeworks object."`; 14 | 15 | exports[`should getHomeWorksFor catch error invalidate 1`] = `"Server returned invalid data."`; 16 | 17 | exports[`should getHomeWorksFor catch error invalidate with not object 1`] = `"Server returned invalid data."`; 18 | 19 | exports[`should getHomeWorksFor catch error invalidate without homeworks 1`] = `"Data object doesn't contains homeworks object."`; 20 | 21 | exports[`should getHomeWorksFor catch error validate 1`] = `"Current Session is not valid"`; 22 | 23 | exports[`should getHomeWorksFor catch error validate with not object 1`] = `"Server returned invalid data."`; 24 | 25 | exports[`should getHomeWorksFor catch error validate without homeworks 1`] = `"Data object doesn't contains homeworks object."`; 26 | 27 | exports[`should getLatestSchoolyear throw error with empty array 1`] = `"Failed to receive school year"`; 28 | 29 | exports[`should getNewsWidget catch data not object 1`] = `"Server returned invalid data."`; 30 | 31 | exports[`should getNewsWidget catch invalid data 1`] = `"Current Session is not valid"`; 32 | 33 | exports[`should method login catch error response not object 1`] = `"Failed to parse server response."`; 34 | 35 | exports[`should method login catch error with empty sessionId 1`] = `"Failed to login. No session id."`; 36 | 37 | exports[`should method login catch error with result has code 1`] = `"Login returned error code: 500"`; 38 | 39 | exports[`should method login catch error with result null 1`] = `"Failed to login. {\"result\":null}"`; 40 | -------------------------------------------------------------------------------- /__tests__/index.test.js: -------------------------------------------------------------------------------- 1 | const MockAdapter = require('axios-mock-adapter'); 2 | const axios = require('axios'); 3 | const cases = require('jest-in-case'); 4 | const { WebUntis } = require('../dist/webuntis.js'); 5 | 6 | const mockAxios = new MockAdapter(axios); 7 | const mockResponse = { 8 | result: { 9 | sessionId: '123', 10 | personId: 'testing-person', 11 | personType: 'testing-type', 12 | klasseId: 'testing-klasse', 13 | }, 14 | }; 15 | const school = 'school'; 16 | const baseURL = `/WebUntis/jsonrpc.do`; 17 | 18 | const getElementObject = (id = mockResponse.result.personId, type = mockResponse.result.personType) => ({ 19 | params: { 20 | options: { 21 | element: { id, type }, 22 | }, 23 | }, 24 | }); 25 | 26 | const initMocks = () => { 27 | mockAxios 28 | .onPost( 29 | baseURL, 30 | expect.objectContaining({ 31 | method: 'getLatestImportTime', 32 | }) 33 | ) 34 | .replyOnce(200, { result: 123 }); 35 | }; 36 | 37 | const createInstance = () => { 38 | const instance = new WebUntis(school, 'username', 'password', 'xyz.webuntis.com'); 39 | 40 | return instance; 41 | }; 42 | 43 | beforeEach(() => { 44 | mockAxios.reset(); 45 | jest.clearAllMocks(); 46 | initMocks(); 47 | }); 48 | 49 | test('should method login return mock result', async () => { 50 | const untis = createInstance(); 51 | 52 | mockAxios.onPost(baseURL).reply(200, mockResponse); 53 | 54 | expect(await untis.login()).toEqual(mockResponse.result); 55 | }); 56 | 57 | cases( 58 | 'should method login catch error', 59 | async ({ response }) => { 60 | const untis = createInstance(); 61 | 62 | mockAxios.onPost(baseURL).reply(200, response); 63 | 64 | await expect(() => untis.login()).rejects.toThrowErrorMatchingSnapshot(); 65 | }, 66 | [ 67 | { name: 'response not object', response: '' }, 68 | { name: 'with result null', response: { result: null } }, 69 | { name: 'with result has code', response: { result: { code: 500 } } }, 70 | { name: 'with empty sessionId', response: { result: {} } }, 71 | ] 72 | ); 73 | 74 | test('should method logout return true', async () => { 75 | const untis = createInstance(); 76 | 77 | mockAxios.onPost(baseURL).reply(200, { result: {} }); 78 | 79 | expect(await untis.logout()).toBe(true); 80 | }); 81 | 82 | cases( 83 | 'should getLatestSchoolyear return object', 84 | async ({ validate, dateFormat }) => { 85 | const name = 'testName'; 86 | const id = 'testId'; 87 | const untis = createInstance(); 88 | 89 | mockAxios.onPost(baseURL, expect.objectContaining({ method: 'getSchoolyears' })).replyOnce(200, { 90 | result: [ 91 | { 92 | id, 93 | name, 94 | startDate: dateFormat === 'string' ? '20191111' : 20191111, 95 | endDate: dateFormat === 'string' ? '20191211' : 20191211, 96 | }, 97 | { 98 | id, 99 | name, 100 | startDate: dateFormat === 'string' ? '20191113' : 20191113, 101 | endDate: dateFormat === 'string' ? '20191115' : 20191115, 102 | }, 103 | ], 104 | }); 105 | 106 | expect(await untis.getLatestSchoolyear(validate)).toEqual({ 107 | name, 108 | id, 109 | startDate: new Date('11/13/2019'), 110 | endDate: new Date('11/15/2019'), 111 | }); 112 | }, 113 | [ 114 | { name: 'with validate, string date', validate: true, dateFormat: 'string' }, 115 | { name: 'with validate, numeric date', validate: true, dateFormat: 'number' }, 116 | { name: 'without validate, string date', validate: false, dateFormat: 'string' }, 117 | ] 118 | ); 119 | 120 | test('should getLatestSchoolyear throw error with empty array', async () => { 121 | const name = 'testName'; 122 | const id = 'testId'; 123 | const untis = createInstance(); 124 | 125 | mockAxios.onPost(baseURL, expect.objectContaining({ method: 'getSchoolyears' })).replyOnce(200, { 126 | result: [], 127 | }); 128 | 129 | await expect(() => untis.getLatestSchoolyear(false)).rejects.toThrowErrorMatchingSnapshot(); 130 | }); 131 | 132 | cases( 133 | 'should getNewsWidget return data', 134 | async ({ validate }) => { 135 | const untis = createInstance(); 136 | const response = { testing: 'dataTest' }; 137 | 138 | mockAxios.onGet(/newsWidgetData/).replyOnce(200, { data: response }); 139 | 140 | expect(await untis.getNewsWidget(new Date('11/13/2019'), validate)).toEqual(response); 141 | }, 142 | [ 143 | { name: 'with validate', validate: true }, 144 | { name: 'without validate', validate: false }, 145 | ] 146 | ); 147 | 148 | test('should getNewsWidget catch invalid data', async () => { 149 | const untis = createInstance(); 150 | const response = { testing: 'dataTest' }; 151 | 152 | mockAxios.reset(); 153 | mockAxios 154 | .onPost( 155 | baseURL, 156 | expect.objectContaining({ 157 | method: 'getLatestImportTime', 158 | }) 159 | ) 160 | .replyOnce(200, { result: 'string' }); 161 | 162 | await expect(() => untis.getNewsWidget(new Date('11/13/2019'))).rejects.toThrowErrorMatchingSnapshot(); 163 | }); 164 | 165 | test('should getNewsWidget catch data not object', async () => { 166 | const untis = createInstance(); 167 | const response = { testing: 'dataTest' }; 168 | 169 | mockAxios.onGet(/newsWidgetData/).replyOnce(200, { data: 123 }); 170 | 171 | await expect(() => untis.getNewsWidget(new Date('11/13/2019'))).rejects.toThrowErrorMatchingSnapshot(); 172 | }); 173 | 174 | cases( 175 | 'should getLatestImportTime return result', 176 | async ({ validate }) => { 177 | const untis = createInstance(); 178 | 179 | mockAxios 180 | .onPost(baseURL, expect.objectContaining({ method: 'getLatestImportTime' })) 181 | .replyOnce(200, { result: 123 }); 182 | 183 | expect(await untis.getLatestImportTime(validate)).toBe(123); 184 | }, 185 | [ 186 | { name: 'with validate', validate: true }, 187 | { name: 'without validate', validate: false }, 188 | ] 189 | ); 190 | 191 | cases( 192 | 'should getOwnTimetableForToday return result', 193 | async ({ validate, postIndex }) => { 194 | const untis = createInstance(); 195 | 196 | mockAxios 197 | .onPost(baseURL, expect.objectContaining({ method: 'getTimetable' })) 198 | .replyOnce(200, { result: 123 }) 199 | .onPost(baseURL) 200 | .reply(200, mockResponse); 201 | 202 | await untis.login(); 203 | const result = await untis.getOwnTimetableForToday(validate); 204 | 205 | expect(result).toBe(123); 206 | expect(JSON.parse(mockAxios.history.post[postIndex].data)).toMatchObject(getElementObject()); 207 | }, 208 | [ 209 | { name: 'with validate', postIndex: 2, validate: true }, 210 | { name: 'without validate', postIndex: 1, validate: false }, 211 | ] 212 | ); 213 | 214 | cases( 215 | 'should getTimetableForToday return result', 216 | async ({ validate, postIndex }) => { 217 | const id = 'test-id'; 218 | const type = 'test-type'; 219 | const untis = createInstance(); 220 | 221 | mockAxios.onPost(baseURL, expect.objectContaining({ method: 'getTimetable' })).replyOnce(200, { result: 123 }); 222 | 223 | expect(await untis.getTimetableForToday(id, type, validate)).toBe(123); 224 | expect(JSON.parse(mockAxios.history.post[postIndex].data)).toMatchObject(getElementObject(id, type)); 225 | }, 226 | [ 227 | { name: 'with validate', postIndex: 1, validate: true }, 228 | { name: 'without validate', postIndex: 0, validate: false }, 229 | ] 230 | ); 231 | 232 | cases( 233 | 'should getOwnTimetableFor return result', 234 | async ({ validate, postIndex }) => { 235 | const date = new Date('11/13/2019'); 236 | const untis = createInstance(); 237 | 238 | mockAxios 239 | .onPost(baseURL, expect.objectContaining({ method: 'getTimetable' })) 240 | .replyOnce(200, { result: 123 }) 241 | .onPost(baseURL) 242 | .reply(200, mockResponse); 243 | 244 | await untis.login(); 245 | 246 | expect(await untis.getOwnTimetableFor(date, validate)).toBe(123); 247 | expect(mockAxios.history.post[postIndex].data).toMatch('20191113'); 248 | expect(JSON.parse(mockAxios.history.post[postIndex].data)).toMatchObject(getElementObject()); 249 | }, 250 | [ 251 | { name: 'with validate', postIndex: 2, validate: true }, 252 | { name: 'without validate', postIndex: 1, validate: false }, 253 | ] 254 | ); 255 | 256 | cases( 257 | 'should getTimetableFor return result', 258 | async (validate) => { 259 | const id = 'test-id'; 260 | const type = 'test-type'; 261 | const date = new Date('11/13/2019'); 262 | const untis = createInstance(); 263 | 264 | mockAxios.onPost(baseURL, expect.objectContaining({ method: 'getTimetable' })).replyOnce(200, { result: 123 }); 265 | 266 | expect(await untis.getTimetableFor(date, id, type, validate)).toBe(123); 267 | expect(mockAxios.history.post[1].data).toMatch('20191113'); 268 | expect(JSON.parse(mockAxios.history.post[1].data)).toMatchObject(getElementObject(id, type)); 269 | }, 270 | [ 271 | { name: 'with validate', validate: true }, 272 | { name: 'without validate', validate: false }, 273 | ] 274 | ); 275 | 276 | cases( 277 | 'should getOwnTimetableForRange return result', 278 | async (validate) => { 279 | const dateStart = new Date('11/13/2019'); 280 | const dateEnd = new Date('11/17/2019'); 281 | const untis = createInstance(); 282 | 283 | mockAxios.onPost(baseURL, expect.objectContaining({ method: 'getTimetable' })).replyOnce(200, { result: 123 }); 284 | 285 | expect(await untis.getOwnTimetableForRange(dateStart, dateEnd, validate)).toBe(123); 286 | expect(mockAxios.history.post[1].data).toMatch('20191113'); 287 | expect(mockAxios.history.post[1].data).toMatch('20191117'); 288 | }, 289 | [ 290 | { name: 'with validate', validate: true }, 291 | { name: 'without validate', validate: false }, 292 | ] 293 | ); 294 | 295 | cases( 296 | 'should getTimetableForRange return result', 297 | async (validate) => { 298 | const id = 'test-id'; 299 | const type = 'test-type'; 300 | const dateStart = new Date('11/13/2019'); 301 | const dateEnd = new Date('11/17/2019'); 302 | const untis = createInstance(); 303 | 304 | mockAxios.onPost(baseURL, expect.objectContaining({ method: 'getTimetable' })).replyOnce(200, { result: 123 }); 305 | 306 | expect(await untis.getTimetableForRange(dateStart, dateEnd, id, type, validate)).toBe(123); 307 | expect(mockAxios.history.post[1].data).toMatch('20191113'); 308 | expect(mockAxios.history.post[1].data).toMatch('20191117'); 309 | expect(JSON.parse(mockAxios.history.post[1].data)).toMatchObject(getElementObject(id, type)); 310 | }, 311 | [ 312 | { name: 'with validate', validate: true }, 313 | { name: 'without validate', validate: false }, 314 | ] 315 | ); 316 | 317 | cases( 318 | 'should getOwnClassTimetableForToday return result', 319 | async (validate) => { 320 | const untis = createInstance(); 321 | 322 | mockAxios 323 | .onPost(baseURL, expect.objectContaining({ method: 'getTimetable' })) 324 | .replyOnce(200, { result: 123 }) 325 | .onPost(baseURL) 326 | .reply(200, mockResponse); 327 | 328 | await untis.login(); 329 | 330 | expect(await untis.getOwnClassTimetableForToday(validate)).toBe(123); 331 | expect(JSON.parse(mockAxios.history.post[2].data)).toMatchObject( 332 | getElementObject(mockResponse.result.klasseId, 1) 333 | ); 334 | }, 335 | [ 336 | { name: 'with validate', validate: true }, 337 | { name: 'without validate', validate: false }, 338 | ] 339 | ); 340 | 341 | cases( 342 | 'should getOwnClassTimetableFor return result', 343 | async (validate) => { 344 | const date = new Date('11/13/2019'); 345 | const untis = createInstance(); 346 | 347 | mockAxios 348 | .onPost(baseURL, expect.objectContaining({ method: 'getTimetable' })) 349 | .replyOnce(200, { result: 123 }) 350 | .onPost(baseURL) 351 | .reply(200, mockResponse); 352 | 353 | await untis.login(); 354 | 355 | expect(await untis.getOwnClassTimetableFor(date, validate)).toBe(123); 356 | expect(mockAxios.history.post[2].data).toMatch('20191113'); 357 | expect(JSON.parse(mockAxios.history.post[2].data)).toMatchObject( 358 | getElementObject(mockResponse.result.klasseId, 1) 359 | ); 360 | }, 361 | [ 362 | { name: 'with validate', validate: true }, 363 | { name: 'without validate', validate: false }, 364 | ] 365 | ); 366 | 367 | cases( 368 | 'should getOwnClassTimetableForRange return result', 369 | async (validate) => { 370 | const dateStart = new Date('11/13/2019'); 371 | const dateEnd = new Date('11/17/2019'); 372 | const untis = createInstance(); 373 | 374 | mockAxios 375 | .onPost(baseURL, expect.objectContaining({ method: 'getTimetable' })) 376 | .replyOnce(200, { result: 123 }) 377 | .onPost(baseURL) 378 | .reply(200, mockResponse); 379 | 380 | await untis.login(); 381 | 382 | expect(await untis.getOwnClassTimetableForRange(dateStart, dateEnd, validate)).toBe(123); 383 | expect(mockAxios.history.post[2].data).toMatch('20191113'); 384 | expect(mockAxios.history.post[2].data).toMatch('20191117'); 385 | expect(JSON.parse(mockAxios.history.post[2].data)).toMatchObject( 386 | getElementObject(mockResponse.result.klasseId, 1) 387 | ); 388 | }, 389 | [ 390 | { name: 'with validate', validate: true }, 391 | { name: 'without validate', validate: false }, 392 | ] 393 | ); 394 | 395 | cases( 396 | 'should getHomeWorksFor return result', 397 | async ({ validate }) => { 398 | const dateStart = new Date('11/13/2019'); 399 | const dateEnd = new Date('11/17/2019'); 400 | const untis = createInstance(); 401 | 402 | mockAxios.onGet(/homeworks\/lessons/).replyOnce(200, { 403 | data: { 404 | homeworks: {}, 405 | }, 406 | }); 407 | 408 | expect(await untis.getHomeWorksFor(dateStart, dateEnd, validate)).toEqual({ 409 | homeworks: {}, 410 | }); 411 | expect(mockAxios.history.get[0].params.startDate).toMatch('20191113'); 412 | expect(mockAxios.history.get[0].params.endDate).toMatch('20191117'); 413 | }, 414 | [ 415 | { name: 'with validate', validate: true }, 416 | { name: 'without validate', validate: false }, 417 | ] 418 | ); 419 | 420 | cases( 421 | 'should getHomeWorksFor catch error', 422 | async ({ validate, response, data }) => { 423 | const dateStart = new Date('11/13/2019'); 424 | const dateEnd = new Date('11/17/2019'); 425 | const untis = createInstance(); 426 | 427 | mockAxios.reset(); 428 | mockAxios 429 | .onPost(baseURL) 430 | .reply(200, response) 431 | .onGet(/homeworks\/lessons/) 432 | .replyOnce(200, { data }); 433 | 434 | await expect(() => untis.getHomeWorksFor(dateStart, dateEnd, validate)).rejects.toThrowErrorMatchingSnapshot(); 435 | }, 436 | [ 437 | { 438 | name: 'validate', 439 | validate: true, 440 | data: '', 441 | response: { result: '' }, 442 | }, 443 | { 444 | name: 'validate with not object', 445 | validate: true, 446 | data: '', 447 | response: { result: 200 }, 448 | }, 449 | { 450 | name: 'validate without homeworks', 451 | validate: true, 452 | data: {}, 453 | response: { result: 200 }, 454 | }, 455 | { 456 | name: 'invalidate', 457 | validate: false, 458 | data: '', 459 | response: { result: '' }, 460 | }, 461 | { 462 | name: 'invalidate with not object', 463 | validate: false, 464 | data: '', 465 | response: { result: 200 }, 466 | }, 467 | { 468 | name: 'invalidate without homeworks', 469 | validate: false, 470 | data: {}, 471 | response: { result: 200 }, 472 | }, 473 | ] 474 | ); 475 | 476 | test('should convertUntisDate converted date', () => { 477 | const date = new Date('11/13/2019'); 478 | expect(WebUntis.convertUntisDate(20191113, date)).toEqual(date); 479 | }); 480 | 481 | test('should convertUntisTime converted time', () => { 482 | const date = new Date('11/13/2019 3:11'); 483 | expect(WebUntis.convertUntisTime(311, date)).toEqual(date); 484 | }); 485 | 486 | cases( 487 | 'should method return result', 488 | async ({ name, method, validate, post }) => { 489 | const untis = createInstance(); 490 | 491 | mockAxios.onPost(baseURL).reply(200, mockResponse); 492 | 493 | expect(await untis[name](validate)).toEqual(mockResponse.result); 494 | expect(JSON.parse(mockAxios.history.post[post].data)).toMatchObject({ 495 | method, 496 | }); 497 | }, 498 | [ 499 | { name: 'getSubjects', method: 'getSubjects', validate: true, post: 1 }, 500 | { 501 | name: 'getSubjects', 502 | method: 'getSubjects', 503 | validate: false, 504 | post: 0, 505 | }, 506 | { name: 'getTeachers', method: 'getTeachers', validate: true, post: 1 }, 507 | { 508 | name: 'getTeachers', 509 | method: 'getTeachers', 510 | validate: false, 511 | post: 0, 512 | }, 513 | { name: 'getStudents', method: 'getStudents', validate: true, post: 1 }, 514 | { 515 | name: 'getStudents', 516 | method: 'getStudents', 517 | validate: false, 518 | post: 0, 519 | }, 520 | { name: 'getRooms', method: 'getRooms', validate: true, post: 1 }, 521 | { name: 'getRooms', method: 'getRooms', validate: false, post: 0 }, 522 | { name: 'getClasses', method: 'getKlassen', validate: true, post: 1 }, 523 | { name: 'getClasses', method: 'getKlassen', validate: false, post: 0 }, 524 | { 525 | name: 'getDepartments', 526 | method: 'getDepartments', 527 | validate: true, 528 | post: 1, 529 | }, 530 | { 531 | name: 'getDepartments', 532 | method: 'getDepartments', 533 | validate: false, 534 | post: 0, 535 | }, 536 | { name: 'getHolidays', method: 'getHolidays', validate: true, post: 1 }, 537 | { 538 | name: 'getHolidays', 539 | method: 'getHolidays', 540 | validate: false, 541 | post: 0, 542 | }, 543 | { 544 | name: 'getStatusData', 545 | method: 'getStatusData', 546 | validate: true, 547 | post: 1, 548 | }, 549 | { 550 | name: 'getStatusData', 551 | method: 'getStatusData', 552 | validate: false, 553 | post: 0, 554 | }, 555 | { 556 | name: 'getTimegrid', 557 | method: 'getTimegridUnits', 558 | validate: true, 559 | post: 1, 560 | }, 561 | { 562 | name: 'getTimegrid', 563 | method: 'getTimegridUnits', 564 | validate: false, 565 | post: 0, 566 | }, 567 | ] 568 | ); 569 | 570 | cases( 571 | 'should getHomeWorkAndLessons return result', 572 | async ({ validate }) => { 573 | const dateStart = new Date('11/13/2019'); 574 | const dateEnd = new Date('11/17/2019'); 575 | const untis = createInstance(); 576 | 577 | mockAxios.onGet(/homeworks\/lessons/).replyOnce(200, { 578 | data: { 579 | homeworks: {}, 580 | }, 581 | }); 582 | 583 | expect(await untis.getHomeWorkAndLessons(dateStart, dateEnd, validate)).toEqual({ 584 | homeworks: {}, 585 | }); 586 | expect(mockAxios.history.get[0].params.startDate).toMatch('20191113'); 587 | expect(mockAxios.history.get[0].params.endDate).toMatch('20191117'); 588 | }, 589 | [ 590 | { name: 'with validate', validate: true }, 591 | { name: 'without validate', validate: false }, 592 | ] 593 | ); 594 | 595 | cases( 596 | 'should getHomeWorkAndLessons catch error', 597 | async ({ validate, response, data }) => { 598 | const dateStart = new Date('11/13/2019'); 599 | const dateEnd = new Date('11/17/2019'); 600 | const untis = createInstance(); 601 | 602 | mockAxios.reset(); 603 | mockAxios 604 | .onPost(baseURL) 605 | .reply(200, response) 606 | .onGet(/homeworks\/lessons/) 607 | .replyOnce(200, { data }); 608 | 609 | await expect(() => 610 | untis.getHomeWorkAndLessons(dateStart, dateEnd, validate) 611 | ).rejects.toThrowErrorMatchingSnapshot(); 612 | }, 613 | [ 614 | { 615 | name: 'validate', 616 | validate: true, 617 | data: '', 618 | response: { result: '' }, 619 | }, 620 | { 621 | name: 'validate with not object', 622 | validate: true, 623 | data: '', 624 | response: { result: 200 }, 625 | }, 626 | { 627 | name: 'validate without homeworks', 628 | validate: true, 629 | data: {}, 630 | response: { result: 200 }, 631 | }, 632 | { 633 | name: 'invalidate', 634 | validate: false, 635 | data: '', 636 | response: { result: '' }, 637 | }, 638 | { 639 | name: 'invalidate with not object', 640 | validate: false, 641 | data: '', 642 | response: { result: 200 }, 643 | }, 644 | { 645 | name: 'invalidate without homeworks', 646 | validate: false, 647 | data: {}, 648 | response: { result: 200 }, 649 | }, 650 | ] 651 | ); 652 | 653 | cases( 654 | 'should convertDateToUntis converted date', 655 | ({ date, result }) => { 656 | expect(WebUntis.convertDateToUntis(new Date(date))).toBe(result); 657 | }, 658 | [ 659 | { name: 'default', date: '11/13/2019', result: '20191113' }, 660 | { name: 'date < 10', date: '11/09/2019', result: '20191109' }, 661 | { name: 'mouth < 10', date: '09/13/2019', result: '20190913' }, 662 | { 663 | name: 'date < 10 && mouth < 10', 664 | date: '09/08/2019', 665 | result: '20190908', 666 | }, 667 | ] 668 | ); 669 | -------------------------------------------------------------------------------- /docs/.nojekyll: -------------------------------------------------------------------------------- 1 | TypeDoc added this file to prevent GitHub Pages from using Jekyll. You can turn off this behavior by setting the `githubPages` option to false. -------------------------------------------------------------------------------- /docs/CNAME: -------------------------------------------------------------------------------- 1 | webuntis.noim.me -------------------------------------------------------------------------------- /docs/assets/highlight.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --light-hl-0: #AF00DB; 3 | --dark-hl-0: #C586C0; 4 | --light-hl-1: #000000; 5 | --dark-hl-1: #D4D4D4; 6 | --light-hl-2: #001080; 7 | --dark-hl-2: #9CDCFE; 8 | --light-hl-3: #A31515; 9 | --dark-hl-3: #CE9178; 10 | --light-hl-4: #0000FF; 11 | --dark-hl-4: #569CD6; 12 | --light-hl-5: #0070C1; 13 | --dark-hl-5: #4FC1FF; 14 | --light-hl-6: #795E26; 15 | --dark-hl-6: #DCDCAA; 16 | --light-hl-7: #008000; 17 | --dark-hl-7: #6A9955; 18 | --light-hl-8: #098658; 19 | --dark-hl-8: #B5CEA8; 20 | --light-code-background: #FFFFFF; 21 | --dark-code-background: #1E1E1E; 22 | } 23 | 24 | @media (prefers-color-scheme: light) { :root { 25 | --hl-0: var(--light-hl-0); 26 | --hl-1: var(--light-hl-1); 27 | --hl-2: var(--light-hl-2); 28 | --hl-3: var(--light-hl-3); 29 | --hl-4: var(--light-hl-4); 30 | --hl-5: var(--light-hl-5); 31 | --hl-6: var(--light-hl-6); 32 | --hl-7: var(--light-hl-7); 33 | --hl-8: var(--light-hl-8); 34 | --code-background: var(--light-code-background); 35 | } } 36 | 37 | @media (prefers-color-scheme: dark) { :root { 38 | --hl-0: var(--dark-hl-0); 39 | --hl-1: var(--dark-hl-1); 40 | --hl-2: var(--dark-hl-2); 41 | --hl-3: var(--dark-hl-3); 42 | --hl-4: var(--dark-hl-4); 43 | --hl-5: var(--dark-hl-5); 44 | --hl-6: var(--dark-hl-6); 45 | --hl-7: var(--dark-hl-7); 46 | --hl-8: var(--dark-hl-8); 47 | --code-background: var(--dark-code-background); 48 | } } 49 | 50 | :root[data-theme='light'] { 51 | --hl-0: var(--light-hl-0); 52 | --hl-1: var(--light-hl-1); 53 | --hl-2: var(--light-hl-2); 54 | --hl-3: var(--light-hl-3); 55 | --hl-4: var(--light-hl-4); 56 | --hl-5: var(--light-hl-5); 57 | --hl-6: var(--light-hl-6); 58 | --hl-7: var(--light-hl-7); 59 | --hl-8: var(--light-hl-8); 60 | --code-background: var(--light-code-background); 61 | } 62 | 63 | :root[data-theme='dark'] { 64 | --hl-0: var(--dark-hl-0); 65 | --hl-1: var(--dark-hl-1); 66 | --hl-2: var(--dark-hl-2); 67 | --hl-3: var(--dark-hl-3); 68 | --hl-4: var(--dark-hl-4); 69 | --hl-5: var(--dark-hl-5); 70 | --hl-6: var(--dark-hl-6); 71 | --hl-7: var(--dark-hl-7); 72 | --hl-8: var(--dark-hl-8); 73 | --code-background: var(--dark-code-background); 74 | } 75 | 76 | .hl-0 { color: var(--hl-0); } 77 | .hl-1 { color: var(--hl-1); } 78 | .hl-2 { color: var(--hl-2); } 79 | .hl-3 { color: var(--hl-3); } 80 | .hl-4 { color: var(--hl-4); } 81 | .hl-5 { color: var(--hl-5); } 82 | .hl-6 { color: var(--hl-6); } 83 | .hl-7 { color: var(--hl-7); } 84 | .hl-8 { color: var(--hl-8); } 85 | pre, code { background: var(--code-background); } 86 | -------------------------------------------------------------------------------- /docs/assets/navigation.js: -------------------------------------------------------------------------------- 1 | window.navigationData = "data:application/octet-stream;base64,H4sIAAAAAAAAA42WXW/aMBRA/4v3SkdhdGt5YwVpaOyjtAhN0x6c5JZ4Tewo92Ylmvbfp3xAErAveeXcc4KxQ/LzryDYk5iKjxJBDEQiKRRTEZsgiwDfhhRHb2r0onQgppPR3eTd9eTf4GhuwdtoUjiXeRMAncU4bKEydazcWvxFBDFoesoTcHVaI+7eUhOkWkZb8LJCegQ/BVqZndJN148kIuCQGe5eYTS+HVSWmArC4ErhVZKqP5JAWBZzfqUDOcta7Jk2Oo9NhrOMQneqM9an+7B2xx7WfQrV78N/rWaGK848BO23tloVW/EsfcBhzbr6+Ob9uY6cj1zg3gSAC02KcmujxflMZFI2c+RcZg6JTKk43NZKg7nIYi9jq14AXvQztG9FhTj5k4lUIO2rrxmvx/Bq0heHX0EusNSe2VvtklxUY0CUO/vq2wNc6HN5C1gTFeLkFSAabZUrxMrcGV71OMBfqtXht+e5YxM7E31SoANI2VQ5waW+wituVbAD+/3QYC6yNsZ+PxSAEx/90JjoB0j7KhrMRkKT0lyStDcOlE2QpAzdjSPmI1ng+l+pGatn3m/wHXrFOP0JpB86TkPNWF3FsNHKfvkDvBTYpSpwBgrIBbbgzb4vi0mSXmS/x09mLuTqtxhXqcb9Is6z0R1hn6QZhaBJ+ZJMa58oT4onaRueVK7vPoxuxq3SZr26L9+QTiKHz62++63q13+/Rz1ZngoAAA==" -------------------------------------------------------------------------------- /docs/assets/search.js: -------------------------------------------------------------------------------- 1 | window.searchData = "data:application/octet-stream;base64,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"; -------------------------------------------------------------------------------- /docs/hierarchy.html: -------------------------------------------------------------------------------- 1 | webuntis

webuntis

Class Hierarchy

Generated using TypeDoc

-------------------------------------------------------------------------------- /docs/interfaces/ColorEntity.html: -------------------------------------------------------------------------------- 1 | ColorEntity | webuntis

Interface ColorEntity

interface ColorEntity {
    backColor: string;
    foreColor: string;
}

Properties

Properties

backColor: string
foreColor: string

Generated using TypeDoc

-------------------------------------------------------------------------------- /docs/interfaces/Inbox.html: -------------------------------------------------------------------------------- 1 | Inbox | webuntis

Interface Inbox

interface Inbox {
    incomingMessages: Inboxmessage[];
}

Properties

Properties

incomingMessages: Inboxmessage[]

Generated using TypeDoc

-------------------------------------------------------------------------------- /docs/types/Authenticator.html: -------------------------------------------------------------------------------- 1 | Authenticator | webuntis

Type alias Authenticator

Authenticator: typeof authenticator

Generated using TypeDoc

-------------------------------------------------------------------------------- /docs/types/URLClass.html: -------------------------------------------------------------------------------- 1 | URLClass | webuntis

Type alias URLClassPrivate

URLClass: typeof URL

Generated using TypeDoc

-------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | // For a detailed explanation regarding each configuration property, visit: 2 | // https://jestjs.io/docs/en/configuration.html 3 | 4 | module.exports = { 5 | // All imported modules in your tests should be mocked automatically 6 | // automock: false, 7 | 8 | // Stop running tests after `n` failures 9 | // bail: 0, 10 | 11 | // The directory where Jest should store its cached dependency information 12 | // cacheDirectory: "/private/var/folders/n5/dn17rs993572rs0smhy59x1w0000gn/T/jest_dx", 13 | 14 | // Automatically clear mock calls and instances between every test 15 | clearMocks: true, 16 | 17 | // Indicates whether the coverage information should be collected while executing the test 18 | // collectCoverage: false, 19 | 20 | // An array of glob patterns indicating a set of files for which coverage information should be collected 21 | // collectCoverageFrom: undefined, 22 | 23 | // The directory where Jest should output its coverage files 24 | coverageDirectory: 'coverage', 25 | 26 | // An array of regexp pattern strings used to skip coverage collection 27 | // coveragePathIgnorePatterns: [ 28 | // "/node_modules/" 29 | // ], 30 | 31 | // Indicates which provider should be used to instrument code for coverage 32 | coverageProvider: 'v8', 33 | 34 | // A list of reporter names that Jest uses when writing coverage reports 35 | // coverageReporters: [ 36 | // "json", 37 | // "text", 38 | // "lcov", 39 | // "clover" 40 | // ], 41 | 42 | // An object that configures minimum threshold enforcement for coverage results 43 | // coverageThreshold: undefined, 44 | 45 | // A path to a custom dependency extractor 46 | // dependencyExtractor: undefined, 47 | 48 | // Make calling deprecated APIs throw helpful error messages 49 | // errorOnDeprecated: false, 50 | 51 | // Force coverage collection from ignored files using an array of glob patterns 52 | // forceCoverageMatch: [], 53 | 54 | // A path to a module which exports an async function that is triggered once before all test suites 55 | // globalSetup: undefined, 56 | 57 | // A path to a module which exports an async function that is triggered once after all test suites 58 | // globalTeardown: undefined, 59 | 60 | // A set of global variables that need to be available in all test environments 61 | // globals: {}, 62 | 63 | // The maximum amount of workers used to run your tests. Can be specified as % or a number. E.g. maxWorkers: 10% will use 10% of your CPU amount + 1 as the maximum worker number. maxWorkers: 2 will use a maximum of 2 workers. 64 | // maxWorkers: "50%", 65 | 66 | // An array of directory names to be searched recursively up from the requiring module's location 67 | // moduleDirectories: [ 68 | // "node_modules" 69 | // ], 70 | 71 | // An array of file extensions your modules use 72 | // moduleFileExtensions: [ 73 | // "js", 74 | // "json", 75 | // "jsx", 76 | // "ts", 77 | // "tsx", 78 | // "node" 79 | // ], 80 | 81 | // A map from regular expressions to module names or to arrays of module names that allow to stub out resources with a single module 82 | // moduleNameMapper: {}, 83 | 84 | // An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader 85 | // modulePathIgnorePatterns: [], 86 | 87 | // Activates notifications for test results 88 | // notify: false, 89 | 90 | // An enum that specifies notification mode. Requires { notify: true } 91 | // notifyMode: "failure-change", 92 | 93 | // A preset that is used as a base for Jest's configuration 94 | // preset: undefined, 95 | 96 | // Run tests from one or more projects 97 | // projects: undefined, 98 | 99 | // Use this configuration option to add custom reporters to Jest 100 | // reporters: undefined, 101 | 102 | // Automatically reset mock state between every test 103 | // resetMocks: false, 104 | 105 | // Reset the module registry before running each individual test 106 | // resetModules: false, 107 | 108 | // A path to a custom resolver 109 | // resolver: undefined, 110 | 111 | // Automatically restore mock state between every test 112 | // restoreMocks: false, 113 | 114 | // The root directory that Jest should scan for tests and modules within 115 | // rootDir: undefined, 116 | 117 | // A list of paths to directories that Jest should use to search for files in 118 | // roots: [ 119 | // "" 120 | // ], 121 | 122 | // Allows you to use a custom runner instead of Jest's default test runner 123 | // runner: "jest-runner", 124 | 125 | // The paths to modules that run some code to configure or set up the testing environment before each test 126 | // setupFiles: [], 127 | 128 | // A list of paths to modules that run some code to configure or set up the testing framework before each test 129 | // setupFilesAfterEnv: [], 130 | 131 | // The number of seconds after which a test is considered as slow and reported as such in the results. 132 | // slowTestThreshold: 5, 133 | 134 | // A list of paths to snapshot serializer modules Jest should use for snapshot testing 135 | // snapshotSerializers: [], 136 | 137 | // The test environment that will be used for testing 138 | testEnvironment: 'node', 139 | 140 | // Options that will be passed to the testEnvironment 141 | // testEnvironmentOptions: {}, 142 | 143 | // Adds a location field to test results 144 | // testLocationInResults: false, 145 | 146 | // The glob patterns Jest uses to detect test files 147 | testMatch: [ 148 | '**/__tests__/**/*.[jt]s?(x)', 149 | // "**/?(*.)+(spec|test).[tj]s?(x)" 150 | ], 151 | 152 | // An array of regexp pattern strings that are matched against all test paths, matched tests are skipped 153 | // testPathIgnorePatterns: [ 154 | // "/node_modules/" 155 | // ], 156 | 157 | // The regexp pattern or array of patterns that Jest uses to detect test files 158 | // testRegex: [], 159 | 160 | // This option allows the use of a custom results processor 161 | // testResultsProcessor: undefined, 162 | 163 | // This option allows use of a custom test runner 164 | // testRunner: "jasmine2", 165 | 166 | // This option sets the URL for the jsdom environment. It is reflected in properties such as location.href 167 | // testURL: "http://localhost", 168 | 169 | // Setting this value to "fake" allows the use of fake timers for functions such as "setTimeout" 170 | // timers: "real", 171 | 172 | // A map from regular expressions to paths to transformers 173 | // transform: undefined, 174 | 175 | // An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation 176 | // transformIgnorePatterns: [ 177 | // "/node_modules/", 178 | // "\\.pnp\\.[^\\/]+$" 179 | // ], 180 | 181 | // An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them 182 | // unmockedModulePathPatterns: undefined, 183 | 184 | // Indicates whether each individual test should be reported during the run 185 | // verbose: undefined, 186 | 187 | // An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode 188 | // watchPathIgnorePatterns: [], 189 | 190 | // Whether to use watchman for file crawling 191 | // watchman: true, 192 | }; 193 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "webuntis", 3 | "version": "2.2.1", 4 | "description": "WebUntis API", 5 | "main": "dist/webuntis.js", 6 | "module": "dist/webuntis.mjs", 7 | "typings": "dist/webuntis.d.ts", 8 | "repository": { 9 | "type": "git", 10 | "url": "https://github.com/SchoolUtils/WebUntis.git" 11 | }, 12 | "author": "Nils Bergmann ", 13 | "license": "MIT", 14 | "private": false, 15 | "files": [ 16 | "dist", 17 | "docs" 18 | ], 19 | "dependencies": { 20 | "axios": "^1.6.7", 21 | "date-fns": "^3.3.1" 22 | }, 23 | "optionalDependencies": { 24 | "otplib": "^12" 25 | }, 26 | "devDependencies": { 27 | "axios-mock-adapter": "^1.22.0", 28 | "dotenv": "^16.4.1", 29 | "esbuild": "^0.20.0", 30 | "jest": "^29.7.0", 31 | "jest-in-case": "^1.0.2", 32 | "otplib": "^12", 33 | "prettier": "^3.2.5", 34 | "rollup": "^4.9.6", 35 | "rollup-plugin-dts": "^6.1.0", 36 | "rollup-plugin-esbuild": "^6.1.1", 37 | "typedoc": "^0.25.7", 38 | "typescript": "^5.3.3" 39 | }, 40 | "scripts": { 41 | "prepublish": "rollup -c && typedoc", 42 | "generate-doc": "typedoc", 43 | "test": "jest", 44 | "format": "prettier -w src/" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /rollup.config.mjs: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'rollup'; 2 | import dts from 'rollup-plugin-dts'; 3 | import esbuild from 'rollup-plugin-esbuild'; 4 | 5 | const name = 'webuntis'; 6 | 7 | const bundle = (config) => ({ 8 | ...config, 9 | input: 'src/index.ts', 10 | external: (id) => !/^[./]/.test(id), 11 | }); 12 | 13 | export default defineConfig([ 14 | bundle({ 15 | plugins: [esbuild()], 16 | output: [ 17 | { 18 | file: `./dist/${name}.js`, 19 | format: 'cjs', 20 | sourcemap: true, 21 | }, 22 | { 23 | file: `./dist/${name}.mjs`, 24 | format: 'es', 25 | sourcemap: true, 26 | }, 27 | ], 28 | }), 29 | bundle({ 30 | plugins: [dts()], 31 | output: { 32 | file: `./dist/${name}.d.ts`, 33 | format: 'es', 34 | }, 35 | }), 36 | ]); 37 | -------------------------------------------------------------------------------- /src/anonymous.ts: -------------------------------------------------------------------------------- 1 | import { InternalWebuntisSecretLogin } from './base'; 2 | 3 | export class WebUntisAnonymousAuth extends InternalWebuntisSecretLogin { 4 | /** 5 | * 6 | * @param {string} school 7 | * @param {string} baseurl 8 | * @param {string} [identity='Awesome'] 9 | * @param {boolean} [disableUserAgent=false] If this is true, axios will not send a custom User-Agent 10 | */ 11 | constructor(school: string, baseurl: string, identity = 'Awesome', disableUserAgent = false) { 12 | // TODO: Make this a bit more beautiful and more type safe 13 | super(school, null as unknown as string, null as unknown as string, baseurl, identity, disableUserAgent); 14 | this.username = '#anonymous#'; 15 | this.anonymous = true; 16 | } 17 | 18 | override async login() { 19 | // Check whether the school has public access or not 20 | const url = `/WebUntis/jsonrpc_intern.do`; 21 | 22 | const response = await this.axios({ 23 | method: 'POST', 24 | url, 25 | params: { 26 | m: 'getAppSharedSecret', 27 | school: this.school, 28 | v: 'i3.5', 29 | }, 30 | data: { 31 | id: this.id, 32 | method: 'getAppSharedSecret', 33 | params: [ 34 | { 35 | userName: '#anonymous#', 36 | password: '', 37 | }, 38 | ], 39 | jsonrpc: '2.0', 40 | }, 41 | }); 42 | 43 | if (response.data && response.data.error) 44 | throw new Error('Failed to login. ' + (response.data.error.message || '')); 45 | 46 | // OTP never changes when using anonymous login 47 | const otp = 100170; 48 | const time = new Date().getTime(); 49 | return await this._otpLogin(otp, this.username, time, true); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/base-64.ts: -------------------------------------------------------------------------------- 1 | const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/='; 2 | export function btoa(input = '') { 3 | let str = input; 4 | let output = ''; 5 | 6 | for ( 7 | let block = 0, charCode, i = 0, map = chars; 8 | str.charAt(i | 0) || ((map = '='), i % 1); 9 | output += map.charAt(63 & (block >> (8 - (i % 1) * 8))) 10 | ) { 11 | charCode = str.charCodeAt((i += 3 / 4)); 12 | 13 | if (charCode > 0xff) { 14 | throw new Error("'btoa' failed: The string to be encoded contains characters outside of the Latin1 range."); 15 | } 16 | 17 | block = (block << 8) | charCode; 18 | } 19 | 20 | return output; 21 | } 22 | -------------------------------------------------------------------------------- /src/cookie.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Shamelessly stolen from @tinyhttp/cookie. 3 | * 4 | * Because @tinyhttp/cookie doesn't provide a commonjs build, I just decided to include the source code. 5 | * We need a cjs build, because we generate both esm and cjs. 6 | */ 7 | 8 | const pairSplitRegExp = /; */; 9 | 10 | /** 11 | * RegExp to match field-content in RFC 7230 sec 3.2 12 | * 13 | * field-content = field-vchar [ 1*( SP / HTAB ) field-vchar ] 14 | * field-vchar = VCHAR / obs-text 15 | * obs-text = %x80-FF 16 | */ 17 | 18 | // eslint-disable-next-line no-control-regex 19 | const fieldContentRegExp = /^[\u0009\u0020-\u007e\u0080-\u00ff]+$/; 20 | 21 | function tryDecode(str: string, decode: (str: string) => string) { 22 | try { 23 | return decode(str); 24 | } catch (e) { 25 | return str; 26 | } 27 | } 28 | 29 | /** 30 | * Parse a cookie header. 31 | * 32 | * Parse the given cookie header string into an object 33 | * The object has the various cookies as keys(names) => values 34 | * 35 | */ 36 | export function parse( 37 | str: string, 38 | options: { 39 | decode: (str: string) => string; 40 | } = { 41 | decode: decodeURIComponent, 42 | }, 43 | ): Record { 44 | const obj: Record = {}; 45 | const pairs = str.split(pairSplitRegExp); 46 | 47 | for (const pair of pairs) { 48 | let eqIdx = pair.indexOf('='); 49 | 50 | // skip things that don't look like key=value 51 | if (eqIdx < 0) continue; 52 | 53 | const key = pair.substr(0, eqIdx).trim(); 54 | let val = pair.substr(++eqIdx, pair.length).trim(); 55 | 56 | // quoted values 57 | if ('"' == val[0]) val = val.slice(1, -1); 58 | 59 | // only assign once 60 | if (obj[key] == null) obj[key] = tryDecode(val, options.decode); 61 | } 62 | 63 | return obj; 64 | } 65 | 66 | export type SerializeOptions = Partial<{ 67 | encode: (str: string) => string; 68 | maxAge: number; 69 | domain: string; 70 | path: string; 71 | httpOnly: boolean; 72 | secure: boolean; 73 | sameSite: boolean | 'Strict' | 'strict' | 'Lax' | 'lax' | 'None' | 'none' | string; 74 | expires: Date; 75 | }>; 76 | 77 | export function serialize(name: string, val: string, opt: SerializeOptions = {}): string { 78 | if (!opt.encode) opt.encode = encodeURIComponent; 79 | 80 | if (!fieldContentRegExp.test(name)) throw new TypeError('argument name is invalid'); 81 | 82 | const value = opt.encode(val); 83 | 84 | if (value && !fieldContentRegExp.test(value)) throw new TypeError('argument val is invalid'); 85 | 86 | let str = name + '=' + value; 87 | 88 | if (null != opt.maxAge) { 89 | const maxAge = opt.maxAge - 0; 90 | 91 | if (isNaN(maxAge) || !isFinite(maxAge)) throw new TypeError('option maxAge is invalid'); 92 | 93 | str += '; Max-Age=' + Math.floor(maxAge); 94 | } 95 | 96 | if (opt.domain) { 97 | if (!fieldContentRegExp.test(opt.domain)) throw new TypeError('option domain is invalid'); 98 | 99 | str += '; Domain=' + opt.domain; 100 | } 101 | 102 | if (opt.path) { 103 | if (!fieldContentRegExp.test(opt.path)) throw new TypeError('option path is invalid'); 104 | 105 | str += '; Path=' + opt.path; 106 | } 107 | 108 | if (opt.expires) str += '; Expires=' + opt.expires.toUTCString(); 109 | 110 | if (opt.httpOnly) str += '; HttpOnly'; 111 | 112 | if (opt.secure) str += '; Secure'; 113 | 114 | if (opt.sameSite) { 115 | const sameSite = typeof opt.sameSite === 'string' ? opt.sameSite.toLowerCase() : opt.sameSite; 116 | 117 | switch (sameSite) { 118 | case true: 119 | case 'strict': 120 | str += '; SameSite=Strict'; 121 | break; 122 | case 'lax': 123 | str += '; SameSite=Lax'; 124 | break; 125 | case 'none': 126 | str += '; SameSite=None'; 127 | break; 128 | default: 129 | throw new TypeError('option sameSite is invalid'); 130 | } 131 | } 132 | 133 | return str; 134 | } 135 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export { Base as WebUntis } from './base'; 2 | export * from './base'; 3 | export * from './secret'; 4 | export * from './qr'; 5 | export * from './anonymous'; 6 | export * from './types'; 7 | -------------------------------------------------------------------------------- /src/internal.ts: -------------------------------------------------------------------------------- 1 | import type { SchoolYear } from './types'; 2 | export type InternalSchoolYear = Omit & { startDate: string; endDate: string }; 3 | 4 | export type SessionInformation = { 5 | klasseId?: number; 6 | personId?: number; 7 | sessionId?: string; 8 | personType?: number; 9 | jwt_token?: string; 10 | }; 11 | -------------------------------------------------------------------------------- /src/qr.ts: -------------------------------------------------------------------------------- 1 | import { WebUntisSecretAuth } from './secret'; 2 | import type { Authenticator } from './secret'; 3 | import type { URL } from 'url'; 4 | 5 | /** 6 | * @private 7 | */ 8 | export type URLClass = typeof URL; 9 | 10 | export class WebUntisQR extends WebUntisSecretAuth { 11 | /** 12 | * Use the data you get from a WebUntis QR code 13 | * @constructor 14 | * @param {string} QRCodeURI A WebUntis uri. This is the data you get from the QR Code from the webuntis webapp under profile->Data access->Display 15 | * @param {string} [identity="Awesome"] A identity like: MyAwesomeApp 16 | * @param {Object} authenticator Custom otplib v12 instance. Default will use the default otplib configuration. 17 | * @param {Object} URL Custom whatwg url implementation. Default will use the nodejs implementation. 18 | * @param {boolean} [disableUserAgent=false] If this is true, axios will not send a custom User-Agent 19 | */ 20 | constructor( 21 | QRCodeURI: string, 22 | identity: string, 23 | authenticator: Authenticator, 24 | URL?: URLClass, 25 | disableUserAgent = false, 26 | ) { 27 | let URLImplementation = URL; 28 | if (!URL) { 29 | if ('import' in globalThis) { 30 | throw new Error( 31 | 'You need to provide the URL object by yourself. We can not eval the require in ESM mode.', 32 | ); 33 | } 34 | // React-Native will not eval this expression 35 | URLImplementation = eval("require('url').URL") as URLClass; 36 | } 37 | const uri = new URLImplementation!(QRCodeURI); 38 | super( 39 | uri.searchParams.get('school')!, 40 | uri.searchParams.get('user')!, 41 | uri.searchParams.get('key')!, 42 | uri.searchParams.get('url')!, 43 | identity, 44 | authenticator, 45 | disableUserAgent, 46 | ); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/secret.ts: -------------------------------------------------------------------------------- 1 | import { InternalWebuntisSecretLogin } from './base'; 2 | import type { authenticator } from 'otplib'; 3 | 4 | export type Authenticator = typeof authenticator; 5 | 6 | export class WebUntisSecretAuth extends InternalWebuntisSecretLogin { 7 | private readonly secret: string; 8 | private authenticator: Authenticator; 9 | 10 | /** 11 | * 12 | * @constructor 13 | * @augments WebUntis 14 | * @param {string} school The school identifier 15 | * @param {string} user 16 | * @param {string} secret 17 | * @param {string} baseurl Just the host name of your WebUntis (Example: mese.webuntis.com) 18 | * @param {string} [identity="Awesome"] A identity like: MyAwesomeApp 19 | * @param {Object} authenticator Custom otplib v12 instance. Default will use the default otplib configuration. 20 | * @param {boolean} [disableUserAgent=false] If this is true, axios will not send a custom User-Agent 21 | */ 22 | constructor( 23 | school: string, 24 | user: string, 25 | secret: string, 26 | baseurl: string, 27 | identity = 'Awesome', 28 | authenticator: Authenticator, 29 | disableUserAgent = false, 30 | ) { 31 | super(school, user, null as unknown as string, baseurl, identity, disableUserAgent); 32 | this.secret = secret; 33 | this.authenticator = authenticator; 34 | if (!authenticator) { 35 | if ('import' in globalThis) { 36 | throw new Error( 37 | 'You need to provide the otplib object by yourself. We can not eval the require in ESM mode.', 38 | ); 39 | } 40 | // React-Native will not eval this expression 41 | const { authenticator } = eval("require('otplib')"); 42 | this.authenticator = authenticator; 43 | } 44 | } 45 | 46 | // @ts-ignore 47 | async login() { 48 | // Get JSESSION 49 | const token = this.authenticator.generate(this.secret); 50 | const time = new Date().getTime(); 51 | return await this._otpLogin(token, this.username, time); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | export interface SchoolYear { 2 | name: string; 3 | id: number; 4 | startDate: Date; 5 | endDate: Date; 6 | } 7 | 8 | export interface MessagesOfDay { 9 | id: number; 10 | subject: string; 11 | text: string; 12 | isExpanded: boolean; 13 | /** 14 | * Unknown type. I have never seen this in use. 15 | */ 16 | attachments: any[]; 17 | } 18 | 19 | export interface NewsWidget { 20 | /** 21 | * Unknown type. I have never seen this in use. 22 | */ 23 | systemMessage: any; 24 | messagesOfDay: MessagesOfDay[]; 25 | rssUrl: string; 26 | } 27 | 28 | export interface Messagesender { 29 | userId: number; 30 | displayName: string; 31 | imageUrl: string; 32 | className: string; 33 | } 34 | 35 | export interface Inboxmessage { 36 | allowMessageDeletion: boolean; 37 | contentPreview: string; 38 | hasAttachments: boolean; 39 | id: number; 40 | isMessageRead: boolean; 41 | isReply: boolean; 42 | isReplyAllowed: boolean; 43 | sender: Messagesender; 44 | sentDateTime: string; 45 | subject: string; 46 | } 47 | 48 | export interface Inbox { 49 | incomingMessages: Inboxmessage[]; 50 | } 51 | 52 | export interface ShortData { 53 | id: number; 54 | name: string; 55 | longname: string; 56 | orgname?: string; 57 | orgid?: number; 58 | } 59 | 60 | export interface Lesson { 61 | id: number; 62 | date: number; 63 | startTime: number; 64 | endTime: number; 65 | kl: ShortData[]; 66 | te: ShortData[]; 67 | su: ShortData[]; 68 | ro: ShortData[]; 69 | lstext?: string; 70 | lsnumber: number; 71 | activityType?: 'Unterricht' | string; 72 | code?: 'cancelled' | 'irregular'; 73 | info?: string; 74 | substText?: string; 75 | statflags?: string; 76 | sg?: string; 77 | bkRemark?: string; 78 | bkText?: string; 79 | } 80 | 81 | export interface Homework { 82 | /** 83 | * Unknown type. I have never seen this in use. 84 | */ 85 | attachments: Array; 86 | completed: boolean; 87 | date: number; 88 | dueDate: number; 89 | id: number; 90 | lessonId: number; 91 | remark: string; 92 | text: string; 93 | } 94 | 95 | export interface Subject { 96 | id: number; 97 | name: string; 98 | longName: string; 99 | alternateName: string | ''; 100 | active: boolean; 101 | foreColor: string; 102 | backColor: string; 103 | } 104 | 105 | export enum WebUntisDay { 106 | Sunday = 1, 107 | Monday = 2, 108 | Tuesday = 3, 109 | Wednesday = 4, 110 | Thursday = 5, 111 | Friday = 6, 112 | Saturday = 7, 113 | } 114 | 115 | export interface TimeUnit { 116 | name: string; 117 | startTime: number; 118 | endTime: number; 119 | } 120 | 121 | export interface Timegrid { 122 | day: WebUntisDay; 123 | timeUnits: TimeUnit[]; 124 | } 125 | 126 | export interface Exam { 127 | id: number; 128 | examType: string; 129 | name: string; 130 | studentClass: string[]; 131 | assignedStudents: { 132 | klasse: { id: number; name: string }; 133 | displayName: string; 134 | id: number; 135 | }[]; 136 | examDate: number; 137 | startTime: number; 138 | endTime: number; 139 | subject: string; 140 | teachers: string[]; 141 | rooms: string[]; 142 | text: string; 143 | grade?: string; 144 | } 145 | 146 | export enum WebUntisElementType { 147 | CLASS = 1, 148 | TEACHER = 2, 149 | SUBJECT = 3, 150 | ROOM = 4, 151 | STUDENT = 5, 152 | } 153 | 154 | export interface WebElement { 155 | type: WebUntisElementType; 156 | id: number; 157 | orgId: number; 158 | missing: boolean; 159 | state: 'REGULAR' | 'ABSENT' | 'SUBSTITUTED'; 160 | } 161 | 162 | export interface WebElementData extends WebElement { 163 | element: { 164 | type: number; 165 | id: number; 166 | name: string; 167 | longName?: string; 168 | displayname?: string; 169 | alternatename?: string; 170 | canViewTimetable: boolean; 171 | externalKey?: string; 172 | roomCapacity: number; 173 | }; 174 | } 175 | 176 | export interface WebAPITimetable { 177 | id: number; 178 | lessonId: number; 179 | lessonNumber: number; 180 | lessonCode: string; 181 | lessonText: string; 182 | periodText: string; 183 | hasPeriodText: false; 184 | periodInfo: string; 185 | periodAttachments: []; 186 | substText: string; 187 | date: number; 188 | startTime: number; 189 | endTime: number; 190 | elements: WebElement[]; 191 | studentGroup: string; 192 | hasInfo: boolean; 193 | code: number; 194 | cellState: 'STANDARD' | 'SUBSTITUTION' | 'ROOMSUBSTITUTION'; 195 | priority: number; 196 | is: { 197 | roomSubstitution?: boolean; 198 | substitution?: boolean; 199 | standard?: boolean; 200 | event: boolean; 201 | }; 202 | roomCapacity: number; 203 | studentCount: number; 204 | classes: WebElementData[]; 205 | teachers: WebElementData[]; 206 | subjects: WebElementData[]; 207 | rooms: WebElementData[]; 208 | students: WebElementData[]; 209 | } 210 | 211 | export interface Teacher { 212 | id: number; 213 | name: string; 214 | foreName: string; 215 | longName: string; 216 | foreColor: string; 217 | backColor: string; 218 | } 219 | 220 | export interface Student { 221 | id: number; 222 | key: number; 223 | name: string; 224 | foreName: string; 225 | longName: string; 226 | gender: string; 227 | } 228 | 229 | export interface Room { 230 | id: number; 231 | name: string; 232 | longName: string; 233 | alternateName: string | ''; 234 | active: boolean; 235 | foreColor: string; 236 | backColor: string; 237 | } 238 | 239 | export interface Klasse { 240 | id: number; 241 | name: string; 242 | longName: string; 243 | active: boolean; 244 | foreColor?: string; 245 | backColor?: string; 246 | did?: number; 247 | teacher1?: number; 248 | teacher2?: number; 249 | } 250 | 251 | export interface Department { 252 | id: number; 253 | name: string; 254 | longName: string; 255 | } 256 | 257 | export interface Holiday { 258 | name: string; 259 | longName: string; 260 | id: number; 261 | startDate: number; 262 | endDate: number; 263 | } 264 | 265 | export interface ColorEntity { 266 | foreColor: string; 267 | backColor: string; 268 | } 269 | 270 | export interface LsEntity { 271 | ls?: ColorEntity | null; 272 | oh?: ColorEntity | null; 273 | sb?: ColorEntity | null; 274 | bs?: ColorEntity | null; 275 | ex?: ColorEntity | null; 276 | } 277 | 278 | export interface CodesEntity { 279 | cancelled?: ColorEntity | null; 280 | irregular?: ColorEntity | null; 281 | } 282 | 283 | export interface StatusData { 284 | lstypes: LsEntity[]; 285 | codes: CodesEntity[]; 286 | } 287 | 288 | export interface Absences { 289 | absences: Absence[]; 290 | absenceReasons: []; 291 | excuseStatuses: boolean; 292 | showAbsenceReasonChange: boolean; 293 | showCreateAbsence: boolean; 294 | } 295 | 296 | export interface Absence { 297 | id: number; 298 | startDate: number; 299 | endDate: number; 300 | startTime: number; 301 | endTime: number; 302 | createDate: number; 303 | lastUpdate: number; 304 | createdUser: string; 305 | updatedUser: string; 306 | reasonId: number; 307 | reason: string; 308 | text: string; 309 | interruptions: []; 310 | canEdit: boolean; 311 | studentName: string; 312 | excuseStatus: string; 313 | isExcused: boolean; 314 | excuse: Excuse; 315 | } 316 | 317 | export interface Excuse { 318 | id: number; 319 | text: string; 320 | excuseDate: number; 321 | excuseStatus: string; 322 | isExcused: boolean; 323 | userId: number; 324 | username: string; 325 | } 326 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | const { WebUntis, WebUntisSecretAuth, WebUntisQR, WebUntisAnonymousAuth } = require('./dist/webuntis'); 2 | const { subDays, endOfMonth } = require('date-fns'); 3 | 4 | require('dotenv').config(); 5 | 6 | const untis = new WebUntis(process.env.SCHOOL, process.env.UNTISUSER, process.env.UNTISPW, process.env.UNTISHOST); 7 | 8 | console.log(process.env.UNTISPW); 9 | 10 | /** 11 | * 12 | * @type {WebUntisSecretAuth} 13 | */ 14 | const untisSecret = new WebUntisSecretAuth( 15 | process.env.SCHOOL, 16 | process.env.UNTISUSER, 17 | process.env.UNTISSECRET, 18 | process.env.UNTISHOST 19 | ); 20 | 21 | /** 22 | * 23 | * @type {WebUntisQR} 24 | */ 25 | const untisQR = new WebUntisQR(process.env.UNTISQR); 26 | 27 | /** 28 | * 29 | */ 30 | const anonymous = new WebUntisAnonymousAuth(process.env.UNTISANONYMOUSSCHOOL, process.env.UNTISANONYMOUSSCHOOLHOST); 31 | 32 | (async function () { 33 | const endOfMonthVar = endOfMonth(new Date()); 34 | const targetDate = subDays(new Date(), 2); 35 | console.log('Normal login:'); 36 | try { 37 | await untis.login(); 38 | const x = await untis.validateSession(); 39 | console.log('Valid session (User/PW): ' + x); 40 | // console.log( 41 | // 'Absent Lessons: ' + JSON.stringify(await untis.getAbsentLesson(new Date('20210913'), new Date(), true)) 42 | // ); 43 | // console.log(await untis.getPdfOfAbsentLesson(new Date(Date.now() - 7 * 24 * 60 * 60), new Date(), true)); 44 | console.log('Session: ' + JSON.stringify(untis.sessionInformation)); 45 | console.log('Timetable: ' + JSON.stringify(await untis.getOwnTimetableFor(targetDate))); 46 | // console.log('Homework: ' + JSON.stringify(await untis.getHomeWorkAndLessons(new Date(), endOfMonthVar))); 47 | console.log('Rooms: ' + JSON.stringify(await untis.getRooms())); 48 | console.log('News: ' + JSON.stringify(await untis.getNewsWidget(targetDate))); 49 | } catch (e) { 50 | console.error(e); 51 | } 52 | console.log('Secret login:'); 53 | try { 54 | await untisSecret.login(); 55 | const x = await untisSecret.validateSession(); 56 | console.log('Valid session (SECRET): ' + x); 57 | console.log('Session: ' + JSON.stringify(untisSecret.sessionInformation)); 58 | console.log('Timetable: ' + JSON.stringify(await untisSecret.getOwnTimetableFor(targetDate))); 59 | console.log('Homework: ' + JSON.stringify(await untisSecret.getHomeWorkAndLessons(new Date(), endOfMonthVar))); 60 | console.log('Rooms: ' + JSON.stringify(await untisSecret.getRooms())); 61 | } catch (e) { 62 | console.trace(e); 63 | } 64 | console.log('QR Login:'); 65 | try { 66 | await untisQR.login(); 67 | const x = await untisQR.validateSession(); 68 | console.log('Valid session (QR): ' + x); 69 | console.log('Session: ' + JSON.stringify(untisQR.sessionInformation)); 70 | console.log('Timetable: ' + JSON.stringify(await untisQR.getOwnTimetableFor(targetDate))); 71 | console.log('Homework: ' + JSON.stringify(await untisQR.getHomeWorkAndLessons(new Date(), endOfMonthVar))); 72 | console.log('Rooms: ' + JSON.stringify(await untisQR.getRooms())); 73 | } catch (e) { 74 | console.trace(e); 75 | } 76 | console.log('Anonymous:'); 77 | try { 78 | await anonymous.login(); 79 | const x = await anonymous.validateSession(); 80 | console.log('Valid session (anonymous): ' + x); 81 | } catch (e) { 82 | console.trace(e); 83 | } 84 | })(); 85 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig to read more about this file */ 4 | 5 | /* Projects */ 6 | // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ 7 | // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ 8 | // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ 9 | // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ 10 | // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ 11 | // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ 12 | 13 | /* Language and Environment */ 14 | "target": "esnext" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, 15 | // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ 16 | // "jsx": "preserve", /* Specify what JSX code is generated. */ 17 | // "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */ 18 | // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ 19 | // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ 20 | // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ 21 | // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ 22 | // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ 23 | // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ 24 | // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ 25 | // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ 26 | 27 | /* Modules */ 28 | "module": "esnext" /* Specify what module code is generated. */, 29 | // "rootDir": "./", /* Specify the root folder within your source files. */ 30 | "moduleResolution": "node" /* Specify how TypeScript looks up a file from a given module specifier. */, 31 | // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ 32 | // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ 33 | // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ 34 | // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ 35 | // "types": [], /* Specify type package names to be included without being referenced in a source file. */ 36 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 37 | // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ 38 | // "resolveJsonModule": true, /* Enable importing .json files. */ 39 | // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ 40 | 41 | /* JavaScript Support */ 42 | "allowJs": true /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */, 43 | "checkJs": true /* Enable error reporting in type-checked JavaScript files. */, 44 | // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ 45 | 46 | /* Emit */ 47 | "declaration": true /* Generate .d.ts files from TypeScript and JavaScript files in your project. */, 48 | // "declarationMap": true, /* Create sourcemaps for d.ts files. */ 49 | // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ 50 | // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ 51 | // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ 52 | // "outDir": "./", /* Specify an output folder for all emitted files. */ 53 | // "removeComments": true, /* Disable emitting comments. */ 54 | // "noEmit": true, /* Disable emitting files from a compilation. */ 55 | // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ 56 | // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */ 57 | // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ 58 | // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ 59 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 60 | // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ 61 | // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ 62 | // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ 63 | // "newLine": "crlf", /* Set the newline character for emitting files. */ 64 | // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ 65 | // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ 66 | // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ 67 | // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ 68 | // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ 69 | // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ 70 | 71 | /* Interop Constraints */ 72 | // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ 73 | // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ 74 | "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */, 75 | // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ 76 | "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */, 77 | 78 | /* Type Checking */ 79 | "strict": true /* Enable all strict type-checking options. */, 80 | // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ 81 | // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ 82 | // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ 83 | // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ 84 | // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ 85 | // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ 86 | // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ 87 | // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ 88 | // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ 89 | // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ 90 | // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ 91 | // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ 92 | // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ 93 | // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ 94 | // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ 95 | // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ 96 | // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ 97 | // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ 98 | 99 | /* Completeness */ 100 | // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ 101 | "skipLibCheck": true /* Skip type checking all .d.ts files. */ 102 | }, 103 | "include": ["src/**/*"] 104 | } 105 | -------------------------------------------------------------------------------- /typedoc.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://typedoc.org/schema.json", 3 | "entryPoints": ["./src/index.ts"], 4 | "out": "docs", 5 | "cname": "webuntis.noim.me", 6 | "githubPages": true 7 | } 8 | --------------------------------------------------------------------------------