From f54bfe5f38a0c95d735e70ee55767b97aa50baa8 Mon Sep 17 00:00:00 2001 From: Christian Landgren Date: Fri, 1 Sep 2023 14:55:55 +0200 Subject: [PATCH 01/30] feat: create new admentum api hooks --- apps/api-test-app/src/main.js | 24 ++- .../data/schoolPlatforms.ts | 9 + apps/skolplattformen-app/project.json | 1 + libs/api-admentum/.babelrc.js | 6 + libs/api-admentum/.eslintrc | 22 ++ libs/api-admentum/.prettierrc | 10 + libs/api-admentum/LICENSE | 201 ++++++++++++++++++ libs/api-admentum/jest.config.js | 6 + libs/api-admentum/lib/api.test.ts | 20 ++ libs/api-admentum/lib/api.ts | 32 +++ libs/api-admentum/lib/fakeData.ts | 32 +++ libs/api-admentum/lib/parse.ts | 7 + libs/api-admentum/package.json | 18 ++ libs/api-admentum/project.json | 23 ++ libs/api-admentum/test-setup.ts | 1 + libs/api-admentum/tsconfig.eslint.json | 5 + libs/api-admentum/tsconfig.json | 20 ++ libs/api-admentum/yarn.lock | 4 + package.json | 2 +- workspace.json | 1 + yarn.lock | 28 +-- 21 files changed, 440 insertions(+), 32 deletions(-) create mode 100644 libs/api-admentum/.babelrc.js create mode 100644 libs/api-admentum/.eslintrc create mode 100644 libs/api-admentum/.prettierrc create mode 100644 libs/api-admentum/LICENSE create mode 100644 libs/api-admentum/jest.config.js create mode 100644 libs/api-admentum/lib/api.test.ts create mode 100644 libs/api-admentum/lib/api.ts create mode 100644 libs/api-admentum/lib/fakeData.ts create mode 100644 libs/api-admentum/lib/parse.ts create mode 100644 libs/api-admentum/package.json create mode 100644 libs/api-admentum/project.json create mode 100644 libs/api-admentum/test-setup.ts create mode 100644 libs/api-admentum/tsconfig.eslint.json create mode 100644 libs/api-admentum/tsconfig.json create mode 100644 libs/api-admentum/yarn.lock diff --git a/apps/api-test-app/src/main.js b/apps/api-test-app/src/main.js index 6bd0599de..514b50f18 100644 --- a/apps/api-test-app/src/main.js +++ b/apps/api-test-app/src/main.js @@ -14,12 +14,13 @@ const path = require('path') const fs = require('fs') const HttpProxyAgent = require('https-proxy-agent') const agentWrapper = require('./app/agentFetchWrapper') -const initSkolplattformen = require('@skolplattformen/api-skolplattformen').default -const initHjarntorget = require('@skolplattformen/api-hjarntorget').default +const initSkolplattformen = + require('@skolplattformen/api-skolplattformen').default +const initAdmentum = require('@skolplattformen/api-admentum').default const [, , personalNumber, platform] = process.argv -const isHjarntorget = platform && platform.startsWith('hj') -const init = isHjarntorget ? initHjarntorget : initSkolplattformen; +const isAdmentum = platform && platform.startsWith('ad') +const init = isAdmentum ? initAdmentum : initSkolplattformen process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0' const cookieJar = new CookieJar() @@ -141,8 +142,10 @@ async function Login(api) { console.log('Attempt to use saved session cookie to login') const rawContent = await readFile(`${recordFolder}/latestSessionCookie.txt`) const sessionCookies = JSON.parse(rawContent) - await api.setSessionCookie(`${sessionCookies[0].key}=${sessionCookies[0].value}`) - + await api.setSessionCookie( + `${sessionCookies[0].key}=${sessionCookies[0].value}` + ) + useBankId = false console.log('Login with old cookie succeeded') } catch (error) { @@ -180,12 +183,13 @@ function ensureDirectoryExistence(filePath) { fs.mkdirSync(dirname) } - function getSessionCookieFromCookieJar() { - const cookieUrl = isHjarntorget ? 'https://hjarntorget.goteborg.se' : 'https://etjanst.stockholm.se' + const cookieUrl = isHjarntorget + ? 'https://admentum.goteborg.se' + : 'https://etjanst.stockholm.se' const cookies = cookieJar.getCookiesSync(cookieUrl) - const sessionCookieKey = isHjarntorget ? 'JSESSIONID' : 'SMSESSION' - return cookies.find(c => c.key === sessionCookieKey) + const sessionCookieKey = isAdmentum ? 'JSESSIONID' : 'SMSESSION' + return cookies.find((c) => c.key === sessionCookieKey) } const record = async (info, data) => { diff --git a/apps/skolplattformen-app/data/schoolPlatforms.ts b/apps/skolplattformen-app/data/schoolPlatforms.ts index 4ee0359a8..179569452 100644 --- a/apps/skolplattformen-app/data/schoolPlatforms.ts +++ b/apps/skolplattformen-app/data/schoolPlatforms.ts @@ -2,6 +2,9 @@ import CookieManager from '@react-native-cookies/cookies' import initHjarntorget, { features as featuresHjarntorget, } from '@skolplattformen/api-hjarntorget' +import initAdmentum, { + features as featuresAdmentum, +} from '@skolplattformen/api-admentum' import initSkolplattformen, { features as featuresSkolPlattformen, } from '@skolplattformen/api-skolplattformen' @@ -19,4 +22,10 @@ export const schoolPlatforms = [ api: initHjarntorget(fetch as any, CookieManager), features: featuresHjarntorget, }, + { + id: 'larandegruppen-admentum', + displayName: 'Lärandegruppen (Admentum)', + api: initAdmentum(fetch as any, CookieManager), + features: featuresAdmentum, + }, ] diff --git a/apps/skolplattformen-app/project.json b/apps/skolplattformen-app/project.json index 5bdabbf57..1d06ee8a3 100644 --- a/apps/skolplattformen-app/project.json +++ b/apps/skolplattformen-app/project.json @@ -85,6 +85,7 @@ "implicitDependencies": [ "api-skolplattformen", "api-hjarntorget", + "api-admentum", "api", "hooks", "curriculum" diff --git a/libs/api-admentum/.babelrc.js b/libs/api-admentum/.babelrc.js new file mode 100644 index 000000000..77c7288da --- /dev/null +++ b/libs/api-admentum/.babelrc.js @@ -0,0 +1,6 @@ +module.exports = { + presets: [ + ['@babel/preset-env', {targets: {node: 'current'}}], + '@babel/preset-typescript', + ], +} diff --git a/libs/api-admentum/.eslintrc b/libs/api-admentum/.eslintrc new file mode 100644 index 000000000..b3edf8346 --- /dev/null +++ b/libs/api-admentum/.eslintrc @@ -0,0 +1,22 @@ +{ + "extends": ["plugin:@nrwl/nx/react", "../../.eslintrc.json"], + "ignorePatterns": ["!**/*", "public", ".cache", "node_modules"], + "overrides": [ + { + "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], + "rules": {} + }, + { + "files": ["*.ts", "*.tsx"], + "rules": { + "@typescript-eslint/ban-ts-comment": "off" + } + }, + { + "files": ["*.js", "*.jsx"], + "rules": { + "@typescript-eslint/no-var-requires": "off" + } + } + ] + } diff --git a/libs/api-admentum/.prettierrc b/libs/api-admentum/.prettierrc new file mode 100644 index 000000000..da36fd16b --- /dev/null +++ b/libs/api-admentum/.prettierrc @@ -0,0 +1,10 @@ +{ + "printWidth": 80, + "tabWidth": 2, + "useTabs": false, + "semi": false, + "singleQuote": true, + "trailingComma": "es5", + "bracketSpacing": true, + "jsxBracketSameLine": false +} diff --git a/libs/api-admentum/LICENSE b/libs/api-admentum/LICENSE new file mode 100644 index 000000000..261eeb9e9 --- /dev/null +++ b/libs/api-admentum/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/libs/api-admentum/jest.config.js b/libs/api-admentum/jest.config.js new file mode 100644 index 000000000..5a99977db --- /dev/null +++ b/libs/api-admentum/jest.config.js @@ -0,0 +1,6 @@ +module.exports = { + displayName: 'api-admentum', + resolver: '@nrwl/jest/plugins/resolver', + moduleFileExtensions: ['ts', 'js', 'html', 'tsx', 'jsx'], + setupFilesAfterEnv: ['/test-setup.ts'], +} diff --git a/libs/api-admentum/lib/api.test.ts b/libs/api-admentum/lib/api.test.ts new file mode 100644 index 000000000..d31676aa3 --- /dev/null +++ b/libs/api-admentum/lib/api.test.ts @@ -0,0 +1,20 @@ +import { ApiAdmentum } from './api' + +describe('api', () => { + let api: ApiAdmentum + + beforeEach(() => { + api = new ApiAdmentum() + }) + + test('should request and return calendar items', async () => { + expect((await api.getCalendar())[0]).toMatchObject({ + allDay: false, + endDate: '2023-08-07T07:30:00.000Z', + id: 2990834, + location: '', + startDate: '2023-08-07T06:00:00.000Z', + title: 'Matematik', + }) + }) +}) diff --git a/libs/api-admentum/lib/api.ts b/libs/api-admentum/lib/api.ts new file mode 100644 index 000000000..2b6337fef --- /dev/null +++ b/libs/api-admentum/lib/api.ts @@ -0,0 +1,32 @@ +import { EventEmitter } from 'events' + +import { CalendarItem } from '@skolplattformen/api' + +import * as fake from './fakeData' +import { parseDate } from './parse' + +const fakeResponse = (data: T): Promise => + new Promise((res) => setTimeout(() => res(data), 200 + Math.random() * 800)) + +export class ApiAdmentum extends EventEmitter { + public async getCalendar(): Promise { + const events = await fakeResponse(fake.calendar) + + return events.map( + ({ + id, + title, + start_date: startDate, + end_date: endDate, + schedule_event: { start_time: startTime, end_time: endTime }, + }: any) => ({ + id, + title, + location: '', + allDay: startTime === '00:00:00', + startDate: parseDate(startDate + 'T' + startTime), + endDate: parseDate(endDate + 'T' + endTime), + }) + ) + } +} diff --git a/libs/api-admentum/lib/fakeData.ts b/libs/api-admentum/lib/fakeData.ts new file mode 100644 index 000000000..9c426867e --- /dev/null +++ b/libs/api-admentum/lib/fakeData.ts @@ -0,0 +1,32 @@ +export const calendar = [ + { + url: 'https://skola.admentum.se/api/v1/schedule_event_instances/2990834/?format=api', + id: 2990834, + school_id: 824, + start_date: '2023-08-07', + end_date: '2023-08-07', + schedule_event: { + url: 'https://skola.admentum.se/api/v1/schedule_events/148722/?format=api', + id: 148722, + eid: null, + schedule_id: 4385, + start_time: '08:00:00', + end_time: '09:30:00', + rooms: [], + teachers: [ + { + url: 'https://skola.admentum.se/api/v1/users/437302/?format=api', + id: 437302, + }, + ], + schedule_groups: [], + primary_groups: [ + { + url: 'https://skola.admentum.se/api/v1/primary_groups/36874/?format=api', + id: 36874, + }, + ], + weekly_interval: '', + }, + }, +] diff --git a/libs/api-admentum/lib/parse.ts b/libs/api-admentum/lib/parse.ts new file mode 100644 index 000000000..8e60abb0e --- /dev/null +++ b/libs/api-admentum/lib/parse.ts @@ -0,0 +1,7 @@ +export const parseDate = (input?: string): string | undefined => { + if (!input) { + return undefined + } + + return new Date(input).toISOString() +} diff --git a/libs/api-admentum/package.json b/libs/api-admentum/package.json new file mode 100644 index 000000000..ae9bdc506 --- /dev/null +++ b/libs/api-admentum/package.json @@ -0,0 +1,18 @@ +{ + "name": "@skolplattformen/api-admentum", + "version": "0.1.0", + "description": "Admentum API wrapper", + "main": "lib/index.ts", + "files": [ + "dist/**/*" + ], + "repository": "git@github.com:kolplattformen/skolplattformen.git", + "author": "Erik Eng ", + "license": "Apache-2.0", + "private": false, + "scripts": { + "lint": "eslint 'lib/**/*.{js,ts}' --quiet --fix", + "test": "jest", + "build": "tsc --build" + } +} diff --git a/libs/api-admentum/project.json b/libs/api-admentum/project.json new file mode 100644 index 000000000..7c98a884c --- /dev/null +++ b/libs/api-admentum/project.json @@ -0,0 +1,23 @@ +{ + "root": "libs/api-admentum", + "sourceRoot": "libs/api-admentum/src", + "projectType": "library", + "targets": { + "lint": { + "executor": "@nrwl/linter:eslint", + "outputs": ["{options.outputFile}"], + "options": { + "lintFilePatterns": ["libs/api-admentum/**/*.{ts,tsx,js,jsx}"] + } + }, + "test": { + "executor": "@nrwl/jest:jest", + "outputs": ["coverage/libs/api-admentum"], + "options": { + "jestConfig": "libs/api-admentum/jest.config.js", + "passWithNoTests": true + } + } + }, + "tags": [] +} diff --git a/libs/api-admentum/test-setup.ts b/libs/api-admentum/test-setup.ts new file mode 100644 index 000000000..fbf15de85 --- /dev/null +++ b/libs/api-admentum/test-setup.ts @@ -0,0 +1 @@ +import '@testing-library/jest-native/extend-expect' diff --git a/libs/api-admentum/tsconfig.eslint.json b/libs/api-admentum/tsconfig.eslint.json new file mode 100644 index 000000000..36ca13bb8 --- /dev/null +++ b/libs/api-admentum/tsconfig.eslint.json @@ -0,0 +1,5 @@ +{ + "extends": "./tsconfig.json", + "include": ["**/*.ts", "**/*.js"], + "exclude": ["node_modules"] +} \ No newline at end of file diff --git a/libs/api-admentum/tsconfig.json b/libs/api-admentum/tsconfig.json new file mode 100644 index 000000000..b4fc9662a --- /dev/null +++ b/libs/api-admentum/tsconfig.json @@ -0,0 +1,20 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "target": "ES6", + "module": "CommonJS", + "declaration": true, + "outDir": "./dist", + "strict": true, + "allowSyntheticDefaultImports": true, + "sourceMap": true + }, + "include": [ + "lib" + ], + "exclude": [ + "node_modules", + "**/__tests__/*", + "**/__mocks__/*" + ] +} diff --git a/libs/api-admentum/yarn.lock b/libs/api-admentum/yarn.lock new file mode 100644 index 000000000..fb57ccd13 --- /dev/null +++ b/libs/api-admentum/yarn.lock @@ -0,0 +1,4 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + diff --git a/package.json b/package.json index a64f28d40..f689bb565 100644 --- a/package.json +++ b/package.json @@ -128,7 +128,7 @@ "metro-react-native-babel-preset": "^0.66.2", "metro-resolver": "0.66.2", "node-blob": "^0.0.2", - "node-fetch": "^3.1.0", + "node-fetch": "^2.0.0", "prettier": "^2.3.1", "react-native-clean-project": "^3.6.7", "react-native-oss-license": "^0.4.1", diff --git a/workspace.json b/workspace.json index 3ab382d4e..aefb956d1 100644 --- a/workspace.json +++ b/workspace.json @@ -2,6 +2,7 @@ "version": 2, "projects": { "api-hjarntorget": "libs/api-hjarntorget", + "api-admentum": "libs/api-admentum", "api-skolplattformen": "libs/api-skolplattformen", "api-vklass": "libs/api-vklass", "api": "libs/api", diff --git a/yarn.lock b/yarn.lock index e514f226a..5a9ee5868 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5212,11 +5212,6 @@ data-uri-to-buffer@^3.0.1: resolved "https://registry.yarnpkg.com/data-uri-to-buffer/-/data-uri-to-buffer-3.0.1.tgz#594b8973938c5bc2c33046535785341abc4f3636" integrity sha512-WboRycPNsVw3B3TL559F7kuBUM4d8CgMEvk6xEJlOp7OBPjt6G7z8WMWlD2rOFZLk6OYfFIUGsCOWzcQH9K2og== -data-uri-to-buffer@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/data-uri-to-buffer/-/data-uri-to-buffer-4.0.0.tgz#b5db46aea50f6176428ac05b73be39a57701a64b" - integrity sha512-Vr3mLBA8qWmcuschSLAOogKgQ/Jwxulv3RNE4FXnYWRGujzrRWQI4m12fQqRkwX06C0KanhLr4hK+GydchZsaA== - data-urls@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/data-urls/-/data-urls-2.0.0.tgz#156485a72963a970f5d5821aaf642bef2bf2db9b" @@ -6648,13 +6643,6 @@ form-data@~2.3.2: combined-stream "^1.0.6" mime-types "^2.1.12" -formdata-polyfill@^4.0.10: - version "4.0.10" - resolved "https://registry.yarnpkg.com/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz#24807c31c9d402e002ab3d8c720144ceb8848423" - integrity sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g== - dependencies: - fetch-blob "^3.1.2" - formik@^2.2.9: version "2.2.9" resolved "https://registry.yarnpkg.com/formik/-/formik-2.2.9.tgz#8594ba9c5e2e5cf1f42c5704128e119fc46232d0" @@ -9582,6 +9570,13 @@ node-fetch@2.6.1: resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.1.tgz#045bd323631f76ed2e2b55573394416b639a0052" integrity sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw== +node-fetch@^2.0.0: + version "2.7.0" + resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.7.0.tgz#d0f0fa6e3e2dc1d27efcd8ad99d550bda94d187d" + integrity sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A== + dependencies: + whatwg-url "^5.0.0" + node-fetch@^2.2.0, node-fetch@^2.6.0, node-fetch@^2.6.1: version "2.6.6" resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.6.tgz#1751a7c01834e8e1697758732e9efb6eeadfaf89" @@ -9589,15 +9584,6 @@ node-fetch@^2.2.0, node-fetch@^2.6.0, node-fetch@^2.6.1: dependencies: whatwg-url "^5.0.0" -node-fetch@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-3.1.0.tgz#714f4922dc270239487654eaeeab86b8206cb52e" - integrity sha512-QU0WbIfMUjd5+MUzQOYhenAazakV7Irh1SGkWCsRzBwvm4fAhzEUaHMJ6QLP7gWT6WO9/oH2zhKMMGMuIrDyKw== - dependencies: - data-uri-to-buffer "^4.0.0" - fetch-blob "^3.1.2" - formdata-polyfill "^4.0.10" - node-forge@^0.10.0: version "0.10.0" resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.10.0.tgz#32dea2afb3e9926f02ee5ce8794902691a676bf3" From 2daa1b52fbdabb750c18d52f966e120b772213f1 Mon Sep 17 00:00:00 2001 From: Christian Landgren Date: Fri, 1 Sep 2023 15:10:39 +0200 Subject: [PATCH 02/30] chore: rename lib until created through nx --- .../.babelrc.js | 0 .../{api-admentum => api-admentum2}/.eslintrc | 0 .../.prettierrc | 0 libs/{api-admentum => api-admentum2}/LICENSE | 0 .../jest.config.js | 0 .../lib/api.test.ts | 0 .../lib/api.ts | 0 .../lib/fakeData.ts | 0 .../lib/parse.ts | 0 .../package.json | 0 .../project.json | 0 libs/api-admentum2/run.js | 124 ++++++++++++++++++ .../test-setup.ts | 0 .../tsconfig.eslint.json | 0 .../tsconfig.json | 0 .../{api-admentum => api-admentum2}/yarn.lock | 0 16 files changed, 124 insertions(+) rename libs/{api-admentum => api-admentum2}/.babelrc.js (100%) rename libs/{api-admentum => api-admentum2}/.eslintrc (100%) rename libs/{api-admentum => api-admentum2}/.prettierrc (100%) rename libs/{api-admentum => api-admentum2}/LICENSE (100%) rename libs/{api-admentum => api-admentum2}/jest.config.js (100%) rename libs/{api-admentum => api-admentum2}/lib/api.test.ts (100%) rename libs/{api-admentum => api-admentum2}/lib/api.ts (100%) rename libs/{api-admentum => api-admentum2}/lib/fakeData.ts (100%) rename libs/{api-admentum => api-admentum2}/lib/parse.ts (100%) rename libs/{api-admentum => api-admentum2}/package.json (100%) rename libs/{api-admentum => api-admentum2}/project.json (100%) create mode 100644 libs/api-admentum2/run.js rename libs/{api-admentum => api-admentum2}/test-setup.ts (100%) rename libs/{api-admentum => api-admentum2}/tsconfig.eslint.json (100%) rename libs/{api-admentum => api-admentum2}/tsconfig.json (100%) rename libs/{api-admentum => api-admentum2}/yarn.lock (100%) diff --git a/libs/api-admentum/.babelrc.js b/libs/api-admentum2/.babelrc.js similarity index 100% rename from libs/api-admentum/.babelrc.js rename to libs/api-admentum2/.babelrc.js diff --git a/libs/api-admentum/.eslintrc b/libs/api-admentum2/.eslintrc similarity index 100% rename from libs/api-admentum/.eslintrc rename to libs/api-admentum2/.eslintrc diff --git a/libs/api-admentum/.prettierrc b/libs/api-admentum2/.prettierrc similarity index 100% rename from libs/api-admentum/.prettierrc rename to libs/api-admentum2/.prettierrc diff --git a/libs/api-admentum/LICENSE b/libs/api-admentum2/LICENSE similarity index 100% rename from libs/api-admentum/LICENSE rename to libs/api-admentum2/LICENSE diff --git a/libs/api-admentum/jest.config.js b/libs/api-admentum2/jest.config.js similarity index 100% rename from libs/api-admentum/jest.config.js rename to libs/api-admentum2/jest.config.js diff --git a/libs/api-admentum/lib/api.test.ts b/libs/api-admentum2/lib/api.test.ts similarity index 100% rename from libs/api-admentum/lib/api.test.ts rename to libs/api-admentum2/lib/api.test.ts diff --git a/libs/api-admentum/lib/api.ts b/libs/api-admentum2/lib/api.ts similarity index 100% rename from libs/api-admentum/lib/api.ts rename to libs/api-admentum2/lib/api.ts diff --git a/libs/api-admentum/lib/fakeData.ts b/libs/api-admentum2/lib/fakeData.ts similarity index 100% rename from libs/api-admentum/lib/fakeData.ts rename to libs/api-admentum2/lib/fakeData.ts diff --git a/libs/api-admentum/lib/parse.ts b/libs/api-admentum2/lib/parse.ts similarity index 100% rename from libs/api-admentum/lib/parse.ts rename to libs/api-admentum2/lib/parse.ts diff --git a/libs/api-admentum/package.json b/libs/api-admentum2/package.json similarity index 100% rename from libs/api-admentum/package.json rename to libs/api-admentum2/package.json diff --git a/libs/api-admentum/project.json b/libs/api-admentum2/project.json similarity index 100% rename from libs/api-admentum/project.json rename to libs/api-admentum2/project.json diff --git a/libs/api-admentum2/run.js b/libs/api-admentum2/run.js new file mode 100644 index 000000000..543e714ad --- /dev/null +++ b/libs/api-admentum2/run.js @@ -0,0 +1,124 @@ +function requestLogger(httpModule) { + var original = httpModule.request + httpModule.request = function (options, callback) { + console.log('-----------------------------------------------') + console.log( + options.href || options.proto + '://' + options.host + options.path, + options.method + ) + console.log(options.headers) + console.log('-----------------------------------------------') + return original(options, callback) + } +} + +requestLogger(require('http')) +requestLogger(require('https')) + +const { DateTime } = require('luxon') +const nodeFetch = require('node-fetch') +const { CookieJar } = require('tough-cookie') +const fetchCookie = require('fetch-cookie/node-fetch') +const { writeFile } = require('fs/promises') +const path = require('path') +const fs = require('fs') +const { inspect } = require('util') +const init = require('@skolplattformen/api-admentum').default + +const [, , personalNumber] = process.argv + +if (!personalNumber) { + console.error( + 'You must pass in a valid personal number, eg `node run 197001011111`' + ) + process.exit(1) +} + +function ensureDirectoryExistence(filePath) { + var dirname = path.dirname(filePath) + if (fs.existsSync(dirname)) { + return true + } + ensureDirectoryExistence(dirname) + fs.mkdirSync(dirname) +} + +const record = async (info, data) => { + const name = info.error ? `${info.name}_error` : info.name + const filename = `./record/${name}.json` + ensureDirectoryExistence(filename) + const content = { + url: info.url, + headers: info.headers, + status: info.status, + statusText: info.statusText, + } + if (data) { + switch (info.type) { + case 'json': + content.json = data + break + case 'text': + content.text = data + break + case 'blob': + // eslint-disable-next-line no-case-declarations + const buffer = await data.arrayBuffer() + content.blob = Buffer.from(buffer).toString('base64') + break + } + } else if (info.error) { + const { message, stack } = info.error + content.error = { + message, + stack, + } + } + await writeFile(filename, JSON.stringify(content, null, 2)) +} + +async function run() { + const cookieJar = new CookieJar() + const fetch = fetchCookie(nodeFetch, cookieJar) + + try { + const api = init(fetch, cookieJar, { record }) + console.log('inited...') + + api.on('login', async () => { + console.log('Logged in!') + await api.getUser() + const children = await api.getChildren() + const now = DateTime.fromJSDate(new Date()) + for (let i = 0; i < children.length; i++) { + const c = children[i] + await api.getCalendar(c) + await api.getNotifications(c) + await api.getTimetable(c, 44, 2021, 'ignored') + } + const news = await api.getNews() + // const news = await api.getNews() + // //console.table(news.map(n => ({ id: n.id, author: n.author, published: n.published}))) + // //news.length && console.log(news[0]) + + // const notifications = await api.getNotifications(children[2]) + // //const ns = notifications.map(n => ({id: n.id, sender: n.sender, type: n.type})) + // //console.table(ns) + // console.log("notifications count", notifications.length) + // notifications.slice(0, 10).forEach(console.log) + + // await api.getCalendar(children[1]) + + // await api.getTimetable(children[1], 38, 2021, "en") + + // await api.getClassmates() + // console.table(schema) + }) + const res = await api.login(personalNumber) + console.log(res) + } catch (err) { + console.error(err) + } +} + +run() diff --git a/libs/api-admentum/test-setup.ts b/libs/api-admentum2/test-setup.ts similarity index 100% rename from libs/api-admentum/test-setup.ts rename to libs/api-admentum2/test-setup.ts diff --git a/libs/api-admentum/tsconfig.eslint.json b/libs/api-admentum2/tsconfig.eslint.json similarity index 100% rename from libs/api-admentum/tsconfig.eslint.json rename to libs/api-admentum2/tsconfig.eslint.json diff --git a/libs/api-admentum/tsconfig.json b/libs/api-admentum2/tsconfig.json similarity index 100% rename from libs/api-admentum/tsconfig.json rename to libs/api-admentum2/tsconfig.json diff --git a/libs/api-admentum/yarn.lock b/libs/api-admentum2/yarn.lock similarity index 100% rename from libs/api-admentum/yarn.lock rename to libs/api-admentum2/yarn.lock From e0adb9797b71959af26e2d7b1ce5015ce7c515e0 Mon Sep 17 00:00:00 2001 From: Christian Landgren Date: Fri, 8 Sep 2023 15:35:17 +0200 Subject: [PATCH 03/30] feat: first working test-app against login endpoint --- apps/api-test-app/src/main.js | 5 +- apps/skolplattformen-app/ios/Podfile.lock | 2 +- .../ios/app.xcodeproj/project.pbxproj | 4 +- .../.babelrc.js | 0 .../{api-admentum2 => api-admentum}/.eslintrc | 0 .../.prettierrc | 0 libs/{api-admentum2 => api-admentum}/LICENSE | 0 .../jest.config.js | 0 libs/api-admentum/lib/apiAdmentum.test.ts | 288 +++++ libs/api-admentum/lib/apiAdmentum.ts | 323 +++++ libs/api-admentum/lib/fake/calendars.ts | 43 + libs/api-admentum/lib/fake/current-user.ts | 21 + .../lib/fake/event-role-members.ts | 225 ++++ libs/api-admentum/lib/fake/events.ts | 38 + libs/api-admentum/lib/fake/fakeFetcher.ts | 36 + libs/api-admentum/lib/fake/information.ts | 118 ++ libs/api-admentum/lib/fake/lessons.ts | 431 +++++++ libs/api-admentum/lib/fake/my-children.ts | 44 + libs/api-admentum/lib/fake/roles-in-event.ts | 18 + libs/api-admentum/lib/fake/wall-events.ts | 1139 +++++++++++++++++ libs/api-admentum/lib/features.ts | 8 + libs/api-admentum/lib/index.ts | 25 + libs/api-admentum/lib/loginStatus.ts | 61 + libs/api-admentum/lib/missingtypes.d.ts | 1 + libs/api-admentum/lib/parse/parsers.ts | 58 + libs/api-admentum/lib/routes.ts | 50 + .../package.json | 0 .../project.json | 0 libs/api-admentum/run.js | 14 + .../test-setup.ts | 0 .../tsconfig.eslint.json | 0 .../tsconfig.json | 0 .../{api-admentum2 => api-admentum}/yarn.lock | 0 libs/api-admentum2/lib/api.test.ts | 20 - libs/api-admentum2/lib/api.ts | 32 - libs/api-admentum2/lib/fakeData.ts | 32 - libs/api-admentum2/lib/parse.ts | 7 - libs/api-admentum2/run.js | 124 -- tsconfig.base.json | 1 + workspace.json | 6 +- 40 files changed, 2949 insertions(+), 225 deletions(-) rename libs/{api-admentum2 => api-admentum}/.babelrc.js (100%) rename libs/{api-admentum2 => api-admentum}/.eslintrc (100%) rename libs/{api-admentum2 => api-admentum}/.prettierrc (100%) rename libs/{api-admentum2 => api-admentum}/LICENSE (100%) rename libs/{api-admentum2 => api-admentum}/jest.config.js (100%) create mode 100644 libs/api-admentum/lib/apiAdmentum.test.ts create mode 100644 libs/api-admentum/lib/apiAdmentum.ts create mode 100644 libs/api-admentum/lib/fake/calendars.ts create mode 100644 libs/api-admentum/lib/fake/current-user.ts create mode 100644 libs/api-admentum/lib/fake/event-role-members.ts create mode 100644 libs/api-admentum/lib/fake/events.ts create mode 100644 libs/api-admentum/lib/fake/fakeFetcher.ts create mode 100644 libs/api-admentum/lib/fake/information.ts create mode 100644 libs/api-admentum/lib/fake/lessons.ts create mode 100644 libs/api-admentum/lib/fake/my-children.ts create mode 100644 libs/api-admentum/lib/fake/roles-in-event.ts create mode 100644 libs/api-admentum/lib/fake/wall-events.ts create mode 100644 libs/api-admentum/lib/features.ts create mode 100644 libs/api-admentum/lib/index.ts create mode 100644 libs/api-admentum/lib/loginStatus.ts create mode 100644 libs/api-admentum/lib/missingtypes.d.ts create mode 100644 libs/api-admentum/lib/parse/parsers.ts create mode 100644 libs/api-admentum/lib/routes.ts rename libs/{api-admentum2 => api-admentum}/package.json (100%) rename libs/{api-admentum2 => api-admentum}/project.json (100%) create mode 100644 libs/api-admentum/run.js rename libs/{api-admentum2 => api-admentum}/test-setup.ts (100%) rename libs/{api-admentum2 => api-admentum}/tsconfig.eslint.json (100%) rename libs/{api-admentum2 => api-admentum}/tsconfig.json (100%) rename libs/{api-admentum2 => api-admentum}/yarn.lock (100%) delete mode 100644 libs/api-admentum2/lib/api.test.ts delete mode 100644 libs/api-admentum2/lib/api.ts delete mode 100644 libs/api-admentum2/lib/fakeData.ts delete mode 100644 libs/api-admentum2/lib/parse.ts delete mode 100644 libs/api-admentum2/run.js diff --git a/apps/api-test-app/src/main.js b/apps/api-test-app/src/main.js index 514b50f18..249fdb707 100644 --- a/apps/api-test-app/src/main.js +++ b/apps/api-test-app/src/main.js @@ -28,10 +28,7 @@ let bankIdUsed = false const recordFolder = `${__dirname}/record` async function run() { - const agent = new HttpProxyAgent('http://localhost:8080') - const agentEnabledFetch = agentWrapper(nodeFetch, agent) - - const fetch = fetchCookie(agentEnabledFetch, cookieJar) + const fetch = fetchCookie(nodeFetch, cookieJar) try { const api = init(fetch, cookieJar, { record }) diff --git a/apps/skolplattformen-app/ios/Podfile.lock b/apps/skolplattformen-app/ios/Podfile.lock index 3353bcb0e..c8dc6e68a 100644 --- a/apps/skolplattformen-app/ios/Podfile.lock +++ b/apps/skolplattformen-app/ios/Podfile.lock @@ -646,4 +646,4 @@ SPEC CHECKSUMS: PODFILE CHECKSUM: f4a92b32cc4938e15ad7ccfefe9898548670abed -COCOAPODS: 1.11.2 +COCOAPODS: 1.12.1 diff --git a/apps/skolplattformen-app/ios/app.xcodeproj/project.pbxproj b/apps/skolplattformen-app/ios/app.xcodeproj/project.pbxproj index a7a6f4e1c..294b9e298 100644 --- a/apps/skolplattformen-app/ios/app.xcodeproj/project.pbxproj +++ b/apps/skolplattformen-app/ios/app.xcodeproj/project.pbxproj @@ -980,7 +980,7 @@ COPY_PHASE_STRIP = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; - "EXCLUDED_ARCHS[sdk=iphonesimulator*]" = "arm64 i386"; + "EXCLUDED_ARCHS[sdk=iphonesimulator*]" = i386; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_DYNAMIC_NO_PIC = NO; GCC_NO_COMMON_BLOCKS = YES; @@ -1042,7 +1042,7 @@ COPY_PHASE_STRIP = YES; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; - "EXCLUDED_ARCHS[sdk=iphonesimulator*]" = "arm64 i386"; + "EXCLUDED_ARCHS[sdk=iphonesimulator*]" = i386; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; diff --git a/libs/api-admentum2/.babelrc.js b/libs/api-admentum/.babelrc.js similarity index 100% rename from libs/api-admentum2/.babelrc.js rename to libs/api-admentum/.babelrc.js diff --git a/libs/api-admentum2/.eslintrc b/libs/api-admentum/.eslintrc similarity index 100% rename from libs/api-admentum2/.eslintrc rename to libs/api-admentum/.eslintrc diff --git a/libs/api-admentum2/.prettierrc b/libs/api-admentum/.prettierrc similarity index 100% rename from libs/api-admentum2/.prettierrc rename to libs/api-admentum/.prettierrc diff --git a/libs/api-admentum2/LICENSE b/libs/api-admentum/LICENSE similarity index 100% rename from libs/api-admentum2/LICENSE rename to libs/api-admentum/LICENSE diff --git a/libs/api-admentum2/jest.config.js b/libs/api-admentum/jest.config.js similarity index 100% rename from libs/api-admentum2/jest.config.js rename to libs/api-admentum/jest.config.js diff --git a/libs/api-admentum/lib/apiAdmentum.test.ts b/libs/api-admentum/lib/apiAdmentum.test.ts new file mode 100644 index 000000000..2f6a16f0d --- /dev/null +++ b/libs/api-admentum/lib/apiAdmentum.test.ts @@ -0,0 +1,288 @@ +import { wrapToughCookie } from '@skolplattformen/api' +import { CookieJar } from 'tough-cookie' +import { ApiHjarntorget } from './apiAdmentum' + +const setupSuccessfullLoginInitiation = (fetcherMock: jest.Mock) => { + // 'begin-login' + fetcherMock.mockReturnValueOnce( + Promise.resolve({ + url: 'some url with url encoded at the end?return=hello', + }) + ) + + // 'init-shibboleth-login' + fetcherMock.mockReturnValueOnce( + Promise.resolve({ + url: 'some url with url encoded at the end?Target=hello', + }) + ) + + // 'init-bankId' + fetcherMock.mockReturnValueOnce( + Promise.resolve({ + text: jest.fn().mockReturnValue( + Promise.resolve(` + + + + + + `) + ), + }) + ) + + // 'pick-mvghost' + fetcherMock.mockReturnValueOnce( + Promise.resolve({ + url: 'some url to a mvghost', + }) + ) + + // 'start-bankId' + fetcherMock.mockReturnValueOnce( + Promise.resolve({ + url: 'some base url to a mvghost to use when polling status', + }) + ) +} + +const setupSuccessfullBankIdLogin = (fetcherMock: jest.Mock) => { + // 'poll-bankid-status' + fetcherMock.mockReturnValueOnce( + Promise.resolve({ + json: jest.fn().mockReturnValue( + Promise.resolve({ + infotext: '', + location: 'an url to go to confirm the login', + }) + ), + }) + ) + + // 'confirm-signature-redirect' + fetcherMock.mockReturnValueOnce( + Promise.resolve({ + text: jest.fn().mockReturnValue( + Promise.resolve(` + + + + + + `) + ), + }) + ) + + // 'authgbg-saml-login' + fetcherMock.mockReturnValueOnce( + Promise.resolve({ + text: jest.fn().mockReturnValue( + Promise.resolve(` + + + + + + `) + ), + }) + ) + + // 'admentum-saml-login' + fetcherMock.mockReturnValueOnce(Promise.resolve({ status: 200 })) +} + +describe('api', () => { + let fetcherMock: jest.Mock + let api: ApiHjarntorget + + beforeEach(() => { + const fetcher = jest.fn() + fetcherMock = fetcher as jest.Mock + + const cookieManager = wrapToughCookie(new CookieJar()) + cookieManager.clearAll() + api = new ApiHjarntorget(jest.fn(), cookieManager) + api.replaceFetcher(fetcher) + }) + it('works', () => { + expect(1 + 1).toBe(2) + }) + // describe('#login', () => { + // it('goes through single sing-on steps', async (done) => { + // setupSuccessfullLoginInitiation(fetcherMock) + // setupSuccessfullBankIdLogin(fetcherMock) + // const personalNumber = 'my personal number' + + // const loginComplete = new Promise((resolve, reject) => { + // api.on('login', () => done()) + // }); + // await api.login(personalNumber) + // }) + // it('checker emits PENDING', async (done) => { + // // 'poll-bankid-status' + // fetcherMock.mockReturnValueOnce(Promise.resolve({ + // json: jest.fn().mockReturnValue(Promise.resolve({ + // infotext: "some prompt to do signing in app", + // location: "" + // })) + // })) + + // const status = checkStatus(fetcherMock, "some url") + // status.on('PENDING', () => { + // status.cancel() + // done() + // }) + // }) + // it('checker emits ERROR', async (done) => { + // // 'poll-bankid-status' + // fetcherMock.mockReturnValueOnce(Promise.resolve({ + // json: jest.fn().mockReturnValue(Promise.resolve({ + // infotext: "some prompt to do signing in app", + // location: "url with error in the name" + // })) + // })) + + // const status = checkStatus(fetcherMock, "some url") + // status.on('ERROR', () => { + // status.cancel() + // done() + // }) + // }) + // it('checker emits ERROR when an exception occurs', async (done) => { + // // 'poll-bankid-status' + // fetcherMock.mockReturnValueOnce(Promise.resolve({ + // json: jest.fn().mockReturnValue(Promise.resolve({ + // infotext: undefined, + // location: undefined + // })) + // })) + + // const status = checkStatus(fetcherMock, "some url") + // status.on('ERROR', () => { + // status.cancel() + // done() + // }) + // }) + // it('remembers used personal number', async (done) => { + // setupSuccessfullLoginInitiation(fetcherMock) + // setupSuccessfullBankIdLogin(fetcherMock) + // const personalNumber = 'my personal number' + // await api.login(personalNumber) + // api.on('login', () => { + // expect(api.getPersonalNumber()).toEqual(personalNumber) + // done() + // }) + // }) + // it('forgets used personal number if sign in is unsuccessful', async (done) => { + // setupSuccessfullLoginInitiation(fetcherMock) + // // 'poll-bankid-status' + // fetcherMock.mockReturnValueOnce(Promise.resolve({ + // json: jest.fn().mockReturnValue(Promise.resolve({ + // infotext: "", + // location: "an url to go to confirm the login" + // })) + // })) + // // 'confirm-signature-redirect' + // fetcherMock.mockReturnValueOnce(Promise.resolve({ + // text: Promise.resolve("some error occured") + // })) + + // const personalNumber = 'my personal number' + // const status = await api.login(personalNumber) + + // status.on('ERROR', () => { + // expect(api.getPersonalNumber()).toEqual(undefined) + // done() + // }) + // }) + + // // TODO: Possibly rewrite the mocking so we mock the responses more properly, + // // that way it would be possible to implement a throwIfNotOk wrapper for the + // // fetch calls. + // // it('throws error on external api error', async () => { + // // const personalNumber = 'my personal number' + // // try { + // // await api.login(personalNumber) + // // } catch (error: any) { + // // expect(error.message).toEqual(expect.stringContaining('Server Error')) + // // } + // // }) + // }) + // describe('#logout', () => { + // // it('clears session', async () => { + // // await api.logout() + // // const session = await api.getSession('') + // // expect(session).toEqual({ + // // headers: { + // // cookie: '', + // // }, + // // }) + // // }) + // it('emits logout event', async () => { + // const listener = jest.fn() + // api.on('logout', listener) + // await api.logout() + // expect(listener).toHaveBeenCalled() + // }) + // it('sets .isLoggedIn', async () => { + // api.isLoggedIn = true + // await api.logout() + // expect(api.isLoggedIn).toBe(false) + // }) + // it('forgets personalNumber', async () => { + // // eslint-disable-next-line @typescript-eslint/no-explicit-any + // (api as any).personalNumber = 'my personal number' + // api.isLoggedIn = true + + // await api.logout() + + // expect(api.getPersonalNumber()).toEqual(undefined) + // }) + // }) + /* + describe('fake', () => { + it('sets fake mode for the correct pnr:s', async () => { + let status + + status = await api.login('121212121212') + expect(status.token).toEqual('fake') + + status = await api.login('201212121212') + expect(status.token).toEqual('fake') + + status = await api.login('1212121212') + expect(status.token).toEqual('fake') + }) + it('delivers fake data', async (done) => { + api.on('login', async () => { + const user = await api.getUser() + expect(user).toEqual({ + firstName: 'Namn', + lastName: 'Namnsson', + isAuthenticated: true, + personalNumber: "195001182046", + }) + + const children = await api.getChildren() + expect(children).toHaveLength(2) + + const calendar1 = await api.getCalendar(children[0]) + expect(calendar1).toHaveLength(20) + const calendar2 = await api.getCalendar(children[1]) + expect(calendar2).toHaveLength(18) + + const skola24Children = await api.getSkola24Children() + expect(skola24Children).toHaveLength(1) + + const timetable = await api.getTimetable(skola24Children[0], 2021, 15, 'sv') + expect(timetable).toHaveLength(32) + + done() + }) + await api.login('121212121212') + }) + })*/ +}) diff --git a/libs/api-admentum/lib/apiAdmentum.ts b/libs/api-admentum/lib/apiAdmentum.ts new file mode 100644 index 000000000..16b3edd0e --- /dev/null +++ b/libs/api-admentum/lib/apiAdmentum.ts @@ -0,0 +1,323 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { + Api, + CalendarItem, + Classmate, + CookieManager, + EtjanstChild, + Fetch, + Fetcher, + FetcherOptions, + FrejaLoginStatusChecker, + LoginStatusChecker, + MenuItem, + NewsItem, + Notification, + ScheduleItem, + SchoolContact, + Skola24Child, + Teacher, + TimetableEntry, + toMarkdown, + User, + wrap, +} from '@skolplattformen/api' +import { EventEmitter } from 'events' +import { decode } from 'he' +import { DateTime, FixedOffsetZone } from 'luxon' +import * as html from 'node-html-parser' +import { fakeFetcher } from './fake/fakeFetcher' +import { checkStatus, DummyStatusChecker } from './loginStatus' +import { extractMvghostRequestBody, parseCalendarItem } from './parse/parsers' +import { bankIdInitUrl, bankIdCheckUrl, apiUrls } from './routes' +import parse from '@skolplattformen/curriculum' + +function getDateOfISOWeek(week: number, year: number) { + const simple = new Date(year, 0, 1 + (week - 1) * 7) + const dow = simple.getDay() + const isoWeekStart = simple + if (dow <= 4) isoWeekStart.setDate(simple.getDate() - simple.getDay() + 1) + else isoWeekStart.setDate(simple.getDate() + 8 - simple.getDay()) + return isoWeekStart +} + +export class ApiAdmentum extends EventEmitter implements Api { + private fetch: Fetcher + private realFetcher: Fetcher + + private personalNumber?: string + + private cookieManager: CookieManager + + public isLoggedIn = false + + private _isFake = false + + public set isFake(fake: boolean) { + this._isFake = fake + if (this._isFake) { + this.fetch = fakeFetcher + } else { + this.fetch = this.realFetcher + } + } + + public get isFake() { + return this._isFake + } + + constructor( + fetch: Fetch, + cookieManager: CookieManager, + options?: FetcherOptions + ) { + super() + this.fetch = wrap(fetch, options) + this.realFetcher = this.fetch + this.cookieManager = cookieManager + } + + public replaceFetcher(fetcher: Fetcher) { + this.fetch = fetcher + } + + async getSchedule( + child: EtjanstChild, + from: DateTime, + to: DateTime + ): Promise<(CalendarItem & ScheduleItem)[]> { + const lessonsResponseJson: any[] = [] + + return lessonsResponseJson.map((l) => { + const start = DateTime.fromMillis(l.startDate.ts, { + zone: FixedOffsetZone.instance(l.startDate.timezoneOffsetMinutes), + }) + const end = DateTime.fromMillis(l.endDate.ts, { + zone: FixedOffsetZone.instance(l.endDate.timezoneOffsetMinutes), + }) + return { + id: l.id, + title: l.title, + description: l.note, + location: l.location, + startDate: start.toISO(), + endDate: end.toISO(), + oneDayEvent: false, + allDayEvent: false, + } + }) + } + + getPersonalNumber(): string | undefined { + return this.personalNumber + } + + public async getSessionHeaders( + url: string + ): Promise<{ [index: string]: string }> { + const cookie = await this.cookieManager.getCookieString(url) + return { + cookie, + } + } + + async setSessionCookie(sessionCookie: string): Promise { + // this.cookieManager.setCookieString(sessionCookie, admentumUrl) + + const user = await this.getUser() + if (!user.isAuthenticated) { + throw new Error('Session cookie is expired') + } + + this.isLoggedIn = true + this.emit('login') + } + + async getUser(): Promise { + console.log('fetching user') + const currentUserResponse = await this.fetch('current-user', apiUrls.users) // + /id? + if (currentUserResponse.status !== 200) { + return { isAuthenticated: false } + } + + const retrivedUser = await currentUserResponse.json() + return { ...retrivedUser, isAuthenticated: true } + } + + async getChildren(): Promise<(Skola24Child & EtjanstChild)[]> { + if (!this.isLoggedIn) { + throw new Error('Not logged in...') + } + console.log('fetching children') + + const myChildrenResponseJson: any[] = [] + + return myChildrenResponseJson.map( + (c) => + ({ + id: c.id, + sdsId: c.id, + personGuid: c.id, + firstName: c.firstName, + lastName: c.lastName, + name: `${c.firstName} ${c.lastName}`, + } as Skola24Child & EtjanstChild) + ) + } + + async getCalendar(child: EtjanstChild): Promise { + return Promise.resolve([]) + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + getClassmates(_child: EtjanstChild): Promise { + // TODO: We could get this from the events a child is associated with... + if (!this.isLoggedIn) { + throw new Error('Not logged in...') + } + return Promise.resolve([]) + } + + public async getTeachers(child: EtjanstChild): Promise { + if (!this.isLoggedIn) { + throw new Error('Not logged in...') + } + return Promise.resolve([]) + } + + public async getSchoolContacts( + child: EtjanstChild + ): Promise { + if (!this.isLoggedIn) { + throw new Error('Not logged in...') + } + return Promise.resolve([]) + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + async getNews(_child: EtjanstChild): Promise { + if (!this.isLoggedIn) { + throw new Error('Not logged in...') + } + return Promise.resolve([]) + } + + async getNewsDetails(_child: EtjanstChild, item: NewsItem): Promise { + return { ...item } + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + getMenu(_child: EtjanstChild): Promise { + if (!this.isLoggedIn) { + throw new Error('Not logged in...') + } + // Have not found this available on hjärntorget. Perhaps do a mapping to https://www.skolmaten.se/ ? + return Promise.resolve([]) + } + + async getChildEventsWithAssociatedMembers(child: EtjanstChild) { + return this.getEventsWithAssociatedMembersForChildren([child]) + } + + async getEventsWithAssociatedMembersForChildren(children: EtjanstChild[]) { + return Promise.resolve([]) + } + + async getNotifications(child: EtjanstChild): Promise { + return Promise.resolve([]) + } + + async getSkola24Children(): Promise { + if (!this.isLoggedIn) { + throw new Error('Not logged in...') + } + return [] + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + async getTimetable( + child: Skola24Child, + week: number, + year: number, + _lang: string + ): Promise { + const startDate = DateTime.fromJSDate(getDateOfISOWeek(week, year)) + const endDate = startDate.plus({ days: 7 }) + + const lessonsResponseJson: any[] = [] + + return lessonsResponseJson.map((l) => { + const start = DateTime.fromMillis(l.startDate.ts, { + zone: FixedOffsetZone.instance(l.startDate.timezoneOffsetMinutes), + }) + const end = DateTime.fromMillis(l.endDate.ts, { + zone: FixedOffsetZone.instance(l.endDate.timezoneOffsetMinutes), + }) + return { + ...parse(l.title, _lang), + id: l.id, + teacher: l.bookedTeacherNames && l.bookedTeacherNames[0], + location: l.location, + timeStart: start.toISOTime().substring(0, 5), + timeEnd: end.toISOTime().substring(0, 5), + dayOfWeek: start.toJSDate().getDay(), + blockName: l.title, + dateStart: start.toISODate(), + dateEnd: end.toISODate(), + } as TimetableEntry + }) + } + + async logout(): Promise { + this.isLoggedIn = false + this.personalNumber = undefined + this.cookieManager.clearAll() + this.emit('logout') + } + + public async login(personalNumber?: string): Promise { + // short circut the bank-id login if in fake mode + if (personalNumber !== undefined && personalNumber.endsWith('1212121212')) + return this.fakeMode() + + this.isFake = false + const sessionId = await this.fetch('init-session', bankIdInitUrl('')) + .then((res) => res.text()) + .then((text) => /sessionsid=(.)/.exec(text)?.[0]) + + if (!sessionId) throw new Error('No session provided') + + console.log('start polling', sessionId) + const statusChecker = checkStatus(this.fetch, bankIdCheckUrl(sessionId)) + + statusChecker.on('OK', async () => { + // setting these similar to how the sthlm api does it + // not sure if it is needed or if the cookies are enough for fetching all info... + this.isLoggedIn = true + this.personalNumber = personalNumber + this.emit('login') + }) + statusChecker.on('ERROR', () => { + this.personalNumber = undefined + }) + + return statusChecker + } + + private async fakeMode(): Promise { + this.isFake = true + + setTimeout(() => { + this.isLoggedIn = true + this.emit('login') + }, 50) + + const emitter: any = new EventEmitter() + emitter.token = 'fake' + return emitter + } + + async loginFreja(): Promise { + throw new Error('Not implemented...') + } +} diff --git a/libs/api-admentum/lib/fake/calendars.ts b/libs/api-admentum/lib/fake/calendars.ts new file mode 100644 index 000000000..699d0b0ee --- /dev/null +++ b/libs/api-admentum/lib/fake/calendars.ts @@ -0,0 +1,43 @@ +import { Response } from '@skolplattformen/api' +/* eslint-disable no-useless-escape */ +export const calendars = () => + ({ + url: 'https://admentum.goteborg.se/pp/system/calendar/cal_events.jsp', + headers: { + 'User-Agent': + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 11_2_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.90 Safari/537.36', + cookie: 'REMOVED', + }, + status: 200, + statusText: '200', + text: () => + Promise.resolve( + '\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\nThe PING PONG Calendar\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\t
\n\t\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\t\n\t\n\n\t\n\n\n\n\t\t\n\n\t\t
\n\t\t\t\n\t\t\t
\n\t\t\t\t

Make a selection

\n\n\t\t\t\t

Here you get an overview of your calendars. Choose from which calendars you wish to see events. Choose if you want to search for a word. Click Show to see the result.

\n\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t
Show calendar events for the checked calendars
\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\n\n\n
\n\n\n\n
removed checkbox
\n\n\n\n
removed checkbox
\n\n\t\t\t\t\t\t\t
\n\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t
Group the events per calendar\n\t\t\t\t\t\t\t\t\t
Don\'t group\n\t\t\t\t\t\t\t\t\t
From - to:\n\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t
Search for
\n\t\t\t\t\t\t\t\t\t\t
\n\t\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t
\n\t\t\t\t\t\t\t\t\t
\n\n\t\t\t\t\t\t
\n\t\t\t
\n\n\t\t\t

Calendar events

\n\n\t\t\t\t\n\t\t\t\t

\n\t\t\t\t\t No events was found \n\t\t\t\t\t\n\t\t\t\t

\n\t\t\t\t\n\n\t\t\t
\n\t\t
\n\t
\n\n\n\n\n' + ), + } as any as Response) + +export const calendar_14241345 = () => + ({ + url: 'https://admentum.goteborg.se/pp/system/calendar/cal_events.jsp?order_by=start_date&show_cal_ids=14241345&mode=separate&filter_start_date=2021-11-09&filter_end_date=2021-12-09&search_for=', + headers: { + 'User-Agent': + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 11_2_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.90 Safari/537.36', + cookie: 'REMOVED', + }, + status: 200, + statusText: '200', + json: null, + ok: true, + text: () => { + const now = new Date() + const dateYearMonth = `${now.getFullYear()}-${now.getMonth() + 1}` + const nextMonthDate = new Date(now.getFullYear(), now.getMonth() + 1, 1) // Should roll over to new year... + const nextMonth = `${nextMonthDate.getFullYear()}-${ + nextMonthDate.getMonth() + 1 + }` + const result = ` + \n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\nThe PING PONG Calendar\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\t
\n\t\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\t\n\t\n\n\t\n\n\n\n\t\t\n\n\t\t
\n\t\t\t\n\t\t\t
\n\t\t\t\t

Make a selection

\n\n\t\t\t\t

Here you get an overview of your calendars. Choose from which calendars you wish to see events. Choose if you want to search for a word. Click Show to see the result.

\n\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t
Show calendar events for the checked calendars
\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\n\n\n
removed checkbox
\"\"
\n\n\n\n
removed checkbox
\"\"
\n\n\n\n
removed checkbox
\"\"
\n\n\t\t\t\t\t\t\t
\n\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t
Group the events per calendar\n\t\t\t\t\t\t\t\t\t
Don't group\n\t\t\t\t\t\t\t\t\t
From - to:\n\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t
Search for
\n\t\t\t\t\t\t\t\t\t\t
\n\t\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t
\n\t\t\t\t\t\t\t\t\t
\n\n\t\t\t\t\t\t
\n\t\t\t
\n\n\t\t\t

Calendar events

\n\n\t\t\t\t\n\n\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\n\n\t\t\t\t\t\t\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\t\t\t\t\t\n\t\t\t\t
\n\t\t\t\t\t\t\t\tTitle\n\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\tDates\n\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\tTimes\n\n\t\t\t\t\t\t\t
138JÄTS Provschema år 8
\"\"
\nProv/komplettering franska ${dateYearMonth}-0113:00-14:00
\"\"
\nTyska läxförhör${dateYearMonth}-0113:00-13:30\"The
\"\"
\nLäxa i franska${dateYearMonth}-0309:40-10:20\"The
\"\"
\nSpanskaprov Repasamos 1- 4${dateYearMonth}-0310:00-11:00
\"\"
\nTyska läxförhör${dateYearMonth}-0813:00-13:30\"The
\"\"
\nLäxa i franska${dateYearMonth}-1009:40-10:20\"The
\"\"
\nDeadline engelska - Postcard from Great Britain${dateYearMonth}-1115:00-16:00\"The
\"\"
\nLäxa engelska${dateYearMonth}-1408:00-09:00\"The
\"\"
\nTyska läxförhör${dateYearMonth}-1513:00-13:30\"The
\"\"
\nLäxa i franska${dateYearMonth}-1709:40-10:20\"The
\"\"
\nLäxa engelska${dateYearMonth}-1908:00-09:00\"The
\"\"
\nProv franska åk 7${dateYearMonth}-2012:00-13:00
\"\"
\nLäxa i franska${dateYearMonth}-2209:40-10:20\"The
\"\"
\nLäxa engelska${nextMonth}-0108:00-09:00\"The
\n\n\t\t\t\t\n\n\t\t\t
\n\t\t
\n\t
\n\n\n\n\n + ` + return Promise.resolve(result) + }, + } as any as Response) diff --git a/libs/api-admentum/lib/fake/current-user.ts b/libs/api-admentum/lib/fake/current-user.ts new file mode 100644 index 000000000..de60727a3 --- /dev/null +++ b/libs/api-admentum/lib/fake/current-user.ts @@ -0,0 +1,21 @@ +export const currentUser = () => + ({ + url: 'https://admentum.goteborg.se/api/core/current-user', + headers: { + 'User-Agent': + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 11_2_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.90 Safari/537.36', + cookie: 'REMOVED', + }, + status: 200, + statusText: '200', + json: () => + Promise.resolve({ + id: '889911_goteborgsstad', + firstName: 'TOLV', + lastName: 'TOLVAN', + email: null, + online: true, + imagePath: '/pp/lookAndFeel/skins/admentum/icons/monalisa_large.png', + extraInfoInCatalog: '', + }), + } as any as Response) diff --git a/libs/api-admentum/lib/fake/event-role-members.ts b/libs/api-admentum/lib/fake/event-role-members.ts new file mode 100644 index 000000000..238114a28 --- /dev/null +++ b/libs/api-admentum/lib/fake/event-role-members.ts @@ -0,0 +1,225 @@ +export const eventRoleMembers21 = () => + ({ + url: 'https://admentum.goteborg.se/api/event-members/members-having-role?eventId=21&roleId=821', + headers: { + 'User-Agent': + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 11_2_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.90 Safari/537.36', + cookie: 'REMOVED', + }, + status: 200, + statusText: '200', + json: () => + Promise.resolve([ + { + id: '__system$virtual$calendar__', + firstName: 'Kalendern', + lastName: 'i PING PONG', + email: null, + online: false, + imagePath: '/pp/lookAndFeel/skins/default/icons/monalisa_large.png', + extraInfoInCatalog: '', + }, + { + // Klass: 8B + id: '133700_goteborgsstad', + firstName: 'Azra', + lastName: 'Göransson', + email: null, + online: false, + imagePath: '/pp/lookAndFeel/skins/admentum/icons/monalisa_large.png', + extraInfoInCatalog: '', + }, + ]), + } as any as Response) + +export const eventRoleMembers14 = () => + ({ + url: 'https://admentum.goteborg.se/api/event-members/members-having-role?eventId=14&roleId=821', + headers: { + 'User-Agent': + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 11_2_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.90 Safari/537.36', + cookie: 'REMOVED', + }, + status: 200, + statusText: '200', + json: () => + Promise.resolve([ + { + // Klass: 8B + id: '133700_goteborgsstad', + firstName: 'Azra', + lastName: 'Göransson', + email: null, + online: false, + imagePath: '/pp/lookAndFeel/skins/admentum/icons/monalisa_large.png', + extraInfoInCatalog: '', + }, + { + id: '362119_goteborgsstad', + firstName: 'Elina', + lastName: 'Cocolis', + email: null, + online: false, + imagePath: '/pp/lookAndFeel/skins/admentum/icons/monalisa_large.png', + extraInfoInCatalog: '', + }, + { + id: '999999_goteborgsstad', + firstName: 'Sanne', + lastName: 'Berggren', + email: null, + online: false, + imagePath: '/pp/lookAndFeel/skins/admentum/icons/monalisa_large.png', + extraInfoInCatalog: '', + }, + { + id: '168925_goteborgsstad', + firstName: 'Teddy', + lastName: 'Karlsson', + email: null, + online: false, + imagePath: '/pp/lookAndFeel/skins/admentum/icons/monalisa_large.png', + extraInfoInCatalog: '', + }, + { + id: '494949_goteborgsstad', + firstName: 'Fideli', + lastName: 'Sundström', + email: null, + online: false, + imagePath: '/pp/lookAndFeel/skins/admentum/icons/monalisa_large.png', + extraInfoInCatalog: '', + }, + ]), + } as any as Response) + +export const eventRoleMembers18 = () => + ({ + url: 'https://admentum.goteborg.se/api/event-members/members-having-role?eventId=18&roleId=821', + headers: { + 'User-Agent': + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 11_2_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.90 Safari/537.36', + cookie: 'REMOVED', + }, + status: 200, + statusText: '200', + json: () => + Promise.resolve([ + { + id: '776655_goteborgsstad', + firstName: 'Walid', + lastName: 'Söderström', + email: null, + online: false, + imagePath: '/pp/lookAndFeel/skins/admentum/icons/monalisa_large.png', + extraInfoInCatalog: '', + }, + { + id: '388601_goteborgsstad', + firstName: 'Rosa', + lastName: 'Fredriksson', + email: null, + online: false, + imagePath: '/pp/lookAndFeel/skins/admentum/icons/monalisa_large.png', + extraInfoInCatalog: '', + }, + { + id: '654654_goteborgsstad', + firstName: 'Moses', + lastName: 'Johansson', + email: null, + online: false, + imagePath: '/pp/lookAndFeel/skins/admentum/icons/monalisa_large.png', + extraInfoInCatalog: '', + }, + { + id: '1313131_goteborgsstad', + firstName: 'Haris', + lastName: 'Jonsson', + email: null, + online: false, + imagePath: '/pp/lookAndFeel/skins/admentum/icons/monalisa_large.png', + extraInfoInCatalog: '', + }, + { + id: '887766_goteborgsstad', + firstName: 'Neo', + lastName: 'Lundström', + email: null, + online: false, + imagePath: '/pp/lookAndFeel/skins/admentum/icons/monalisa_large.png', + extraInfoInCatalog: '', + }, + { + // Klass: 5A + id: '123456_goteborgsstad', + firstName: 'Jon', + lastName: 'Göransson', + email: null, + online: false, + imagePath: '/pp/lookAndFeel/skins/admentum/icons/monalisa_large.png', + extraInfoInCatalog: '', + }, + ]), + } as any as Response) + +export const eventRoleMembers24 = () => + ({ + url: 'https://admentum.goteborg.se/api/event-members/members-having-role?eventId=24&roleId=821', + headers: { + 'User-Agent': + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 11_2_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.90 Safari/537.36', + cookie: 'REMOVED', + }, + status: 200, + statusText: '200', + json: () => + Promise.resolve([ + { + id: '393939_goteborgsstad', + firstName: 'Malik Maria', + lastName: 'Henriksson', + email: null, + online: false, + imagePath: '/pp/lookAndFeel/skins/admentum/icons/monalisa_large.png', + extraInfoInCatalog: '', + }, + { + id: '444444_goteborgsstad', + firstName: 'Idas', + lastName: 'Svensson', + email: null, + online: false, + imagePath: '/pp/lookAndFeel/skins/admentum/icons/monalisa_large.png', + extraInfoInCatalog: '', + }, + { + id: '818181_goteborgsstad', + firstName: 'Nadja', + lastName: 'Ekström', + email: null, + online: false, + imagePath: '/pp/lookAndFeel/skins/admentum/icons/monalisa_large.png', + extraInfoInCatalog: '', + }, + { + id: '919191_goteborgsstad', + firstName: 'Karim', + lastName: 'Fakir', + email: null, + online: false, + imagePath: '/pp/lookAndFeel/skins/admentum/icons/monalisa_large.png', + extraInfoInCatalog: '', + }, + { + // Klass: Förskola + id: '133737_goteborgsstad', + firstName: 'Havin', + lastName: 'Göransson', + email: null, + online: false, + imagePath: '/pp/lookAndFeel/skins/admentum/icons/monalisa_large.png', + extraInfoInCatalog: '', + }, + ]), + } as any as Response) diff --git a/libs/api-admentum/lib/fake/events.ts b/libs/api-admentum/lib/fake/events.ts new file mode 100644 index 000000000..b2932ba78 --- /dev/null +++ b/libs/api-admentum/lib/fake/events.ts @@ -0,0 +1,38 @@ +export const events = () => + ({ + url: 'https://admentum.goteborg.se/api/events/events-sorted-by-name?offset=0&limit=100', + headers: { + 'User-Agent': + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 11_2_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.90 Safari/537.36', + cookie: 'REMOVED', + }, + status: 200, + statusText: '200', + json: () => + Promise.resolve([ + { + id: 18, + name: '138JÄTS 21/22 5A', + url: 'https://admentum.goteborg.se/o/apiAccessWithKey.do?forwardUrl=%2FlaunchCourse.do%3Fid%3D12', + state: 'ONGOING', + }, + { + id: 14, + name: '138JÄTS 21/22 8B', + url: 'https://admentum.goteborg.se/o/apiAccessWithKey.do?forwardUrl=%2FlaunchCourse.do%3Fid%3D14', + state: 'ONGOING', + }, + { + id: 21, + name: '138JÄTS Provschema år 8', + url: 'https://admentum.goteborg.se/o/apiAccessWithKey.do?forwardUrl=%2FlaunchCourse.do%3Fid%3D21', + state: 'ONGOING', + }, + { + id: 24, + name: '139SS27F Södra Bangatan förskola', + url: 'https://admentum.goteborg.se/o/apiAccessWithKey.do?forwardUrl=%2FlaunchCourse.do%3Fid%3D24', + state: 'ONGOING', + }, + ]), + } as any as Response) diff --git a/libs/api-admentum/lib/fake/fakeFetcher.ts b/libs/api-admentum/lib/fake/fakeFetcher.ts new file mode 100644 index 000000000..d9a9e6f5e --- /dev/null +++ b/libs/api-admentum/lib/fake/fakeFetcher.ts @@ -0,0 +1,36 @@ +import { Fetcher, Response } from '@skolplattformen/api' +import { calendars, calendar_14241345 } from './calendars'; +import { currentUser } from './current-user'; +import { events } from './events'; +import { lessons_123456_goteborgsstad, lessons_133700_goteborgsstad, lessons_133737_goteborgsstad } from './lessons'; +import { myChildren } from './my-children'; +import { wallEvents } from './wall-events'; +import { information } from './information' +import { genericRolesInEvent } from './roles-in-event'; +import { eventRoleMembers14, eventRoleMembers18, eventRoleMembers21, eventRoleMembers24 } from './event-role-members'; + +const fetchMappings: { [name:string]: () => Response} = { + 'current-user': currentUser, + 'events': events, + 'my-children': myChildren, + 'wall-events': wallEvents, + 'lessons-133700_goteborgsstad': lessons_133700_goteborgsstad, + 'lessons-133737_goteborgsstad': lessons_133737_goteborgsstad, + 'lessons-123456_goteborgsstad': lessons_123456_goteborgsstad, + 'info': information, + 'roles-in-event-14': genericRolesInEvent, + 'roles-in-event-18': genericRolesInEvent, + 'roles-in-event-21': genericRolesInEvent, + 'roles-in-event-24': genericRolesInEvent, + 'event-role-members-14-821': eventRoleMembers14, + 'event-role-members-18-821': eventRoleMembers18, + 'event-role-members-21-821': eventRoleMembers21, + 'event-role-members-24-821': eventRoleMembers24, + 'calendars': calendars, + 'calendar-14241345': calendar_14241345, +} + +export const fakeFetcher: Fetcher = (name: string, url: string, init?: any): Promise => { + const responder = fetchMappings[name] ?? (() => {throw new Error("Request not faked for name: " + name)}) + return Promise.resolve(responder()); +} diff --git a/libs/api-admentum/lib/fake/information.ts b/libs/api-admentum/lib/fake/information.ts new file mode 100644 index 000000000..7c4ab0bab --- /dev/null +++ b/libs/api-admentum/lib/fake/information.ts @@ -0,0 +1,118 @@ +/* eslint-disable no-useless-escape */ +export const information = () => + ({ + url: 'https://admentum.goteborg.se/api/information/messages-by-date-desc?messageStatus=CURRENT&offset=0&limit=10&language=en', + headers: { + 'User-Agent': + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 11_2_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.90 Safari/537.36', + cookie: 'REMOVED', + }, + status: 200, + statusText: '200', + json: () => + Promise.resolve([ + { + id: 3276034, + title: 'Nu får du och ditt barn tillgång till Polyglutt hemma', + body: '

Nu får alla barn som går i kommunal förskola i Göteborg tillgång till bilderboksappen Polyglutt hemifrån! Det innebär att du som vårdnadshavare och barn kan ta del av ett bibliotek av böcker på både svenska och 60 andra språk, inklusive TAKK och teckenspråk via telefon eller läsplatta.

\r\n

Polyglutt är en app med bilderböcker som fungerar som ett verktyg för att arbeta med språkutveckling och litteratur i förskolan och hemma.

\r\n

Polyglutt Home Access är en tjänst som innebär att alla barn som går i kommunal förskola i Göteborg får tillgång till ett bibliotek av böcker på både svenska och 60 andra språk, inklusive TAKK och teckenspråk hemifrån. Varje förskola kan också skapa egna bokhyllor med boktips i appen som du och ditt barn kan läsa hemma.

\r\n

Tjänsten fungerar på iPad, Androidplattor och i mobilen.

\r\n

Vill du veta mer om tjänsten, kontakta pedagogerna på ditt barns förskola.

', + creator: { + id: '501747_goteborgsstad', + firstName: 'Information Digitalisering', + lastName: 'Innovation', + email: + 'information.digitaliseringochinnovation@forskola.goteborg.se', + online: false, + imagePath: + '/pp/lookAndFeel/skins/admentum/icons/monalisa_large.png', + extraInfoInCatalog: '', + }, + recipientGroups: [ + { + id: 1121821, + name: 'DL Göteborg Vhavare förskolor', + }, + ], + created: { + ts: 1629970713111, + timezoneOffsetMinutes: 120, + }, + attachments: [], + readByUser: false, + archivedByUser: false, + }, + { + id: 3270718, + title: 'Information från grundskoleförvaltningen', + body: '

Till vårdnadshavare med barn på Göteborgs Stads grundskolor och grundsärskolor.

\r\n

Spridningen av covid-19 har ökat. Därför är det viktigt att alla hjälper till att minska spridningen av smitta.

\r\n

Vi fortsätter hålla avstånd

\r\n
    \r\n
  • Om du vill ha kontakt med någon på ditt barns skola vill vi gärna att du ringer eller skickar e-post.
  • \r\n
  • Lämna och hämta ditt barn utomhus på skolgården.
  • \r\n
  • En del möten som skolan har kommer att vara digitala.
  • \r\n
  • Uppmuntra ditt barn att promenera till och från skolan för att minska trängseln i kollektivtrafiken.
  • \r\n
\r\n

Detta gäller när ditt barn är sjukt

\r\n
    \r\n
  • Barn som bara är lite sjuka, som till exempel är snuviga eller har ont i halsen, ska stanna hemma.
  • \r\n
  • Berätta alltid för skolan om ditt barn har konstaterad covid-19.
  • \r\n
\r\n

Här hittar du mer information om vad som gäller när ditt barn är sjukt.

\r\n

Om ditt barn har varit på resa utomlands

\r\n

Folkhälsomyndigheten rekommenderar alla som har varit i länder utanför Norden att ta ett test för covid-19 när de kommer tillbaka Sverige. Detta gäller oavsett om man har symtom eller inte.

\r\n

Läs mer på Krisinformation.se om vad som gäller för resor från olika länder:
Utrikesresor och att vistas utomlands - Krisinformation.se

\r\n

Undervisning på skolan

\r\n

Från och med höstterminen 2021 har alla skolor undervisning på plats i skolan. Detta gäller även för årskurs 7-9.

\r\n

För förskoleklass till och med årskurs 9 finns det fortfarande möjlighet att få undervisning på distans om:

\r\n
    \r\n
  • Många av de som jobbar på skolan är frånvarande på grund av covid-19 och det inte går att ha undervisning i skolan.
  • \r\n
  • Det är stor spridningen av covid-19 bland elever och medarbetare.
  • \r\n
\r\n

Nytt test för covid-19 på skolorna

\r\n

Inom kort börjar Västra Götalandsregionen med ett nytt test för covid-19 riktat mot elever.  Om ditt barn har haft nära kontakt med en person på skolan som har konstaterad covid-19 får ni med ett paket hem med ett test. 

\r\n

Du som vårdnadshavare hjälper ditt barn att ta testet. Testet lämnar du som vårdnadshavare sedan till en utvald vårdcentral.

\r\n

Om ditt barn ska ta ett test får du mer information från ditt barns skola om hur testet går till och vilken vårdcentral du ska lämna det till.

\r\n

Kontakt

\r\n

Har du frågor eller funderingar kontaktar du ditt barns skola.

\r\n

Läs mer om covid-19 och vad som gäller för grundskoleförvaltningen.

\r\n

 

', + creator: { + id: '486497_goteborgsstad', + firstName: 'Grundskola', + lastName: 'Informerar', + email: null, + online: false, + imagePath: + '/pp/lookAndFeel/skins/admentum/icons/monalisa_large.png', + extraInfoInCatalog: '', + }, + recipientGroups: [ + { + id: 4925595, + name: 'DL Göteborg Grundskola Vhavare Alla', + }, + { + id: 4525636, + name: 'Grundskola - informationskonto', + }, + { + id: 4925600, + name: 'DL Göteborg Grundsärskola Vhavare Alla', + }, + ], + created: { + ts: 1629096850743, + timezoneOffsetMinutes: 120, + }, + attachments: [ + { + id: 67888219, + name: 'Om ditt barn är sjukt eller borta från skolan_information till vårdnadshavare_uppdaterad 13 augusti 2021.pdf', + size: 70466, + }, + ], + readByUser: false, + archivedByUser: false, + }, + { + id: 2982365, + title: 'Nya regler för skolplacering i förskoleklass och grundskola', + body: '

Grundskolenämnden har beslutat om nya regler för skolplacering i förskoleklass och grundskola. Reglerna ska stärka elevernas rätt till en skola nära hemmet och börjar gälla 1 januari 2021.

\r\n

Du kan läsa mer på sidan Nya regler för skolplacering i förskoleklass och grundskola

\r\n

Om du har frågor kan du kontakta grundskoleförvaltningen på telefon: 031-365 09 60 eller e-post: grundskola@grundskola.goteborg.se

\r\n

Observera att detta meddelande inte går att svara på. 

\r\n

 

', + creator: { + id: '486497_goteborgsstad', + firstName: 'Grundskola', + lastName: 'Informerar', + email: null, + online: false, + imagePath: + '/pp/lookAndFeel/skins/admentum/icons/monalisa_large.png', + extraInfoInCatalog: '', + }, + recipientGroups: [ + { + id: 4925595, + name: 'DL Göteborg Grundskola Vhavare Alla', + }, + { + id: 4525636, + name: 'Grundskola - informationskonto', + }, + ], + created: { + ts: 1603974943027, + timezoneOffsetMinutes: 60, + }, + attachments: [], + readByUser: false, + archivedByUser: false, + }, + ]), + } as any as Response) diff --git a/libs/api-admentum/lib/fake/lessons.ts b/libs/api-admentum/lib/fake/lessons.ts new file mode 100644 index 000000000..dcea12103 --- /dev/null +++ b/libs/api-admentum/lib/fake/lessons.ts @@ -0,0 +1,431 @@ +import { toNamespacedPath } from 'path' + +// TODO: fix the startDate/endDate of all lessons +export const lessons_133700_goteborgsstad = () => { + const baseTime = 1636357800000 + const baseDate = new Date(baseTime) + const today = new Date() + const currentHour = today.getHours() + today.setHours(baseDate.getHours()) + today.setMinutes(baseDate.getMinutes()) + today.setSeconds(0) + + let offset = Math.abs(baseTime - today.getTime()) + const weekDay = today.getDay() + + if (weekDay == 6 || (weekDay == 5 && currentHour >= 18)) + offset = offset + 2 * 86400000 + if (weekDay == 0) offset = offset + 86400000 + if (weekDay > 0 && weekDay < 6 && currentHour >= 18) + offset = offset + 86400000 + + return { + url: 'https://admentum.goteborg.se/api/schema/lessons?forUser=133700_goteborgsstad&startDateIso=2021-11-01&endDateIso=2021-11-08', + headers: { + 'User-Agent': + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 11_2_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.90 Safari/537.36', + cookie: 'REMOVED', + }, + status: 200, + statusText: '200', + json: () => + Promise.resolve([ + { + id: '36080472:1', + title: 'HKK', + location: 'A402', + calendars: ['138JÄTS 21/22 8B/HKK'], + startDate: { + ts: offset + 1636357800000, + timezoneOffsetMinutes: 60, + }, + endDate: { + ts: offset + 1636360500000, + timezoneOffsetMinutes: 60, + }, + ownPlannings: null, + teacherPlannings: null, + teacherAndStudentPlannings: null, + ownGeneralPlannings: null, + teacherGeneralPlannings: null, + teacherAndStudentGeneralPlannings: null, + bookedResourceNames: [], + bookedTeacherNames: ['Noel Nyström (NNM)'], + hasTest: false, + hasHomework: false, + hasAssignment: false, + url: null, + note: '', + }, + { + id: '36080497:1', + title: 'BL', + location: 'B260', + calendars: ['138JÄTS 21/22 8B/BL'], + startDate: { + ts: offset + 1636361700000, + timezoneOffsetMinutes: 60, + }, + endDate: { + ts: offset + 1636365000000, + timezoneOffsetMinutes: 60, + }, + ownPlannings: null, + teacherPlannings: null, + teacherAndStudentPlannings: null, + ownGeneralPlannings: null, + teacherGeneralPlannings: null, + teacherAndStudentGeneralPlannings: null, + bookedResourceNames: [], + bookedTeacherNames: ['Joseph Ekström (JHE)'], + hasTest: false, + hasHomework: false, + hasAssignment: false, + url: null, + note: '', + }, + { + id: '37164864:1', + title: 'IDH', + location: 'IDH Ute', + calendars: ['138JÄTS 21/22 8B/IDH'], + startDate: { + ts: offset + 1636365600000, + timezoneOffsetMinutes: 60, + }, + endDate: { + ts: offset + 1636369800000, + timezoneOffsetMinutes: 60, + }, + ownPlannings: null, + teacherPlannings: null, + teacherAndStudentPlannings: null, + ownGeneralPlannings: null, + teacherGeneralPlannings: null, + teacherAndStudentGeneralPlannings: null, + bookedResourceNames: [], + bookedTeacherNames: ['Katja Fransson (KAF)'], + hasTest: false, + hasHomework: false, + hasAssignment: false, + url: null, + note: '', + }, + { + id: '36080557:1', + title: 'LUNCH', + location: '-', + calendars: ['138JÄTS 21/22 8B'], + startDate: { + ts: offset + 1636370700000, + timezoneOffsetMinutes: 60, + }, + endDate: { + ts: offset + 1636372800000, + timezoneOffsetMinutes: 60, + }, + ownPlannings: null, + teacherPlannings: null, + teacherAndStudentPlannings: null, + ownGeneralPlannings: null, + teacherGeneralPlannings: null, + teacherAndStudentGeneralPlannings: null, + bookedResourceNames: [], + bookedTeacherNames: [], + hasTest: false, + hasHomework: false, + hasAssignment: false, + url: null, + note: '', + }, + { + id: '36080576:1', + title: 'EN', + location: 'A402', + calendars: ['138JÄTS 21/22 8B/EN'], + startDate: { + ts: offset + 1636372800000, + timezoneOffsetMinutes: 60, + }, + endDate: { + ts: offset + 1636376400000, + timezoneOffsetMinutes: 60, + }, + ownPlannings: null, + teacherPlannings: null, + teacherAndStudentPlannings: null, + ownGeneralPlannings: null, + teacherGeneralPlannings: null, + teacherAndStudentGeneralPlannings: null, + bookedResourceNames: [], + bookedTeacherNames: ['Henrietta Fransson (HAF)'], + hasTest: false, + hasHomework: false, + hasAssignment: false, + url: null, + note: '', + }, + { + id: '36080591:1', + title: 'MA', + location: 'A402', + calendars: ['138JÄTS 21/22 8B/MA'], + startDate: { + ts: offset + 1636377000000, + timezoneOffsetMinutes: 60, + }, + endDate: { + ts: offset + 1636380600000, + timezoneOffsetMinutes: 60, + }, + ownPlannings: null, + teacherPlannings: null, + teacherAndStudentPlannings: null, + ownGeneralPlannings: null, + teacherGeneralPlannings: null, + teacherAndStudentGeneralPlannings: null, + bookedResourceNames: [], + bookedTeacherNames: ['Amin Månsson (ANM)'], + hasTest: false, + hasHomework: false, + hasAssignment: false, + url: null, + note: '', + }, + ]), + } as any as Response +} + +export const lessons_123456_goteborgsstad = () => { + const baseTime = 1636357800000 + const baseDate = new Date(baseTime) + const today = new Date() + const currentHour = today.getHours() + today.setHours(baseDate.getHours()) + today.setMinutes(baseDate.getMinutes()) + today.setSeconds(0) + + let offset = Math.abs(baseTime - today.getTime()) + const weekDay = today.getDay() + + if (weekDay == 6 || (weekDay == 5 && currentHour >= 18)) + offset = offset + 2 * 86400000 + if (weekDay == 0) offset = offset + 86400000 + if (weekDay > 0 && weekDay < 6 && currentHour >= 18) + offset = offset + 86400000 + + return { + url: 'https://admentum.goteborg.se/api/schema/lessons?forUser=123456_goteborgsstad&startDateIso=2021-11-01&endDateIso=2021-11-08', + headers: { + 'User-Agent': + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 11_2_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.90 Safari/537.36', + cookie: 'REMOVED', + }, + status: 200, + statusText: '200', + json: () => [ + { + id: '36080454:1', + title: 'EV', + location: 'P18', + calendars: ['138JÄTS 21/22 5A'], + startDate: { + ts: offset + 1636355400000, + timezoneOffsetMinutes: 60, + }, + endDate: { + ts: offset + 1636357500000, + timezoneOffsetMinutes: 60, + }, + ownPlannings: null, + teacherPlannings: null, + teacherAndStudentPlannings: null, + ownGeneralPlannings: null, + teacherGeneralPlannings: null, + teacherAndStudentGeneralPlannings: null, + bookedResourceNames: [], + bookedTeacherNames: ['Petra Modin (PMO)', 'Joakim Ness (JNE)'], + hasTest: false, + hasHomework: false, + hasAssignment: false, + url: null, + note: '', + }, + { + id: '36080467:1', + title: 'MENT', + location: 'P18', + calendars: ['138JÄTS 21/22 5A'], + startDate: { + ts: offset + 1636357500000, + timezoneOffsetMinutes: 60, + }, + endDate: { + ts: offset + 1636358100000, + timezoneOffsetMinutes: 60, + }, + ownPlannings: null, + teacherPlannings: null, + teacherAndStudentPlannings: null, + ownGeneralPlannings: null, + teacherGeneralPlannings: null, + teacherAndStudentGeneralPlannings: null, + bookedResourceNames: [], + bookedTeacherNames: ['Petra Modin (PMO)', 'Joakim Ness (JNE)'], + hasTest: false, + hasHomework: false, + hasAssignment: false, + url: null, + note: '', + }, + { + id: '36080474:1', + title: 'EN', + location: 'P18', + calendars: ['138JÄTS 21/22 5A'], + startDate: { + ts: offset + 1636358400000, + timezoneOffsetMinutes: 60, + }, + endDate: { + ts: offset + 1636362000000, + timezoneOffsetMinutes: 60, + }, + ownPlannings: null, + teacherPlannings: null, + teacherAndStudentPlannings: null, + ownGeneralPlannings: null, + teacherGeneralPlannings: null, + teacherAndStudentGeneralPlannings: null, + bookedResourceNames: [], + bookedTeacherNames: ['Petra Modin (PMO)'], + hasTest: false, + hasHomework: false, + hasAssignment: false, + url: null, + note: '', + }, + { + id: '36080502:1', + title: 'SV', + location: 'P18', + calendars: ['138JÄTS 21/22 5A'], + startDate: { + ts: offset + 1636362900000, + timezoneOffsetMinutes: 60, + }, + endDate: { + ts: offset + 1636366500000, + timezoneOffsetMinutes: 60, + }, + ownPlannings: null, + teacherPlannings: null, + teacherAndStudentPlannings: null, + ownGeneralPlannings: null, + teacherGeneralPlannings: null, + teacherAndStudentGeneralPlannings: null, + bookedResourceNames: [], + bookedTeacherNames: ['Joakim Ness (JNE)'], + hasTest: false, + hasHomework: false, + hasAssignment: false, + url: null, + note: '', + }, + { + id: '36080529:1', + title: 'LUNCH', + location: '-', + calendars: ['138JÄTS 21/22 5A'], + startDate: { + ts: offset + 1636366500000, + timezoneOffsetMinutes: 60, + }, + endDate: { + ts: offset + 1636368300000, + timezoneOffsetMinutes: 60, + }, + ownPlannings: null, + teacherPlannings: null, + teacherAndStudentPlannings: null, + ownGeneralPlannings: null, + teacherGeneralPlannings: null, + teacherAndStudentGeneralPlannings: null, + bookedResourceNames: [], + bookedTeacherNames: [], + hasTest: false, + hasHomework: false, + hasAssignment: false, + url: null, + note: '', + }, + { + id: '36080545:1', + title: 'MA', + location: 'P18', + calendars: ['138JÄTS 21/22 5A'], + startDate: { + ts: offset + 1636369200000, + timezoneOffsetMinutes: 60, + }, + endDate: { + ts: offset + 1636372800000, + timezoneOffsetMinutes: 60, + }, + ownPlannings: null, + teacherPlannings: null, + teacherAndStudentPlannings: null, + ownGeneralPlannings: null, + teacherGeneralPlannings: null, + teacherAndStudentGeneralPlannings: null, + bookedResourceNames: [], + bookedTeacherNames: ['Ali Gupta (AGU)'], + hasTest: false, + hasHomework: false, + hasAssignment: false, + url: null, + note: '', + }, + { + id: '36080578:1', + title: 'NO', + location: 'P18', + calendars: ['138JÄTS 21/22 5A'], + startDate: { + ts: offset + 1636373400000, + timezoneOffsetMinutes: 60, + }, + endDate: { + ts: offset + 1636376400000, + timezoneOffsetMinutes: 60, + }, + ownPlannings: null, + teacherPlannings: null, + teacherAndStudentPlannings: null, + ownGeneralPlannings: null, + teacherGeneralPlannings: null, + teacherAndStudentGeneralPlannings: null, + bookedResourceNames: [], + bookedTeacherNames: ['Ali Gupta (AGU)'], + hasTest: false, + hasHomework: false, + hasAssignment: false, + url: null, + note: '', + }, + ], + } as any as Response +} + +export const lessons_133737_goteborgsstad = () => + ({ + url: 'https://admentum.goteborg.se/api/schema/lessons?forUser=133737_goteborgsstad&startDateIso=2021-11-01&endDateIso=2021-11-08', + headers: { + 'User-Agent': + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 11_2_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.90 Safari/537.36', + cookie: 'REMOVED', + }, + status: 200, + statusText: '200', + json: () => Promise.resolve([] as any[]), + } as any as Response) diff --git a/libs/api-admentum/lib/fake/my-children.ts b/libs/api-admentum/lib/fake/my-children.ts new file mode 100644 index 000000000..28b173173 --- /dev/null +++ b/libs/api-admentum/lib/fake/my-children.ts @@ -0,0 +1,44 @@ +export const myChildren = () => + ({ + url: 'https://admentum.goteborg.se/api/person/children', + headers: { + 'User-Agent': + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 11_2_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.90 Safari/537.36', + cookie: 'REMOVED', + }, + status: 200, + statusText: '200', + json: () => + Promise.resolve([ + { + // Klass: Förskola + id: '133737_goteborgsstad', + firstName: 'Havin', + lastName: 'Göransson', + email: null, + online: false, + imagePath: '/pp/lookAndFeel/skins/admentum/icons/monalisa_large.png', + extraInfoInCatalog: '', + }, + { + // Klass: 8B + id: '133700_goteborgsstad', + firstName: 'Azra', + lastName: 'Göransson', + email: null, + online: false, + imagePath: '/pp/lookAndFeel/skins/admentum/icons/monalisa_large.png', + extraInfoInCatalog: '', + }, + { + // Klass: 5A + id: '123456_goteborgsstad', + firstName: 'Jon', + lastName: 'Göransson', + email: null, + online: false, + imagePath: '/pp/lookAndFeel/skins/admentum/icons/monalisa_large.png', + extraInfoInCatalog: '', + }, + ]), + } as any as Response) diff --git a/libs/api-admentum/lib/fake/roles-in-event.ts b/libs/api-admentum/lib/fake/roles-in-event.ts new file mode 100644 index 000000000..c48e8903c --- /dev/null +++ b/libs/api-admentum/lib/fake/roles-in-event.ts @@ -0,0 +1,18 @@ +export const genericRolesInEvent = () => + ({ + url: 'https://admentum.goteborg.se/api/event-members/roles?eventId=XXX&language=en', + headers: { + 'User-Agent': + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 11_2_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.90 Safari/537.36', + cookie: 'REMOVED', + }, + status: 200, + statusText: '200', + json: () => + Promise.resolve([ + { + id: 821, + name: 'SINGLE ROLE', + }, + ]), + } as any as Response) diff --git a/libs/api-admentum/lib/fake/wall-events.ts b/libs/api-admentum/lib/fake/wall-events.ts new file mode 100644 index 000000000..78de1f16d --- /dev/null +++ b/libs/api-admentum/lib/fake/wall-events.ts @@ -0,0 +1,1139 @@ +export const wallEvents = () => + ({ + url: 'https://admentum.goteborg.se/api/wall/events?language=en&limit=500', + headers: { + 'User-Agent': + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 11_2_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.90 Safari/537.36', + cookie: 'REMOVED', + }, + status: 200, + statusText: '200', + json: () => + Promise.resolve([ + { + id: 12312312, + type: 'PIM_SENT', + intId: 27817023, + eventId: null, + eventName: null, + created: { + ts: 1635946042792, + timezoneOffsetMinutes: 60, + }, + creator: { + id: '494949_goteborgsstad', + firstName: 'Fideli', + lastName: 'Sundström', + email: null, + online: false, + imagePath: + '/pp/lookAndFeel/skins/admentum/icons/monalisa_large.png', + extraInfoInCatalog: '', + }, + title: 'PIM from PING PONG', + body: 'Uppdaterade matriser - Engelska\n\nHej,\r Löksås ipsum räv jäst har tre nu om, mjuka träutensilierna olika hans dag inom bra gamla, att oss kan sista erfarenheter olika. Vidsträckt själv upprätthållande så groda smultron nya år regn mjuka strand upprätthållande som rot erfarenheter, blivit brunsås dag kanske sjö miljoner ta söka räv blivit vemod rännil. Björnbär strand sjö inom icke i det, faktor denna verkligen av trevnadens, så tre miljoner för helt.', + attribute1: 'Fideli Sundström', + attribute2: null, + url: 'https://admentum.goteborg.se/pimShowInboxMessage.do?id=27817023&linkOrigin=wall', + onclick: null, + anonymous: false, + images: [], + videos: [], + unread: false, + }, + { + id: 123123133, + type: 'PIM_SENT', + intId: 112233, + eventId: null, + eventName: null, + created: { + ts: 1635707596339, + timezoneOffsetMinutes: 60, + }, + creator: { + id: '1313131_goteborgsstad', + firstName: 'Haris', + lastName: 'Jonsson', + email: null, + online: false, + imagePath: + '/pp/lookAndFeel/skins/admentum/icons/monalisa_large.png', + extraInfoInCatalog: '', + }, + title: 'PIM from PING PONG', + body: 'Föräldramöte \n\nHej alla föräldrar! \r\nDen Löksås ipsum räv jäst har tre nu om, mjuka träutensilierna olika hans dag inom bra gamla, att oss kan sista erfarenheter olika. Vidsträckt själv upprätthållande så groda smultron nya år regn mjuka strand upprätthållande som rot erfarenheter, blivit brunsås dag kanske sjö miljoner ta söka räv blivit vemod rännil. Björnbär strand sjö inom icke i det, faktor denna verkligen av trevnadens, så tre miljoner för helt.', + attribute1: 'Haris Jonsson', + attribute2: null, + url: 'https://admentum.goteborg.se/pimShowInboxMessage.do?id=112233&linkOrigin=wall', + onclick: null, + anonymous: false, + images: [], + videos: [], + unread: false, + }, + { + id: 123123, + type: 'PIM_SENT', + intId: 445566, + eventId: null, + eventName: null, + created: { + ts: 1635539349363, + timezoneOffsetMinutes: 120, + }, + creator: { + id: '494949_goteborgsstad', + firstName: 'Fideli', + lastName: 'Sundström', + email: null, + online: false, + imagePath: + '/pp/lookAndFeel/skins/admentum/icons/monalisa_large.png', + extraInfoInCatalog: '', + }, + title: 'PIM from PING PONG', + body: 'Veckobrev vecka 43\n\nHej på er,\n Löksås ipsum räv jäst har tre nu om, mjuka träutensilierna olika hans dag inom bra gamla, att oss kan sista erfarenheter olika. Vidsträckt själv upprätthållande så groda smultron nya år regn mjuka strand upprätthållande som rot erfarenheter, blivit brunsås dag kanske sjö miljoner ta söka räv blivit vemod rännil. Björnbär strand sjö inom icke i det, faktor denna verkligen av trevnadens, så tre miljoner för helt.', + attribute1: 'Fideli Sundström', + attribute2: null, + url: 'https://admentum.goteborg.se/pimShowInboxMessage.do?id=445566&linkOrigin=wall', + onclick: null, + anonymous: false, + images: [], + videos: [], + unread: false, + }, + { + id: 33060728, + type: 'PIM_SENT', + intId: 27784844, + eventId: null, + eventName: null, + created: { + ts: 1635433031919, + timezoneOffsetMinutes: 120, + }, + creator: { + id: '887766_goteborgsstad', + firstName: 'Neo', + lastName: 'Lundström', + email: null, + online: false, + imagePath: + '/pp/lookAndFeel/skins/admentum/icons/monalisa_large.png', + extraInfoInCatalog: '', + }, + title: 'PIM from PING PONG', + body: 'Schemabrytande dag fredag 29/10\n\nHej!\n Löksås ipsum räv jäst har tre nu om, mjuka träutensilierna olika hans dag inom bra gamla, att oss kan sista erfarenheter olika. Vidsträckt själv upprätthållande så groda smultron nya år regn mjuka strand upprätthållande som rot erfarenheter, blivit brunsås dag kanske sjö miljoner ta söka räv blivit vemod rännil. Björnbär strand sjö inom icke i det, faktor denna verkligen av trevnadens, så tre miljoner för helt.', + attribute1: 'Neo Lundström', + attribute2: null, + url: 'https://admentum.goteborg.se/pimShowInboxMessage.do?id=27784844&linkOrigin=wall', + onclick: null, + anonymous: false, + images: [], + videos: [], + unread: false, + }, + { + id: 33060544, + type: 'PIM_SENT', + intId: 27784728, + eventId: null, + eventName: null, + created: { + ts: 1635432357047, + timezoneOffsetMinutes: 120, + }, + creator: { + id: '776655_goteborgsstad', + firstName: 'Walid', + lastName: 'Söderström', + email: null, + online: false, + imagePath: + '/pp/lookAndFeel/skins/admentum/icons/monalisa_large.png', + extraInfoInCatalog: '', + }, + title: 'PIM from PING PONG', + body: 'Info mattematik\n\nHej! Här kommer lite Löksås ipsum räv jäst har tre nu om, mjuka träutensilierna olika hans dag inom bra gamla, att oss kan sista erfarenheter olika. Vidsträckt själv upprätthållande så groda smultron nya år regn mjuka strand upprätthållande som rot erfarenheter, blivit brunsås dag kanske sjö miljoner ta söka räv blivit vemod rännil. Björnbär strand sjö inom icke i det, faktor denna verkligen av trevnadens, så tre miljoner för helt.', + attribute1: 'Walid Söderström', + attribute2: null, + url: 'https://admentum.goteborg.se/pimShowInboxMessage.do?id=27784728&linkOrigin=wall', + onclick: null, + anonymous: false, + images: [], + videos: [], + unread: false, + }, + { + id: 33060526, + type: 'PIM_SENT', + intId: 27784714, + eventId: null, + eventName: null, + created: { + ts: 1635432262110, + timezoneOffsetMinutes: 120, + }, + creator: { + id: '393939_goteborgsstad', + firstName: 'Malik Maria', + lastName: 'Henriksson', + email: null, + online: false, + imagePath: + '/pp/lookAndFeel/skins/admentum/icons/monalisa_large.png', + extraInfoInCatalog: '', + }, + title: 'PIM from PING PONG', + body: 'Förändrade Covid-19 restriktioner den 1/11\n Löksås ipsum räv jäst har tre nu om, mjuka träutensilierna olika hans dag inom bra gamla, att oss kan sista erfarenheter olika. Vidsträckt själv upprätthållande så groda smultron nya år regn mjuka strand upprätthållande som rot erfarenheter, blivit brunsås dag kanske sjö miljoner ta söka räv blivit vemod rännil. Björnbär strand sjö inom icke i det, faktor denna verkligen av trevnadens, så tre miljoner för helt.', + attribute1: 'Malik Maria Henriksson', + attribute2: null, + url: 'https://admentum.goteborg.se/pimShowInboxMessage.do?id=27784714&linkOrigin=wall', + onclick: null, + anonymous: false, + images: [], + videos: [], + unread: false, + }, + { + id: 1231232, + type: 'PIM_SENT', + intId: 666777, + eventId: null, + eventName: null, + created: { + ts: 1635423440616, + timezoneOffsetMinutes: 120, + }, + creator: { + id: '444444_goteborgsstad', + firstName: 'Idas', + lastName: 'Svensson', + email: null, + online: false, + imagePath: + '/pp/lookAndFeel/skins/admentum/icons/monalisa_large.png', + extraInfoInCatalog: '', + }, + title: 'PIM from PING PONG', + body: 'Klädbyte\n\nHej! Här kommer lite sen Löksås ipsum räv jäst har tre nu om, mjuka träutensilierna olika hans dag inom bra gamla, att oss kan sista erfarenheter olika. Vidsträckt själv upprätthållande så groda smultron nya år regn mjuka strand upprätthållande som rot erfarenheter, blivit brunsås dag kanske sjö miljoner ta söka räv blivit vemod rännil. Björnbär strand sjö inom icke i det, faktor denna verkligen av trevnadens, så tre miljoner för helt.', + attribute1: 'Idas Svensson', + attribute2: null, + url: 'https://admentum.goteborg.se/pimShowInboxMessage.do?id=666777&linkOrigin=wall', + onclick: null, + anonymous: false, + images: [], + videos: [], + unread: false, + }, + { + id: 33038976, + type: 'PIM_SENT', + intId: 27767727, + eventId: null, + eventName: null, + created: { + ts: 1635343618117, + timezoneOffsetMinutes: 120, + }, + creator: { + id: '388601_goteborgsstad', + firstName: 'Rosa', + lastName: 'Fredriksson', + email: null, + online: false, + imagePath: + '/pp/lookAndFeel/skins/admentum/icons/monalisa_large.png', + extraInfoInCatalog: '', + }, + title: 'PIM from PING PONG', + body: 'Info: Teater i klassrummet \n\nHej, Löksås ipsum räv jäst har tre nu om, mjuka träutensilierna olika hans dag inom bra gamla, att oss kan sista erfarenheter olika. Vidsträckt själv upprätthållande så groda smultron nya år regn mjuka strand upprätthållande som rot erfarenheter, blivit brunsås dag kanske sjö miljoner ta söka räv blivit vemod rännil. Björnbär strand sjö inom icke i det, faktor denna verkligen av trevnadens, så tre miljoner för helt.', + attribute1: 'Rosa Fredriksson', + attribute2: null, + url: 'https://admentum.goteborg.se/pimShowInboxMessage.do?id=27767727&linkOrigin=wall', + onclick: null, + anonymous: false, + images: [], + videos: [], + unread: false, + }, + { + id: 33026905, + type: 'PIM_SENT', + intId: 27758800, + eventId: null, + eventName: null, + created: { + ts: 1635320813305, + timezoneOffsetMinutes: 120, + }, + creator: { + id: '393939_goteborgsstad', + firstName: 'Malik Maria', + lastName: 'Henriksson', + email: null, + online: false, + imagePath: + '/pp/lookAndFeel/skins/admentum/icons/monalisa_large.png', + extraInfoInCatalog: '', + }, + title: 'PIM from PING PONG', + body: 'Inbjudan till digitalt föräldramöte\n\nHej!\r Löksås ipsum räv jäst har tre nu om, mjuka träutensilierna olika hans dag inom bra gamla, att oss kan sista erfarenheter olika. Vidsträckt själv upprätthållande så groda smultron nya år regn mjuka strand upprätthållande som rot erfarenheter, blivit brunsås dag kanske sjö miljoner ta söka räv blivit vemod rännil. Björnbär strand sjö inom icke i det, faktor denna verkligen av trevnadens, så tre miljoner för helt.', + attribute1: 'Malik Maria Henriksson', + attribute2: null, + url: 'https://admentum.goteborg.se/pimShowInboxMessage.do?id=27758800&linkOrigin=wall', + onclick: null, + anonymous: false, + images: [], + videos: [], + unread: false, + }, + { + id: 33000399, + type: 'PIM_SENT', + intId: 27737280, + eventId: null, + eventName: null, + created: { + ts: 1635191359954, + timezoneOffsetMinutes: 120, + }, + creator: { + id: '1313131_goteborgsstad', + firstName: 'Haris', + lastName: 'Jonsson', + email: null, + online: false, + imagePath: + '/pp/lookAndFeel/skins/admentum/icons/monalisa_large.png', + extraInfoInCatalog: '', + }, + title: 'PIM from PING PONG', + body: 'Aktiviteten guls blogg är uppdaterad\n\nKlicka på Löksås ipsum räv jäst har tre nu om, mjuka träutensilierna olika hans dag inom bra gamla, att oss kan sista erfarenheter olika. Vidsträckt själv upprätthållande så groda smultron nya år regn mjuka strand upprätthållande som rot erfarenheter, blivit brunsås dag kanske sjö miljoner ta söka räv blivit vemod rännil. Björnbär strand sjö inom icke i det, faktor denna verkligen av trevnadens, så tre miljoner för helt.', + attribute1: 'Haris Jonsson', + attribute2: null, + url: 'https://admentum.goteborg.se/pimShowInboxMessage.do?id=27737280&linkOrigin=wall', + onclick: null, + anonymous: false, + images: [], + videos: [], + unread: false, + }, + { + id: 32989753, + type: 'PIM_SENT', + intId: 27728754, + eventId: null, + eventName: null, + created: { + ts: 1635158030341, + timezoneOffsetMinutes: 120, + }, + creator: { + id: '776655_goteborgsstad', + firstName: 'Walid', + lastName: 'Söderström', + email: null, + online: false, + imagePath: + '/pp/lookAndFeel/skins/admentum/icons/monalisa_large.png', + extraInfoInCatalog: '', + }, + title: 'PIM from PING PONG', + body: 'Veckobrev\n\nHej!\r\n\r\nDen Löksås ipsum räv jäst har tre nu om, mjuka träutensilierna olika hans dag inom bra gamla, att oss kan sista erfarenheter olika. Vidsträckt själv upprätthållande så groda smultron nya år regn mjuka strand upprätthållande som rot erfarenheter, blivit brunsås dag kanske sjö miljoner ta söka räv blivit vemod rännil. Björnbär strand sjö inom icke i det, faktor denna verkligen av trevnadens, så tre miljoner för helt.', + attribute1: 'Walid Söderström', + attribute2: null, + url: 'https://admentum.goteborg.se/pimShowInboxMessage.do?id=27728754&linkOrigin=wall', + onclick: null, + anonymous: false, + images: [], + videos: [], + unread: false, + }, + { + id: 32974490, + type: 'PIM_SENT', + intId: 27716539, + eventId: null, + eventName: null, + created: { + ts: 1635061518818, + timezoneOffsetMinutes: 120, + }, + creator: { + id: '__system$virtual$calendar__', + firstName: 'Kalendern', + lastName: 'i PING PONG', + email: null, + online: false, + imagePath: '/pp/lookAndFeel/skins/default/icons/monalisa_large.png', + extraInfoInCatalog: '', + }, + title: 'PIM from PING PONG', + body: 'Alarm från kalendern!\n\n2021-10-27 Löksås ipsum räv jäst har tre nu om, mjuka träutensilierna olika hans dag inom bra gamla, att oss kan sista erfarenheter olika. Vidsträckt själv upprätthållande så groda smultron nya år regn mjuka strand upprätthållande som rot erfarenheter, blivit brunsås dag kanske sjö miljoner ta söka räv blivit vemod rännil. Björnbär strand sjö inom icke i det, faktor denna verkligen av trevnadens, så tre miljoner för helt.', + attribute1: 'Kalendern i PING PONG', + attribute2: null, + url: 'https://admentum.goteborg.se/pimShowInboxMessage.do?id=27716539&linkOrigin=wall', + onclick: null, + anonymous: false, + images: [], + videos: [], + unread: false, + }, + { + id: 32971325, + type: 'PIM_SENT', + intId: 27714073, + eventId: null, + eventName: null, + created: { + ts: 1634917402164, + timezoneOffsetMinutes: 120, + }, + creator: { + id: '393939_goteborgsstad', + firstName: 'Malik Maria', + lastName: 'Henriksson', + email: null, + online: false, + imagePath: + '/pp/lookAndFeel/skins/admentum/icons/monalisa_large.png', + extraInfoInCatalog: '', + }, + title: 'PIM from PING PONG', + body: 'Information om Rs-virus och om jul och Löksås ipsum räv jäst har tre nu om, mjuka träutensilierna olika hans dag inom bra gamla, att oss kan sista erfarenheter olika. Vidsträckt själv upprätthållande så groda smultron nya år regn mjuka strand upprätthållande som rot erfarenheter, blivit brunsås dag kanske sjö miljoner ta söka räv blivit vemod rännil. Björnbär strand sjö inom icke i det, faktor denna verkligen av trevnadens, så tre miljoner för helt.', + attribute1: 'Malik Maria Henriksson', + attribute2: null, + url: 'https://admentum.goteborg.se/pimShowInboxMessage.do?id=27714073&linkOrigin=wall', + onclick: null, + anonymous: false, + images: [], + videos: [], + unread: false, + }, + { + id: 32967200, + type: 'PIM_SENT', + intId: 27711059, + eventId: null, + eventName: null, + created: { + ts: 1634903242904, + timezoneOffsetMinutes: 120, + }, + creator: { + id: '362119_goteborgsstad', + firstName: 'Elina', + lastName: 'Cocolis', + email: null, + online: false, + imagePath: + '/pp/lookAndFeel/skins/admentum/icons/monalisa_large.png', + extraInfoInCatalog: '', + }, + title: 'PIM from PING PONG', + body: 'Veckobrev v. 42\n\nHej!\r Löksås ipsum räv jäst har tre nu om, mjuka träutensilierna olika hans dag inom bra gamla, att oss kan sista erfarenheter olika. Vidsträckt själv upprätthållande så groda smultron nya år regn mjuka strand upprätthållande som rot erfarenheter, blivit brunsås dag kanske sjö miljoner ta söka räv blivit vemod rännil. Björnbär strand sjö inom icke i det, faktor denna verkligen av trevnadens, så tre miljoner för helt.', + attribute1: 'Elina Cocolis', + attribute2: null, + url: 'https://admentum.goteborg.se/pimShowInboxMessage.do?id=27711059&linkOrigin=wall', + onclick: null, + anonymous: false, + images: [], + videos: [], + unread: false, + }, + { + id: 32921663, + type: 'PIM_SENT', + intId: 27675194, + eventId: null, + eventName: null, + created: { + ts: 1634722320717, + timezoneOffsetMinutes: 120, + }, + creator: { + id: '654654_goteborgsstad', + firstName: 'Moses', + lastName: 'Johansson', + email: null, + online: false, + imagePath: + '/pp/lookAndFeel/skins/admentum/icons/monalisa_large.png', + extraInfoInCatalog: '', + }, + title: 'PIM from PING PONG', + body: 'Information om vaccination Covid 19 \n\n\r Löksås ipsum räv jäst har tre nu om, mjuka träutensilierna olika hans dag inom bra gamla, att oss kan sista erfarenheter olika. Vidsträckt själv upprätthållande så groda smultron nya år regn mjuka strand upprätthållande som rot erfarenheter, blivit brunsås dag kanske sjö miljoner ta söka räv blivit vemod rännil. Björnbär strand sjö inom icke i det, faktor denna verkligen av trevnadens, så tre miljoner för helt.', + attribute1: 'Katarina Krokström', + attribute2: null, + url: 'https://admentum.goteborg.se/pimShowInboxMessage.do?id=27675194&linkOrigin=wall', + onclick: null, + anonymous: false, + images: [], + videos: [], + unread: false, + }, + { + id: 32885504, + type: 'PIM_SENT', + intId: 27646298, + eventId: null, + eventName: null, + created: { + ts: 1634562692304, + timezoneOffsetMinutes: 120, + }, + creator: { + id: '494949_goteborgsstad', + firstName: 'Fideli', + lastName: 'Sundström', + email: null, + online: false, + imagePath: + '/pp/lookAndFeel/skins/admentum/icons/monalisa_large.png', + extraInfoInCatalog: '', + }, + title: 'PIM from PING PONG', + body: 'Klargörande gällande tillgång till classroom för föräldrar \n Löksås ipsum räv jäst har tre nu om, mjuka träutensilierna olika hans dag inom bra gamla, att oss kan sista erfarenheter olika. Vidsträckt själv upprätthållande så groda smultron nya år regn mjuka strand upprätthållande som rot erfarenheter, blivit brunsås dag kanske sjö miljoner ta söka räv blivit vemod rännil. Björnbär strand sjö inom icke i det, faktor denna verkligen av trevnadens, så tre miljoner för helt.', + attribute1: 'Fideli Sundström', + attribute2: null, + url: 'https://admentum.goteborg.se/pimShowInboxMessage.do?id=27646298&linkOrigin=wall', + onclick: null, + anonymous: false, + images: [], + videos: [], + unread: false, + }, + { + id: 32883748, + type: 'PIM_SENT', + intId: 27645002, + eventId: null, + eventName: null, + created: { + ts: 1634559012123, + timezoneOffsetMinutes: 120, + }, + creator: { + id: '776655_goteborgsstad', + firstName: 'Walid', + lastName: 'Söderström', + email: null, + online: false, + imagePath: + '/pp/lookAndFeel/skins/admentum/icons/monalisa_large.png', + extraInfoInCatalog: '', + }, + title: 'PIM from PING PONG', + body: 'Veckobrev\n\nHej!\r\nNu har jobbat Löksås ipsum räv jäst har tre nu om, mjuka träutensilierna olika hans dag inom bra gamla, att oss kan sista erfarenheter olika. Vidsträckt själv upprätthållande så groda smultron nya år regn mjuka strand upprätthållande som rot erfarenheter, blivit brunsås dag kanske sjö miljoner ta söka räv blivit vemod rännil. Björnbär strand sjö inom icke i det, faktor denna verkligen av trevnadens, så tre miljoner för helt.', + attribute1: 'Walid Söderström', + attribute2: null, + url: 'https://admentum.goteborg.se/pimShowInboxMessage.do?id=27645002&linkOrigin=wall', + onclick: null, + anonymous: false, + images: [], + videos: [], + unread: false, + }, + { + id: 32866619, + type: 'PIM_SENT', + intId: 27630980, + eventId: null, + eventName: null, + created: { + ts: 1634369141552, + timezoneOffsetMinutes: 120, + }, + creator: { + id: '818181_goteborgsstad', + firstName: 'Nadja', + lastName: 'Ekström', + email: null, + online: false, + imagePath: + '/pp/lookAndFeel/skins/admentum/icons/monalisa_large.png', + extraInfoInCatalog: '', + }, + title: 'PIM from PING PONG', + body: 'Nytt blogginlägg Södra Bangatan förskola stänger kl 16 mån Löksås ipsum räv jäst har tre nu om, mjuka träutensilierna olika hans dag inom bra gamla, att oss kan sista erfarenheter olika. Vidsträckt själv upprätthållande så groda smultron nya år regn mjuka strand upprätthållande som rot erfarenheter, blivit brunsås dag kanske sjö miljoner ta söka räv blivit vemod rännil. Björnbär strand sjö inom icke i det, faktor denna verkligen av trevnadens, så tre miljoner för helt.', + attribute1: 'Nadja Ekström', + attribute2: null, + url: 'https://admentum.goteborg.se/pimShowInboxMessage.do?id=27630980&linkOrigin=wall', + onclick: null, + anonymous: false, + images: [], + videos: [], + unread: false, + }, + { + id: 32865172, + type: 'PIM_SENT', + intId: 27629801, + eventId: null, + eventName: null, + created: { + ts: 1634306923490, + timezoneOffsetMinutes: 120, + }, + creator: { + id: '388601_goteborgsstad', + firstName: 'Rosa', + lastName: 'Fredriksson', + email: null, + online: false, + imagePath: + '/pp/lookAndFeel/skins/admentum/icons/monalisa_large.png', + extraInfoInCatalog: '', + }, + title: 'PIM from PING PONG', + body: 'Veckobrev (v.41)\n\nHej, \r Löksås ipsum räv jäst har tre nu om, mjuka träutensilierna olika hans dag inom bra gamla, att oss kan sista erfarenheter olika. Vidsträckt själv upprätthållande så groda smultron nya år regn mjuka strand upprätthållande som rot erfarenheter, blivit brunsås dag kanske sjö miljoner ta söka räv blivit vemod rännil. Björnbär strand sjö inom icke i det, faktor denna verkligen av trevnadens, så tre miljoner för helt.', + attribute1: 'Rosa Fredriksson', + attribute2: null, + url: 'https://admentum.goteborg.se/pimShowInboxMessage.do?id=27629801&linkOrigin=wall', + onclick: null, + anonymous: false, + images: [], + videos: [], + unread: false, + }, + { + id: 32850672, + type: 'PIM_SENT', + intId: 27618230, + eventId: null, + eventName: null, + created: { + ts: 1634271404561, + timezoneOffsetMinutes: 120, + }, + creator: { + id: '818181_goteborgsstad', + firstName: 'Nadja', + lastName: 'Ekström', + email: null, + online: false, + imagePath: + '/pp/lookAndFeel/skins/admentum/icons/monalisa_large.png', + extraInfoInCatalog: '', + }, + title: 'PIM from PING PONG', + body: 'Lägg in nytt schema snarast!\n\nHej Löksås ipsum räv jäst har tre nu om, mjuka träutensilierna olika hans dag inom bra gamla, att oss kan sista erfarenheter olika. Vidsträckt själv upprätthållande så groda smultron nya år regn mjuka strand upprätthållande som rot erfarenheter, blivit brunsås dag kanske sjö miljoner ta söka räv blivit vemod rännil. Björnbär strand sjö inom icke i det, faktor denna verkligen av trevnadens, så tre miljoner för helt.', + attribute1: 'Nadja Ekström', + attribute2: null, + url: 'https://admentum.goteborg.se/pimShowInboxMessage.do?id=27618230&linkOrigin=wall', + onclick: null, + anonymous: false, + images: [], + videos: [], + unread: false, + }, + { + id: 32847588, + type: 'PIM_SENT', + intId: 27615823, + eventId: null, + eventName: null, + created: { + ts: 1634222507832, + timezoneOffsetMinutes: 120, + }, + creator: { + id: '887766_goteborgsstad', + firstName: 'Neo', + lastName: 'Lundström', + email: null, + online: false, + imagePath: + '/pp/lookAndFeel/skins/admentum/icons/monalisa_large.png', + extraInfoInCatalog: '', + }, + title: 'PIM from PING PONG', + body: 'Länk till föräldramöte\n\nHär är länken till Löksås ipsum räv jäst har tre nu om, mjuka träutensilierna olika hans dag inom bra gamla, att oss kan sista erfarenheter olika. Vidsträckt själv upprätthållande så groda smultron nya år regn mjuka strand upprätthållande som rot erfarenheter, blivit brunsås dag kanske sjö miljoner ta söka räv blivit vemod rännil. Björnbär strand sjö inom icke i det, faktor denna verkligen av trevnadens, så tre miljoner för helt.', + attribute1: 'Neo Lundström', + attribute2: null, + url: 'https://admentum.goteborg.se/pimShowInboxMessage.do?id=27615823&linkOrigin=wall', + onclick: null, + anonymous: false, + images: [], + videos: [], + unread: false, + }, + { + id: 32846324, + type: 'PIM_SENT', + intId: 27614784, + eventId: null, + eventName: null, + created: { + ts: 1634218630977, + timezoneOffsetMinutes: 120, + }, + creator: { + id: '393939_goteborgsstad', + firstName: 'Malik Maria', + lastName: 'Henriksson', + email: null, + online: false, + imagePath: + '/pp/lookAndFeel/skins/admentum/icons/monalisa_large.png', + extraInfoInCatalog: '', + }, + title: 'PIM from PING PONG', + body: 'Kort information från förskolan\n\nHej!\r Löksås ipsum räv jäst har tre nu om, mjuka träutensilierna olika hans dag inom bra gamla, att oss kan sista erfarenheter olika. Vidsträckt själv upprätthållande så groda smultron nya år regn mjuka strand upprätthållande som rot erfarenheter, blivit brunsås dag kanske sjö miljoner ta söka räv blivit vemod rännil. Björnbär strand sjö inom icke i det, faktor denna verkligen av trevnadens, så tre miljoner för helt.', + attribute1: 'Malik Maria Henriksson', + attribute2: null, + url: 'https://admentum.goteborg.se/pimShowInboxMessage.do?id=27614784&linkOrigin=wall', + onclick: null, + anonymous: false, + images: [], + videos: [], + unread: false, + }, + { + id: 32827306, + type: 'PIM_SENT', + intId: 27599759, + eventId: null, + eventName: null, + created: { + ts: 1634133056730, + timezoneOffsetMinutes: 120, + }, + creator: { + id: '654654_goteborgsstad', + firstName: 'Moses', + lastName: 'Johansson', + email: null, + online: false, + imagePath: + '/pp/lookAndFeel/skins/admentum/icons/monalisa_large.png', + extraInfoInCatalog: '', + }, + title: 'PIM from PING PONG', + body: 'Information om vaccination Covid 19 \n\n\r Löksås ipsum räv jäst har tre nu om, mjuka träutensilierna olika hans dag inom bra gamla, att oss kan sista erfarenheter olika. Vidsträckt själv upprätthållande så groda smultron nya år regn mjuka strand upprätthållande som rot erfarenheter, blivit brunsås dag kanske sjö miljoner ta söka räv blivit vemod rännil. Björnbär strand sjö inom icke i det, faktor denna verkligen av trevnadens, så tre miljoner för helt.', + attribute1: 'Moses Johansson', + attribute2: null, + url: 'https://admentum.goteborg.se/pimShowInboxMessage.do?id=27599759&linkOrigin=wall', + onclick: null, + anonymous: false, + images: [], + videos: [], + unread: false, + }, + { + id: 32808563, + type: 'PIM_SENT', + intId: 27584917, + eventId: null, + eventName: null, + created: { + ts: 1634045704615, + timezoneOffsetMinutes: 120, + }, + creator: { + id: '654654_goteborgsstad', + firstName: 'Moses', + lastName: 'Johansson', + email: null, + online: false, + imagePath: + '/pp/lookAndFeel/skins/admentum/icons/monalisa_large.png', + extraInfoInCatalog: '', + }, + title: 'PIM from PING PONG', + body: 'Erbjudande om ABC-föräldrakurs\n\nHej vårdnadshavare F Löksås ipsum räv jäst har tre nu om, mjuka träutensilierna olika hans dag inom bra gamla, att oss kan sista erfarenheter olika. Vidsträckt själv upprätthållande så groda smultron nya år regn mjuka strand upprätthållande som rot erfarenheter, blivit brunsås dag kanske sjö miljoner ta söka räv blivit vemod rännil. Björnbär strand sjö inom icke i det, faktor denna verkligen av trevnadens, så tre miljoner för helt.', + attribute1: 'Katarina Krokström', + attribute2: null, + url: 'https://admentum.goteborg.se/pimShowInboxMessage.do?id=27584917&linkOrigin=wall', + onclick: null, + anonymous: false, + images: [], + videos: [], + unread: false, + }, + { + id: 32805895, + type: 'PIM_SENT', + intId: 27582958, + eventId: null, + eventName: null, + created: { + ts: 1634039816865, + timezoneOffsetMinutes: 120, + }, + creator: { + id: '999999_goteborgsstad', + firstName: 'Sanne', + lastName: 'Berggren', + email: null, + online: false, + imagePath: + '/pp/lookAndFeel/skins/admentum/icons/monalisa_large.png', + extraInfoInCatalog: '', + }, + title: 'PIM from PING PONG', + body: 'Utvecklingssamtal v. 42 & v. 43 Löksås ipsum räv jäst har tre nu om, mjuka träutensilierna olika hans dag inom bra gamla, att oss kan sista erfarenheter olika. Vidsträckt själv upprätthållande så groda smultron nya år regn mjuka strand upprätthållande som rot erfarenheter, blivit brunsås dag kanske sjö miljoner ta söka räv blivit vemod rännil. Björnbär strand sjö inom icke i det, faktor denna verkligen av trevnadens, så tre miljoner för helt.', + attribute1: 'Sanne Berggren', + attribute2: null, + url: 'https://admentum.goteborg.se/pimShowInboxMessage.do?id=27582958&linkOrigin=wall', + onclick: null, + anonymous: false, + images: [], + videos: [], + unread: false, + }, + { + id: 32788940, + type: 'PIM_SENT', + intId: 27569652, + eventId: null, + eventName: null, + created: { + ts: 1633959214453, + timezoneOffsetMinutes: 120, + }, + creator: { + id: '168925_goteborgsstad', + firstName: 'Teddy', + lastName: 'Karlsson', + email: null, + online: false, + imagePath: + '/pp/lookAndFeel/skins/admentum/icons/monalisa_large.png', + extraInfoInCatalog: '', + }, + title: 'PIM from PING PONG', + body: 'No-prov v.42.\n\nHejsan!\n Löksås ipsum räv jäst har tre nu om, mjuka träutensilierna olika hans dag inom bra gamla, att oss kan sista erfarenheter olika. Vidsträckt själv upprätthållande så groda smultron nya år regn mjuka strand upprätthållande som rot erfarenheter, blivit brunsås dag kanske sjö miljoner ta söka räv blivit vemod rännil. Björnbär strand sjö inom icke i det, faktor denna verkligen av trevnadens, så tre miljoner för helt.', + attribute1: 'Teddy Karlsson', + attribute2: null, + url: 'https://admentum.goteborg.se/pimShowInboxMessage.do?id=27569652&linkOrigin=wall', + onclick: null, + anonymous: false, + images: [], + videos: [], + unread: false, + }, + { + id: 11223344, + type: 'PIM_SENT', + intId: 4949494, + eventId: null, + eventName: null, + created: { + ts: 1633952791428, + timezoneOffsetMinutes: 120, + }, + creator: { + id: '776655_goteborgsstad', + firstName: 'Walid', + lastName: 'Söderström', + email: null, + online: false, + imagePath: + '/pp/lookAndFeel/skins/admentum/icons/monalisa_large.png', + extraInfoInCatalog: '', + }, + title: 'PIM from PING PONG', + body: 'Veckobrev\n\nHej!\r\n\r\nVi Löksås ipsum räv jäst har tre nu om, mjuka träutensilierna olika hans dag inom bra gamla, att oss kan sista erfarenheter olika. Vidsträckt själv upprätthållande så groda smultron nya år regn mjuka strand upprätthållande som rot erfarenheter, blivit brunsås dag kanske sjö miljoner ta söka räv blivit vemod rännil. Björnbär strand sjö inom icke i det, faktor denna verkligen av trevnadens, så tre miljoner för helt.', + attribute1: 'Walid Söderström', + attribute2: null, + url: 'https://admentum.goteborg.se/pimShowInboxMessage.do?id=4949494&linkOrigin=wall', + onclick: null, + anonymous: false, + images: [ + { + fileId: 68860163, + width: 1413, + height: 774, + }, + { + fileId: 68860164, + width: 1400, + height: 761, + }, + ], + videos: [], + unread: false, + }, + { + id: 32780295, + type: 'PIM_SENT', + intId: 27562530, + eventId: null, + eventName: null, + created: { + ts: 1633939225240, + timezoneOffsetMinutes: 120, + }, + creator: { + id: '__system$virtual$calendar__', + firstName: 'Kalendern', + lastName: 'i PING PONG', + email: null, + online: false, + imagePath: '/pp/lookAndFeel/skins/default/icons/monalisa_large.png', + extraInfoInCatalog: '', + }, + title: 'PIM from PING PONG', + body: 'Alarm från kalendern!\n\n2021-10-12 Löksås ipsum räv jäst har tre nu om, mjuka träutensilierna olika hans dag inom bra gamla, att oss kan sista erfarenheter olika. Vidsträckt själv upprätthållande så groda smultron nya år regn mjuka strand upprätthållande som rot erfarenheter, blivit brunsås dag kanske sjö miljoner ta söka räv blivit vemod rännil. Björnbär strand sjö inom icke i det, faktor denna verkligen av trevnadens, så tre miljoner för helt.', + attribute1: 'Kalendern i PING PONG', + attribute2: null, + url: 'https://admentum.goteborg.se/pimShowInboxMessage.do?id=27562530&linkOrigin=wall', + onclick: null, + anonymous: false, + images: [], + videos: [], + unread: false, + }, + { + id: 32773706, + type: 'PIM_SENT', + intId: 27556897, + eventId: null, + eventName: null, + created: { + ts: 1633890789600, + timezoneOffsetMinutes: 120, + }, + creator: { + id: '1313131_goteborgsstad', + firstName: 'Haris', + lastName: 'Jonsson', + email: null, + online: false, + imagePath: + '/pp/lookAndFeel/skins/admentum/icons/monalisa_large.png', + extraInfoInCatalog: '', + }, + title: 'PIM from PING PONG', + body: 'Aktiviteten rosas blogg är uppdaterad\n\nKlicka på Löksås ipsum räv jäst har tre nu om, mjuka träutensilierna olika hans dag inom bra gamla, att oss kan sista erfarenheter olika. Vidsträckt själv upprätthållande så groda smultron nya år regn mjuka strand upprätthållande som rot erfarenheter, blivit brunsås dag kanske sjö miljoner ta söka räv blivit vemod rännil. Björnbär strand sjö inom icke i det, faktor denna verkligen av trevnadens, så tre miljoner för helt.', + attribute1: 'Haris Jonsson', + attribute2: null, + url: 'https://admentum.goteborg.se/pimShowInboxMessage.do?id=27556897&linkOrigin=wall', + onclick: null, + anonymous: false, + images: [], + videos: [], + unread: false, + }, + { + id: 67676767, + type: 'PIM_SENT', + intId: 525252, + eventId: null, + eventName: null, + created: { + ts: 1633701687774, + timezoneOffsetMinutes: 120, + }, + creator: { + id: '919191_goteborgsstad', + firstName: 'Karim', + lastName: 'Fakir', + email: null, + online: false, + imagePath: + '/pp/lookAndFeel/skins/admentum/icons/monalisa_large.png', + extraInfoInCatalog: '', + }, + title: 'PIM from PING PONG', + body: 'Veckobrev v. 40\n\nHej\r\n Löksås ipsum räv jäst har tre nu om, mjuka träutensilierna olika hans dag inom bra gamla, att oss kan sista erfarenheter olika. Vidsträckt själv upprätthållande så groda smultron nya år regn mjuka strand upprätthållande som rot erfarenheter, blivit brunsås dag kanske sjö miljoner ta söka räv blivit vemod rännil. Björnbär strand sjö inom icke i det, faktor denna verkligen av trevnadens, så tre miljoner för helt.', + attribute1: 'Karim Fakir', + attribute2: null, + url: 'https://admentum.goteborg.se/pimShowInboxMessage.do?id=525252&linkOrigin=wall', + onclick: null, + anonymous: false, + images: [], + videos: [], + unread: false, + }, + { + id: 32751305, + type: 'PIM_SENT', + intId: 969696, + eventId: null, + eventName: null, + created: { + ts: 1633621344421, + timezoneOffsetMinutes: 120, + }, + creator: { + id: '818181_goteborgsstad', + firstName: 'Nadja', + lastName: 'Ekström', + email: null, + online: false, + imagePath: + '/pp/lookAndFeel/skins/admentum/icons/monalisa_large.png', + extraInfoInCatalog: '', + }, + title: 'PIM from PING PONG', + body: 'Nytt blogginlägg för förra veckan....\n\nhttps Löksås ipsum räv jäst har tre nu om, mjuka träutensilierna olika hans dag inom bra gamla, att oss kan sista erfarenheter olika. Vidsträckt själv upprätthållande så groda smultron nya år regn mjuka strand upprätthållande som rot erfarenheter, blivit brunsås dag kanske sjö miljoner ta söka räv blivit vemod rännil. Björnbär strand sjö inom icke i det, faktor denna verkligen av trevnadens, så tre miljoner för helt.', + attribute1: 'Nadja Ekström', + attribute2: null, + url: 'https://admentum.goteborg.se/pimShowInboxMessage.do?id=969696&linkOrigin=wall', + onclick: null, + anonymous: false, + images: [], + videos: [], + unread: false, + }, + { + id: 32750570, + type: 'PIM_SENT', + intId: 27539459, + eventId: null, + eventName: null, + created: { + ts: 1633618458315, + timezoneOffsetMinutes: 120, + }, + creator: { + id: '887766_goteborgsstad', + firstName: 'Neo', + lastName: 'Lundström', + email: null, + online: false, + imagePath: + '/pp/lookAndFeel/skins/admentum/icons/monalisa_large.png', + extraInfoInCatalog: '', + }, + title: 'PIM from PING PONG', + body: 'Inbjudan föräldramöte åk 7, tors 14/10 Löksås ipsum räv jäst har tre nu om, mjuka träutensilierna olika hans dag inom bra gamla, att oss kan sista erfarenheter olika. Vidsträckt själv upprätthållande så groda smultron nya år regn mjuka strand upprätthållande som rot erfarenheter, blivit brunsås dag kanske sjö miljoner ta söka räv blivit vemod rännil. Björnbär strand sjö inom icke i det, faktor denna verkligen av trevnadens, så tre miljoner för helt.', + attribute1: 'Neo Lundström', + attribute2: null, + url: 'https://admentum.goteborg.se/pimShowInboxMessage.do?id=27539459&linkOrigin=wall', + onclick: null, + anonymous: false, + images: [], + videos: [], + unread: false, + }, + { + id: 32748594, + type: 'PIM_SENT', + intId: 27538060, + eventId: null, + eventName: null, + created: { + ts: 1633612340812, + timezoneOffsetMinutes: 120, + }, + creator: { + id: '887766_goteborgsstad', + firstName: 'Neo', + lastName: 'Lundström', + email: null, + online: false, + imagePath: + '/pp/lookAndFeel/skins/admentum/icons/monalisa_large.png', + extraInfoInCatalog: '', + }, + title: 'PIM from PING PONG', + body: '7C: Fredag 8/10 börjar matten 9 Löksås ipsum räv jäst har tre nu om, mjuka träutensilierna olika hans dag inom bra gamla, att oss kan sista erfarenheter olika. Vidsträckt själv upprätthållande så groda smultron nya år regn mjuka strand upprätthållande som rot erfarenheter, blivit brunsås dag kanske sjö miljoner ta söka räv blivit vemod rännil. Björnbär strand sjö inom icke i det, faktor denna verkligen av trevnadens, så tre miljoner för helt.', + attribute1: 'Neo Lundström', + attribute2: null, + url: 'https://admentum.goteborg.se/pimShowInboxMessage.do?id=27538060&linkOrigin=wall', + onclick: null, + anonymous: false, + images: [], + videos: [], + unread: false, + }, + { + id: 7917197, + type: 'PIM_SENT', + intId: 971971, + eventId: null, + eventName: null, + created: { + ts: 1633608467229, + timezoneOffsetMinutes: 120, + }, + creator: { + id: '393939_goteborgsstad', + firstName: 'Malik Maria', + lastName: 'Henriksson', + email: null, + online: false, + imagePath: + '/pp/lookAndFeel/skins/admentum/icons/monalisa_large.png', + extraInfoInCatalog: '', + }, + title: 'PIM from PING PONG', + body: 'Påminnelse om Hämta/Lämna uppdateringen\n\nHej!\n Löksås ipsum räv jäst har tre nu om, mjuka träutensilierna olika hans dag inom bra gamla, att oss kan sista erfarenheter olika. Vidsträckt själv upprätthållande så groda smultron nya år regn mjuka strand upprätthållande som rot erfarenheter, blivit brunsås dag kanske sjö miljoner ta söka räv blivit vemod rännil. Björnbär strand sjö inom icke i det, faktor denna verkligen av trevnadens, så tre miljoner för helt.', + attribute1: 'Malik Maria Henriksson', + attribute2: null, + url: 'https://admentum.goteborg.se/pimShowInboxMessage.do?id=971971&linkOrigin=wall', + onclick: null, + anonymous: false, + images: [], + videos: [], + unread: false, + }, + { + id: 32726984, + type: 'PIM_SENT', + intId: 27520912, + eventId: null, + eventName: null, + created: { + ts: 1633519169842, + timezoneOffsetMinutes: 120, + }, + creator: { + id: '776655_goteborgsstad', + firstName: 'Walid', + lastName: 'Söderström', + email: null, + online: false, + imagePath: + '/pp/lookAndFeel/skins/admentum/icons/monalisa_large.png', + extraInfoInCatalog: '', + }, + title: 'PIM from PING PONG', + body: 'Ordet\n\nHej igen!\r\nKlassen har Löksås ipsum räv jäst har tre nu om, mjuka träutensilierna olika hans dag inom bra gamla, att oss kan sista erfarenheter olika. Vidsträckt själv upprätthållande så groda smultron nya år regn mjuka strand upprätthållande som rot erfarenheter, blivit brunsås dag kanske sjö miljoner ta söka räv blivit vemod rännil. Björnbär strand sjö inom icke i det, faktor denna verkligen av trevnadens, så tre miljoner för helt.', + attribute1: 'Walid Söderström', + attribute2: null, + url: 'https://admentum.goteborg.se/pimShowInboxMessage.do?id=27520912&linkOrigin=wall', + onclick: null, + anonymous: false, + images: [], + videos: [], + unread: false, + }, + { + id: 32713989, + type: 'PIM_SENT', + intId: 27510300, + eventId: null, + eventName: null, + created: { + ts: 1633444717085, + timezoneOffsetMinutes: 120, + }, + creator: { + id: '654654_goteborgsstad', + firstName: 'Moses', + lastName: 'Johansson', + email: null, + online: false, + imagePath: + '/pp/lookAndFeel/skins/admentum/icons/monalisa_large.png', + extraInfoInCatalog: '', + }, + title: 'PIM from PING PONG', + body: 'Information från grundskoleförvaltningen-rekommendationer för att minska spridningen Löksås ipsum räv jäst har tre nu om, mjuka träutensilierna olika hans dag inom bra gamla, att oss kan sista erfarenheter olika. Vidsträckt själv upprätthållande så groda smultron nya år regn mjuka strand upprätthållande som rot erfarenheter, blivit brunsås dag kanske sjö miljoner ta söka räv blivit vemod rännil. Björnbär strand sjö inom icke i det, faktor denna verkligen av trevnadens, så tre miljoner för helt.', + attribute1: 'Katarina Krokström', + attribute2: null, + url: 'https://admentum.goteborg.se/pimShowInboxMessage.do?id=27510300&linkOrigin=wall', + onclick: null, + anonymous: false, + images: [], + videos: [], + unread: false, + }, + ]), + } as any as Response) diff --git a/libs/api-admentum/lib/features.ts b/libs/api-admentum/lib/features.ts new file mode 100644 index 000000000..b0e530222 --- /dev/null +++ b/libs/api-admentum/lib/features.ts @@ -0,0 +1,8 @@ +import { Features } from '@skolplattformen/api' + +export const features: Features = { + LOGIN_BANK_ID_SAME_DEVICE_WITHOUT_ID: false, + LOGIN_FREJA_EID: false, + FOOD_MENU: false, + CLASS_LIST: false, +} diff --git a/libs/api-admentum/lib/index.ts b/libs/api-admentum/lib/index.ts new file mode 100644 index 000000000..003229691 --- /dev/null +++ b/libs/api-admentum/lib/index.ts @@ -0,0 +1,25 @@ +import { + Api, + Fetch, + FetcherOptions, + RNCookieManager, + ToughCookieJar, + wrapReactNativeCookieManager, + wrapToughCookie, +} from '@skolplattformen/api' +import { ApiAdmentum } from './apiAdmentum' +export { features } from './features' + +const init = ( + fetchImpl: Fetch, + cookieManagerImpl: RNCookieManager | ToughCookieJar, + options?: FetcherOptions +): Api => { + // prettier-ignore + const cookieManager = ((cookieManagerImpl as RNCookieManager).get) + ? wrapReactNativeCookieManager(cookieManagerImpl as RNCookieManager) + : wrapToughCookie(cookieManagerImpl as ToughCookieJar) + return new ApiAdmentum(fetchImpl as any, cookieManager, options) +} + +export default init diff --git a/libs/api-admentum/lib/loginStatus.ts b/libs/api-admentum/lib/loginStatus.ts new file mode 100644 index 000000000..8c3d5f77b --- /dev/null +++ b/libs/api-admentum/lib/loginStatus.ts @@ -0,0 +1,61 @@ +import { Fetcher, LoginStatusChecker } from '@skolplattformen/api' +import { EventEmitter } from 'events' +import { bankIdCheckUrl } from './routes' + +export class GrandidChecker extends EventEmitter implements LoginStatusChecker { + private fetcher: Fetcher + + private basePollingUrl: string + + public token: string + + private cancelled = false + + constructor(fetcher: Fetcher, basePollingUrl: string) { + super() + this.token = '' // not used, but needed for compatability with the LoginStatusChecker + this.fetcher = fetcher + this.basePollingUrl = basePollingUrl + + this.check() + } + + async check(): Promise { + // try { + // console.log('polling bankid signature') + // // https://mNN-mg-local.idp.funktionstjanster.se/mg-local/auth/ccp11/grp/pollstatus + // if (true) + // this.emit('OK') + // } else if (isError) { + // console.log('polling error') + // this.emit('ERROR') + // } else if (!this.cancelled && keepPolling) { + // console.log('keep on polling...') + // this.emit('PENDING') + // setTimeout(() => this.check(), 3000) + // } + // } catch (er) { + // console.log('Error validating login to Hjärntorget', er) + // this.emit('ERROR') + // } + } + + async cancel(): Promise { + this.cancelled = true + } +} + +export const checkStatus = ( + fetch: Fetcher, + basePollingUrl: string +): LoginStatusChecker => new GrandidChecker(fetch, basePollingUrl) + +export class DummyStatusChecker + extends EventEmitter + implements LoginStatusChecker +{ + token = '' + async cancel(): Promise { + // do nothing + } +} diff --git a/libs/api-admentum/lib/missingtypes.d.ts b/libs/api-admentum/lib/missingtypes.d.ts new file mode 100644 index 000000000..0334e2034 --- /dev/null +++ b/libs/api-admentum/lib/missingtypes.d.ts @@ -0,0 +1 @@ +declare module 'h2m' diff --git a/libs/api-admentum/lib/parse/parsers.ts b/libs/api-admentum/lib/parse/parsers.ts new file mode 100644 index 000000000..0ca462600 --- /dev/null +++ b/libs/api-admentum/lib/parse/parsers.ts @@ -0,0 +1,58 @@ +import * as html from 'node-html-parser' +import { decode } from 'he' + +// TODO: Move this into the parse folder and convert it to follow the pattern of other parsers (include tests). + +export const extractInputField = (sought: string, attrs: string[]) => { + // there must be a better way to do this... + const s = attrs.find(e => e.indexOf(sought) >= 0) || "" + const v = s.substring(s.indexOf('value="') + 'value="'.length) + return v.substring(0, v.length - 2) +} + +export function extractMvghostRequestBody(initBankIdResponseText: string) { + const doc = html.parse(decode(initBankIdResponseText)) + const inputAttrs = doc.querySelectorAll('input').map(i => (i as any).rawAttrs) + const relayState = extractInputField('RelayState', inputAttrs) + const samlRequest = extractInputField("SAMLRequest", inputAttrs) + const mvghostRequestBody = `RelayState=${encodeURIComponent(relayState)}&SAMLRequest=${encodeURIComponent(samlRequest)}` + + return mvghostRequestBody +} + +export function extractHjarntorgetSAMLLogin(authGbgLoginResponseText: string) { + const authGbgLoginDoc = html.parse(decode(authGbgLoginResponseText)) + const inputAttrs = authGbgLoginDoc.querySelectorAll('input').map(i => (i as any).rawAttrs) + const RelayStateText = extractInputField('RelayState', inputAttrs) + const SAMLResponseText = extractInputField("SAMLResponse", inputAttrs) + + return `SAMLResponse=${encodeURIComponent(SAMLResponseText || '')}&RelayState=${encodeURIComponent(RelayStateText || '')}` +} + +export function extractAuthGbgLoginRequestBody(signatureResponseText: string) { + const signatureResponseDoc = html.parse(decode(signatureResponseText)) + const signatureResponseTextAreas = signatureResponseDoc.querySelectorAll('textarea') + const SAMLResponseElem = signatureResponseTextAreas.find(ta => { + const nameAttr = ta.getAttribute("name") + return nameAttr === 'SAMLResponse' + }) + const SAMLResponseText = SAMLResponseElem?.rawText + const RelayStateElem = signatureResponseTextAreas.find(ta => { + const nameAttr = ta.getAttribute("name") + return nameAttr === 'RelayState' + }) + const RelayStateText = RelayStateElem?.rawText + const authGbgLoginBody = `SAMLResponse=${encodeURIComponent(SAMLResponseText || '')}&RelayState=${encodeURIComponent(RelayStateText || '')}` + return authGbgLoginBody +} + +export const parseCalendarItem = (x: html.HTMLElement): { id: number; title: string; startDate: string; endDate: string } => { + const info = Array.from(x.querySelectorAll('a')) + // TODO: the identifier is realy on this format: '\d+:\d+' currently we only take the first part so Id will clash between items + const id = info[0].getAttribute("onClick")?.replace(new RegExp("return viewEvent\\('(\\d+).+"), "$1") || NaN + const day = info[1].textContent + const timeSpan = info[2].textContent + const [startTime, endTime] = timeSpan.replace(".", ":").split("-") + + return { id: +id, title: info[0].textContent, startDate: `${day} ${startTime}`, endDate: `${day} ${endTime}` } +} \ No newline at end of file diff --git a/libs/api-admentum/lib/routes.ts b/libs/api-admentum/lib/routes.ts new file mode 100644 index 000000000..f9bbc2796 --- /dev/null +++ b/libs/api-admentum/lib/routes.ts @@ -0,0 +1,50 @@ +const baseUrl = 'https://skola.admentum.se/api/v1/' + +export const apiUrls = { + assignments: baseUrl + 'assignments', + attendance_summary_users: baseUrl + 'attendance/summary/users', + course_sections: baseUrl + 'course_sections', + courses: baseUrl + 'courses', + forecast_collections: baseUrl + 'forecast_collections', + forecasts: baseUrl + 'forecasts', + grade_permissions: baseUrl + 'grade_permissions', + grades: baseUrl + 'grades', + gymnasium_courses: baseUrl + 'gymnasium_courses', + leisure_group_enrollments: baseUrl + 'leisure_group_enrollments', + leisure_groups: baseUrl + 'leisure_groups', + lesson_infos: baseUrl + 'lesson_infos', + lessons: baseUrl + 'lessons', + organisations: baseUrl + 'organisations', + orientations: baseUrl + 'orientations', + permission_groups: baseUrl + 'permission_groups', + primary_group_enrollments: baseUrl + 'primary_group_enrollments', + primary_group_municipality_statistics: + baseUrl + 'primary_groups/municipality_statistic', + primary_groups: baseUrl + 'primary_groups', + program_courses: baseUrl + 'program_courses', + programs: baseUrl + 'programs', + reviews: baseUrl + 'reviews', + rooms: baseUrl + 'rooms', + schedule_breaks: baseUrl + 'schedule_breaks', + schedule_event_instances: baseUrl + 'schedule_event_instances', + schedule_events: baseUrl + 'schedule_events', + schedule_group_enrollments: baseUrl + 'schedule_group_enrollments', + schedule_group_teacher_enrollments: + baseUrl + 'schedule_group_teacher_enrollments', + schedule_groups: baseUrl + 'schedule_groups', + schedules: baseUrl + 'schedules', + school_enrollments: baseUrl + 'school_enrollments', + school_years: baseUrl + 'school_years', + schools: baseUrl + 'schools', + sickness: baseUrl + 'sickness', + subjects: baseUrl + 'subjects', + teachers: baseUrl + 'teachers', + upper_secondary_subjects: baseUrl + 'upper_secondary_subjects', + users: baseUrl + 'users', +} + +export const bankIdCheckUrl = (sessionId: string) => + `https://login.grandid.com/?sessionid=${sessionId}&eleg=1&bankid=1` + +export const bankIdInitUrl = (returnUrl: string) => + `https://auth.admentum.se/larande${returnUrl ? `?next=${returnUrl}` : ''}` diff --git a/libs/api-admentum2/package.json b/libs/api-admentum/package.json similarity index 100% rename from libs/api-admentum2/package.json rename to libs/api-admentum/package.json diff --git a/libs/api-admentum2/project.json b/libs/api-admentum/project.json similarity index 100% rename from libs/api-admentum2/project.json rename to libs/api-admentum/project.json diff --git a/libs/api-admentum/run.js b/libs/api-admentum/run.js new file mode 100644 index 000000000..70d122b3b --- /dev/null +++ b/libs/api-admentum/run.js @@ -0,0 +1,14 @@ +const Admentum = require('./lib/index.ts') +const nodeFetch = require('node-fetch') +const { CookieJar } = require('tough-cookie') +const fetchCookie = require('fetch-cookie/node-fetch') + +const cookieJar = new CookieJar() +const fetch = fetchCookie(nodeFetch, cookieJar) +const admentum = new Admentum(fetch, {}) + +const run = async () => { + const sessionId = await admentum.login('7612040233') +} + +run() diff --git a/libs/api-admentum2/test-setup.ts b/libs/api-admentum/test-setup.ts similarity index 100% rename from libs/api-admentum2/test-setup.ts rename to libs/api-admentum/test-setup.ts diff --git a/libs/api-admentum2/tsconfig.eslint.json b/libs/api-admentum/tsconfig.eslint.json similarity index 100% rename from libs/api-admentum2/tsconfig.eslint.json rename to libs/api-admentum/tsconfig.eslint.json diff --git a/libs/api-admentum2/tsconfig.json b/libs/api-admentum/tsconfig.json similarity index 100% rename from libs/api-admentum2/tsconfig.json rename to libs/api-admentum/tsconfig.json diff --git a/libs/api-admentum2/yarn.lock b/libs/api-admentum/yarn.lock similarity index 100% rename from libs/api-admentum2/yarn.lock rename to libs/api-admentum/yarn.lock diff --git a/libs/api-admentum2/lib/api.test.ts b/libs/api-admentum2/lib/api.test.ts deleted file mode 100644 index d31676aa3..000000000 --- a/libs/api-admentum2/lib/api.test.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { ApiAdmentum } from './api' - -describe('api', () => { - let api: ApiAdmentum - - beforeEach(() => { - api = new ApiAdmentum() - }) - - test('should request and return calendar items', async () => { - expect((await api.getCalendar())[0]).toMatchObject({ - allDay: false, - endDate: '2023-08-07T07:30:00.000Z', - id: 2990834, - location: '', - startDate: '2023-08-07T06:00:00.000Z', - title: 'Matematik', - }) - }) -}) diff --git a/libs/api-admentum2/lib/api.ts b/libs/api-admentum2/lib/api.ts deleted file mode 100644 index 2b6337fef..000000000 --- a/libs/api-admentum2/lib/api.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { EventEmitter } from 'events' - -import { CalendarItem } from '@skolplattformen/api' - -import * as fake from './fakeData' -import { parseDate } from './parse' - -const fakeResponse = (data: T): Promise => - new Promise((res) => setTimeout(() => res(data), 200 + Math.random() * 800)) - -export class ApiAdmentum extends EventEmitter { - public async getCalendar(): Promise { - const events = await fakeResponse(fake.calendar) - - return events.map( - ({ - id, - title, - start_date: startDate, - end_date: endDate, - schedule_event: { start_time: startTime, end_time: endTime }, - }: any) => ({ - id, - title, - location: '', - allDay: startTime === '00:00:00', - startDate: parseDate(startDate + 'T' + startTime), - endDate: parseDate(endDate + 'T' + endTime), - }) - ) - } -} diff --git a/libs/api-admentum2/lib/fakeData.ts b/libs/api-admentum2/lib/fakeData.ts deleted file mode 100644 index 9c426867e..000000000 --- a/libs/api-admentum2/lib/fakeData.ts +++ /dev/null @@ -1,32 +0,0 @@ -export const calendar = [ - { - url: 'https://skola.admentum.se/api/v1/schedule_event_instances/2990834/?format=api', - id: 2990834, - school_id: 824, - start_date: '2023-08-07', - end_date: '2023-08-07', - schedule_event: { - url: 'https://skola.admentum.se/api/v1/schedule_events/148722/?format=api', - id: 148722, - eid: null, - schedule_id: 4385, - start_time: '08:00:00', - end_time: '09:30:00', - rooms: [], - teachers: [ - { - url: 'https://skola.admentum.se/api/v1/users/437302/?format=api', - id: 437302, - }, - ], - schedule_groups: [], - primary_groups: [ - { - url: 'https://skola.admentum.se/api/v1/primary_groups/36874/?format=api', - id: 36874, - }, - ], - weekly_interval: '', - }, - }, -] diff --git a/libs/api-admentum2/lib/parse.ts b/libs/api-admentum2/lib/parse.ts deleted file mode 100644 index 8e60abb0e..000000000 --- a/libs/api-admentum2/lib/parse.ts +++ /dev/null @@ -1,7 +0,0 @@ -export const parseDate = (input?: string): string | undefined => { - if (!input) { - return undefined - } - - return new Date(input).toISOString() -} diff --git a/libs/api-admentum2/run.js b/libs/api-admentum2/run.js deleted file mode 100644 index 543e714ad..000000000 --- a/libs/api-admentum2/run.js +++ /dev/null @@ -1,124 +0,0 @@ -function requestLogger(httpModule) { - var original = httpModule.request - httpModule.request = function (options, callback) { - console.log('-----------------------------------------------') - console.log( - options.href || options.proto + '://' + options.host + options.path, - options.method - ) - console.log(options.headers) - console.log('-----------------------------------------------') - return original(options, callback) - } -} - -requestLogger(require('http')) -requestLogger(require('https')) - -const { DateTime } = require('luxon') -const nodeFetch = require('node-fetch') -const { CookieJar } = require('tough-cookie') -const fetchCookie = require('fetch-cookie/node-fetch') -const { writeFile } = require('fs/promises') -const path = require('path') -const fs = require('fs') -const { inspect } = require('util') -const init = require('@skolplattformen/api-admentum').default - -const [, , personalNumber] = process.argv - -if (!personalNumber) { - console.error( - 'You must pass in a valid personal number, eg `node run 197001011111`' - ) - process.exit(1) -} - -function ensureDirectoryExistence(filePath) { - var dirname = path.dirname(filePath) - if (fs.existsSync(dirname)) { - return true - } - ensureDirectoryExistence(dirname) - fs.mkdirSync(dirname) -} - -const record = async (info, data) => { - const name = info.error ? `${info.name}_error` : info.name - const filename = `./record/${name}.json` - ensureDirectoryExistence(filename) - const content = { - url: info.url, - headers: info.headers, - status: info.status, - statusText: info.statusText, - } - if (data) { - switch (info.type) { - case 'json': - content.json = data - break - case 'text': - content.text = data - break - case 'blob': - // eslint-disable-next-line no-case-declarations - const buffer = await data.arrayBuffer() - content.blob = Buffer.from(buffer).toString('base64') - break - } - } else if (info.error) { - const { message, stack } = info.error - content.error = { - message, - stack, - } - } - await writeFile(filename, JSON.stringify(content, null, 2)) -} - -async function run() { - const cookieJar = new CookieJar() - const fetch = fetchCookie(nodeFetch, cookieJar) - - try { - const api = init(fetch, cookieJar, { record }) - console.log('inited...') - - api.on('login', async () => { - console.log('Logged in!') - await api.getUser() - const children = await api.getChildren() - const now = DateTime.fromJSDate(new Date()) - for (let i = 0; i < children.length; i++) { - const c = children[i] - await api.getCalendar(c) - await api.getNotifications(c) - await api.getTimetable(c, 44, 2021, 'ignored') - } - const news = await api.getNews() - // const news = await api.getNews() - // //console.table(news.map(n => ({ id: n.id, author: n.author, published: n.published}))) - // //news.length && console.log(news[0]) - - // const notifications = await api.getNotifications(children[2]) - // //const ns = notifications.map(n => ({id: n.id, sender: n.sender, type: n.type})) - // //console.table(ns) - // console.log("notifications count", notifications.length) - // notifications.slice(0, 10).forEach(console.log) - - // await api.getCalendar(children[1]) - - // await api.getTimetable(children[1], 38, 2021, "en") - - // await api.getClassmates() - // console.table(schema) - }) - const res = await api.login(personalNumber) - console.log(res) - } catch (err) { - console.error(err) - } -} - -run() diff --git a/tsconfig.base.json b/tsconfig.base.json index ad4198c61..18771cd87 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -15,6 +15,7 @@ "baseUrl": ".", "paths": { "@skolplattformen/api": ["libs/api/lib/index.ts"], + "@skolplattformen/api-admentum": ["libs/api-admentum/lib/index.ts"], "@skolplattformen/api-hjarntorget": ["libs/api-hjarntorget/lib/index.ts"], "@skolplattformen/api-skolplattformen": [ "libs/api-skolplattformen/lib/index.ts" diff --git a/workspace.json b/workspace.json index aefb956d1..7515d8e35 100644 --- a/workspace.json +++ b/workspace.json @@ -1,14 +1,14 @@ { "version": 2, "projects": { - "api-hjarntorget": "libs/api-hjarntorget", + "api": "libs/api", "api-admentum": "libs/api-admentum", + "api-hjarntorget": "libs/api-hjarntorget", "api-skolplattformen": "libs/api-skolplattformen", + "api-test-app": "apps/api-test-app", "api-vklass": "libs/api-vklass", - "api": "libs/api", "curriculum": "libs/curriculum", "hooks": "libs/hooks", - "api-test-app": "apps/api-test-app", "skolplattformen-app": "apps/skolplattformen-app" } } From 6289c434706612406243c0d55f6445a9be328bd3 Mon Sep 17 00:00:00 2001 From: Christian Landgren Date: Fri, 8 Sep 2023 16:23:31 +0200 Subject: [PATCH 04/30] feat: add init url and start bankId session --- libs/api-admentum/README.md | 5 +++++ libs/api-admentum/lib/apiAdmentum.ts | 21 +++++++++++++++++---- libs/api-admentum/lib/routes.ts | 5 ++++- 3 files changed, 26 insertions(+), 5 deletions(-) create mode 100644 libs/api-admentum/README.md diff --git a/libs/api-admentum/README.md b/libs/api-admentum/README.md new file mode 100644 index 000000000..1351bc461 --- /dev/null +++ b/libs/api-admentum/README.md @@ -0,0 +1,5 @@ +## Admentum API + +To test + + nx serve api-test-app --args=1212121212,admentum diff --git a/libs/api-admentum/lib/apiAdmentum.ts b/libs/api-admentum/lib/apiAdmentum.ts index 16b3edd0e..06bdf109d 100644 --- a/libs/api-admentum/lib/apiAdmentum.ts +++ b/libs/api-admentum/lib/apiAdmentum.ts @@ -29,7 +29,12 @@ import * as html from 'node-html-parser' import { fakeFetcher } from './fake/fakeFetcher' import { checkStatus, DummyStatusChecker } from './loginStatus' import { extractMvghostRequestBody, parseCalendarItem } from './parse/parsers' -import { bankIdInitUrl, bankIdCheckUrl, apiUrls } from './routes' +import { + bankIdInitUrl, + bankIdCheckUrl, + bankIdSessionUrl, + apiUrls, +} from './routes' import parse from '@skolplattformen/curriculum' function getDateOfISOWeek(week: number, year: number) { @@ -281,12 +286,20 @@ export class ApiAdmentum extends EventEmitter implements Api { return this.fakeMode() this.isFake = false - const sessionId = await this.fetch('init-session', bankIdInitUrl('')) - .then((res) => res.text()) - .then((text) => /sessionsid=(.)/.exec(text)?.[0]) + const sessionId = await this.fetch('get-session', bankIdSessionUrl('')) + .then((res) => { + console.log('got res', res, (res as any).url) + return (res as any).url + }) + .then((url) => url.split('=').pop()) // https://login.grandid.com/?sessionid=234324 if (!sessionId) throw new Error('No session provided') + this.fetch('bankid-init', bankIdInitUrl(sessionId), { + method: 'POST', + body: 'ssn=' + personalNumber, + }) + console.log('start polling', sessionId) const statusChecker = checkStatus(this.fetch, bankIdCheckUrl(sessionId)) diff --git a/libs/api-admentum/lib/routes.ts b/libs/api-admentum/lib/routes.ts index f9bbc2796..fe4eff45f 100644 --- a/libs/api-admentum/lib/routes.ts +++ b/libs/api-admentum/lib/routes.ts @@ -46,5 +46,8 @@ export const apiUrls = { export const bankIdCheckUrl = (sessionId: string) => `https://login.grandid.com/?sessionid=${sessionId}&eleg=1&bankid=1` -export const bankIdInitUrl = (returnUrl: string) => +export const bankIdSessionUrl = (returnUrl: string) => `https://auth.admentum.se/larande${returnUrl ? `?next=${returnUrl}` : ''}` + +export const bankIdInitUrl = (sessionId: string) => + `https://login.grandid.com/?sessionid=${sessionId}&eleg=1&bankid=1&bChoice=other` From 58a0ed9825b0878534bff7c9cc23fc0fa6c44f1c Mon Sep 17 00:00:00 2001 From: Christian Landgren Date: Thu, 14 Sep 2023 11:48:26 +0200 Subject: [PATCH 05/30] feat: bankId login almost working --- libs/api-admentum/lib/apiAdmentum.ts | 19 ++++++++---- libs/api-admentum/lib/loginStatus.ts | 44 +++++++++++++++++----------- libs/api-admentum/lib/routes.ts | 2 +- 3 files changed, 42 insertions(+), 23 deletions(-) diff --git a/libs/api-admentum/lib/apiAdmentum.ts b/libs/api-admentum/lib/apiAdmentum.ts index 06bdf109d..daa753f23 100644 --- a/libs/api-admentum/lib/apiAdmentum.ts +++ b/libs/api-admentum/lib/apiAdmentum.ts @@ -286,17 +286,26 @@ export class ApiAdmentum extends EventEmitter implements Api { return this.fakeMode() this.isFake = false - const sessionId = await this.fetch('get-session', bankIdSessionUrl('')) - .then((res) => { - console.log('got res', res, (res as any).url) + const url = await this.fetch('get-session', bankIdSessionUrl('')).then( + (res) => { + console.log('got res', res, (res as any).url, res.headers) return (res as any).url - }) - .then((url) => url.split('=').pop()) // https://login.grandid.com/?sessionid=234324 + } + ) + await this.fetch('get-cookies', url) + + // https://login.grandid.com/?sessionid=234324 + // => 234324 + const sessionId = url.split('=').pop() if (!sessionId) throw new Error('No session provided') + console.log('url', bankIdInitUrl(sessionId)) this.fetch('bankid-init', bankIdInitUrl(sessionId), { method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, body: 'ssn=' + personalNumber, }) diff --git a/libs/api-admentum/lib/loginStatus.ts b/libs/api-admentum/lib/loginStatus.ts index 8c3d5f77b..fa882cd2b 100644 --- a/libs/api-admentum/lib/loginStatus.ts +++ b/libs/api-admentum/lib/loginStatus.ts @@ -21,23 +21,33 @@ export class GrandidChecker extends EventEmitter implements LoginStatusChecker { } async check(): Promise { - // try { - // console.log('polling bankid signature') - // // https://mNN-mg-local.idp.funktionstjanster.se/mg-local/auth/ccp11/grp/pollstatus - // if (true) - // this.emit('OK') - // } else if (isError) { - // console.log('polling error') - // this.emit('ERROR') - // } else if (!this.cancelled && keepPolling) { - // console.log('keep on polling...') - // this.emit('PENDING') - // setTimeout(() => this.check(), 3000) - // } - // } catch (er) { - // console.log('Error validating login to Hjärntorget', er) - // this.emit('ERROR') - // } + try { + console.log('polling bankid signature', this.basePollingUrl) + const result = await this.fetcher( + 'bankid-checker', + bankIdCheckUrl(this.basePollingUrl) + ).then((res) => { + console.log('checker response', res) + return res.text() + }) + console.log('bankid result', result) + const ok = result.includes('OK') + const isError = result.includes('Unauthorized') + // https://mNN-mg-local.idp.funktionstjanster.se/mg-local/auth/ccp11/grp/pollstatus + if (ok) { + this.emit('OK') + } else if (isError) { + console.log('polling error') + this.emit('ERROR') + } else if (!this.cancelled) { + console.log('keep on polling...') + this.emit('PENDING') + setTimeout(() => this.check(), 3000) + } + } catch (err) { + console.log('Error validating login to Hjärntorget', err) + this.emit('ERROR') + } } async cancel(): Promise { diff --git a/libs/api-admentum/lib/routes.ts b/libs/api-admentum/lib/routes.ts index fe4eff45f..47cb23e4b 100644 --- a/libs/api-admentum/lib/routes.ts +++ b/libs/api-admentum/lib/routes.ts @@ -44,7 +44,7 @@ export const apiUrls = { } export const bankIdCheckUrl = (sessionId: string) => - `https://login.grandid.com/?sessionid=${sessionId}&eleg=1&bankid=1` + `https://login.grandid.com/?sessionid=${sessionId}&collect=1` export const bankIdSessionUrl = (returnUrl: string) => `https://auth.admentum.se/larande${returnUrl ? `?next=${returnUrl}` : ''}` From bd6a808a7eda9038558a11b6bf708c96203ce9da Mon Sep 17 00:00:00 2001 From: Christian Landgren Date: Thu, 14 Sep 2023 17:04:34 +0200 Subject: [PATCH 06/30] feat: bankId flow now works in test app --- apps/api-test-app/src/main.js | 5 +- libs/api-admentum/lib/apiAdmentum.test.ts | 288 ---------------------- libs/api-admentum/lib/apiAdmentum.ts | 5 +- libs/api-admentum/lib/loginStatus.ts | 16 +- 4 files changed, 13 insertions(+), 301 deletions(-) delete mode 100644 libs/api-admentum/lib/apiAdmentum.test.ts diff --git a/apps/api-test-app/src/main.js b/apps/api-test-app/src/main.js index 249fdb707..bbfb41c0e 100644 --- a/apps/api-test-app/src/main.js +++ b/apps/api-test-app/src/main.js @@ -181,8 +181,8 @@ function ensureDirectoryExistence(filePath) { } function getSessionCookieFromCookieJar() { - const cookieUrl = isHjarntorget - ? 'https://admentum.goteborg.se' + const cookieUrl = isAdmentum + ? 'https://admentum.se' : 'https://etjanst.stockholm.se' const cookies = cookieJar.getCookiesSync(cookieUrl) const sessionCookieKey = isAdmentum ? 'JSESSIONID' : 'SMSESSION' @@ -193,6 +193,7 @@ const record = async (info, data) => { const name = info.error ? `${info.name}_error` : info.name const filename = `${recordFolder}/${name}.json` ensureDirectoryExistence(filename) + console.log('recording session', filename) const content = { url: info.url, headers: info.headers, diff --git a/libs/api-admentum/lib/apiAdmentum.test.ts b/libs/api-admentum/lib/apiAdmentum.test.ts deleted file mode 100644 index 2f6a16f0d..000000000 --- a/libs/api-admentum/lib/apiAdmentum.test.ts +++ /dev/null @@ -1,288 +0,0 @@ -import { wrapToughCookie } from '@skolplattformen/api' -import { CookieJar } from 'tough-cookie' -import { ApiHjarntorget } from './apiAdmentum' - -const setupSuccessfullLoginInitiation = (fetcherMock: jest.Mock) => { - // 'begin-login' - fetcherMock.mockReturnValueOnce( - Promise.resolve({ - url: 'some url with url encoded at the end?return=hello', - }) - ) - - // 'init-shibboleth-login' - fetcherMock.mockReturnValueOnce( - Promise.resolve({ - url: 'some url with url encoded at the end?Target=hello', - }) - ) - - // 'init-bankId' - fetcherMock.mockReturnValueOnce( - Promise.resolve({ - text: jest.fn().mockReturnValue( - Promise.resolve(` - - - - - - `) - ), - }) - ) - - // 'pick-mvghost' - fetcherMock.mockReturnValueOnce( - Promise.resolve({ - url: 'some url to a mvghost', - }) - ) - - // 'start-bankId' - fetcherMock.mockReturnValueOnce( - Promise.resolve({ - url: 'some base url to a mvghost to use when polling status', - }) - ) -} - -const setupSuccessfullBankIdLogin = (fetcherMock: jest.Mock) => { - // 'poll-bankid-status' - fetcherMock.mockReturnValueOnce( - Promise.resolve({ - json: jest.fn().mockReturnValue( - Promise.resolve({ - infotext: '', - location: 'an url to go to confirm the login', - }) - ), - }) - ) - - // 'confirm-signature-redirect' - fetcherMock.mockReturnValueOnce( - Promise.resolve({ - text: jest.fn().mockReturnValue( - Promise.resolve(` - - - - - - `) - ), - }) - ) - - // 'authgbg-saml-login' - fetcherMock.mockReturnValueOnce( - Promise.resolve({ - text: jest.fn().mockReturnValue( - Promise.resolve(` - - - - - - `) - ), - }) - ) - - // 'admentum-saml-login' - fetcherMock.mockReturnValueOnce(Promise.resolve({ status: 200 })) -} - -describe('api', () => { - let fetcherMock: jest.Mock - let api: ApiHjarntorget - - beforeEach(() => { - const fetcher = jest.fn() - fetcherMock = fetcher as jest.Mock - - const cookieManager = wrapToughCookie(new CookieJar()) - cookieManager.clearAll() - api = new ApiHjarntorget(jest.fn(), cookieManager) - api.replaceFetcher(fetcher) - }) - it('works', () => { - expect(1 + 1).toBe(2) - }) - // describe('#login', () => { - // it('goes through single sing-on steps', async (done) => { - // setupSuccessfullLoginInitiation(fetcherMock) - // setupSuccessfullBankIdLogin(fetcherMock) - // const personalNumber = 'my personal number' - - // const loginComplete = new Promise((resolve, reject) => { - // api.on('login', () => done()) - // }); - // await api.login(personalNumber) - // }) - // it('checker emits PENDING', async (done) => { - // // 'poll-bankid-status' - // fetcherMock.mockReturnValueOnce(Promise.resolve({ - // json: jest.fn().mockReturnValue(Promise.resolve({ - // infotext: "some prompt to do signing in app", - // location: "" - // })) - // })) - - // const status = checkStatus(fetcherMock, "some url") - // status.on('PENDING', () => { - // status.cancel() - // done() - // }) - // }) - // it('checker emits ERROR', async (done) => { - // // 'poll-bankid-status' - // fetcherMock.mockReturnValueOnce(Promise.resolve({ - // json: jest.fn().mockReturnValue(Promise.resolve({ - // infotext: "some prompt to do signing in app", - // location: "url with error in the name" - // })) - // })) - - // const status = checkStatus(fetcherMock, "some url") - // status.on('ERROR', () => { - // status.cancel() - // done() - // }) - // }) - // it('checker emits ERROR when an exception occurs', async (done) => { - // // 'poll-bankid-status' - // fetcherMock.mockReturnValueOnce(Promise.resolve({ - // json: jest.fn().mockReturnValue(Promise.resolve({ - // infotext: undefined, - // location: undefined - // })) - // })) - - // const status = checkStatus(fetcherMock, "some url") - // status.on('ERROR', () => { - // status.cancel() - // done() - // }) - // }) - // it('remembers used personal number', async (done) => { - // setupSuccessfullLoginInitiation(fetcherMock) - // setupSuccessfullBankIdLogin(fetcherMock) - // const personalNumber = 'my personal number' - // await api.login(personalNumber) - // api.on('login', () => { - // expect(api.getPersonalNumber()).toEqual(personalNumber) - // done() - // }) - // }) - // it('forgets used personal number if sign in is unsuccessful', async (done) => { - // setupSuccessfullLoginInitiation(fetcherMock) - // // 'poll-bankid-status' - // fetcherMock.mockReturnValueOnce(Promise.resolve({ - // json: jest.fn().mockReturnValue(Promise.resolve({ - // infotext: "", - // location: "an url to go to confirm the login" - // })) - // })) - // // 'confirm-signature-redirect' - // fetcherMock.mockReturnValueOnce(Promise.resolve({ - // text: Promise.resolve("some error occured") - // })) - - // const personalNumber = 'my personal number' - // const status = await api.login(personalNumber) - - // status.on('ERROR', () => { - // expect(api.getPersonalNumber()).toEqual(undefined) - // done() - // }) - // }) - - // // TODO: Possibly rewrite the mocking so we mock the responses more properly, - // // that way it would be possible to implement a throwIfNotOk wrapper for the - // // fetch calls. - // // it('throws error on external api error', async () => { - // // const personalNumber = 'my personal number' - // // try { - // // await api.login(personalNumber) - // // } catch (error: any) { - // // expect(error.message).toEqual(expect.stringContaining('Server Error')) - // // } - // // }) - // }) - // describe('#logout', () => { - // // it('clears session', async () => { - // // await api.logout() - // // const session = await api.getSession('') - // // expect(session).toEqual({ - // // headers: { - // // cookie: '', - // // }, - // // }) - // // }) - // it('emits logout event', async () => { - // const listener = jest.fn() - // api.on('logout', listener) - // await api.logout() - // expect(listener).toHaveBeenCalled() - // }) - // it('sets .isLoggedIn', async () => { - // api.isLoggedIn = true - // await api.logout() - // expect(api.isLoggedIn).toBe(false) - // }) - // it('forgets personalNumber', async () => { - // // eslint-disable-next-line @typescript-eslint/no-explicit-any - // (api as any).personalNumber = 'my personal number' - // api.isLoggedIn = true - - // await api.logout() - - // expect(api.getPersonalNumber()).toEqual(undefined) - // }) - // }) - /* - describe('fake', () => { - it('sets fake mode for the correct pnr:s', async () => { - let status - - status = await api.login('121212121212') - expect(status.token).toEqual('fake') - - status = await api.login('201212121212') - expect(status.token).toEqual('fake') - - status = await api.login('1212121212') - expect(status.token).toEqual('fake') - }) - it('delivers fake data', async (done) => { - api.on('login', async () => { - const user = await api.getUser() - expect(user).toEqual({ - firstName: 'Namn', - lastName: 'Namnsson', - isAuthenticated: true, - personalNumber: "195001182046", - }) - - const children = await api.getChildren() - expect(children).toHaveLength(2) - - const calendar1 = await api.getCalendar(children[0]) - expect(calendar1).toHaveLength(20) - const calendar2 = await api.getCalendar(children[1]) - expect(calendar2).toHaveLength(18) - - const skola24Children = await api.getSkola24Children() - expect(skola24Children).toHaveLength(1) - - const timetable = await api.getTimetable(skola24Children[0], 2021, 15, 'sv') - expect(timetable).toHaveLength(32) - - done() - }) - await api.login('121212121212') - }) - })*/ -}) diff --git a/libs/api-admentum/lib/apiAdmentum.ts b/libs/api-admentum/lib/apiAdmentum.ts index daa753f23..b26f7a1b5 100644 --- a/libs/api-admentum/lib/apiAdmentum.ts +++ b/libs/api-admentum/lib/apiAdmentum.ts @@ -292,16 +292,15 @@ export class ApiAdmentum extends EventEmitter implements Api { return (res as any).url } ) - await this.fetch('get-cookies', url) - // https://login.grandid.com/?sessionid=234324 // => 234324 const sessionId = url.split('=').pop() + console.log('sessionId', sessionId) if (!sessionId) throw new Error('No session provided') console.log('url', bankIdInitUrl(sessionId)) - this.fetch('bankid-init', bankIdInitUrl(sessionId), { + await this.fetch('bankid-init', bankIdInitUrl(sessionId), { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', diff --git a/libs/api-admentum/lib/loginStatus.ts b/libs/api-admentum/lib/loginStatus.ts index fa882cd2b..440aa347d 100644 --- a/libs/api-admentum/lib/loginStatus.ts +++ b/libs/api-admentum/lib/loginStatus.ts @@ -23,16 +23,16 @@ export class GrandidChecker extends EventEmitter implements LoginStatusChecker { async check(): Promise { try { console.log('polling bankid signature', this.basePollingUrl) - const result = await this.fetcher( - 'bankid-checker', - bankIdCheckUrl(this.basePollingUrl) - ).then((res) => { - console.log('checker response', res) - return res.text() + const result = await this.fetcher('bankid-checker', this.basePollingUrl, { + headers: { + 'x-requested-with': 'XMLHttpRequest', + }, + }).then((res) => { + return res.json() }) console.log('bankid result', result) - const ok = result.includes('OK') - const isError = result.includes('Unauthorized') + const ok = result.response?.status === 'complete' + const isError = result.response?.status === 'error' // https://mNN-mg-local.idp.funktionstjanster.se/mg-local/auth/ccp11/grp/pollstatus if (ok) { this.emit('OK') From 3b7942ac1a6119ee0df39a568325ff8636233eba Mon Sep 17 00:00:00 2001 From: Christian Landgren Date: Fri, 15 Sep 2023 09:11:29 +0200 Subject: [PATCH 07/30] Added .aider* to .gitignore. --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index b2d685ab5..41f48a028 100644 --- a/.gitignore +++ b/.gitignore @@ -110,3 +110,4 @@ xcshareddata xcuserdata /.vs *.hprof +.aider* From 23f81cf11340692652f38634c35e002f1475138b Mon Sep 17 00:00:00 2001 From: Christian Landgren Date: Fri, 15 Sep 2023 09:12:54 +0200 Subject: [PATCH 08/30] aider: Updated React Native version to 0.67.3 in package.json. # Aider chat conversation: USER: help me upgrade react native to a version compatible with the rest of the dependencies ASSISTANT: Sure, I can guide you through the process of upgrading React Native to a version compatible with the rest of your dependencies. However, I need to know the current version of React Native you're using and the versions of the other dependencies in your project. Please share your `package.json` file. This file contains the list of dependencies and their versions. Once you've shared the `package.json` file, I can provide you with the steps to upgrade React Native. USER: I added these *read-write* files: package.json ASSISTANT: Thank you for providing the `package.json` file. From the provided `package.json` file, I can see that the current version of React Native in your project is `0.66.4`. To upgrade React Native, you can use the `react-native upgrade` command which will automatically upgrade your project to the latest version. However, this command might not always work as expected, especially for major version upgrades. Alternatively, you can manually update the version of React Native in your `package.json` file and then run `yarn install` or `npm install` to install the new version. Here's how you can manually upgrade React Native to version `0.67.3` (the latest stable version at the time of writing): ```json package.json <<<<<<< HEAD "react-native": "0.66.4", ======= "react-native": "0.67.3", >>>>>>> updated ``` After updating the `package.json` file, run `yarn install` or `npm install` to install the new version of React Native. Please note that upgrading React Native might require you to make additional changes to your native iOS and Android projects. You can refer to the [React Native Upgrade Guide](https://react-native-community.github.io/upgrade-helper/) for detailed instructions on how to upgrade your project. Also, remember to test your application thoroughly after the upgrade to ensure that everything still works as expected. --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index f689bb565..bd395a66e 100644 --- a/package.json +++ b/package.json @@ -53,7 +53,7 @@ "personnummer": "^3.1.4", "react": "17.0.2", "react-dom": "17.0.2", - "react-native": "0.66.4", + "react-native": "0.67.3", "react-native-animatable": "^1.3.3", "react-native-async-storage": "^0.0.1", "react-native-calendar-events": "2.2.0", From 501ec8030463065e510356e70e9b24c90acd28a5 Mon Sep 17 00:00:00 2001 From: William Ryder Date: Fri, 15 Sep 2023 10:11:47 +0200 Subject: [PATCH 09/30] wip: fetch children from adentum --- libs/api-admentum/lib/apiAdmentum.ts | 36 ++++++++++++++++------------ libs/api-admentum/lib/routes.ts | 3 +++ 2 files changed, 24 insertions(+), 15 deletions(-) diff --git a/libs/api-admentum/lib/apiAdmentum.ts b/libs/api-admentum/lib/apiAdmentum.ts index 06bdf109d..3b591df28 100644 --- a/libs/api-admentum/lib/apiAdmentum.ts +++ b/libs/api-admentum/lib/apiAdmentum.ts @@ -34,6 +34,7 @@ import { bankIdCheckUrl, bankIdSessionUrl, apiUrls, + getUserUrl, } from './routes' import parse from '@skolplattformen/curriculum' @@ -153,21 +154,22 @@ export class ApiAdmentum extends EventEmitter implements Api { if (!this.isLoggedIn) { throw new Error('Not logged in...') } - console.log('fetching children') - - const myChildrenResponseJson: any[] = [] - - return myChildrenResponseJson.map( - (c) => - ({ - id: c.id, - sdsId: c.id, - personGuid: c.id, - firstName: c.firstName, - lastName: c.lastName, - name: `${c.firstName} ${c.lastName}`, - } as Skola24Child & EtjanstChild) - ) + const testUserId = '436838' + const fetchUrl = apiUrls.users + '/' + testUserId + console.log('fetching children for user id', testUserId, 'from', fetchUrl) + const currentUserResponse = await this.fetch('current-user', fetchUrl) + if (currentUserResponse.status !== 200) { + throw new Error('Could not fetch children. Response code: ' + currentUserResponse.status) + } + const myChildrenResponseJson = await currentUserResponse.json(); + return myChildrenResponseJson.students.map((student: { id: any; first_name: any; last_name: any }) => ({ + id: student.id, + sdsId: student.id, + personGuid: student.id, + firstName: student.first_name, + lastName: student.last_name, + name: `${student.first_name} ${student.last_name}`, + }) as Skola24Child & EtjanstChild); } async getCalendar(child: EtjanstChild): Promise { @@ -285,6 +287,8 @@ export class ApiAdmentum extends EventEmitter implements Api { if (personalNumber !== undefined && personalNumber.endsWith('1212121212')) return this.fakeMode() + const testChildren = await this.getChildren() + console.log('login adentum', personalNumber) this.isFake = false const sessionId = await this.fetch('get-session', bankIdSessionUrl('')) .then((res) => { @@ -293,6 +297,8 @@ export class ApiAdmentum extends EventEmitter implements Api { }) .then((url) => url.split('=').pop()) // https://login.grandid.com/?sessionid=234324 + console.log('test children', testChildren) + console.log('adentum session id', sessionId) if (!sessionId) throw new Error('No session provided') this.fetch('bankid-init', bankIdInitUrl(sessionId), { diff --git a/libs/api-admentum/lib/routes.ts b/libs/api-admentum/lib/routes.ts index fe4eff45f..e2f3ebd4d 100644 --- a/libs/api-admentum/lib/routes.ts +++ b/libs/api-admentum/lib/routes.ts @@ -51,3 +51,6 @@ export const bankIdSessionUrl = (returnUrl: string) => export const bankIdInitUrl = (sessionId: string) => `https://login.grandid.com/?sessionid=${sessionId}&eleg=1&bankid=1&bChoice=other` + +export const getUserUrl = (userId: string) => + `https://skola.admentum.se/api/v1/users/${userId}` \ No newline at end of file From 8912342541cc769571bca882fee001ecaf5084bb Mon Sep 17 00:00:00 2001 From: William Ryder Date: Fri, 15 Sep 2023 13:20:58 +0200 Subject: [PATCH 10/30] wip: request headers --- libs/api-admentum/lib/apiAdmentum.ts | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/libs/api-admentum/lib/apiAdmentum.ts b/libs/api-admentum/lib/apiAdmentum.ts index 6a1c27c6d..324ff2644 100644 --- a/libs/api-admentum/lib/apiAdmentum.ts +++ b/libs/api-admentum/lib/apiAdmentum.ts @@ -156,9 +156,21 @@ export class ApiAdmentum extends EventEmitter implements Api { } const testUserId = '436838' const fetchUrl = apiUrls.users + '/' + testUserId - console.log('fetching children for user id', testUserId, 'from', fetchUrl) - const currentUserResponse = await this.fetch('current-user', fetchUrl) + console.log('v3.1 fetching children for user id', testUserId, 'from', fetchUrl) + const currentUserResponse = await this.fetch('current-user', getUserUrl(testUserId), { + method: 'GET', + headers: { + 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8', + 'Accept-Encoding': 'gzip, deflate, br', + 'Accept-Language': 'en-GB,en;q=0.8', + 'Cookie': 'active_school_id=X; csrftoken=Y; sessionid=Z', + 'Referer': 'https://skola.admentum.se/api/v1/users/436838/', + }, + }) + + if (currentUserResponse.status !== 200) { + console.error('Error headers', currentUserResponse.headers) throw new Error('Could not fetch children. Response code: ' + currentUserResponse.status) } const myChildrenResponseJson = await currentUserResponse.json(); @@ -287,7 +299,7 @@ export class ApiAdmentum extends EventEmitter implements Api { if (personalNumber !== undefined && personalNumber.endsWith('1212121212')) return this.fakeMode() - const testChildren = await this.getChildren() + console.log('login adentum', personalNumber) this.isFake = false const url = await this.fetch('get-session', bankIdSessionUrl('')).then( @@ -300,8 +312,6 @@ export class ApiAdmentum extends EventEmitter implements Api { // => 234324 const sessionId = url.split('=').pop() console.log('sessionId', sessionId) - - console.log('test children', testChildren) console.log('adentum session id', sessionId) if (!sessionId) throw new Error('No session provided') @@ -322,6 +332,8 @@ export class ApiAdmentum extends EventEmitter implements Api { // not sure if it is needed or if the cookies are enough for fetching all info... this.isLoggedIn = true this.personalNumber = personalNumber + const testChildren = await this.getChildren() + console.log('test children', testChildren) this.emit('login') }) statusChecker.on('ERROR', () => { From 0ff5fad4d9c6c05556bca1db9d6ad97505379a93 Mon Sep 17 00:00:00 2001 From: Christian Landgren Date: Fri, 15 Sep 2023 14:59:58 +0200 Subject: [PATCH 11/30] fix: try to get callback auth to work --- apps/api-test-app/src/main.js | 15 ++-------- libs/api-admentum/lib/apiAdmentum.ts | 20 +++++++++----- libs/api-admentum/lib/routes.ts | 6 +++- libs/api/lib/types.ts | 6 ++-- yarn.lock | 41 ++++++++++++++++------------ 5 files changed, 47 insertions(+), 41 deletions(-) diff --git a/apps/api-test-app/src/main.js b/apps/api-test-app/src/main.js index bbfb41c0e..6c1123d58 100644 --- a/apps/api-test-app/src/main.js +++ b/apps/api-test-app/src/main.js @@ -36,18 +36,9 @@ async function run() { api.on('login', async () => { console.log('Logged in') - if (bankIdUsed) { - const sessionCookie = getSessionCookieFromCookieJar() - ensureDirectoryExistence(recordFolder) - await writeFile( - `${recordFolder}/latestSessionCookie.txt`, - JSON.stringify(sessionCookie) - ) - console.log( - `Session cookie saved to file ${recordFolder}/latesSessionCookie.txt` - ) - } - console.log('user') //- + const cookies = cookieJar.toJSON() + + console.log('cookies', cookies) const user = await api.getUser() console.log(user) diff --git a/libs/api-admentum/lib/apiAdmentum.ts b/libs/api-admentum/lib/apiAdmentum.ts index b26f7a1b5..002c80956 100644 --- a/libs/api-admentum/lib/apiAdmentum.ts +++ b/libs/api-admentum/lib/apiAdmentum.ts @@ -33,6 +33,7 @@ import { bankIdInitUrl, bankIdCheckUrl, bankIdSessionUrl, + bankIdCallbackUrl, apiUrls, } from './routes' import parse from '@skolplattformen/curriculum' @@ -127,20 +128,26 @@ export class ApiAdmentum extends EventEmitter implements Api { } async setSessionCookie(sessionCookie: string): Promise { - // this.cookieManager.setCookieString(sessionCookie, admentumUrl) + if (!sessionCookie) throw Error('cookie required') + this.cookieManager.setCookieString( + `sessionid=${sessionCookie}; Path=/;`, + 'skola.admentum.se' + ) const user = await this.getUser() if (!user.isAuthenticated) { throw new Error('Session cookie is expired') } - - this.isLoggedIn = true - this.emit('login') } async getUser(): Promise { console.log('fetching user') - const currentUserResponse = await this.fetch('current-user', apiUrls.users) // + /id? + const userId = '437302' + const currentUserResponse = await this.fetch( + 'current-user', + apiUrls.user(userId) + ) // + /id? + console.log('current-user', currentUserResponse) if (currentUserResponse.status !== 200) { return { isAuthenticated: false } } @@ -312,10 +319,9 @@ export class ApiAdmentum extends EventEmitter implements Api { const statusChecker = checkStatus(this.fetch, bankIdCheckUrl(sessionId)) statusChecker.on('OK', async () => { - // setting these similar to how the sthlm api does it - // not sure if it is needed or if the cookies are enough for fetching all info... this.isLoggedIn = true this.personalNumber = personalNumber + this.setSessionCookie(sessionId) this.emit('login') }) statusChecker.on('ERROR', () => { diff --git a/libs/api-admentum/lib/routes.ts b/libs/api-admentum/lib/routes.ts index 47cb23e4b..c671d3f4f 100644 --- a/libs/api-admentum/lib/routes.ts +++ b/libs/api-admentum/lib/routes.ts @@ -40,7 +40,8 @@ export const apiUrls = { subjects: baseUrl + 'subjects', teachers: baseUrl + 'teachers', upper_secondary_subjects: baseUrl + 'upper_secondary_subjects', - users: baseUrl + 'users', + users: baseUrl + 'users?format=json', + user: (userId: string) => baseUrl + `users/${userId}/?format=json`, } export const bankIdCheckUrl = (sessionId: string) => @@ -51,3 +52,6 @@ export const bankIdSessionUrl = (returnUrl: string) => export const bankIdInitUrl = (sessionId: string) => `https://login.grandid.com/?sessionid=${sessionId}&eleg=1&bankid=1&bChoice=other` + +export const bankIdCallbackUrl = (grandSessionId: string) => + `https://auth.admentum.se/larande/callback?grandidsession=${grandSessionId}` diff --git a/libs/api/lib/types.ts b/libs/api/lib/types.ts index 9add943e4..ed7de48bd 100644 --- a/libs/api/lib/types.ts +++ b/libs/api/lib/types.ts @@ -88,8 +88,6 @@ export interface Classmate { guardians: Guardian[] } - - export interface Fetch { (url: string, init?: RequestInit): Promise } @@ -146,9 +144,9 @@ export interface RequestInit { /** * Set to `manual` to extract redirect headers, `error` to reject redirect */ redirect?: string + follow?: number // how many redirects should we allow to follow? } - /** * @export * @interface ScheduleItem @@ -215,7 +213,7 @@ export interface TimetableEntry extends Subject { dateEnd: string } - export interface Teacher { +export interface Teacher { id: number sisId: string firstname: string diff --git a/yarn.lock b/yarn.lock index 5a9ee5868..67c560470 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2236,10 +2236,10 @@ resolved "https://registry.yarnpkg.com/@react-native/assets/-/assets-1.0.0.tgz#c6f9bf63d274bafc8e970628de24986b30a55c8e" integrity sha512-KrwSpS1tKI70wuKl68DwJZYEvXktDHdZMG0k2AXD/rJVSlB23/X2CB2cutVR0HwNMJIal9HOUOBB2rVfa6UGtQ== -"@react-native/normalize-color@1.0.0": - version "1.0.0" - resolved "https://registry.yarnpkg.com/@react-native/normalize-color/-/normalize-color-1.0.0.tgz#c52a99d4fe01049102d47dc45d40cbde4f720ab6" - integrity sha512-xUNRvNmCl3UGCPbbHvfyFMnpvLPoOjDCcp5bT9m2k+TF/ZBklEQwhPZlkrxRx2NhgFh1X3a5uL7mJ7ZR+8G7Qg== +"@react-native/normalize-color@2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@react-native/normalize-color/-/normalize-color-2.0.0.tgz#da955909432474a9a0fe1cbffc66576a0447f567" + integrity sha512-Wip/xsc5lw8vsBlmY2MO/gFLp3MvuZ2baBZjDeTjjndMgM0h5sxz7AZR62RDPGgstp8Np7JzjvVqVT7tpFZqsw== "@react-native/polyfills@2.0.0": version "2.0.0" @@ -10924,7 +10924,15 @@ raw-loader@^4.0.2: loader-utils "^2.0.0" schema-utils "^3.0.0" -react-devtools-core@^4.13.0, react-devtools-core@^4.19.1: +react-devtools-core@4.19.1: + version "4.19.1" + resolved "https://registry.yarnpkg.com/react-devtools-core/-/react-devtools-core-4.19.1.tgz#bc37c2ef2f48f28c6af4c7292be9dca1b63deace" + integrity sha512-2wJiGffPWK0KggBjVwnTaAk+Z3MSxKInHmdzPTrBh1mAarexsa93Kw+WMX88+XjN+TtYgAiLe9xeTqcO5FfJTw== + dependencies: + shell-quote "^1.6.1" + ws "^7" + +react-devtools-core@^4.19.1: version "4.21.0" resolved "https://registry.yarnpkg.com/react-devtools-core/-/react-devtools-core-4.21.0.tgz#a54c9a0fd7261491e616d6c87d1869e011d8521d" integrity sha512-clGWwJHV5MHwTwYyKc+7FZHwzdbzrD2/AoZSkicUcr6YLc3Za9a9FaLhccWDHfjQ+ron9yzNhDT6Tv+FiPkD3g== @@ -10990,10 +10998,10 @@ react-native-clean-project@^3.6.7: resolved "https://registry.yarnpkg.com/react-native-clean-project/-/react-native-clean-project-3.6.7.tgz#6d22ad22fe3a1e6efdb040eb66f9bdfb2273ac2e" integrity sha512-GSJG1oNRJKtINPMzfHgRmu7HJs7phwgtwQ6GvyGARiySqQlU/bAc5zsI9rOeNPGlpgYongQtjOGdmqSpsWMJTw== -react-native-codegen@^0.0.7: - version "0.0.7" - resolved "https://registry.yarnpkg.com/react-native-codegen/-/react-native-codegen-0.0.7.tgz#86651c5c5fec67a8077ef7f4e36f7ed459043e14" - integrity sha512-dwNgR8zJ3ALr480QnAmpTiqvFo+rDtq6V5oCggKhYFlRjzOmVSFn3YD41u8ltvKS5G2nQ8gCs2vReFFnRGLYng== +react-native-codegen@^0.0.8: + version "0.0.8" + resolved "https://registry.yarnpkg.com/react-native-codegen/-/react-native-codegen-0.0.8.tgz#b7796a54074139d956fff2862cf1285db43c891b" + integrity sha512-k/944+0XD+8l7zDaiKfYabyEKmAmyZgS1mj+4LcSRPyHnrjgCHKrh/Y6jM6kucQ6xU1+1uyMmF/dSkikxK8i+Q== dependencies: flow-parser "^0.121.0" jscodeshift "^0.11.0" @@ -11126,17 +11134,17 @@ react-native-webview@^11.15.0: escape-string-regexp "2.0.0" invariant "2.2.4" -react-native@0.66.4: - version "0.66.4" - resolved "https://registry.yarnpkg.com/react-native/-/react-native-0.66.4.tgz#bf89a5fb18bd23046d889fb4de4ea2822a4d7805" - integrity sha512-9vx5dlSfQlKbbDtr8+xMon6qsmSu7jvjdXWZpEKh3XVKpUidbbODv7048gwVKX8YAel1egeR7hN8vzSeI6ssTw== +react-native@0.67.3: + version "0.67.3" + resolved "https://registry.yarnpkg.com/react-native/-/react-native-0.67.3.tgz#ee8bafb644afbe511a4a2c8e9c144a9720d06dd8" + integrity sha512-epMVRMRH7dLCis97+YwiV4dmTVZO6qKmQgwcTNcxVt/kEMxAa+OYK7h81+99/n7XCeMFk/U2zYOBuQqc7c5Amg== dependencies: "@jest/create-cache-key-function" "^27.0.1" "@react-native-community/cli" "^6.0.0" "@react-native-community/cli-platform-android" "^6.0.0" "@react-native-community/cli-platform-ios" "^6.0.0" "@react-native/assets" "1.0.0" - "@react-native/normalize-color" "1.0.0" + "@react-native/normalize-color" "2.0.0" "@react-native/polyfills" "2.0.0" abort-controller "^3.0.0" anser "^1.4.9" @@ -11145,7 +11153,6 @@ react-native@0.66.4: hermes-engine "~0.9.0" invariant "^2.2.4" jsc-android "^250230.2.1" - metro-babel-register "0.66.2" metro-react-native-babel-transformer "0.66.2" metro-runtime "0.66.2" metro-source-map "0.66.2" @@ -11153,8 +11160,8 @@ react-native@0.66.4: pretty-format "^26.5.2" promise "^8.0.3" prop-types "^15.7.2" - react-devtools-core "^4.13.0" - react-native-codegen "^0.0.7" + react-devtools-core "4.19.1" + react-native-codegen "^0.0.8" react-refresh "^0.4.0" regenerator-runtime "^0.13.2" scheduler "^0.20.2" From 48d8ae22089585db23ba1e4408f35e30e0869994 Mon Sep 17 00:00:00 2001 From: William Ryder Date: Fri, 15 Sep 2023 15:17:54 +0200 Subject: [PATCH 12/30] wip: fetch --- libs/api-admentum/lib/apiAdmentum.ts | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/libs/api-admentum/lib/apiAdmentum.ts b/libs/api-admentum/lib/apiAdmentum.ts index 324ff2644..2188be2fe 100644 --- a/libs/api-admentum/lib/apiAdmentum.ts +++ b/libs/api-admentum/lib/apiAdmentum.ts @@ -155,16 +155,12 @@ export class ApiAdmentum extends EventEmitter implements Api { throw new Error('Not logged in...') } const testUserId = '436838' - const fetchUrl = apiUrls.users + '/' + testUserId - console.log('v3.1 fetching children for user id', testUserId, 'from', fetchUrl) - const currentUserResponse = await this.fetch('current-user', getUserUrl(testUserId), { + const fetchUrl = getUserUrl(testUserId) + console.log('v3.2 fetching children for user id', testUserId, 'from', fetchUrl) + const currentUserResponse = await this.fetch('current-user', fetchUrl, { method: 'GET', headers: { - 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8', - 'Accept-Encoding': 'gzip, deflate, br', - 'Accept-Language': 'en-GB,en;q=0.8', - 'Cookie': 'active_school_id=X; csrftoken=Y; sessionid=Z', - 'Referer': 'https://skola.admentum.se/api/v1/users/436838/', + 'Accept': 'application/json, text/plain, */*', }, }) From a5ebc0f9f8c684eb7b3577dd1a764487da1e2219 Mon Sep 17 00:00:00 2001 From: William Ryder Date: Fri, 15 Sep 2023 15:21:21 +0200 Subject: [PATCH 13/30] fix: remove invalid import --- libs/api-admentum/lib/apiAdmentum.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/libs/api-admentum/lib/apiAdmentum.ts b/libs/api-admentum/lib/apiAdmentum.ts index 9791f8678..4f4d8c37d 100644 --- a/libs/api-admentum/lib/apiAdmentum.ts +++ b/libs/api-admentum/lib/apiAdmentum.ts @@ -35,7 +35,6 @@ import { bankIdSessionUrl, bankIdCallbackUrl, apiUrls, - getUserUrl, } from './routes' import parse from '@skolplattformen/curriculum' @@ -162,7 +161,7 @@ export class ApiAdmentum extends EventEmitter implements Api { throw new Error('Not logged in...') } const testUserId = '436838' - const fetchUrl = getUserUrl(testUserId) + const fetchUrl = apiUrls.user(testUserId) console.log('v3.2 fetching children for user id', testUserId, 'from', fetchUrl) const currentUserResponse = await this.fetch('current-user', fetchUrl, { method: 'GET', From 78e045e81815269d0ac65e7ae520eb4d01ba3d8c Mon Sep 17 00:00:00 2001 From: Christian Landgren Date: Mon, 18 Sep 2023 23:22:03 +0200 Subject: [PATCH 14/30] fix: dont set session here --- libs/api-admentum/lib/apiAdmentum.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/libs/api-admentum/lib/apiAdmentum.ts b/libs/api-admentum/lib/apiAdmentum.ts index 002c80956..6e31b027e 100644 --- a/libs/api-admentum/lib/apiAdmentum.ts +++ b/libs/api-admentum/lib/apiAdmentum.ts @@ -321,7 +321,6 @@ export class ApiAdmentum extends EventEmitter implements Api { statusChecker.on('OK', async () => { this.isLoggedIn = true this.personalNumber = personalNumber - this.setSessionCookie(sessionId) this.emit('login') }) statusChecker.on('ERROR', () => { From bd2ccfca233573d7038976ce71a3bad6f303b6c5 Mon Sep 17 00:00:00 2001 From: William Ryder Date: Wed, 20 Sep 2023 14:08:06 +0200 Subject: [PATCH 15/30] feat: follow redirects --- libs/api-admentum/lib/apiAdmentum.ts | 44 ++++++++++++++++++++++++---- 1 file changed, 39 insertions(+), 5 deletions(-) diff --git a/libs/api-admentum/lib/apiAdmentum.ts b/libs/api-admentum/lib/apiAdmentum.ts index 4f4d8c37d..b476335ad 100644 --- a/libs/api-admentum/lib/apiAdmentum.ts +++ b/libs/api-admentum/lib/apiAdmentum.ts @@ -13,6 +13,7 @@ import { MenuItem, NewsItem, Notification, + Response, ScheduleItem, SchoolContact, Skola24Child, @@ -134,10 +135,10 @@ export class ApiAdmentum extends EventEmitter implements Api { 'skola.admentum.se' ) - const user = await this.getUser() - if (!user.isAuthenticated) { - throw new Error('Session cookie is expired') - } + //const user = await this.getUser() + //if (!user.isAuthenticated) { + // throw new Error('Session cookie is expired') + // } } async getUser(): Promise { @@ -160,9 +161,11 @@ export class ApiAdmentum extends EventEmitter implements Api { if (!this.isLoggedIn) { throw new Error('Not logged in...') } + console.log("get no children") + return [] const testUserId = '436838' const fetchUrl = apiUrls.user(testUserId) - console.log('v3.2 fetching children for user id', testUserId, 'from', fetchUrl) + console.log('v3.3 fetching children for user id', testUserId, 'from', fetchUrl) const currentUserResponse = await this.fetch('current-user', fetchUrl, { method: 'GET', headers: { @@ -333,6 +336,10 @@ export class ApiAdmentum extends EventEmitter implements Api { this.isLoggedIn = true this.personalNumber = personalNumber this.setSessionCookie(sessionId) + + console.log('callback url', bankIdCallbackUrl(sessionId)); + const callbackResponse = await this.followRedirects(bankIdCallbackUrl(sessionId)); + console.log('Final response:', callbackResponse); //const testChildren = await this.getChildren() //console.log('test children', testChildren) this.emit('login') @@ -343,6 +350,33 @@ export class ApiAdmentum extends EventEmitter implements Api { return statusChecker } + + async followRedirects(initialUrl: string): Promise { + let currentUrl = initialUrl; + let redirectCount = 0; + const maxRedirects = 10; + + while (redirectCount < maxRedirects) { + console.log('fetching (redirect number ' + redirectCount + ')', currentUrl); + const response = await this.fetch('follow-redirect', currentUrl, { + method: 'GET', + redirect: 'manual', // Disable automatic redirection + }); + + if (response.status >= 300 && response.status < 400) { + const newLocation = response.headers.get('location'); + if (!newLocation) { + throw new Error('Redirect response missing location header'); + } + currentUrl = newLocation; + redirectCount++; + } else { + // The response is not a redirect, return it + return response; + } + } + throw new Error('Max redirects reached'); + }; private async fakeMode(): Promise { this.isFake = true From 8a1a0d757ec41f46a1d70bfb9972ea132881d9fc Mon Sep 17 00:00:00 2001 From: William Ryder Date: Thu, 21 Sep 2023 01:40:05 +0200 Subject: [PATCH 16/30] wip: redirect --- libs/api-admentum/lib/apiAdmentum.ts | 12 ++++++++++-- libs/api-admentum/lib/routes.ts | 3 +++ 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/libs/api-admentum/lib/apiAdmentum.ts b/libs/api-admentum/lib/apiAdmentum.ts index 3de30a425..36f30092c 100644 --- a/libs/api-admentum/lib/apiAdmentum.ts +++ b/libs/api-admentum/lib/apiAdmentum.ts @@ -35,6 +35,7 @@ import { bankIdCheckUrl, bankIdSessionUrl, bankIdCallbackUrl, + redirectLocomotive, apiUrls, } from './routes' import parse from '@skolplattformen/curriculum' @@ -336,8 +337,15 @@ export class ApiAdmentum extends EventEmitter implements Api { this.isLoggedIn = true this.personalNumber = personalNumber - console.log('callback url', bankIdCallbackUrl(sessionId)); - const callbackResponse = await this.followRedirects(bankIdCallbackUrl(sessionId)); + + const locomotiveUrl = redirectLocomotive(sessionId) + const response = await this.fetch('follow-locomotive', locomotiveUrl, { + method: 'GET', + redirect: 'follow', + }); + console.log('locomotive response', response) + console.log('locomotive url', locomotiveUrl); + const callbackResponse = await this.followRedirects(locomotiveUrl); console.log('final response:', callbackResponse); //const testChildren = await this.getChildren() //console.log('test children', testChildren) diff --git a/libs/api-admentum/lib/routes.ts b/libs/api-admentum/lib/routes.ts index c671d3f4f..637982e68 100644 --- a/libs/api-admentum/lib/routes.ts +++ b/libs/api-admentum/lib/routes.ts @@ -47,6 +47,9 @@ export const apiUrls = { export const bankIdCheckUrl = (sessionId: string) => `https://login.grandid.com/?sessionid=${sessionId}&collect=1` +export const redirectLocomotive = (sessionId: string) => + `https://login.grandid.com/?sessionid=${sessionId}` + export const bankIdSessionUrl = (returnUrl: string) => `https://auth.admentum.se/larande${returnUrl ? `?next=${returnUrl}` : ''}` From 63dc2eb664f77712bf2e21d59b7a448b9e73b911 Mon Sep 17 00:00:00 2001 From: William Ryder Date: Fri, 22 Sep 2023 09:33:29 +0200 Subject: [PATCH 17/30] wip: more or less redirect --- libs/api-admentum/lib/apiAdmentum.ts | 23 ++++++++++++++++++----- libs/api-admentum/lib/loginStatus.ts | 27 +++++++++++++++++++++++++-- libs/api-admentum/run.js | 9 +++++++++ 3 files changed, 52 insertions(+), 7 deletions(-) diff --git a/libs/api-admentum/lib/apiAdmentum.ts b/libs/api-admentum/lib/apiAdmentum.ts index 36f30092c..1104cc338 100644 --- a/libs/api-admentum/lib/apiAdmentum.ts +++ b/libs/api-admentum/lib/apiAdmentum.ts @@ -316,6 +316,17 @@ export class ApiAdmentum extends EventEmitter implements Api { ) // https://login.grandid.com/?sessionid=234324 // => 234324 + console.log('url', url) + + // Logged in: https://skola.admentum.se/overview + if (url.includes('overview')) { + console.log('already logged in to admentum') + this.isLoggedIn = true + this.personalNumber = personalNumber + this.emit('login') + return new DummyStatusChecker() + } + const sessionId = url.split('=').pop() console.log('sessionId', sessionId) console.log('adentum session id', sessionId) @@ -339,12 +350,12 @@ export class ApiAdmentum extends EventEmitter implements Api { const locomotiveUrl = redirectLocomotive(sessionId) - const response = await this.fetch('follow-locomotive', locomotiveUrl, { + console.log('calling locomotive url: ', locomotiveUrl); + /*const response = await this.fetch('follow-locomotive', locomotiveUrl, { method: 'GET', redirect: 'follow', - }); - console.log('locomotive response', response) - console.log('locomotive url', locomotiveUrl); + });*/ + //console.log('locomotive response', response) const callbackResponse = await this.followRedirects(locomotiveUrl); console.log('final response:', callbackResponse); //const testChildren = await this.getChildren() @@ -369,8 +380,9 @@ export class ApiAdmentum extends EventEmitter implements Api { method: 'GET', redirect: 'manual', // Disable automatic redirects }); - + console.log('follow-redirect response', response); if (response.status >= 300 && response.status < 400) { + console.log('response status:', response.status); const newLocation = response.headers.get('location'); if (!newLocation) { throw new Error('Redirect response missing location header'); @@ -378,6 +390,7 @@ export class ApiAdmentum extends EventEmitter implements Api { currentUrl = newLocation; redirectCount++; } else { + console.log('response status, not reidrect:', response.status); // The response is not a redirect, return it return response; } diff --git a/libs/api-admentum/lib/loginStatus.ts b/libs/api-admentum/lib/loginStatus.ts index 440aa347d..6d8e465aa 100644 --- a/libs/api-admentum/lib/loginStatus.ts +++ b/libs/api-admentum/lib/loginStatus.ts @@ -1,6 +1,6 @@ import { Fetcher, LoginStatusChecker } from '@skolplattformen/api' import { EventEmitter } from 'events' -import { bankIdCheckUrl } from './routes' +import { bankIdCheckUrl, redirectLocomotive } from './routes' export class GrandidChecker extends EventEmitter implements LoginStatusChecker { private fetcher: Fetcher @@ -28,6 +28,7 @@ export class GrandidChecker extends EventEmitter implements LoginStatusChecker { 'x-requested-with': 'XMLHttpRequest', }, }).then((res) => { + console.log('bankid full result', res) return res.json() }) console.log('bankid result', result) @@ -35,6 +36,28 @@ export class GrandidChecker extends EventEmitter implements LoginStatusChecker { const isError = result.response?.status === 'error' // https://mNN-mg-local.idp.funktionstjanster.se/mg-local/auth/ccp11/grp/pollstatus if (ok) { + //=== + /*const parts = this.basePollingUrl.split('?'); + let locoUrl = ''; + if (parts.length === 2) { + const queryString = parts[1]; + const queryParams = queryString.split('&'); + for (const param of queryParams) { + const [key, value] = param.split('='); + if (key === 'sessionid') { + locoUrl = redirectLocomotive(value); + } + } + } else { + console.log("Invalid URL format."); + } + + console.log('calling fff locomotive url: ', locoUrl) + const response = await this.fetcher('follow-locomotive', locoUrl, { + method: 'GET', + redirect: 'manual', + }); + console.log('locomotive response', response)*/ this.emit('OK') } else if (isError) { console.log('polling error') @@ -45,7 +68,7 @@ export class GrandidChecker extends EventEmitter implements LoginStatusChecker { setTimeout(() => this.check(), 3000) } } catch (err) { - console.log('Error validating login to Hjärntorget', err) + console.log('Error validating login to Admentum', err) this.emit('ERROR') } } diff --git a/libs/api-admentum/run.js b/libs/api-admentum/run.js index 70d122b3b..1889c1965 100644 --- a/libs/api-admentum/run.js +++ b/libs/api-admentum/run.js @@ -9,6 +9,15 @@ const admentum = new Admentum(fetch, {}) const run = async () => { const sessionId = await admentum.login('7612040233') + + admentum.on('login', async () => { + console.log('login YEAYEAY', ) + + // ITerate and log all cookies + cookieJar.getCookies('https://www.admentum.se').forEach((cookie) => { + console.log(cookie.toString()) + }) + }) } run() From 3f469de3d5ca327af2ea11e6c7ec113940f47463 Mon Sep 17 00:00:00 2001 From: William Ryder Date: Fri, 22 Sep 2023 11:00:37 +0200 Subject: [PATCH 18/30] feat: login and get children --- apps/api-test-app/src/main.js | 6 +- libs/api-admentum/lib/apiAdmentum.ts | 20 +++-- libs/api-admentum/lib/parse/parsers.ts | 120 ++++++++++++++++++++++--- libs/api-admentum/lib/routes.ts | 3 +- libs/api-admentum/package.json | 2 +- 5 files changed, 129 insertions(+), 22 deletions(-) diff --git a/apps/api-test-app/src/main.js b/apps/api-test-app/src/main.js index 6c1123d58..ca61f19d2 100644 --- a/apps/api-test-app/src/main.js +++ b/apps/api-test-app/src/main.js @@ -24,7 +24,7 @@ const init = isAdmentum ? initAdmentum : initSkolplattformen process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0' const cookieJar = new CookieJar() -let bankIdUsed = false +let bankIdUsed = true const recordFolder = `${__dirname}/record` async function run() { @@ -45,12 +45,12 @@ async function run() { console.log('children') const children = await api.getChildren() console.log(children) - /* + console.log('calendar') const calendar = await api.getCalendar(children[0]) console.log(calendar) - console.log('classmates') + /*console.log('classmates') const classmates = await api.getClassmates(children[0]) console.log(classmates) diff --git a/libs/api-admentum/lib/apiAdmentum.ts b/libs/api-admentum/lib/apiAdmentum.ts index 1104cc338..9fc7a163b 100644 --- a/libs/api-admentum/lib/apiAdmentum.ts +++ b/libs/api-admentum/lib/apiAdmentum.ts @@ -144,7 +144,7 @@ export class ApiAdmentum extends EventEmitter implements Api { async getUser(): Promise { console.log('fetching user') - const userId = '437302' + const userId = '437236' const currentUserResponse = await this.fetch( 'current-user', apiUrls.user(userId) @@ -162,11 +162,10 @@ export class ApiAdmentum extends EventEmitter implements Api { if (!this.isLoggedIn) { throw new Error('Not logged in...') } - console.log("get no children") - return [] - const testUserId = '436838' + console.log("get children") + const testUserId = '437236' const fetchUrl = apiUrls.user(testUserId) - console.log('v3.3 fetching children for user id', testUserId, 'from', fetchUrl) + console.log('v3.4 fetching children for user id', testUserId, 'from', fetchUrl) const currentUserResponse = await this.fetch('current-user', fetchUrl, { method: 'GET', headers: { @@ -174,7 +173,6 @@ export class ApiAdmentum extends EventEmitter implements Api { }, }) - if (currentUserResponse.status !== 200) { console.error('Error headers', currentUserResponse.headers) throw new Error('Could not fetch children. Response code: ' + currentUserResponse.status) @@ -191,7 +189,15 @@ export class ApiAdmentum extends EventEmitter implements Api { } async getCalendar(child: EtjanstChild): Promise { - return Promise.resolve([]) + if (!this.isLoggedIn) { + throw new Error('Not logged in...') + } + + const [year, week] = new DateTime().toISOWeekDate().split('-') + const isoWeek = week.replace('W','') + const fetchUrl = apiUrls.schedule(year, isoWeek) + const calendarResponse = await this.fetch('get-calendar', fetchUrl) + return calendarResponse.map(parseCalendarItem) } // eslint-disable-next-line @typescript-eslint/no-unused-vars diff --git a/libs/api-admentum/lib/parse/parsers.ts b/libs/api-admentum/lib/parse/parsers.ts index 0ca462600..83c22e5fb 100644 --- a/libs/api-admentum/lib/parse/parsers.ts +++ b/libs/api-admentum/lib/parse/parsers.ts @@ -46,13 +46,113 @@ export function extractAuthGbgLoginRequestBody(signatureResponseText: string) { return authGbgLoginBody } -export const parseCalendarItem = (x: html.HTMLElement): { id: number; title: string; startDate: string; endDate: string } => { - const info = Array.from(x.querySelectorAll('a')) - // TODO: the identifier is realy on this format: '\d+:\d+' currently we only take the first part so Id will clash between items - const id = info[0].getAttribute("onClick")?.replace(new RegExp("return viewEvent\\('(\\d+).+"), "$1") || NaN - const day = info[1].textContent - const timeSpan = info[2].textContent - const [startTime, endTime] = timeSpan.replace(".", ":").split("-") - - return { id: +id, title: info[0].textContent, startDate: `${day} ${startTime}`, endDate: `${day} ${endTime}` } -} \ No newline at end of file +export const parseCalendarItem = (jsonRow: any): any => { + + return {} + +} + +/* +{ + "week_number": 40, + "days": [ + { + "date": "2023-10-02", + "formated_date": "2 okt", + "name": "Måndag", + "lessons": [ + { + "title": "BI", + "tooltip_title": "Biologi", + "subject_name": "Biologi", + "subject_code": "BI", + "teachers": "FCa", + "intervals": 3.0, + "overlaps": 1, + "start_pos": 4.0, + "color": "#e97f23", + "time": "10:00 - 11:30", + "room": "", + "groups": "6 A BI", + "tooltip": "10:00 - 11:30
6 A BI", + "lesson_id": 14998270, + "lesson_info": "", + "lesson_groups": "6 A BI", + "body": "BI", + "information": null + } + ], + "breaks": [], + "events": [] + }, + { + "date": "2023-10-03", + "formated_date": "3 okt", + "name": "Tisdag", + "lessons": [], + "breaks": [], + "events": [] + }, + { + "date": "2023-10-04", + "formated_date": "4 okt", + "name": "Onsdag", + "lessons": [], + "breaks": [], + "events": [] + }, + { + "date": "2023-10-05", + "formated_date": "5 okt", + "name": "Torsdag", + "lessons": [], + "breaks": [], + "events": [] + }, + { + "date": "2023-10-06", + "formated_date": "6 okt", + "name": "Fredag", + "lessons": [], + "breaks": [], + "events": [] + } + ], + "query": "week=40&user_id=437235", + "time_range": [ + "8:00", + "8:30", + "9:00", + "9:30", + "10:00", + "10:30", + "11:00", + "11:30", + "12:00", + "12:30", + "13:00", + "13:30", + "14:00", + "14:30", + "15:00", + "15:30", + "16:00", + "16:30", + "17:00" + ], + "section_count": 18, + "breaks": [], + "schedule_event_instances": [], + "schedule": { + "id": 4385, + "start_week": 31, + "start_year": 2023, + "end_week": 22, + "end_year": 2024 + }, + "next_week": 41, + "prev_week": 39, + "weeks_amount": 52, + "break_week": 27 +} +*/ \ No newline at end of file diff --git a/libs/api-admentum/lib/routes.ts b/libs/api-admentum/lib/routes.ts index 637982e68..7b9572095 100644 --- a/libs/api-admentum/lib/routes.ts +++ b/libs/api-admentum/lib/routes.ts @@ -33,7 +33,8 @@ export const apiUrls = { baseUrl + 'schedule_group_teacher_enrollments', schedule_groups: baseUrl + 'schedule_groups', schedules: baseUrl + 'schedules', - school_enrollments: baseUrl + 'school_enrollments', + schedule: (year: string, week: string) => baseUrl + `schedule?week=${week}&year=${year}`, + school_enrollments: `${baseUrl}school_enrollments`, school_years: baseUrl + 'school_years', schools: baseUrl + 'schools', sickness: baseUrl + 'sickness', diff --git a/libs/api-admentum/package.json b/libs/api-admentum/package.json index ae9bdc506..22151be99 100644 --- a/libs/api-admentum/package.json +++ b/libs/api-admentum/package.json @@ -7,7 +7,7 @@ "dist/**/*" ], "repository": "git@github.com:kolplattformen/skolplattformen.git", - "author": "Erik Eng ", + "author": "Christian Landgren, William Ryder ", "license": "Apache-2.0", "private": false, "scripts": { From 6130fdb796a478296f4e445ff06d907f3825212b Mon Sep 17 00:00:00 2001 From: William Ryder Date: Fri, 22 Sep 2023 15:17:39 +0200 Subject: [PATCH 19/30] feat: more data --- apps/api-test-app/src/main.js | 41 +++++---- libs/api-admentum/lib/apiAdmentum.ts | 119 +++++++++++++++---------- libs/api-admentum/lib/features.ts | 2 +- libs/api-admentum/lib/parse/parsers.ts | 60 ++++++++++++- libs/api-admentum/lib/routes.ts | 85 +++++++++--------- 5 files changed, 199 insertions(+), 108 deletions(-) diff --git a/apps/api-test-app/src/main.js b/apps/api-test-app/src/main.js index ca61f19d2..78988d175 100644 --- a/apps/api-test-app/src/main.js +++ b/apps/api-test-app/src/main.js @@ -27,6 +27,10 @@ const cookieJar = new CookieJar() let bankIdUsed = true const recordFolder = `${__dirname}/record` +const now = DateTime.local() +const [year, week] = now.toISOWeekDate().split('-') +const isoWeek = week.replace('W','') + async function run() { const fetch = fetchCookie(nodeFetch, cookieJar) @@ -45,7 +49,26 @@ async function run() { console.log('children') const children = await api.getChildren() console.log(children) - + + + try { + console.log('timetable') + const timetable = await api.getTimetable( + children[0], + isoWeek, + year, + 'sv' + ) + console.log(inspect(timetable, false, 1000, true)) + } catch (error) { + console.error(error) + } + + console.log('menu') + const menu = await api.getMenu(children[0]) + console.log(menu) + + /* console.log('calendar') const calendar = await api.getCalendar(children[0]) console.log(calendar) @@ -74,18 +97,6 @@ async function run() { console.error(error) } - try { - console.log('timetable') - const timetable = await api.getTimetable( - skola24children[0], - 15, - 2021, - 'sv' - ) - console.log(inspect(timetable, false, 1000, true)) - } catch (error) { - console.error(error) - } /* console.log('news') @@ -100,9 +111,7 @@ async function run() { ) console.log(newsItems) */ - /* console.log('menu') - const menu = await api.getMenu(children[0]) - console.log(menu) */ + // console.log('notifications') // const notifications = await api.getNotifications(children[0]) diff --git a/libs/api-admentum/lib/apiAdmentum.ts b/libs/api-admentum/lib/apiAdmentum.ts index 9fc7a163b..7c4145708 100644 --- a/libs/api-admentum/lib/apiAdmentum.ts +++ b/libs/api-admentum/lib/apiAdmentum.ts @@ -54,6 +54,7 @@ export class ApiAdmentum extends EventEmitter implements Api { private realFetcher: Fetcher private personalNumber?: string + private userId: string private cookieManager: CookieManager @@ -83,6 +84,7 @@ export class ApiAdmentum extends EventEmitter implements Api { this.fetch = wrap(fetch, options) this.realFetcher = this.fetch this.cookieManager = cookieManager + this.userId = '' } public replaceFetcher(fetcher: Fetcher) { @@ -143,12 +145,15 @@ export class ApiAdmentum extends EventEmitter implements Api { } async getUser(): Promise { + const user = await this.fetch('fetch-me', apiUrls.me); + const userJson = await user.json(); + this.userId = userJson.user?.id; + console.log('userId: ', this.userId); console.log('fetching user') - const userId = '437236' const currentUserResponse = await this.fetch( 'current-user', - apiUrls.user(userId) - ) // + /id? + apiUrls.user(this.userId) + ) console.log('current-user', currentUserResponse) if (currentUserResponse.status !== 200) { return { isAuthenticated: false } @@ -163,9 +168,7 @@ export class ApiAdmentum extends EventEmitter implements Api { throw new Error('Not logged in...') } console.log("get children") - const testUserId = '437236' - const fetchUrl = apiUrls.user(testUserId) - console.log('v3.4 fetching children for user id', testUserId, 'from', fetchUrl) + const fetchUrl = apiUrls.user(this.userId) const currentUserResponse = await this.fetch('current-user', fetchUrl, { method: 'GET', headers: { @@ -193,11 +196,19 @@ export class ApiAdmentum extends EventEmitter implements Api { throw new Error('Not logged in...') } - const [year, week] = new DateTime().toISOWeekDate().split('-') - const isoWeek = week.replace('W','') - const fetchUrl = apiUrls.schedule(year, isoWeek) - const calendarResponse = await this.fetch('get-calendar', fetchUrl) - return calendarResponse.map(parseCalendarItem) + return [] + // const fetchUrl = apiUrls.schedule_events + // const events = await this.fetch('scheduled-events', fetchUrl, { + // method: 'GET', + // headers: { + // 'Accept': 'application/json, text/plain, */*', + // }, + // }).then(res => res.json()).then(json => json.results) + + + + + // return events.map(parseScheduleEvent)*/ } // eslint-disable-next-line @typescript-eslint/no-unused-vars @@ -236,14 +247,45 @@ export class ApiAdmentum extends EventEmitter implements Api { async getNewsDetails(_child: EtjanstChild, item: NewsItem): Promise { return { ...item } } - +/* + "data": { + "food_week": { + "id": 12846, + "week": 38, + "year": 2023, + "food_days": [ + { + "id": 60620, + "date": "2023-09-18", + "menu": "Förrätt: Morotssoppa med knäckebröd\r\nHuvudrätt: Kycklinggryta med ris och grönsaker\r\nEfterrätt: Fruktkompott", + "weekday": "Måndag", + "weekday_nbr": 0 + }, + { + "id": 60621, + "date": "2023-09-19", + "menu": "Förrätt: Gurksallad\ +*/ // eslint-disable-next-line @typescript-eslint/no-unused-vars - getMenu(_child: EtjanstChild): Promise { + async getMenu(_child: EtjanstChild): Promise { if (!this.isLoggedIn) { throw new Error('Not logged in...') } - // Have not found this available on hjärntorget. Perhaps do a mapping to https://www.skolmaten.se/ ? - return Promise.resolve([]) + const now = DateTime.local() + const [year, week] = now.toISOWeekDate().split('-') + const isoWeek = week.replace('W','') + + const fetchUrl = apiUrls.menu(year.toString(), isoWeek.toString()) + + const menuResponse = (await this.fetch('get-menu', fetchUrl)) + const menuResponseJson = await menuResponse.json() + console.log('menu response', menuResponseJson) + const days = (await menuResponseJson)?.data?.food_week?.food_days + + return Promise.resolve(days.map(({ menu, date } : any) => ({ + title: date, + description: menu + }))) } async getChildEventsWithAssociatedMembers(child: EtjanstChild) { @@ -272,31 +314,13 @@ export class ApiAdmentum extends EventEmitter implements Api { year: number, _lang: string ): Promise { - const startDate = DateTime.fromJSDate(getDateOfISOWeek(week, year)) - const endDate = startDate.plus({ days: 7 }) - const lessonsResponseJson: any[] = [] - - return lessonsResponseJson.map((l) => { - const start = DateTime.fromMillis(l.startDate.ts, { - zone: FixedOffsetZone.instance(l.startDate.timezoneOffsetMinutes), - }) - const end = DateTime.fromMillis(l.endDate.ts, { - zone: FixedOffsetZone.instance(l.endDate.timezoneOffsetMinutes), - }) - return { - ...parse(l.title, _lang), - id: l.id, - teacher: l.bookedTeacherNames && l.bookedTeacherNames[0], - location: l.location, - timeStart: start.toISOTime().substring(0, 5), - timeEnd: end.toISOTime().substring(0, 5), - dayOfWeek: start.toJSDate().getDay(), - blockName: l.title, - dateStart: start.toISODate(), - dateEnd: end.toISODate(), - } as TimetableEntry - }) + const fetchUrl = apiUrls.schedule(year.toString(), week.toString()) + console.log('fetching timetable', fetchUrl) + const calendarResponse = await this.fetch('get-calendar', fetchUrl) + const calendarResponseJson = await calendarResponse.json() + const timetableEntries = parseCalendarItem(calendarResponseJson) + return timetableEntries; } async logout(): Promise { @@ -314,6 +338,16 @@ export class ApiAdmentum extends EventEmitter implements Api { console.log('login adentum', personalNumber) this.isFake = false + + const authenticatedUser = await this.getUser(); + if (authenticatedUser && authenticatedUser.isAuthenticated) { + console.log('already logged in to admentum') + this.isLoggedIn = true + this.personalNumber = personalNumber + this.emit('login') + return new DummyStatusChecker() + } + const url = await this.fetch('get-session', bankIdSessionUrl('')).then( (res) => { console.log('got res', res, (res as any).url, res.headers) @@ -357,15 +391,8 @@ export class ApiAdmentum extends EventEmitter implements Api { const locomotiveUrl = redirectLocomotive(sessionId) console.log('calling locomotive url: ', locomotiveUrl); - /*const response = await this.fetch('follow-locomotive', locomotiveUrl, { - method: 'GET', - redirect: 'follow', - });*/ - //console.log('locomotive response', response) const callbackResponse = await this.followRedirects(locomotiveUrl); console.log('final response:', callbackResponse); - //const testChildren = await this.getChildren() - //console.log('test children', testChildren) this.emit('login') }) statusChecker.on('ERROR', () => { diff --git a/libs/api-admentum/lib/features.ts b/libs/api-admentum/lib/features.ts index b0e530222..a2a7db739 100644 --- a/libs/api-admentum/lib/features.ts +++ b/libs/api-admentum/lib/features.ts @@ -3,6 +3,6 @@ import { Features } from '@skolplattformen/api' export const features: Features = { LOGIN_BANK_ID_SAME_DEVICE_WITHOUT_ID: false, LOGIN_FREJA_EID: false, - FOOD_MENU: false, + FOOD_MENU: true, CLASS_LIST: false, } diff --git a/libs/api-admentum/lib/parse/parsers.ts b/libs/api-admentum/lib/parse/parsers.ts index 83c22e5fb..00fa09b40 100644 --- a/libs/api-admentum/lib/parse/parsers.ts +++ b/libs/api-admentum/lib/parse/parsers.ts @@ -1,5 +1,7 @@ import * as html from 'node-html-parser' import { decode } from 'he' +import { CalendarItem, TimetableEntry } from 'libs/api/lib/types' +import { DateTime, FixedOffsetZone } from 'luxon' // TODO: Move this into the parse folder and convert it to follow the pattern of other parsers (include tests). @@ -46,10 +48,60 @@ export function extractAuthGbgLoginRequestBody(signatureResponseText: string) { return authGbgLoginBody } -export const parseCalendarItem = (jsonRow: any): any => { - - return {} - +/* +return myChildrenResponseJson.students.map((student: { id: any; first_name: any; last_name: any }) => ({ + id: student.id, + sdsId: student.id, + personGuid: student.id, + firstName: student.first_name, + lastName: student.last_name, + name: `${student.first_name} ${student.last_name}`, + }) as Skola24Child & EtjanstChild); +*/ +/* +export const parseScheduleEvent = (({ + url, id, eid, school_id, schedule_id, name, start_time, end_time, rooms: [room], teachers, schedule_groups, primary_groups, weekly_interval +})): CalendarItem => ({ + id + title: name + location?: room?.name + startDate?: start_time + endDate?: end_time + allDay?: start_time === '00:00:00' && end_time === '23:59:00' +}) + */ + +enum DayOfWeek { + 'Måndag'= 1, + 'Tisdag'= 2, + 'Onsdag'= 3, + 'Torsdag'= 4, + 'Fredag'= 5, + 'Lördag'= 6, + 'Söndag'= 7, +} + +export const parseCalendarItem = (jsonData: any): any => { + const timetableEntries: TimetableEntry[] = [] + if (jsonData && jsonData.days && Array.isArray(jsonData.days) && jsonData.days.length > 0) { + jsonData.days.forEach((day: { name: string, lessons: any[] }) => { + day.lessons.forEach(lesson => { + const dayOfWeek = DayOfWeek[day.name as keyof typeof DayOfWeek] + timetableEntries.push({ + id: lesson.id, + teacher: lesson.bookedTeacherNames && lesson.bookedTeacherNames[0], + location: lesson.location, + timeStart: lesson.time.substring(0, 5), + timeEnd: lesson.time.substring(9), + dayOfWeek, + blockName: lesson.title || lesson.subject_name + } as TimetableEntry) + }); + }) + } else { + console.error("Failed to parse calendar item, no days found in json data.") + } + return timetableEntries; } /* diff --git a/libs/api-admentum/lib/routes.ts b/libs/api-admentum/lib/routes.ts index 7b9572095..f78249fd4 100644 --- a/libs/api-admentum/lib/routes.ts +++ b/libs/api-admentum/lib/routes.ts @@ -1,48 +1,51 @@ -const baseUrl = 'https://skola.admentum.se/api/v1/' +const baseUrl = 'https://skola.admentum.se/' +const api = baseUrl + 'api/v1/' export const apiUrls = { - assignments: baseUrl + 'assignments', - attendance_summary_users: baseUrl + 'attendance/summary/users', - course_sections: baseUrl + 'course_sections', - courses: baseUrl + 'courses', - forecast_collections: baseUrl + 'forecast_collections', - forecasts: baseUrl + 'forecasts', - grade_permissions: baseUrl + 'grade_permissions', - grades: baseUrl + 'grades', - gymnasium_courses: baseUrl + 'gymnasium_courses', - leisure_group_enrollments: baseUrl + 'leisure_group_enrollments', - leisure_groups: baseUrl + 'leisure_groups', - lesson_infos: baseUrl + 'lesson_infos', - lessons: baseUrl + 'lessons', - organisations: baseUrl + 'organisations', - orientations: baseUrl + 'orientations', - permission_groups: baseUrl + 'permission_groups', - primary_group_enrollments: baseUrl + 'primary_group_enrollments', + assignments: api + 'assignments', + attendance_summary_users: api + 'attendance/summary/users', + course_sections: api + 'course_sections', + courses: api + 'courses', + forecast_collections: api + 'forecast_collections', + forecasts: api + 'forecasts', + grade_permissions: api + 'grade_permissions', + grades: api + 'grades', + gymnasium_courses: api + 'gymnasium_courses', + leisure_group_enrollments: api + 'leisure_group_enrollments', + leisure_groups: api + 'leisure_groups', + lesson_infos: api + 'lesson_infos', + lessons: api + 'lessons', + organisations: api + 'organisations', + orientations: api + 'orientations', + permission_groups: api + 'permission_groups', + primary_group_enrollments: api + 'primary_group_enrollments', primary_group_municipality_statistics: - baseUrl + 'primary_groups/municipality_statistic', - primary_groups: baseUrl + 'primary_groups', - program_courses: baseUrl + 'program_courses', - programs: baseUrl + 'programs', - reviews: baseUrl + 'reviews', - rooms: baseUrl + 'rooms', - schedule_breaks: baseUrl + 'schedule_breaks', - schedule_event_instances: baseUrl + 'schedule_event_instances', - schedule_events: baseUrl + 'schedule_events', - schedule_group_enrollments: baseUrl + 'schedule_group_enrollments', + api + 'primary_groups/municipality_statistic', + primary_groups: api + 'primary_groups', + program_courses: api + 'program_courses', + programs: api + 'programs', + reviews: api + 'reviews', + rooms: api + 'rooms', + schedule_breaks: api + 'schedule_breaks', + schedule_event_instances: api + 'schedule_event_instances', + schedule_events: api + 'schedule_events', + schedule_group_enrollments: api + 'schedule_group_enrollments', schedule_group_teacher_enrollments: - baseUrl + 'schedule_group_teacher_enrollments', - schedule_groups: baseUrl + 'schedule_groups', - schedules: baseUrl + 'schedules', - schedule: (year: string, week: string) => baseUrl + `schedule?week=${week}&year=${year}`, - school_enrollments: `${baseUrl}school_enrollments`, - school_years: baseUrl + 'school_years', - schools: baseUrl + 'schools', - sickness: baseUrl + 'sickness', - subjects: baseUrl + 'subjects', - teachers: baseUrl + 'teachers', - upper_secondary_subjects: baseUrl + 'upper_secondary_subjects', - users: baseUrl + 'users?format=json', - user: (userId: string) => baseUrl + `users/${userId}/?format=json`, + api + 'schedule_group_teacher_enrollments', + schedule_groups: api + 'schedule_groups', + schedules: api + 'schedules', + schedule: (year: string, week: string) => baseUrl + `schedule/schedule?week=${week}&year=${year}`, + school_enrollments: `${api}school_enrollments`, + school_years: api + 'school_years', + schools: api + 'schools', + sickness: api + 'sickness', + subjects: api + 'subjects', + teachers: api + 'teachers', + menu: (year: string, week: string) => baseUrl + `api/food/week/${week}/${year}`, + upper_secondary_subjects: api + 'upper_secondary_subjects', + users: api + 'users?format=json', + user: (userId: string) => api + `users/${userId}/?format=json`, + me: baseUrl + 'api/me?format=json', } export const bankIdCheckUrl = (sessionId: string) => From 9276db3c76c94989f9d4bc38b3db03917de2595d Mon Sep 17 00:00:00 2001 From: William Ryder Date: Fri, 6 Oct 2023 09:43:21 +0200 Subject: [PATCH 20/30] wip: fetch more data --- apps/api-test-app/src/main.js | 4 +- libs/api-admentum/lib/apiAdmentum.ts | 165 ++++++++++++++++++++++--- libs/api-admentum/lib/parse/parsers.ts | 2 +- libs/api-admentum/lib/routes.ts | 1 + 4 files changed, 152 insertions(+), 20 deletions(-) diff --git a/apps/api-test-app/src/main.js b/apps/api-test-app/src/main.js index 78988d175..18cc2c636 100644 --- a/apps/api-test-app/src/main.js +++ b/apps/api-test-app/src/main.js @@ -68,11 +68,11 @@ async function run() { const menu = await api.getMenu(children[0]) console.log(menu) - /* + console.log('calendar') const calendar = await api.getCalendar(children[0]) console.log(calendar) - +/* /*console.log('classmates') const classmates = await api.getClassmates(children[0]) console.log(classmates) diff --git a/libs/api-admentum/lib/apiAdmentum.ts b/libs/api-admentum/lib/apiAdmentum.ts index 7c4145708..9c66b2e2b 100644 --- a/libs/api-admentum/lib/apiAdmentum.ts +++ b/libs/api-admentum/lib/apiAdmentum.ts @@ -191,11 +191,100 @@ export class ApiAdmentum extends EventEmitter implements Api { }) as Skola24Child & EtjanstChild); } + + async getCalendar(child: EtjanstChild): Promise { + try { + if (!this.isLoggedIn) { + throw new Error('Not logged in...') + } + const now = DateTime.local() + const [year, week] = now.toISOWeekDate().split('-') + const isoWeek = week.replace('W','') + + const fetchUrl = apiUrls.overview(year.toString(), isoWeek.toString()) + console.log('fetching calendar', fetchUrl) + //const calendarResponse = await this.fetch('get-calendar', fetchUrl) + //const calendarResponseJson = await calendarResponse.json() + const overviewResponse = await this.fetch('get-overview', fetchUrl) + console.log('overview response', overviewResponse) + const overviewJson = await overviewResponse.json() + console.log('overview response', overviewJson) + const schedule_events = (await overviewJson)?.data?.schedule_events // .breaks: [] | .assignments: [] + if (!schedule_events) { + return Promise.resolve([]) + } + /* +"url": "https://skola.admentum.se/api/v1/schedule_event_instances/2990834/", + "id": 2990834, + "school_id": 824, + "start_date": "2023-08-07", + "end_date": "2023-08-07", + "schedule_event": { + "url": "https://skola.admentum.se/api/v1/schedule_events/148722/", + "id": 148722, + "eid": null, + "schedule_id": 4385, + "name": "Engelska", + "start_time": "08:00:00", + "end_time": "09:30:00", + "rooms": [ + { + "url": "https://skola.admentum.se/api/v1/rooms/7200/", + "id": 7200 + } + ], + "teachers": [ + { + "url": "https://skola.admentum.se/api/v1/users/437302/", + "id": 437302 + } + ], + "schedule_groups": [], + "primary_groups": [ + { + "url": "https://skola.admentum.se/api/v1/primary_groups/36874/", + "id": 36874 + } + ], + "weekly_interval": "" + } + */ + return Promise.resolve(schedule_events.map(({ menu, date } : any) => ({ + title: date, + description: menu + }))) + } catch (e) { + console.error('Error fetching menu', e) + return Promise.resolve([]) + } + } + + async getScheduledEvents(child: EtjanstChild): Promise { if (!this.isLoggedIn) { throw new Error('Not logged in...') } + console.log('get calendar') + const fetchUrl = apiUrls.schedule_events; + console.log('fetching calendar', fetchUrl) + const eventsResponse = await this.fetch('scheduled-events', fetchUrl, { + method: 'GET', + headers: { + 'Accept': 'application/json, text/plain, */*', + }, + }) + if (eventsResponse.status === 403) { + console.error('Not allwed. Error headers', eventsResponse.headers) + return [] + } + if (eventsResponse.status !== 200) { + console.error('Error headers', eventsResponse.headers) + throw new Error('Could not fetch children. Response code: ' + eventsResponse.status) + } + + const eventsResponseJson = await eventsResponse.json(); + console.log('eventsResponseJson', eventsResponseJson) return [] // const fetchUrl = apiUrls.schedule_events // const events = await this.fetch('scheduled-events', fetchUrl, { @@ -214,6 +303,38 @@ export class ApiAdmentum extends EventEmitter implements Api { // eslint-disable-next-line @typescript-eslint/no-unused-vars getClassmates(_child: EtjanstChild): Promise { // TODO: We could get this from the events a child is associated with... + /* + GET /api/v1/schedule_groups/423145/ +{ + "url": "https://skola.admentum.se/api/v1/schedule_groups/423145/", + "id": 423145, + "eid": null, + "schedule": { + "url": "https://skola.admentum.se/api/v1/schedules/4385/", + "id": 4385, + "school_year": "23/24" + }, + "name": "1 A SV", + "guid": null, + "users": [ + { + "url": "https://skola.admentum.se/api/v1/users/436741/", + "id": 436741, + "email": null, + "first_name": "Arvid", + "last_name": "Forslin", + "role": 1 + }, + { + "url": "https://skola.admentum.se/api/v1/users/436747/", + "id": 436747, + "email": null, + "first_name": "Emmy", + "last_name": "Granström", + "role": 1 + } + ... + */ if (!this.isLoggedIn) { throw new Error('Not logged in...') } @@ -268,24 +389,34 @@ export class ApiAdmentum extends EventEmitter implements Api { */ // eslint-disable-next-line @typescript-eslint/no-unused-vars async getMenu(_child: EtjanstChild): Promise { - if (!this.isLoggedIn) { - throw new Error('Not logged in...') + try { + if (!this.isLoggedIn) { + throw new Error('Not logged in...') + } + const now = DateTime.local() + const [year, week] = now.toISOWeekDate().split('-') + const isoWeek = week.replace('W', '') + + const fetchUrl = apiUrls.menu(year.toString(), isoWeek.toString()) + console.log('fetching menu', fetchUrl) + const menuResponse = (await this.fetch('get-menu', fetchUrl)) + console.log('menu response', menuResponse) + const menuResponseJson = await menuResponse.text() + console.log('menu response', menuResponseJson) + const days = (await menuResponseJson) + //if (!days) { + return Promise.resolve([]) + //} + /*return Promise.resolve( + days.map(({ menu, date }: any) => ({ + title: date, + description: menu, + })) + )*/ + } catch (e) { + console.error('Error fetching menu', e) + return Promise.resolve([]) } - const now = DateTime.local() - const [year, week] = now.toISOWeekDate().split('-') - const isoWeek = week.replace('W','') - - const fetchUrl = apiUrls.menu(year.toString(), isoWeek.toString()) - - const menuResponse = (await this.fetch('get-menu', fetchUrl)) - const menuResponseJson = await menuResponse.json() - console.log('menu response', menuResponseJson) - const days = (await menuResponseJson)?.data?.food_week?.food_days - - return Promise.resolve(days.map(({ menu, date } : any) => ({ - title: date, - description: menu - }))) } async getChildEventsWithAssociatedMembers(child: EtjanstChild) { diff --git a/libs/api-admentum/lib/parse/parsers.ts b/libs/api-admentum/lib/parse/parsers.ts index 00fa09b40..8a5e78726 100644 --- a/libs/api-admentum/lib/parse/parsers.ts +++ b/libs/api-admentum/lib/parse/parsers.ts @@ -94,7 +94,7 @@ export const parseCalendarItem = (jsonData: any): any => { timeStart: lesson.time.substring(0, 5), timeEnd: lesson.time.substring(9), dayOfWeek, - blockName: lesson.title || lesson.subject_name + blockName: lesson.title || lesson.subject_name } as TimetableEntry) }); }) diff --git a/libs/api-admentum/lib/routes.ts b/libs/api-admentum/lib/routes.ts index f78249fd4..04ddc1b67 100644 --- a/libs/api-admentum/lib/routes.ts +++ b/libs/api-admentum/lib/routes.ts @@ -17,6 +17,7 @@ export const apiUrls = { lessons: api + 'lessons', organisations: api + 'organisations', orientations: api + 'orientations', + overview: (year: string, week: string) => baseUrl + `overview?week=${week}&year=${year}`, permission_groups: api + 'permission_groups', primary_group_enrollments: api + 'primary_group_enrollments', primary_group_municipality_statistics: From c4c52b9819e2cd6fe08844f7ff8e1c56946cc15a Mon Sep 17 00:00:00 2001 From: William Ryder Date: Fri, 6 Oct 2023 09:49:56 +0200 Subject: [PATCH 21/30] fix: wrong debug --- libs/api-admentum/lib/apiAdmentum.ts | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/libs/api-admentum/lib/apiAdmentum.ts b/libs/api-admentum/lib/apiAdmentum.ts index 9c66b2e2b..d7b595cf7 100644 --- a/libs/api-admentum/lib/apiAdmentum.ts +++ b/libs/api-admentum/lib/apiAdmentum.ts @@ -400,19 +400,18 @@ export class ApiAdmentum extends EventEmitter implements Api { const fetchUrl = apiUrls.menu(year.toString(), isoWeek.toString()) console.log('fetching menu', fetchUrl) const menuResponse = (await this.fetch('get-menu', fetchUrl)) - console.log('menu response', menuResponse) - const menuResponseJson = await menuResponse.text() + const menuResponseJson = await menuResponse.json() console.log('menu response', menuResponseJson) - const days = (await menuResponseJson) - //if (!days) { - return Promise.resolve([]) - //} - /*return Promise.resolve( + const days = (await menuResponseJson)?.data?.food_week?.food_days + if (!days) { + return Promise.resolve([]) + } + return Promise.resolve( days.map(({ menu, date }: any) => ({ title: date, description: menu, })) - )*/ + ) } catch (e) { console.error('Error fetching menu', e) return Promise.resolve([]) From 618bdbf73c7f789f2b2dd6674c579f2a823cd04d Mon Sep 17 00:00:00 2001 From: Christian Landgren Date: Fri, 6 Oct 2023 10:18:24 +0200 Subject: [PATCH 22/30] fix: handle errors in login flow correctly --- libs/api-admentum/lib/loginStatus.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libs/api-admentum/lib/loginStatus.ts b/libs/api-admentum/lib/loginStatus.ts index 6d8e465aa..b7d598d0e 100644 --- a/libs/api-admentum/lib/loginStatus.ts +++ b/libs/api-admentum/lib/loginStatus.ts @@ -33,7 +33,7 @@ export class GrandidChecker extends EventEmitter implements LoginStatusChecker { }) console.log('bankid result', result) const ok = result.response?.status === 'complete' - const isError = result.response?.status === 'error' + const isError = result.response?.status === 'failed' // https://mNN-mg-local.idp.funktionstjanster.se/mg-local/auth/ccp11/grp/pollstatus if (ok) { //=== @@ -60,7 +60,7 @@ export class GrandidChecker extends EventEmitter implements LoginStatusChecker { console.log('locomotive response', response)*/ this.emit('OK') } else if (isError) { - console.log('polling error') + console.log('polling error', result.response?.hintCode) this.emit('ERROR') } else if (!this.cancelled) { console.log('keep on polling...') From 8385d30aaf4dd2ba5a5d83769232ddebec523b32 Mon Sep 17 00:00:00 2001 From: Christian Landgren Date: Fri, 6 Oct 2023 10:23:51 +0200 Subject: [PATCH 23/30] fix: url for week data --- libs/api-admentum/lib/apiAdmentum.ts | 139 +++++++++++++++------------ libs/api-admentum/lib/routes.ts | 9 +- 2 files changed, 81 insertions(+), 67 deletions(-) diff --git a/libs/api-admentum/lib/apiAdmentum.ts b/libs/api-admentum/lib/apiAdmentum.ts index d7b595cf7..704ee1962 100644 --- a/libs/api-admentum/lib/apiAdmentum.ts +++ b/libs/api-admentum/lib/apiAdmentum.ts @@ -141,14 +141,14 @@ export class ApiAdmentum extends EventEmitter implements Api { //const user = await this.getUser() //if (!user.isAuthenticated) { // throw new Error('Session cookie is expired') - // } + // } } async getUser(): Promise { - const user = await this.fetch('fetch-me', apiUrls.me); - const userJson = await user.json(); - this.userId = userJson.user?.id; - console.log('userId: ', this.userId); + const user = await this.fetch('fetch-me', apiUrls.me) + const userJson = await user.json() + this.userId = userJson.user?.id + console.log('userId: ', this.userId) console.log('fetching user') const currentUserResponse = await this.fetch( 'current-user', @@ -167,32 +167,35 @@ export class ApiAdmentum extends EventEmitter implements Api { if (!this.isLoggedIn) { throw new Error('Not logged in...') } - console.log("get children") + console.log('get children') const fetchUrl = apiUrls.user(this.userId) const currentUserResponse = await this.fetch('current-user', fetchUrl, { method: 'GET', headers: { - 'Accept': 'application/json, text/plain, */*', + Accept: 'application/json, text/plain, */*', }, - }) + }) if (currentUserResponse.status !== 200) { console.error('Error headers', currentUserResponse.headers) - throw new Error('Could not fetch children. Response code: ' + currentUserResponse.status) + throw new Error( + 'Could not fetch children. Response code: ' + currentUserResponse.status + ) } - const myChildrenResponseJson = await currentUserResponse.json(); - return myChildrenResponseJson.students.map((student: { id: any; first_name: any; last_name: any }) => ({ - id: student.id, - sdsId: student.id, - personGuid: student.id, - firstName: student.first_name, - lastName: student.last_name, - name: `${student.first_name} ${student.last_name}`, - }) as Skola24Child & EtjanstChild); + const myChildrenResponseJson = await currentUserResponse.json() + return myChildrenResponseJson.students.map( + (student: { id: any; first_name: any; last_name: any }) => + ({ + id: student.id, + sdsId: student.id, + personGuid: student.id, + firstName: student.first_name, + lastName: student.last_name, + name: `${student.first_name} ${student.last_name}`, + } as Skola24Child & EtjanstChild) + ) } - - async getCalendar(child: EtjanstChild): Promise { try { if (!this.isLoggedIn) { @@ -200,16 +203,23 @@ export class ApiAdmentum extends EventEmitter implements Api { } const now = DateTime.local() const [year, week] = now.toISOWeekDate().split('-') - const isoWeek = week.replace('W','') + const isoWeek = week.replace('W', '') - const fetchUrl = apiUrls.overview(year.toString(), isoWeek.toString()) + const fetchUrl = apiUrls.overview( + 'get-week-data', + year.toString(), + isoWeek.toString() + ) console.log('fetching calendar', fetchUrl) - //const calendarResponse = await this.fetch('get-calendar', fetchUrl) + //const calendarResponse = await this.fetch('get-calendar', fetchUrl) //const calendarResponseJson = await calendarResponse.json() - const overviewResponse = await this.fetch('get-overview', fetchUrl) - console.log('overview response', overviewResponse) + const overviewResponse = await this.fetch('get-week-data', fetchUrl, { + headers: { + 'x-requested-with': 'XMLHttpRequest', + }, + }) const overviewJson = await overviewResponse.json() - console.log('overview response', overviewJson) + console.log('get-week-data response', overviewJson) const schedule_events = (await overviewJson)?.data?.schedule_events // .breaks: [] | .assignments: [] if (!schedule_events) { return Promise.resolve([]) @@ -250,10 +260,12 @@ export class ApiAdmentum extends EventEmitter implements Api { "weekly_interval": "" } */ - return Promise.resolve(schedule_events.map(({ menu, date } : any) => ({ - title: date, - description: menu - }))) + return Promise.resolve( + schedule_events.map(({ menu, date }: any) => ({ + title: date, + description: menu, + })) + ) } catch (e) { console.error('Error fetching menu', e) return Promise.resolve([]) @@ -265,12 +277,12 @@ export class ApiAdmentum extends EventEmitter implements Api { throw new Error('Not logged in...') } console.log('get calendar') - const fetchUrl = apiUrls.schedule_events; + const fetchUrl = apiUrls.schedule_events console.log('fetching calendar', fetchUrl) const eventsResponse = await this.fetch('scheduled-events', fetchUrl, { method: 'GET', headers: { - 'Accept': 'application/json, text/plain, */*', + Accept: 'application/json, text/plain, */*', }, }) @@ -280,10 +292,12 @@ export class ApiAdmentum extends EventEmitter implements Api { } if (eventsResponse.status !== 200) { console.error('Error headers', eventsResponse.headers) - throw new Error('Could not fetch children. Response code: ' + eventsResponse.status) + throw new Error( + 'Could not fetch children. Response code: ' + eventsResponse.status + ) } - const eventsResponseJson = await eventsResponse.json(); + const eventsResponseJson = await eventsResponse.json() console.log('eventsResponseJson', eventsResponseJson) return [] // const fetchUrl = apiUrls.schedule_events @@ -294,9 +308,6 @@ export class ApiAdmentum extends EventEmitter implements Api { // }, // }).then(res => res.json()).then(json => json.results) - - - // return events.map(parseScheduleEvent)*/ } @@ -368,7 +379,7 @@ export class ApiAdmentum extends EventEmitter implements Api { async getNewsDetails(_child: EtjanstChild, item: NewsItem): Promise { return { ...item } } -/* + /* "data": { "food_week": { "id": 12846, @@ -399,7 +410,7 @@ export class ApiAdmentum extends EventEmitter implements Api { const fetchUrl = apiUrls.menu(year.toString(), isoWeek.toString()) console.log('fetching menu', fetchUrl) - const menuResponse = (await this.fetch('get-menu', fetchUrl)) + const menuResponse = await this.fetch('get-menu', fetchUrl) const menuResponseJson = await menuResponse.json() console.log('menu response', menuResponseJson) const days = (await menuResponseJson)?.data?.food_week?.food_days @@ -444,13 +455,12 @@ export class ApiAdmentum extends EventEmitter implements Api { year: number, _lang: string ): Promise { - const fetchUrl = apiUrls.schedule(year.toString(), week.toString()) console.log('fetching timetable', fetchUrl) - const calendarResponse = await this.fetch('get-calendar', fetchUrl) + const calendarResponse = await this.fetch('get-calendar', fetchUrl) const calendarResponseJson = await calendarResponse.json() const timetableEntries = parseCalendarItem(calendarResponseJson) - return timetableEntries; + return timetableEntries } async logout(): Promise { @@ -465,11 +475,10 @@ export class ApiAdmentum extends EventEmitter implements Api { if (personalNumber !== undefined && personalNumber.endsWith('1212121212')) return this.fakeMode() - console.log('login adentum', personalNumber) this.isFake = false - const authenticatedUser = await this.getUser(); + const authenticatedUser = await this.getUser() if (authenticatedUser && authenticatedUser.isAuthenticated) { console.log('already logged in to admentum') this.isLoggedIn = true @@ -518,11 +527,10 @@ export class ApiAdmentum extends EventEmitter implements Api { this.isLoggedIn = true this.personalNumber = personalNumber - const locomotiveUrl = redirectLocomotive(sessionId) - console.log('calling locomotive url: ', locomotiveUrl); - const callbackResponse = await this.followRedirects(locomotiveUrl); - console.log('final response:', callbackResponse); + console.log('calling locomotive url: ', locomotiveUrl) + const callbackResponse = await this.followRedirects(locomotiveUrl) + console.log('final response:', callbackResponse) this.emit('login') }) statusChecker.on('ERROR', () => { @@ -531,35 +539,38 @@ export class ApiAdmentum extends EventEmitter implements Api { return statusChecker } - + async followRedirects(initialUrl: string): Promise { - let currentUrl = initialUrl; - let redirectCount = 0; - const maxRedirects = 10; + let currentUrl = initialUrl + let redirectCount = 0 + const maxRedirects = 10 while (redirectCount < maxRedirects) { - console.log('fetching (redirect number ' + redirectCount + ')', currentUrl); + console.log( + 'fetching (redirect number ' + redirectCount + ')', + currentUrl + ) const response = await this.fetch('follow-redirect', currentUrl, { method: 'GET', redirect: 'manual', // Disable automatic redirects - }); - console.log('follow-redirect response', response); + }) + console.log('follow-redirect response', response) if (response.status >= 300 && response.status < 400) { - console.log('response status:', response.status); - const newLocation = response.headers.get('location'); + console.log('response status:', response.status) + const newLocation = response.headers.get('location') if (!newLocation) { - throw new Error('Redirect response missing location header'); + throw new Error('Redirect response missing location header') } - currentUrl = newLocation; - redirectCount++; + currentUrl = newLocation + redirectCount++ } else { - console.log('response status, not reidrect:', response.status); + console.log('response status, not reidrect:', response.status) // The response is not a redirect, return it - return response; + return response } } - throw new Error('Max redirects reached'); - }; + throw new Error('Max redirects reached') + } private async fakeMode(): Promise { this.isFake = true diff --git a/libs/api-admentum/lib/routes.ts b/libs/api-admentum/lib/routes.ts index 04ddc1b67..4a0ae017b 100644 --- a/libs/api-admentum/lib/routes.ts +++ b/libs/api-admentum/lib/routes.ts @@ -17,7 +17,8 @@ export const apiUrls = { lessons: api + 'lessons', organisations: api + 'organisations', orientations: api + 'orientations', - overview: (year: string, week: string) => baseUrl + `overview?week=${week}&year=${year}`, + overview: (action: string, year: string, week: string) => + baseUrl + `overview?action=${action}&week=${week}&year=${year}`, permission_groups: api + 'permission_groups', primary_group_enrollments: api + 'primary_group_enrollments', primary_group_municipality_statistics: @@ -35,14 +36,16 @@ export const apiUrls = { api + 'schedule_group_teacher_enrollments', schedule_groups: api + 'schedule_groups', schedules: api + 'schedules', - schedule: (year: string, week: string) => baseUrl + `schedule/schedule?week=${week}&year=${year}`, + schedule: (year: string, week: string) => + baseUrl + `schedule/schedule?week=${week}&year=${year}`, school_enrollments: `${api}school_enrollments`, school_years: api + 'school_years', schools: api + 'schools', sickness: api + 'sickness', subjects: api + 'subjects', teachers: api + 'teachers', - menu: (year: string, week: string) => baseUrl + `api/food/week/${week}/${year}`, + menu: (year: string, week: string) => + baseUrl + `api/food/week/${week}/${year}`, upper_secondary_subjects: api + 'upper_secondary_subjects', users: api + 'users?format=json', user: (userId: string) => api + `users/${userId}/?format=json`, From f1c622ef60cf2cf9c418bd9ab1a0c96da19691ee Mon Sep 17 00:00:00 2001 From: William Ryder Date: Fri, 6 Oct 2023 12:29:06 +0200 Subject: [PATCH 24/30] wip: fix timetable and minor things --- apps/api-test-app/src/main.js | 8 +++----- libs/api-admentum/lib/apiAdmentum.ts | 22 +++++++++++++--------- libs/api-admentum/lib/parse/parsers.ts | 7 ++++--- libs/api-admentum/lib/routes.ts | 3 +++ 4 files changed, 23 insertions(+), 17 deletions(-) diff --git a/apps/api-test-app/src/main.js b/apps/api-test-app/src/main.js index 18cc2c636..41550e4ce 100644 --- a/apps/api-test-app/src/main.js +++ b/apps/api-test-app/src/main.js @@ -72,6 +72,9 @@ async function run() { console.log('calendar') const calendar = await api.getCalendar(children[0]) console.log(calendar) + + console.log('news') + const news = await api.getNews(children[0]) /* /*console.log('classmates') const classmates = await api.getClassmates(children[0]) @@ -97,11 +100,6 @@ async function run() { console.error(error) } - - /* - console.log('news') - const news = await api.getNews(children[0]) -*/ /* console.log('news details') const newsItems = await Promise.all( news.map((newsItem) => diff --git a/libs/api-admentum/lib/apiAdmentum.ts b/libs/api-admentum/lib/apiAdmentum.ts index 704ee1962..19f804abf 100644 --- a/libs/api-admentum/lib/apiAdmentum.ts +++ b/libs/api-admentum/lib/apiAdmentum.ts @@ -211,8 +211,6 @@ export class ApiAdmentum extends EventEmitter implements Api { isoWeek.toString() ) console.log('fetching calendar', fetchUrl) - //const calendarResponse = await this.fetch('get-calendar', fetchUrl) - //const calendarResponseJson = await calendarResponse.json() const overviewResponse = await this.fetch('get-week-data', fetchUrl, { headers: { 'x-requested-with': 'XMLHttpRequest', @@ -260,14 +258,9 @@ export class ApiAdmentum extends EventEmitter implements Api { "weekly_interval": "" } */ - return Promise.resolve( - schedule_events.map(({ menu, date }: any) => ({ - title: date, - description: menu, - })) - ) + return Promise.resolve([]) } catch (e) { - console.error('Error fetching menu', e) + console.error('Error fetching overview', e) return Promise.resolve([]) } } @@ -373,6 +366,17 @@ export class ApiAdmentum extends EventEmitter implements Api { if (!this.isLoggedIn) { throw new Error('Not logged in...') } + + const fetchUrl = apiUrls.messages(this.userId, '1') + console.log('fetching messages', fetchUrl) + const messagesResponse = await this.fetch('get-messages', fetchUrl, { + headers: { + 'x-requested-with': 'XMLHttpRequest', + }, + }) + const messagesResponseJson = await messagesResponse.json() + console.log('messages response', messagesResponseJson) + return Promise.resolve([]) } diff --git a/libs/api-admentum/lib/parse/parsers.ts b/libs/api-admentum/lib/parse/parsers.ts index 8a5e78726..26c466121 100644 --- a/libs/api-admentum/lib/parse/parsers.ts +++ b/libs/api-admentum/lib/parse/parsers.ts @@ -2,6 +2,7 @@ import * as html from 'node-html-parser' import { decode } from 'he' import { CalendarItem, TimetableEntry } from 'libs/api/lib/types' import { DateTime, FixedOffsetZone } from 'luxon' +import { teacher } from 'libs/api-skolplattformen/lib/parse' // TODO: Move this into the parse folder and convert it to follow the pattern of other parsers (include tests). @@ -89,12 +90,12 @@ export const parseCalendarItem = (jsonData: any): any => { const dayOfWeek = DayOfWeek[day.name as keyof typeof DayOfWeek] timetableEntries.push({ id: lesson.id, - teacher: lesson.bookedTeacherNames && lesson.bookedTeacherNames[0], - location: lesson.location, + teacher: lesson.teachers, + location: lesson.room || lesson.title || lesson.subject_name, timeStart: lesson.time.substring(0, 5), timeEnd: lesson.time.substring(9), dayOfWeek, - blockName: lesson.title || lesson.subject_name + blockName: lesson.title || lesson.subject_name, } as TimetableEntry) }); }) diff --git a/libs/api-admentum/lib/routes.ts b/libs/api-admentum/lib/routes.ts index 4a0ae017b..ea55668ae 100644 --- a/libs/api-admentum/lib/routes.ts +++ b/libs/api-admentum/lib/routes.ts @@ -15,6 +15,9 @@ export const apiUrls = { leisure_groups: api + 'leisure_groups', lesson_infos: api + 'lesson_infos', lessons: api + 'lessons', + // start at page 1 + messages: (userId: string, page: string) => + `https://messages.admentum.se/api/users/${userId}/conversations?page=${page}`, // unread_only=1 organisations: api + 'organisations', orientations: api + 'orientations', overview: (action: string, year: string, week: string) => From 53ee3c1cad49f3e51c643f94ee34cc02dee7a29f Mon Sep 17 00:00:00 2001 From: Christian Landgren Date: Fri, 6 Oct 2023 14:50:27 +0200 Subject: [PATCH 25/30] feat: get messages and canvas token --- libs/api-admentum/lib/apiAdmentum.ts | 8 +++++++- libs/api-admentum/lib/routes.ts | 5 +++-- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/libs/api-admentum/lib/apiAdmentum.ts b/libs/api-admentum/lib/apiAdmentum.ts index 19f804abf..bf77e6e9e 100644 --- a/libs/api-admentum/lib/apiAdmentum.ts +++ b/libs/api-admentum/lib/apiAdmentum.ts @@ -366,12 +366,18 @@ export class ApiAdmentum extends EventEmitter implements Api { if (!this.isLoggedIn) { throw new Error('Not logged in...') } + const token = await this.fetch('get-messages', apiUrls.messages) + .then((res) => res.text()) + .then((html) => /token:\s+'(.*)'/.exec(html)?.pop()) // HACK: this could probably be found at a better place than the html code.. - const fetchUrl = apiUrls.messages(this.userId, '1') + console.log('token', token) + + const fetchUrl = apiUrls.conversations(this.userId, '1') console.log('fetching messages', fetchUrl) const messagesResponse = await this.fetch('get-messages', fetchUrl, { headers: { 'x-requested-with': 'XMLHttpRequest', + 'user-info': token, }, }) const messagesResponseJson = await messagesResponse.json() diff --git a/libs/api-admentum/lib/routes.ts b/libs/api-admentum/lib/routes.ts index ea55668ae..0b29d3442 100644 --- a/libs/api-admentum/lib/routes.ts +++ b/libs/api-admentum/lib/routes.ts @@ -15,9 +15,10 @@ export const apiUrls = { leisure_groups: api + 'leisure_groups', lesson_infos: api + 'lesson_infos', lessons: api + 'lessons', + messages: 'https://skola.admentum.se/messages/', // start at page 1 - messages: (userId: string, page: string) => - `https://messages.admentum.se/api/users/${userId}/conversations?page=${page}`, // unread_only=1 + conversations: (userId: string, page: string) => + `https://messages.admentum.se/api/users/${userId}/conversations?page=${page}`, // unread_only=1 organisations: api + 'organisations', orientations: api + 'orientations', overview: (action: string, year: string, week: string) => From b0764cf65ade5441811b633a655e16d53ae043dc Mon Sep 17 00:00:00 2001 From: Christian Landgren Date: Fri, 6 Oct 2023 14:51:11 +0200 Subject: [PATCH 26/30] fix: correct name for fetch token --- libs/api-admentum/lib/apiAdmentum.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/api-admentum/lib/apiAdmentum.ts b/libs/api-admentum/lib/apiAdmentum.ts index bf77e6e9e..5750d1538 100644 --- a/libs/api-admentum/lib/apiAdmentum.ts +++ b/libs/api-admentum/lib/apiAdmentum.ts @@ -366,7 +366,7 @@ export class ApiAdmentum extends EventEmitter implements Api { if (!this.isLoggedIn) { throw new Error('Not logged in...') } - const token = await this.fetch('get-messages', apiUrls.messages) + const token = await this.fetch('get-token', apiUrls.messages) .then((res) => res.text()) .then((html) => /token:\s+'(.*)'/.exec(html)?.pop()) // HACK: this could probably be found at a better place than the html code.. From ef5e5601c298fce6301ff788d35009ea1cd0b902 Mon Sep 17 00:00:00 2001 From: William Ryder Date: Fri, 6 Oct 2023 15:17:16 +0200 Subject: [PATCH 27/30] feat: fetch week data --- libs/api-admentum/lib/apiAdmentum.ts | 104 +++++-------------------- libs/api-admentum/lib/parse/parsers.ts | 67 +++++++++++++++- 2 files changed, 85 insertions(+), 86 deletions(-) diff --git a/libs/api-admentum/lib/apiAdmentum.ts b/libs/api-admentum/lib/apiAdmentum.ts index 19f804abf..301228991 100644 --- a/libs/api-admentum/lib/apiAdmentum.ts +++ b/libs/api-admentum/lib/apiAdmentum.ts @@ -29,7 +29,12 @@ import { DateTime, FixedOffsetZone } from 'luxon' import * as html from 'node-html-parser' import { fakeFetcher } from './fake/fakeFetcher' import { checkStatus, DummyStatusChecker } from './loginStatus' -import { extractMvghostRequestBody, parseCalendarItem } from './parse/parsers' +import { + extractMvghostRequestBody, + parseTimetableData, + parseScheduleEventData, + parseBreaksData, + } from './parse/parsers' import { bankIdInitUrl, bankIdCheckUrl, @@ -204,7 +209,7 @@ export class ApiAdmentum extends EventEmitter implements Api { const now = DateTime.local() const [year, week] = now.toISOWeekDate().split('-') const isoWeek = week.replace('W', '') - + const fetchUrl = apiUrls.overview( 'get-week-data', year.toString(), @@ -216,94 +221,25 @@ export class ApiAdmentum extends EventEmitter implements Api { 'x-requested-with': 'XMLHttpRequest', }, }) + const calendarItems: CalendarItem[] = [] + const overviewJson = await overviewResponse.json() - console.log('get-week-data response', overviewJson) - const schedule_events = (await overviewJson)?.data?.schedule_events // .breaks: [] | .assignments: [] - if (!schedule_events) { - return Promise.resolve([]) - } - /* -"url": "https://skola.admentum.se/api/v1/schedule_event_instances/2990834/", - "id": 2990834, - "school_id": 824, - "start_date": "2023-08-07", - "end_date": "2023-08-07", - "schedule_event": { - "url": "https://skola.admentum.se/api/v1/schedule_events/148722/", - "id": 148722, - "eid": null, - "schedule_id": 4385, - "name": "Engelska", - "start_time": "08:00:00", - "end_time": "09:30:00", - "rooms": [ - { - "url": "https://skola.admentum.se/api/v1/rooms/7200/", - "id": 7200 - } - ], - "teachers": [ - { - "url": "https://skola.admentum.se/api/v1/users/437302/", - "id": 437302 - } - ], - "schedule_groups": [], - "primary_groups": [ - { - "url": "https://skola.admentum.se/api/v1/primary_groups/36874/", - "id": 36874 - } - ], - "weekly_interval": "" - } - */ - return Promise.resolve([]) + + const scheduleEventJson = (await overviewJson)?.data?.schedule_events // .breaks: [] | .assignments: [] + const schedule_events = parseScheduleEventData(scheduleEventJson) + calendarItems.push(...schedule_events) + + const breaks = (await overviewJson)?.data?.breaks + const break_events = parseBreaksData(breaks); + calendarItems.push(...break_events) + + return calendarItems } catch (e) { console.error('Error fetching overview', e) return Promise.resolve([]) } } - async getScheduledEvents(child: EtjanstChild): Promise { - if (!this.isLoggedIn) { - throw new Error('Not logged in...') - } - console.log('get calendar') - const fetchUrl = apiUrls.schedule_events - console.log('fetching calendar', fetchUrl) - const eventsResponse = await this.fetch('scheduled-events', fetchUrl, { - method: 'GET', - headers: { - Accept: 'application/json, text/plain, */*', - }, - }) - - if (eventsResponse.status === 403) { - console.error('Not allwed. Error headers', eventsResponse.headers) - return [] - } - if (eventsResponse.status !== 200) { - console.error('Error headers', eventsResponse.headers) - throw new Error( - 'Could not fetch children. Response code: ' + eventsResponse.status - ) - } - - const eventsResponseJson = await eventsResponse.json() - console.log('eventsResponseJson', eventsResponseJson) - return [] - // const fetchUrl = apiUrls.schedule_events - // const events = await this.fetch('scheduled-events', fetchUrl, { - // method: 'GET', - // headers: { - // 'Accept': 'application/json, text/plain, */*', - // }, - // }).then(res => res.json()).then(json => json.results) - - // return events.map(parseScheduleEvent)*/ - } - // eslint-disable-next-line @typescript-eslint/no-unused-vars getClassmates(_child: EtjanstChild): Promise { // TODO: We could get this from the events a child is associated with... @@ -463,7 +399,7 @@ export class ApiAdmentum extends EventEmitter implements Api { console.log('fetching timetable', fetchUrl) const calendarResponse = await this.fetch('get-calendar', fetchUrl) const calendarResponseJson = await calendarResponse.json() - const timetableEntries = parseCalendarItem(calendarResponseJson) + const timetableEntries = parseTimetableData(calendarResponseJson) return timetableEntries } diff --git a/libs/api-admentum/lib/parse/parsers.ts b/libs/api-admentum/lib/parse/parsers.ts index 26c466121..7bc908b9b 100644 --- a/libs/api-admentum/lib/parse/parsers.ts +++ b/libs/api-admentum/lib/parse/parsers.ts @@ -71,6 +71,69 @@ export const parseScheduleEvent = (({ allDay?: start_time === '00:00:00' && end_time === '23:59:00' }) */ +/* OVERVIEW: +"status": 200, + "data": { + "year": 2023, + "week": 38, + "assignments": [], + "breaks": [ + { + "id": 11031, + "break_type": 1, + "break_period": 1, + "name": "Studiedag", + "date": "2023-09-21", + "week": null, + "start_date": null, + "end_date": null + } + ], + "schedule_events": [ + { + "id": 3110610, + "name": "Utvecklingssamtal", + "formatted_time": "Heldag", + "formatted_date": "2023-09-22" + } + ] + } +} +*/ +export const parseBreaksData = (jsonData: any): CalendarItem[] => { + const breakItems: CalendarItem[] = [] + if (jsonData) { + jsonData.forEach((event: { id: any; name: any; date: any, start_date: any; end_date: any }) => { + breakItems.push({ + id: event.id, + title: event.name, + startDate: event.start_date || event.date, + endDate: event.end_date || event.date, + } as CalendarItem) + }); + } else { + console.error("Failed to parse breaks, no breaks found in json data.") + } + return breakItems; +} + +export const parseScheduleEventData = (jsonData: any): CalendarItem[] => { + const calendarItems: CalendarItem[] = [] + if (jsonData) { + jsonData.forEach((event: { id: any; name: any; formatted_date: any; formatted_time: any }) => { + calendarItems.push({ + id: event.id, + title: event.name, + startDate: event.formatted_date, + endDate: event.formatted_date, + allDay: event.formatted_time === 'Heldag', + } as CalendarItem) + }); + } else { + console.error("Failed to parse schedule events, no schedule events found in json data.") + } + return calendarItems; +} enum DayOfWeek { 'Måndag'= 1, @@ -82,7 +145,7 @@ enum DayOfWeek { 'Söndag'= 7, } -export const parseCalendarItem = (jsonData: any): any => { +export const parseTimetableData = (jsonData: any): any => { const timetableEntries: TimetableEntry[] = [] if (jsonData && jsonData.days && Array.isArray(jsonData.days) && jsonData.days.length > 0) { jsonData.days.forEach((day: { name: string, lessons: any[] }) => { @@ -100,7 +163,7 @@ export const parseCalendarItem = (jsonData: any): any => { }); }) } else { - console.error("Failed to parse calendar item, no days found in json data.") + console.error("Failed to parse timetable, no days found in json data.") } return timetableEntries; } From dd0f8f3cc4bff9b77fd4d2225af090adff604834 Mon Sep 17 00:00:00 2001 From: William Ryder Date: Fri, 6 Oct 2023 16:17:21 +0200 Subject: [PATCH 28/30] feat: parse news data --- libs/api-admentum/lib/apiAdmentum.ts | 5 ++- libs/api-admentum/lib/parse/parsers.ts | 60 +++++++++++++++++++++++++- 2 files changed, 62 insertions(+), 3 deletions(-) diff --git a/libs/api-admentum/lib/apiAdmentum.ts b/libs/api-admentum/lib/apiAdmentum.ts index 325fae8ad..698c6ecac 100644 --- a/libs/api-admentum/lib/apiAdmentum.ts +++ b/libs/api-admentum/lib/apiAdmentum.ts @@ -34,6 +34,7 @@ import { parseTimetableData, parseScheduleEventData, parseBreaksData, + parseNewsData, } from './parse/parsers' import { bankIdInitUrl, @@ -319,7 +320,9 @@ export class ApiAdmentum extends EventEmitter implements Api { const messagesResponseJson = await messagesResponse.json() console.log('messages response', messagesResponseJson) - return Promise.resolve([]) + const newsItems = parseNewsData(messagesResponseJson) + console.log('newsItems', newsItems) + return newsItems } async getNewsDetails(_child: EtjanstChild, item: NewsItem): Promise { diff --git a/libs/api-admentum/lib/parse/parsers.ts b/libs/api-admentum/lib/parse/parsers.ts index 7bc908b9b..52da7f788 100644 --- a/libs/api-admentum/lib/parse/parsers.ts +++ b/libs/api-admentum/lib/parse/parsers.ts @@ -1,8 +1,8 @@ import * as html from 'node-html-parser' import { decode } from 'he' -import { CalendarItem, TimetableEntry } from 'libs/api/lib/types' +import { CalendarItem, NewsItem, TimetableEntry } from 'libs/api/lib/types' import { DateTime, FixedOffsetZone } from 'luxon' -import { teacher } from 'libs/api-skolplattformen/lib/parse' +import { news, teacher } from 'libs/api-skolplattformen/lib/parse' // TODO: Move this into the parse folder and convert it to follow the pattern of other parsers (include tests). @@ -135,6 +135,62 @@ export const parseScheduleEventData = (jsonData: any): CalendarItem[] => { return calendarItems; } +/* +"conversations": [ + { + "id": "14b643b9-fd09-4b4a-9313-b1e75a94a0a8", + "latest_message": { + "id": "72bb3c4b-efb9-4056-822b-9fbd93c7905c", + "content": "text", + "message_type": 1, + "meta_id": "", + "created_at": "2023-10-06 10:57:54.795854+00:00", + "formatted_created_at": "Idag, 10:57" + }, + "recipients": [], + "title": "Veckobrev v 40", + "json_recipients": { + "names": { + "primary_groups": { + "36886": "6 A", + "36887": "6 B" + } + }, + "parents": { + "primary_groups": [ + 36886, + 36887 + ] + }, + "is_information": true + }, + "is_unread": false, + "creator": { + "id": 437302, + "first_name": "Christian", + "last_name": "Landgren" + }, + "flag": 0 + }, +*/ +export const parseNewsData = (jsonData: any): NewsItem[] => { + const newsItems: NewsItem[] = [] + if (jsonData && jsonData.conversations && Array.isArray(jsonData.conversations) && jsonData.conversations.length > 0) { + jsonData.conversations.forEach((item: any) => { + newsItems.push({ + id: item.id, + author: item.creator?.first_name + ' ' + item.creator?.last_name, + header: item.title, + body: item.latest_message?.content, + published: item.latest_message?.created_at.split(" ")[0], + } as NewsItem); + }); + } else { + console.error("Failed to parse news, no news found in json data.") + } + return newsItems; +} + enum DayOfWeek { 'Måndag'= 1, 'Tisdag'= 2, From ab9b3a5b42b3e87abda79c83d7e531cdfa6f37f6 Mon Sep 17 00:00:00 2001 From: Christian Landgren Date: Fri, 6 Oct 2023 16:44:06 +0200 Subject: [PATCH 29/30] feat: convert newsletter to markdown --- libs/api-admentum/lib/parse/parsers.ts | 152 ++++++++++++++++--------- 1 file changed, 96 insertions(+), 56 deletions(-) diff --git a/libs/api-admentum/lib/parse/parsers.ts b/libs/api-admentum/lib/parse/parsers.ts index 52da7f788..acd5c42ef 100644 --- a/libs/api-admentum/lib/parse/parsers.ts +++ b/libs/api-admentum/lib/parse/parsers.ts @@ -3,49 +3,61 @@ import { decode } from 'he' import { CalendarItem, NewsItem, TimetableEntry } from 'libs/api/lib/types' import { DateTime, FixedOffsetZone } from 'luxon' import { news, teacher } from 'libs/api-skolplattformen/lib/parse' +import { toMarkdown } from '@skolplattformen/api' // TODO: Move this into the parse folder and convert it to follow the pattern of other parsers (include tests). export const extractInputField = (sought: string, attrs: string[]) => { // there must be a better way to do this... - const s = attrs.find(e => e.indexOf(sought) >= 0) || "" + const s = attrs.find((e) => e.indexOf(sought) >= 0) || '' const v = s.substring(s.indexOf('value="') + 'value="'.length) return v.substring(0, v.length - 2) } export function extractMvghostRequestBody(initBankIdResponseText: string) { const doc = html.parse(decode(initBankIdResponseText)) - const inputAttrs = doc.querySelectorAll('input').map(i => (i as any).rawAttrs) + const inputAttrs = doc + .querySelectorAll('input') + .map((i) => (i as any).rawAttrs) const relayState = extractInputField('RelayState', inputAttrs) - const samlRequest = extractInputField("SAMLRequest", inputAttrs) - const mvghostRequestBody = `RelayState=${encodeURIComponent(relayState)}&SAMLRequest=${encodeURIComponent(samlRequest)}` - + const samlRequest = extractInputField('SAMLRequest', inputAttrs) + const mvghostRequestBody = `RelayState=${encodeURIComponent( + relayState + )}&SAMLRequest=${encodeURIComponent(samlRequest)}` + return mvghostRequestBody } export function extractHjarntorgetSAMLLogin(authGbgLoginResponseText: string) { const authGbgLoginDoc = html.parse(decode(authGbgLoginResponseText)) - const inputAttrs = authGbgLoginDoc.querySelectorAll('input').map(i => (i as any).rawAttrs) + const inputAttrs = authGbgLoginDoc + .querySelectorAll('input') + .map((i) => (i as any).rawAttrs) const RelayStateText = extractInputField('RelayState', inputAttrs) - const SAMLResponseText = extractInputField("SAMLResponse", inputAttrs) + const SAMLResponseText = extractInputField('SAMLResponse', inputAttrs) - return `SAMLResponse=${encodeURIComponent(SAMLResponseText || '')}&RelayState=${encodeURIComponent(RelayStateText || '')}` + return `SAMLResponse=${encodeURIComponent( + SAMLResponseText || '' + )}&RelayState=${encodeURIComponent(RelayStateText || '')}` } export function extractAuthGbgLoginRequestBody(signatureResponseText: string) { const signatureResponseDoc = html.parse(decode(signatureResponseText)) - const signatureResponseTextAreas = signatureResponseDoc.querySelectorAll('textarea') - const SAMLResponseElem = signatureResponseTextAreas.find(ta => { - const nameAttr = ta.getAttribute("name") + const signatureResponseTextAreas = + signatureResponseDoc.querySelectorAll('textarea') + const SAMLResponseElem = signatureResponseTextAreas.find((ta) => { + const nameAttr = ta.getAttribute('name') return nameAttr === 'SAMLResponse' }) const SAMLResponseText = SAMLResponseElem?.rawText - const RelayStateElem = signatureResponseTextAreas.find(ta => { - const nameAttr = ta.getAttribute("name") + const RelayStateElem = signatureResponseTextAreas.find((ta) => { + const nameAttr = ta.getAttribute('name') return nameAttr === 'RelayState' }) const RelayStateText = RelayStateElem?.rawText - const authGbgLoginBody = `SAMLResponse=${encodeURIComponent(SAMLResponseText || '')}&RelayState=${encodeURIComponent(RelayStateText || '')}` + const authGbgLoginBody = `SAMLResponse=${encodeURIComponent( + SAMLResponseText || '' + )}&RelayState=${encodeURIComponent(RelayStateText || '')}` return authGbgLoginBody } @@ -103,36 +115,53 @@ export const parseScheduleEvent = (({ export const parseBreaksData = (jsonData: any): CalendarItem[] => { const breakItems: CalendarItem[] = [] if (jsonData) { - jsonData.forEach((event: { id: any; name: any; date: any, start_date: any; end_date: any }) => { - breakItems.push({ - id: event.id, - title: event.name, - startDate: event.start_date || event.date, - endDate: event.end_date || event.date, - } as CalendarItem) - }); + jsonData.forEach( + (event: { + id: any + name: any + date: any + start_date: any + end_date: any + }) => { + breakItems.push({ + id: event.id, + title: event.name, + startDate: event.start_date || event.date, + endDate: event.end_date || event.date, + } as CalendarItem) + } + ) } else { - console.error("Failed to parse breaks, no breaks found in json data.") + console.error('Failed to parse breaks, no breaks found in json data.') } - return breakItems; + return breakItems } export const parseScheduleEventData = (jsonData: any): CalendarItem[] => { const calendarItems: CalendarItem[] = [] if (jsonData) { - jsonData.forEach((event: { id: any; name: any; formatted_date: any; formatted_time: any }) => { - calendarItems.push({ - id: event.id, - title: event.name, - startDate: event.formatted_date, - endDate: event.formatted_date, - allDay: event.formatted_time === 'Heldag', - } as CalendarItem) - }); + jsonData.forEach( + (event: { + id: any + name: any + formatted_date: any + formatted_time: any + }) => { + calendarItems.push({ + id: event.id, + title: event.name, + startDate: event.formatted_date, + endDate: event.formatted_date, + allDay: event.formatted_time === 'Heldag', + } as CalendarItem) + } + ) } else { - console.error("Failed to parse schedule events, no schedule events found in json data.") + console.error( + 'Failed to parse schedule events, no schedule events found in json data.' + ) } - return calendarItems; + return calendarItems } /* @@ -175,37 +204,48 @@ export const parseScheduleEventData = (jsonData: any): CalendarItem[] => { */ export const parseNewsData = (jsonData: any): NewsItem[] => { const newsItems: NewsItem[] = [] - if (jsonData && jsonData.conversations && Array.isArray(jsonData.conversations) && jsonData.conversations.length > 0) { + if ( + jsonData && + jsonData.conversations && + Array.isArray(jsonData.conversations) && + jsonData.conversations.length > 0 + ) { jsonData.conversations.forEach((item: any) => { + const bodyText = toMarkdown(item.latest_message?.content) newsItems.push({ id: item.id, author: item.creator?.first_name + ' ' + item.creator?.last_name, header: item.title, - body: item.latest_message?.content, - published: item.latest_message?.created_at.split(" ")[0], - } as NewsItem); - }); + body: bodyText, + published: item.latest_message?.created_at.split(' ')[0], + } as NewsItem) + }) } else { - console.error("Failed to parse news, no news found in json data.") + console.error('Failed to parse news, no news found in json data.') } - return newsItems; + return newsItems } enum DayOfWeek { - 'Måndag'= 1, - 'Tisdag'= 2, - 'Onsdag'= 3, - 'Torsdag'= 4, - 'Fredag'= 5, - 'Lördag'= 6, - 'Söndag'= 7, + 'Måndag' = 1, + 'Tisdag' = 2, + 'Onsdag' = 3, + 'Torsdag' = 4, + 'Fredag' = 5, + 'Lördag' = 6, + 'Söndag' = 7, } export const parseTimetableData = (jsonData: any): any => { const timetableEntries: TimetableEntry[] = [] - if (jsonData && jsonData.days && Array.isArray(jsonData.days) && jsonData.days.length > 0) { - jsonData.days.forEach((day: { name: string, lessons: any[] }) => { - day.lessons.forEach(lesson => { + if ( + jsonData && + jsonData.days && + Array.isArray(jsonData.days) && + jsonData.days.length > 0 + ) { + jsonData.days.forEach((day: { name: string; lessons: any[] }) => { + day.lessons.forEach((lesson) => { const dayOfWeek = DayOfWeek[day.name as keyof typeof DayOfWeek] timetableEntries.push({ id: lesson.id, @@ -216,12 +256,12 @@ export const parseTimetableData = (jsonData: any): any => { dayOfWeek, blockName: lesson.title || lesson.subject_name, } as TimetableEntry) - }); + }) }) } else { - console.error("Failed to parse timetable, no days found in json data.") + console.error('Failed to parse timetable, no days found in json data.') } - return timetableEntries; + return timetableEntries } /* @@ -327,4 +367,4 @@ export const parseTimetableData = (jsonData: any): any => { "weeks_amount": 52, "break_week": 27 } -*/ \ No newline at end of file +*/ From 7dc5b80b6569f2505f9ba9a6d6f1a0fc8f3550e0 Mon Sep 17 00:00:00 2001 From: Christian Landgren Date: Sat, 7 Oct 2023 00:56:45 +0200 Subject: [PATCH 30/30] fix: padStart in schedule --- libs/api-admentum/lib/apiAdmentum.ts | 17 +++++++++-------- libs/api-admentum/lib/parse/parsers.ts | 4 ++-- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/libs/api-admentum/lib/apiAdmentum.ts b/libs/api-admentum/lib/apiAdmentum.ts index 698c6ecac..3f29ab11a 100644 --- a/libs/api-admentum/lib/apiAdmentum.ts +++ b/libs/api-admentum/lib/apiAdmentum.ts @@ -29,13 +29,13 @@ import { DateTime, FixedOffsetZone } from 'luxon' import * as html from 'node-html-parser' import { fakeFetcher } from './fake/fakeFetcher' import { checkStatus, DummyStatusChecker } from './loginStatus' -import { - extractMvghostRequestBody, +import { + extractMvghostRequestBody, parseTimetableData, parseScheduleEventData, parseBreaksData, parseNewsData, - } from './parse/parsers' +} from './parse/parsers' import { bankIdInitUrl, bankIdCheckUrl, @@ -210,7 +210,7 @@ export class ApiAdmentum extends EventEmitter implements Api { const now = DateTime.local() const [year, week] = now.toISOWeekDate().split('-') const isoWeek = week.replace('W', '') - + const fetchUrl = apiUrls.overview( 'get-week-data', year.toString(), @@ -223,15 +223,15 @@ export class ApiAdmentum extends EventEmitter implements Api { }, }) const calendarItems: CalendarItem[] = [] - + const overviewJson = await overviewResponse.json() - + const scheduleEventJson = (await overviewJson)?.data?.schedule_events // .breaks: [] | .assignments: [] const schedule_events = parseScheduleEventData(scheduleEventJson) calendarItems.push(...schedule_events) - + const breaks = (await overviewJson)?.data?.breaks - const break_events = parseBreaksData(breaks); + const break_events = parseBreaksData(breaks) calendarItems.push(...break_events) return calendarItems @@ -408,6 +408,7 @@ export class ApiAdmentum extends EventEmitter implements Api { console.log('fetching timetable', fetchUrl) const calendarResponse = await this.fetch('get-calendar', fetchUrl) const calendarResponseJson = await calendarResponse.json() + console.log('calendar json', calendarResponseJson) const timetableEntries = parseTimetableData(calendarResponseJson) return timetableEntries } diff --git a/libs/api-admentum/lib/parse/parsers.ts b/libs/api-admentum/lib/parse/parsers.ts index acd5c42ef..7688b0f23 100644 --- a/libs/api-admentum/lib/parse/parsers.ts +++ b/libs/api-admentum/lib/parse/parsers.ts @@ -251,8 +251,8 @@ export const parseTimetableData = (jsonData: any): any => { id: lesson.id, teacher: lesson.teachers, location: lesson.room || lesson.title || lesson.subject_name, - timeStart: lesson.time.substring(0, 5), - timeEnd: lesson.time.substring(9), + timeStart: lesson.time.split(' - ')[0].padStart(5, '0'), // 8:00 - 9:30 + timeEnd: lesson.time.split(' - ')[1].padStart(5, '0'), dayOfWeek, blockName: lesson.title || lesson.subject_name, } as TimetableEntry)