324 lines
8.7 KiB
TypeScript
324 lines
8.7 KiB
TypeScript
/* 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<void> {
|
|
// 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<User> {
|
|
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<CalendarItem[]> {
|
|
return Promise.resolve([])
|
|
}
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
getClassmates(_child: EtjanstChild): Promise<Classmate[]> {
|
|
// 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<Teacher[]> {
|
|
if (!this.isLoggedIn) {
|
|
throw new Error('Not logged in...')
|
|
}
|
|
return Promise.resolve([])
|
|
}
|
|
|
|
public async getSchoolContacts(
|
|
child: EtjanstChild
|
|
): Promise<SchoolContact[]> {
|
|
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<NewsItem[]> {
|
|
if (!this.isLoggedIn) {
|
|
throw new Error('Not logged in...')
|
|
}
|
|
return Promise.resolve([])
|
|
}
|
|
|
|
async getNewsDetails(_child: EtjanstChild, item: NewsItem): Promise<any> {
|
|
return { ...item }
|
|
}
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
getMenu(_child: EtjanstChild): Promise<MenuItem[]> {
|
|
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<Notification[]> {
|
|
return Promise.resolve([])
|
|
}
|
|
|
|
async getSkola24Children(): Promise<Skola24Child[]> {
|
|
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<TimetableEntry[]> {
|
|
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<void> {
|
|
this.isLoggedIn = false
|
|
this.personalNumber = undefined
|
|
this.cookieManager.clearAll()
|
|
this.emit('logout')
|
|
}
|
|
|
|
public async login(personalNumber?: string): Promise<LoginStatusChecker> {
|
|
// 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<LoginStatusChecker> {
|
|
this.isFake = true
|
|
|
|
setTimeout(() => {
|
|
this.isLoggedIn = true
|
|
this.emit('login')
|
|
}, 50)
|
|
|
|
const emitter: any = new EventEmitter()
|
|
emitter.token = 'fake'
|
|
return emitter
|
|
}
|
|
|
|
async loginFreja(): Promise<FrejaLoginStatusChecker> {
|
|
throw new Error('Not implemented...')
|
|
}
|
|
}
|