├── .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,H4sIAAAAAAAAA8WdX5PbNrLov8qt8eusV/hDUfJbjuO9J+fsJlnbOalTrlSKM6JntNaIsyLlsSuV736LACE1mt0ACGp8nzbrAdFNdANo/LoJ/XF1aJ7aq1cf/rj6tN1vrl4Jubq+2lcP9dWrq1/rm1/23ba9ur46HnZXr65ud1Xb1u1f3R9e3ncPu6tr9+9Xr66u/rw+9bSQ+tTV+//9+c27cD8vXBvQ2/XVY3Wo9x1U5ixBLvRZ2dtm/7k+dKbN91VXR4QRzefLfb99mCJ3aD5Lbq/7+ybBTC/IB5JlF0JC0W13ON52zSEuE7RMFua5Tnt73zS7iKBTozkybqq2XuokSaemefKObX0w/xmWBZrlyXms2vapOWwickCzPDn9cPQCwmLOrfKk3DbNp20d93PXKk/KNjZa2/xxauu23Tb7H/Yfm8ND1W2bfczbqAfyZFf7Zv/1oTnGxg+2y5T0ZdtEpQxt8ha/XXPXHLuIiFOjbBnbmHlcmzwJd3X396qr2+6dWVO+1lVsPaWfyJZ+7iVmrlHjbJk/1k/tr9vNXR0zH26bLfGH/U3zJS7MNcuT8/vtfX376bvEOTZufZbatZu/bNu/PB62n21ckq7DzXG727xOWiVx24vIv6u7//r1fUzyqdUlZH6udttN1dXv7EIZkT1uPXPm/vDw2By6hLiPfiLT17rtQ91VN7v6bf3vY93GZhLV/hJjf1d3Pz3t37vO/9Yc3jeb6mt8JJinsm2RocJF5aP3mTwAl3r3aa99wTd+W+3vEmYA89RF3j1RhYvK/+lp/7p/Nm8KsI9eVJ88VS6tRbqDsI9m6/OfzUP9a3P41CaNBmqdH1sdb/5V33YpgdW55ayZcHeIHl38lrNH9Lv95u912zb7hLckn8nW4M2X6qFN9yvc/CIrzq91/WnagjM8calVN00B+qH8Eair2/s65cAAWubPou64qfdJs+jcMlva26Z5SBDlmmXLeT08GJV0bpgt6/v6sTp0D2mD6DeesT70YfbXpFXh1HKGj1TdsYe4VYqXgLb59jse+sZTju3UI5nR/yEt6D9cOtb/7qat951dvOMvjFpnj/XPm48/fZwmmnokWb4Wa60WZ5j1H1V73mAems1xVw+Shr/QyQ+QRvlh3/UYd/drfXPshb2rbw9193cSLwXajpMtyJqTUi8xQUQ2ZhC33d/Xh21Xb+BAht4xN2ETVTGYw3lGbcnj/iRtR8f/59E2lBxK1ZfLF83XODWllKJpNMuUMYi/N91j3ix9AR+9nD79NmKA3d8OzcM7938ytGM7Stkp0jRPSeFFFSWyevPdLj3xl6gfkQu8sJZsujCqIZlBvLB2bJIxqh2Zd7ywdlxqMqocla28sG5cQjNhxWO5/aV0I1hCVK3ts1oyIXkan7HhfOqFNeZTrlFF6SzspfUjE7Vx3Ua52wuHL0x6N6oYkfG9vGY58cA4T3xhvVJSyVEtY9nly+scSkCnqMvlpC+vaSBtnaIok8m+vJ50sjtFxXH++8LahdPT8ag1MWP9fy6tNpPVTgqzw4nuS6saS4ZHVQ7lx59rxQqk0NNXLCarfnmdkxLeKXpHc+CX1/0yin9rrWPJ9YzB/objPHuIv+3o0vm0jCEeZ9qed5zzFf/WWqcXDSQOe0Idwbd5i4u9wP8H3Wc5fkLNwuXfIljWkKI4W+nwDHE+VwyRFORT9RHPs5qQJRSpi8ioquL5bB4ovJhieaYW4/J6h8s1UlTmKzied18hayymbiujmotn38ez1Y5VizzDaHMFJUmjTNWYPMP6xZWhJK1fVGXK5XWki1dSFBzXs1xeO67kJUU/qgrm8hqGCmVStORqZ55jF2DKa9LWfqLi5jlmDFuUkzZnyDqdZ/DLaClPkocGq3suDaS4AqA4kYrXBF0aScXqhlJGly3lubyu8WqjFIXDBUiX0HpdiuJcPfHdsbuv9932toL1E93Xx7r9q/e38AfbxKffVn7fB1t2dW5yqc/BUY8TS5EI1ed+Mo4Vyi48ytMt+HlRSLdJPHSybimfnjPa5RQVBfWb+nk6oVfyh+qkAihT3bdIFn5qnlJ+k65FRa4KMWXwU7N1mvBRLdYl7fPauFS+kouTTBdwXWb6pNZxsbpNLt8KbuoT3Dp8E8PIradVb03VI3JbA63NxFqtdJ2iNzpgfSZXZqXrEr31AesyuQ4rXZfYzRBYlalVV+maxG6PGG8J02qs0jUJfKaFlZhAFKdsT8m3UIy3quz6qQkbV/Qr+tGmNblaaoI2wdssRppMqo2atHGGbrwgds4plVDpeky5swIrNae+aJKGKfdaEMrlVBNN0ivh7gtCrYzaoUlahe/HIBSaVik0ITBKur9iFA5doi5oWvQWuuSCitZmVAGlK5Z6EwZWMLfmJ2PNSLgtg10zMip8Jmk46SYLQstZ9TyTNJ2l5jfSMfVGjPhAPv8Y5g7fNxm58Afu8eGbli/NHsPJan4jHaff0EEP6cwam9k6z1X322ma47Az62cm6Zx0GwihZla1zLSoNXZjCBWyTq2NmTzDg7eKMBN7UiVMlvUSbh4J2DCj7mWSlmm3kxAK5lW5ZK/awQtEIov2pMqQOTvgVCXnVLBMG8nYTSjUCE6tV5m2gsRuS6FWkKnVKZM0Ct+oQqgzrRZlki6xW1cIbaZWnkzSJ+VmFkKnnDqTiWts5PYWcmWdWFUy0a+jN7yQnj25hmSaPyXfAkN5VnbFyARUEbspZsQqZtaHTBq+pDtdiJHLqquYpFn6lTOEevm1HxEd/ZKPX97+3SxOqNrD/fOUy2KIso9/vmVf+Z9vL1XmMfSUWd7xz7ezyzqcArPLOdJ0CXIvSpcs2BXVJaVsA2kzp1zD02dqmQbQI7k8gx+AcPGBkzXtW+241HjxgZOcX3QQkD6x2OCky/MUGXiKTikucHplFhXE5EaKCXzpmUUEvA7R4gEnP7togJcdLRZwsrOLBHjZseIAJzq3KICXHCsGOC88eUUAvOQATXFCM+BJwL/Tk/0nL5+f5Of1iSf3nRr5Sf2A9GAy/yQ5K4kf3HhCyXuw8+Qk7Xm5U5L1TolLJOmDGqUk54Eyc5LyQT0SkvFAjRlJ+KAW4eQ7UCAv6R4IDJKS7adw4JJJ9nC0Ekquw+jkAkl1XpHUZLpTaG4SPWEOJyTPR3N4RtI8qNGkZDnQ6iJJ8qBmWWo9s06pyXB+oJ5vjKYOz7OOTDhfwg9PXrokeYyS1XpmnaYns/0hu1ASe7KOueo9v2ZTHO5CSeqgjknJaaDWrKR0OEqLJaNhiJabhI7OwGDyGU28rKRzkjUSks2ETWYkmYNapSWXgULzksrJq2QwT8ssklnJ2Sk7SqpSl0gah0cqliyGI5SbJA7P6FhyGM7o3KRwUINwMhiIz0sCB2XHkr9Aem7SNyg/JdkLdJiT5I2saZHkrreSZSZ1I34YTeZ6npidxA37Q3LyFnrG7KRt4OgbS9aezr4XStIGhycp+wlGZlbCM6hJejIWqDM/CesjRCJFevrxzuDH8V6rSyVOx51m5lD9d5ibTiXUmp1ZnaxhkI9ENMziJFM0TEm98jrOycJyWk5NyNLaJedmkwYrnKYlNEj7TDxJdjxZS8jPz9um6TQxhUtp+DzZXE79KYldQtvMHO8EbSLpXlanzMxvkmbRJDChVXY+OEmjaGqY0Cg7S5ykUSxhTCiUmztO0ieWRiaXy7yMcpI+AVpCqJLBS9LmWHrKmZpp87PPSVrGE9GEcvk56TSdgulpSp+sTHXqNhxKWtP7cE7+OkmbKalsQrVLZLVT9UxJcNMqzsl1p2qXkPamlZuRAU/VLZwMp9XKy4unBV5JKXIq3Lpktjw5RgwlzpmY8AI59CT1UtPphJpzM+vTVpeEJHtodZmRb0/Vc1Lqndb1Iln4VH3nKvvtNE1N0ycN6jcZzxlD+a1GMZylShrKvLxVznjmKPvtNJ2e/meH90KVAHM0v4DS31TfTEe+UNVAquZJBQS0srNqCZJj41hZARMY51YYTFkFgsUG/OTPqjuYas+EEoSwVWdUI6TqmlaYQKs5r0YhZ60PVgbEl/qsCoHMPTRD1UvUMySPaqy0gRnN3CqH5LUmVvDArDW5tQ+peoXLIGil8ioiUjWKFUfQOuXWSaRqlVIyQWs2p3oifWWOFFJw63FmTUW610fLKzi/z660SPaz5KILxuNm11+kwZRYKQZFUy5UlZE6lEkVEfQozqqLSNUvvWyDVnJ+BQerabE8KWo96X+hJ277X2D4WN3W7V/Pf02v3PAyeHxfL0ZZO/gKQK14oicgBAWm00S0XWVrDlIkwca5Auv9JlXcuekEYdDw/6jbtrqr258+fg/Oz0Ci1yDd/LRdxp2FTOPrxlnHnogSpZ1bzxDZ1V9S5Q1NZwjbtm++PFb7TZ08oPCBGYKrrqtu7/2oIyzZf2KaaOiTRGYGCD7/Nd0b269tVz8MshM6fYEfoF8GKMoIfohNLywYP5Ar+NC2v4D6gIDEU8sJoqgVpN5vanL38Bqk26wv6fgh6PTnDl+cGoe9zurICNxs28dd9fVHZuMipPpPzBC9fajuasZehFzQfIZQ8+iEt4Xtp4mF7mLSlA/8TIR/T3eWardrngbx39e72qu1CPX+gnmSfkFPebYUZt/V++7nQ/15Wz+lKTF6Jl/8fdV+F165x+JHz+SLp/f9scjAtp8kph1M9rauUiWiR+YIf1s/7sjlnBLrGs8W+F3vqnQcwMo9P5Mvnl/ax2JPbWeJsyW+W3ppIoXCJ2aI5oNJQmoklmQEjhZDVtqEaHt/2zxs93duAQ53+YJoHngF7sh435iTTkWeU9wf554Y/I6CB7mTPlNPpL6I8IE0JmTX7O8SBYGmmcKaQ6qsc8t8UUkGcu3SxUCfQoQEiLF/metNoJeQKw1qcHEiczaHnW9Ch/Jw94YgcGsflAEb5giq95sUMedmOUI+kTEt7P8TH8aGu45bId8Gx+jgHzO7PjSxrk2LnK53LUcmYPenVnki9seHGzoY8IWc2uWIqW677edt9/X918eokVHbHHG3zSYqZmiT0/12/zFq9KFNnrPetN37BMvDhpkrU/dxV92R4QVamU4NswTdRSXcZXZ98+lt/VAdPsUEgHZ5YlIMcmqVKAJulX09w1NDv4j724Tzc/jI6HWYxPlO6rFT7uFxV3f0acYXB5vmCeN2bF9OcM+OijjWHLNHUk4N8wTRgY0vIxDaxLrfGZ+juZsvBLTME3VgJ6Iv6BCeiDEx3J7oCwnuiYQI7yTEnxqHP80+BYFugmegQZOpJyDYfficEBbQH2k4ougJAQ2zBFU78/PwXZ0kDbfOE9nHGQmyXLMsIR+bQ/262TV0GhbKgS2zRN1Ut5/SRMGWqaLG9wTABEi9Pz6cE9/RxCKcavtNtJ8Xp1a0tlAjUso/mhQpp1aZUt4f6zZBzLlZppxf680+SRJsmPtO98dD0kud22VK+tthmyDn1CpTyruqOx5SPO7cbookuIv0h+xf9ltyG3F/m19+4fUUXutPCuWACl9OHFXEhAVghS8qhisIQdgKXv0zEtT/Ld0KGzrp63X0IuQ2J3W44GZ4GzJq98XApsnC4ND0pcmUmP7f5wY4pz5C0Y1RgPOPL9UDRwvOnYNW00VwU+rcfXA6hbpubamt/0MmpAjUcrqoqm23d/t6MyoKJsURrfNswx2OfNsET0bhAQysRXD0YutQ8D34NQi8RmT9Cb4Ff5AA7xBJO4UEdLhCnZTQUQXpqSIOXlE32f9hVMOdrj99loO6B85xoa7vDhUNAc99uyZJnY8D4De7umcm3irlhxOgRWpA/Prv3717l9jdC9c4HKJAPekg7813r//zzdtUqefmM+W+++U//uvN6/epcs/NZ8p9+9NP/0gVOrSd+6bvf/n+zY/pb3pqniMX7vG/1jdDI2oqnP+avt93zKaM+nrRhbZloNaksAILCQQXcRHN4Y7mYliKa5gr6GHbtts9ycCxqHPTXGE9q08ykGs4QRDtWVzNgN8i3cPqFIc9Z6jPzWNvghLVy6JQ5/f5/fe4Z4+Evjw9lSL7ZR02Xb4CLzO0cLpnTkBWlaQ5OUERLkxPUCUYv2cpEwKiCQpFMWmWUkPZ6oyB8nu4oGonUjtDOdzHBdW7rfb/s62fTl+UZmpIdHNBJesv5v13/12T8CFBP7+HC6rWR/6vq8fqdtvl6oa6uKByU5bTvu1fhCc/8ikWvadkLqHbzcWFJ0Q3XpUXinDmK5AU9ZxVoCKf+UokRENnFcYRUZYCKEr67ucfYusLbDKXvFHdReJkT8OMPDIpMppOniT4R7ZKKSD8x3DJ0iQFXjMlRQHxr0MVRpOEc7UnAeHBQpRE4Y/1YdtsJgn3Hpkl/L5qf54uHz91gff/gan2Crz/D6Hir0nCI59+BHRI+QAkUZVgQRqpQrwyLVE0V+lDSg0W/KS+awg80+8aZdCJogM4mhQcI9OpYu0mlu5f4IGZQ21yEP/30BwfJ4y299DcJWbS5D63nyWWq04lZQbLVFMF1rvdu0AgREgFT8xbxQ7b5sAfDojF6/zALMHbdH/e5njyBHhESUwAR95j2zZ0BHvXL7fb7sh9JRlW4iXRRapWkSNYO0+x9nmU6qr9pjpMiJ7PCp0fvZAy9WcedYY0cc9dSI2Eg/xYm9Sj+4TN4HVznDAc6KF5i+TwcPISeWo/S2wog0vKjSZz04M6/8LChJhufFVhhmA2ocz62KUijQnvGiuP4AV7xUfWVmQtj/3T3NM+7CZ0yneaTK0ug92Hi8vCAvrqVo6be0JAwyxBIUDvCYqi+IQ3YsttR68ULreNiApW9nqi4pW9Y1Fe5bv1e7KC2P5pduU76CZY+T5ownT+iWbhXu+fArQ70j1bWA/7DxfWhwWE5oMnJDofIoKCFfxQULyCPyzojr1UwBNzF75PYCwEumd/WSQlof/3uY556iPklUaBiT5z7jjoMKGuQ0Y8dx+1YEhE9AOMs5y0ry+CwthPL4CU8HcXoe6DS/NZQnxdDgkJLspnIfEVGQmB/v7f5gFKgP3LXJ8HvYS8flBjot/DzoOeH+4+5PtQRNT7w2J4l4RCIk4ZFhF0Sygl7phhQUHXhILizhkWtIm71Sbbr4aDjohJAO1miJGJYmS6GDiTzxf3UnLOf507o1FPoVkNVJo4s7GQ4OyOiwnNcCwqOstJcf531rst8xnH8Kf5H+XAjsKj47TJGBpPSHRcIoK4r5CBiOBHyMHOgxeuejLit61GRAWuWvUExe5ZHYuBPmTWzDf7joFl4M/pvhTcHXCPCVsE1DFnnxiJjG8WtEjvPqCWHzX3t/Qh29EXV8B+XuwCd1a04eFp7uPdmzZ53bc38e5Nm7zubxIG5yZ/cGrymjO/+/rLlO79CbapA64C/pzuLbfVvk9r0TdU4B5fwNact5915FbVw6G+O+7oS7hHImHrKSJ9gDO6JN87ibu/Tplkfd6AdCbU3YtzU+5wf1IukBpNEuUaThAEh8ncv35Li3J/Sx+iKrW3F6AlrflJsbCot3Xl/WROTOC5fZ7Y+svtsa3tsKa856h9ntj2vnn6Dr7B63vvJ3hY+fyD+Yq8PtRVVw8t01TAjyQLJ3w1IHHuoQF2EwrznCY5YZ4nIx7mRUQFwjxPUCzMS3kjriZn/EbBapz4GyUJihX9RMTcGo9MGjuvaZawXdV2vzxyJVyeMK/pjDfb/NLSEJp4Ndc2S9zRKJsmzm+bJe5gVjG6/NaTBRrOEJQoJlcI99mvJyL45W9EgOnzcHzsC0dC29Ww5KHWef5X7d9s6Ns+fN87tctckkyChIMCaFGCbfOWJbCRx9cmv3Ge5do3ppeEjQq0nPFuiW81QYh/xwYnw/5l7tYNegnftGHUmDgdYeeR7/BD3dsB5C+MOAvxWuaL4t11LCzirWFxQWf1TBP11bAg/rczoJTIj2bERXA8EwsJIk0n5rfrq+1+U3+5evXH1ef6YH7j/NWVfKlerq+urz5u692mvXr1wUHU2+bBUNvfhr/9T33bNYe+hW3y18XV9YfFdbF6uVoUv/12/cE9Yf5g/sE0E1fXH8S1Ll6uhPKaCa+ZvLr+IKlm0mumrq4/KKqZ8prpq+sPmmqmvWbF1fWHgmpWeM2WV9cfllSzpdesvLr+UFLNSq/Z6ur6w+pa65dCS6/Zymu2vrr+sKZ6W/vD24+2WJADjAxhLEGbwreF6MdcyGu1eKlL1NI3h+iHXZAGEb5FRD/ygrSJ8I0i+sEXpFmEbxfRj78gLSN804jeBII0jvCtI3oriBXZ0jeQ6A0hSBMJ30ayN4QkbSR9G8neEJK0kUTzxUwYSc0/6dtI9oaQpI2kbyPZG0KSNpK+jWRvCEnaSPo2kr0hJGkj6dtI9oaQJflGvo1kbwhJ2kj6NpK9ISRpI+nbSPWGUKSNlG8j1RtCkTZSvo1UbwhFrmoKLWtmXaMXNt9GqjeEIm2kfBup3hCKtJHybaR6QyjSRsq3keoNoch5pHwbqd4QirSR8m2kekMo0kbKt5HuDaFJG2nfRro3hCZtpH0b6d4QmrSR9m2ke0No0kYa7T5m+6H3H99GujeEJm2kfRvp3hCatJH2baR7Q2jSRtq3ke4NoUkbad9GujeEJm2kfRsVvSEK0kaFb6OiN0RB2qjwbVT0hihIGxW+jYreEAVpo8K3UdEboiBtVKAgwUQJBbUuFb6Nit4QxZJs6duoKLmoqPBNVKy4uKjwLVSsucio8A20XHCx0dK3z1Jw0dHSN8/SmKe81uXLlVj7LX3zLI15VmRL3zxLzcZcvnWWBRd1LVEQt+TirqVvm2XJRV5L3zbLFRt6LX3jLNds6LX0rVMu2NCr9M1TCjb0Kn37lJINvUrfPqViQ6/St0+p2dCr9A1UFmzoVfoWKpds6FWiSLtkQ6/St1G5YkOv0rdRuWZDr9K30WrBBlQr30YrwQZUK99GK8kGVCvfRivFBlQr30YrzYZJK99Gq4INk1a+jVZLNkxa+TZalWyYtEIHohUbJq18G63WbJi08m20XrBh0tq30VqwYdLat9FasmHS2rfRWrFh0tq30VqzYdLat9G6YMOktW+j9ZINk9a+jdYlGyatfRutV2yYtEbn1jUbJq3x0XXBxkn2b7CtYCMl+zfYVrKxkv0bbKvYaMn+DbbVbLxk/wbbFmzEZP8G2y7ZmMn+DbYt2ajJ/g22XbFxk/0bbLtmIyf7N9DWgAU6dhIj6GCiiPV1oV4u1yVqi+xm4MKSZEoCgwdLHmgVkNkMX6BHAaMHAxjoQcDswRAGZgyQzQxioF0X0wfDGJaCHgJkMkMZuKFFJjOcgQEgiEEIQxroeE5ITIokG9EJxCGEoQ30rEQgQhjcwGiLTGZ4AxnXCYQihAEO9JRELEIY4sBgNUQjhGEODFhDPEIY6sChNWQzwx04uIZsZsgDg9cQlRCGPTCATWG+p3jEhsiEMPyBQWeITQhDIBh4huiEMAyCjuEE4hPCUAg6ihOIUAjDIeg4TiBGIQyJoCM5gSiFMCyCjuUE4hTC0Ag6mhOIVAjDI+h4TiBWIQyRoCM6oTGZ1WxMJxCvEIZK0FGdQMRCGC5Bx3UCMQthyAQd2QlELYRhE3RsJxC3EIZO0NGdQORCGD5Bx3cCsQthCAUd4QlEL4RhFHSMJxC/EIZS0FGeQARDGE5Bx3miwEy9YCM9gSiGMKyCjvUE4hjC4Ao62hMIZQhDLJhwD9EMYaAFE+4hoCEs0WDSAMhuBl0w4R7CGsLACybcQ2BDGHzBhHsIbQhDMJhwD9ENYSAGE+4tcTZkyYd7iHEIgzKYcA9hDmFgBhPuIdAhDM5YkmkJgVCHMEBjqci2CHaIkk8jItohSjaVKBDuECWbThSId4iSTSkKBDxEyaYVBSIewiIPJi+FU1glH5Yh6iEM22DCMsQ9hKEb9OxB4EMYvEFri8iHMHyDDssQ+hAGcNBTB7EPYQgHE5Yh+iEM42DCMsQ/hKEcTFiGCIgwnIMJyxADEYZ0MGEZoiDCsA4mLEMcRBjawYRliIQIwzuYsAyxEGGIB5fTRGYzzIMJyxAPEYZ6MGEZIiLCcA8mLENMRBjywYRliIoIwz6YsAxxEWHoBxOWITIi1nw2UiA2ItZ8PlIgOiIXfEZSIjoiF3xOUiI6Ihd8VlIiOiIXfF5SIjoiF3xmUiI6Ihd8blIiOiIXfHZSIjoiF3x+UiI6Ihd8hlIiOiIXfI5SIjoiBZ+llIiOSMHnKSWiI1LwmUqJ6IgUfK5SIjwiBZ+tlIiPSMHnKyUCJFLwGUuJCIkUfM5SIkQiBZ+1lAiRSMHnLSUu0pB85lLiMg3J5y7lqFCDz15KXKoh+fylxMUaks9gSlyuYUjIUtPVIshuBoUwr4bMZks26KbIagaEMC+GjGY4CPNeyGYGg9DuiAiJtISEdjFESKTiM5oSERKp+JymRIREKjarKREgkYrNa0rER6RiM5sS4RGp2NymRHREKj67KREdkYrPb0pER6TmM5wS0RGp+RynRHREaj7LKREdkZrPc0pER6TmM50S0RFpCAgdaklER6QhIHSoJREdkYaAMAVkiI5IQ0CYEjJER6QhIExpGKIj0hAQpjgM0RFpCAhXHobsZggIUyCG6Ig0BIQp/EJ0RBoCwpR+IToiDQFhQi1ER6QhIEyoheiINASECbUQHZGGgDChFqIj0hZ7MLZAdjMEhAm1EB2RhoAwoRaiI9IQECbUQnREGgLChFqIjkhDQJhQC9ERaQgIE2ohOiINAWFCLURHpCEgTKiF6Ig0BIQJtRAdkYaAMKEWoiPSEBAm1EJ0RBoEwoRaCI9Iw0CYUAvxEWkgCBNqIUAiLSCh7YYIibSEhLYbQiTSYBAm1EKIRFpEQtsNIRJpEQltN4RIpOEgTKiFGIk0IGRJlotJBEmkISHL5bUqXy4XqF9ESeSKL8qWCJNIg0KW5XWxeKkUCgkQJpEGhSxXdFtkN4NClmTGUSJMIi0mYfRFdjMopKT9AWESaVBISfsDwiTSoJCSLpJGmEQaFFIq8t0QJpEGhZR0OI0wiTQopKT9AWESaVAIM74Ik0iDQkqy2FAiTCINCinp0mqESaRBIeWK1gHZzaCQktEX2c2gkBWZ3JcIk0iDQlZkFlwiTKIMClmRNlYIkyiDQlYkcVYIkyiDQlakjRXCJMqgkBVpY4UwiTIohJ4XCmESZVDIirSxQphEGRSyIm2sECZRBoWsVnRbVMdtUMhqTbdFldwGhaxJGyuESZRBIfScVwiTKINC1qQ/KIRJlEEha9ofECZRBoWsaX9AmESJgN0QJlEGhdDrukKYRBkUstbXevFS6RK1RXYzKGRN+xnCJMqgkPWSWtcVwiTKoJA17TsIkyjJn98UwiTKoJA1ubcohEmUQSFr+jMAhEmUtGkA2nkQJ1GGhYgF7T0IlCgDQ8SCdh9ESpQlJQvafxArUUNFCb2gIFqibEnJgrY04iXK1pQs6GUCf+Zii0oWtK3xly6WmSzIzUDhj11sWcmCXilG37sYC4oF3TOyoC0sEbQF8VcvtrJE0BbEH74YPiIEbUH87YshJELQFsSfv1h8ImgL4i9gDCOhYw+F+Imy/ETQ1kYARenABEQARVmAImjPQARFWYJCrwKIoChLUATtRQihKItQBO1FiKEow0noWFAhhqI0H2MqxFCU4STc2yHbGU7CrPWIoSjDSZi1HjEUZTiJkAvSdgiiqMIeDgTdGNmusLajv0tDGEUVdtujP01DHEUVNo1KzxAEUlRhDwj0DEEkRdkPZiTt9QilqMKaj/ZkxFJUYSvy6GAIwRS1tJsf7Z2IpqilpZd0OIRwijLIRCh6jUM8RS3tZ7f0GoeAirKf0tAOioCKMtCE2YQRUFHLZWATRkRFGWoiFL3QIqSi7Ic1inYjxFSU/bZG0W6EoIqyn9co2o0QVVG25oReBRBVUfYbG0W7HMIqyqATxiIIqyj7oY2i3RNxFWW/tVG0eyKwomztiabdE5EVZegJ4xiIrKhyFXAMhFaUwSfMsozQijL4RGhBbiSIraiVrYiVdGNkPgNQmI0EwRVla1A07cqIrihbhKJpV0Z4RdkqFE27MuIryvIVvaTfD5kvAFgUAizK1qHoku4Ymc8Womh6b0eIRdlKFE27J2IsypaiFPT+hyCLWvNwTCHIogxIYQ5GCLIoA1JEQXscoixqbS8uoD0OYRZlUIooaC9CnEUZliIK2osQaFFrW0dU0Grgb6/XoVHGn1/zRz+NUIte8GhTI9SiDU6hwyKNUIs2OIVGThqhFj2gFvLlNGItemGtR24NGsEWvbDWI5d7jWiLXljrkXNEI9yiFyveIBrxFr3gvybQiLdow1Rot9eIt2h7VQjt9hoBF21vC6HdXiPiou1nOwW5BGiEXHQAuWiEXLS9NYQ+ZGjEXPRwcQi59WkEXbS9O4T+Jkcj6qLt9SF0AaxG2EXbG0ToCliNuIu23IWu4dAIvGgDVwSdhdCIvGhDV8SS9nyEXrQMHNw1Qi/a0BX6+KkRedGSDz01Ai9aBkJPjcCLtuBlSc9VBF60BS9LMtzSCLxoC16WtDMj8KIH8EI7KAIveihWoR0UgRdtwUtJOygCL9qCFzonohF40Ra80FuJRuBFW/BS0t6MwIu24IVOoWgEXrQFL3QORSPwoi14oRMjGoEXbQtXmKUZ3z5iyQudRtH4AhJbulLSjoTvIBlqV+j1Fl9DYtFLSXvd6CYSzUfBGl9GMpSv0F6H7yPRgfhT4ytJdMmv5PhSEgNYmMgBX0tiAItYMQOH7BegLxrRFz3QF/peGERf9EBf6KthEH3Rlr7QEbNG9EUXgR0QwRdt4QudCdMIvmjDV5hBRuxFW/bCDDJiL9rgFW6QkfUseqHzcRqhF23RC30s0Qi96CV/dNeIvGgDV5ihQOBFG7bCvB3iLnoZij4ReNHLggd4GpEXbckL45yIvGhLXhjnRORFW/JCZz01Ii96GYg+EXjR9lsfepARd9GGrTCDjLiLLgPgUyPuossA+NQIvOgyAD41Ai/aghc6A6wReNHDPSf0DoXAi7ZXndB5XY3Ii7bkhU7sakRedBkwHyIvehUwHwIvehUwH+Iu2ha10NlljcCLXvHYTCPuog1a4ZRAxgt8/KMRddEGrNCVNRpBF23ACl1ZoxF00Ra60KlzjaCLttCF8WMEXbSFLowfI+iih+9/6FASQRdtoQudateIumhDVgSda9cIu2iLXdb0MoSwi7bYhc6Ka4RdtMUua3ruIeyiLXah8+IaYRdtscuannsIu2iLXda03yPsUizs9ZGkbxSIuxSGrUg6210g8FIs7CdcpAULRF4KQ1fkgsxSFgi9FIausI3RBWuGrkg6NV4g9FIs7G0A9H1sCL0UC1s4TV/JhtBLsbB3RZHmLhB6KQxekQvS3AViL4XgwVmB2Eth8Aq9whQIvRT2whRyhSkQeSlsrQt5vi4QeCkseKHP1wUiL4Ww1iN9uUDkpTBwRQralxF5KYS9zIH2ZUReCmHr3mlfRuSlEPayL3I1KhB5Kez1rXTev0DkpTBwhc4kFAi8FPYOV7pGoEDgpbDXuNJ5/wKBl8Le5Ern8gtEXgp7mSuNzgqEXgp7n6sgAWyB0Ethvw+SJCwqEHop7K2uXGNkwMA9KgUiL4WBK4xJEHgpDFuRknY5BF4KJQMDh8BLoVRg4BB4KQxboYswC8RdCsVegFMM1MXc/P25PnT15gd7A/iHD1e//97/ONTV9R9Xvw/Xgvegx/TZ3xDeZwpe/fHn9VXPeV798eef5xvB+//Xi/n95rjdbW6b5tO2/02hc0dSnfuRynazWtj/7W9Wsf+x1vY/+i9NGAG39/Xtp2rf7L8+NEdfhAQiJPf8Xd1Z9T4emofW/R/YT7E697McVBW2w+ur/lN5q6JSARH/euo81TRQbXjHlXBvv3ZvXwxdFwuu66Z73DV3272nbwn0dWpK13tZOn3ZITnU/z7WradwAcaycH0unJmWy6FPNyrSSqE677YPdVfd7GpCigSaS66Hyv22EnhuAUzU76zBJw/uh7VgBxp2UIQ7QI8q+OjgH338wvQx/BwxmFNSgDnlRrc/0dv/KDlDma623dfRJF2AcewL9pjHd7vm6aFu2+qu3tS7uv8pFdjNCrxYX/rM9TL8srhp6r3WAr7W4H96od37sSNETWYBDCSGLkrniMo598o5t2bHrG23d/t6M/zOii9DrYHGJesFXVfd3j/gp0u4oq2HpUwp1hmP3X2977a3Vdf/PCeYakCJ/vo7+2aC7efLtvHfAqzQYvCh0i1byg3Zyi1bmlu2+t8OvbW/HQqNCp2icLY8O23pnNb9ac25303V+sss0Lvgxr5/qP+pC8/bwfsOw750K5Ms3fsqt+ytuK4/HeqH6vDJ61vA11VL9lH7UyvwQQ0f5NayG99yazAEes2NAfiZS/jsCj7LveRtta/NLxmBBUws4drOucNttf+8rZ9Oy7fvcqALzS6fvdZtZ34pBjxbAvtpxS0z5ndL/AUBRiIrVubweyeeRDhUOigRL2orASc5Nyf7X7r0XXQFvcEthezKPPxSJtxk4P674BY381w9/AQr9I0S+gbnjGayU09DA61Yz2oeHnd1h7wS+lZfecw9u2+7w/EWr4TgnYd5fVoO3bwuXYwUGMx9V++7x0Pd+69nTDhHA4Nq4uL+5+C65rjvtp5lQBfDird0UaxYOh1dFCt5hzEyTO8bNEFgFDsEF251cxuhcDuhZKMmKKHb+k4N17hBggtB3QYi3A7S38TASRgF+ULArWwYHte1dG+xcsPD7pXDb//hgVmAnbIQ7ApgHz6aX/KDy56AT3PjhoXCZVa5oFppN6UlF9SYn6+HoQb0vSU3ppv6sTp05p/gwyu4gJXsw1t/Lq7g7sJGN5tt+7irvuKFr1zDhc/t8gtut9gcR+bqPxsAawE33epdPXrfAg4Wu34NT/oeuIRLHzs76v0Gq7sEYyydi2oXYPbojO0Kz6+ekgGnGWaScqcm5Y6QfVnK0DvnRvVnPDYl3HrZ4Kb/WW+4pMOH1lxYU3+pHnynhat5yS24/WMj42s4BGVI4ug0o6DfsIch95N/8PwPZBYuEizYBcz2MFpl4OmsYOMT+3A7/DIdXGbg0VQOy2nBHr5hPzgGgPvhgh9Ccx7bfar9BQdSAc0+/bE51ETQD/ag/uuPIeh3i55zZO02iT7BxPc/OijCoErrteuem6wfD9vRagq9a8nZ6K7eb/xNQMAzri44re/6kLcPrHZ12/qH5AKe/t0pa+H2OHcckm7SS3a17jnUOFbV0Gou5nGQQbhwSLpwSLLTo+/+eOjDu/b2vml2X+vKj7W83dAJchIdi5Bux5PsFL6ru/Ou5b8L3LXc5u9wgHCkqr+faxDBbS53ddcvFO3H5nCwPxEOhEBfGpR125Uo3Gu4YEQuA69x3+x6R/PfYQ0XTvcOLkhydFCKtXsHbiE3/T/UT83hU7XfWL/yJcGoewgp1y6idAdv6Q68csnt50BQP2Be3OctrIMI51vOCaRzArkM+NZ2f9N88dc9sH0OXZVuwJww4aRJHZgWu6qr+5j1sTl0eGuVYEdyu6c76gtnf+EcQBaBV7Bi6NkBF3Hnn6UzhnIu7KBPf48TK2ZfP7VP281d7R/CgbXl0I0LC4RyktZujmhubbyru+ZpbxaS00kdWx2emwfPXbuRcj4tnU/LJbfNM6JGM9ILGoY3cS8iCjdkC4eQA6s3Ka9r0GbgRdaDaZxlhFt3pFt3ZGjRb5727CjCHWfofeXspF3nC/d6LNAaSxkPINx9h4Fz+K6/JH0Q5XyD3TfHokZjJ72gdxDl3Fs7o7m0jywCKzQS9VTXHlnTcOMdJuzarQ+Fm6/CrQ/sCemu7h43H5uP7P4Mwb57E7dqCscJpUvCyDLgDoemefDXaBhGO+M7QwgHAqQDAf09S2zn54UHMUGw8gzj41I4wk1f4eav1IH9zAaVm6qrvAGCQbIbFzcbxfKELtzLsAdHI2EM1SGm007fhVtvHGiQDjRI9ljR93+8+Vd96/ev4IF8MKdLGwrHpaVLVcllwGW7urq9r/3x1xDwuG7dtBaOxEhHYuQy4D/9fLg7+KdyL+UwDIbL+wkXUEoXUEo2aTJ0Ty5VML00DLw7xwpnEulM0l9QlyRivE55rHyQ46a0C16lC15lEZjS4UXKQxGDHDdkbi0RbjGRReKQjVYosOy6pc+tfKJwU1o4D1uyy+6hQjQYrn1qxTnkfdUymSYIchSLYO6rdrv/2Pg0BIavLI28r9rH+rBtNqOsxhLOBsnN1CFw9gMoKHnFhWEuUvW5EXxdlnAg2AWM50xUulnrnEY4r5EuwJYrZ063lysXKCg3xZVbCpXzNeWiClU6IOgCHO2WCu2msHb9aDcl9CmNtjydqF1jFzJrp2rh9q6CNd/2obrDSbIVjC/Z7NAoil97Swe3e5nHhkSyJxQGLgvOW7b72+Zhu78bOvA8fQ17YJHbyM0hGFYsatnuLSl5qm8MFm/r20PdjcspYHqMWxtNX4fjY59DR/wHUhu2DGV7ONR3xx06day90yZrb5TdgusWW5SybQfc5SsLifqJRgY7eaz2G78XqIBkSdC2HQx+qCvv8RU8L7C5xG17qB933hqzgsdZlnAND5riB19vSNXVglvOMVeDTq7ZMP8TSlnDecXmDD4ZHuRLgwswGxTvqrY7Po5IJsyUF4IboHEMDdOHii1EsM+NUp9LD8hwo2ofRms4PHUrF3xpNtFj+9gfH24Q5FtClBJ57/GWB+0r2BHHqwZMdjg653KVwu0gwjEEyXLlXXPXHJFGoOfheZdhFC7uEi7wkprzrl2zv8Modg3Pzq5zVZz2MLernf7FBcDaoS/ttkC9Ov0HqwE66cCRXnMr5Y5McEMnW7Nj2RLOsYDLDbun7tqRY0Acr9ikwq7t8xkI5EPWyhZWuf1wBK1LWCXHVom4x5uPKBSDMaA8RUBrbo1/2Lbtdn/n5xLg8rU+TUz2TZo9Dgcht1MsscDuuYS0z/mXckdH5eCVcqdWdUpIuNONdqGadhG8duBZu/hQr07/wQ0KDfFKWNXJemFz77suBCGsFZrDnb84wgyeckhSOZ6qBRdtNYfxpIeOzK7Qj1XbPjUHX4nzg8PQu4SHcMGDOEXUbBGUPWswZx0Bba7Z+Mn2MT7vQPggOYtwhx1oGXbbezxsmwNej0q4HrEBqC1E9bdoiKvYA5Z9cItiN5iJYtWlatxgtMyi6wMKseFOwebVe2LmH31hKLE6bR7sizbNw231WN3iAYY12dplKDRbQDIidwKe7tVpq2I3S9PB8abttt0R18mK0mOZ7AC2LTqUwdS9XHOi26o7HkYJT7gCs4mZ9sZfamDwyMszINJbds+PDbPaLbDC5TWFO6hK9lMA229fwNl/2AxG4dz7YMflKU5yYtyKLNnRpfM2cP7LFedl9uDnPecVLnF7+3hrhpRDLQLP2XIylMxaezkFzpfbum23zb5f6w4P1cgZgS86eu5CMuESFcLRDclWP7b+jg95iuKNcN+YQjkPMMMNTglug2vvmyevPv/2HvNFCUO1gj2g9T3Zmi/yS4ES9sFaqKv2m8rf7PrLRcA0Z8V3lS0W9PwJhmyrU5XEqdyHtULf2chNoJO5Ly6UmzTKZQf0qdiEH/RRMS78zkM5Dq1Z8tJ38HFX3aGqP7g48esBmY6QCy8fwU5Zm2nwl3QvE3AqI+Ff3nQxLiz2MgpsAt893peE+m4CSzbYktfh8btDc3z0ow64s7HHiuFxHMpJeOwsWLBAfvwAEaFmw4D26IcBcHJL7mg95G28VR8CSZeNUOKEVt2/uH1FrdgJRySFBCxX1OwnDvyODndKtuzfPD6u/IeruDuPa7auvj2OD0dehp6V/rXt6gcCu8J1SrLnaDTxF16SmbP+kCHzpx08ibglTbOJyKEL4fseDPLZ0qLhUek/6lVIcU5CpfYErBVSp0MfW5mKLQ2DfOliWeXywModfApX/lKwJerd/fHQjrwALvNshEzlFIWXiWMTU/2jx73/MYjwig3YZKl7FC2c0G/ZRGh3rMfvCo8FbGJw/DEqnGrrU+7FLf3s9j5CMjDcG4zlDrLiFOG6BCm7zFnkSlSaw4832GjyeNiN9iIIHwU7or1A3wPgFy5yfeL43H7Qd4C3EtDD4NquzFO4wEK4c5NUJxHcyvG56nOCXT1Er974wPVq6NpNHuEyc8K9hSy4YOmpvqket8wHSnA/YOOhp/qGKj/XXo6em4nnh3FMI2CRqGLLrV0eCj4KHDwg1zx2+myy/7LQcyKIpdil3XUzmprwoM/mN9zTwwiMZioso1AsEHa9/Ns/xMEjDVse4mfxRmMAPxJkw9mnerMnFicYD9LlYr9dXz1uH+vddl9fvfrw259//j9ShM1cy6QBAA=="; -------------------------------------------------------------------------------- /docs/hierarchy.html: -------------------------------------------------------------------------------- 1 |
Generated using TypeDoc
Generated using TypeDoc
Generated using TypeDoc
Generated using TypeDoc
Private
Generated using TypeDoc