2021-01-03 12:08:53 +00:00
|
|
|
import { DateTime } from 'luxon'
|
2020-12-30 13:39:49 +00:00
|
|
|
import { EventEmitter } from 'events'
|
2021-03-01 09:36:45 +00:00
|
|
|
import { decode } from 'he'
|
|
|
|
import * as html from 'node-html-parser'
|
2020-12-30 13:39:49 +00:00
|
|
|
import {
|
2021-02-08 20:22:38 +00:00
|
|
|
checkStatus,
|
|
|
|
LoginStatusChecker,
|
2020-12-30 13:39:49 +00:00
|
|
|
} from './loginStatus'
|
|
|
|
import {
|
|
|
|
AuthTicket,
|
|
|
|
CalendarItem,
|
|
|
|
Child,
|
|
|
|
Classmate,
|
2021-03-11 08:58:55 +00:00
|
|
|
CookieManager,
|
2020-12-30 13:39:49 +00:00
|
|
|
Fetch,
|
|
|
|
MenuItem,
|
|
|
|
NewsItem,
|
|
|
|
Notification,
|
|
|
|
RequestInit,
|
|
|
|
ScheduleItem,
|
|
|
|
User,
|
|
|
|
} from './types'
|
|
|
|
import * as routes from './routes'
|
|
|
|
import * as parse from './parse'
|
|
|
|
import wrap, { Fetcher, FetcherOptions } from './fetcher'
|
2021-01-07 08:26:38 +00:00
|
|
|
import * as fake from './fakeData'
|
2021-01-06 22:46:45 +00:00
|
|
|
|
2021-02-18 07:55:47 +00:00
|
|
|
const fakeResponse = <T>(data: T): Promise<T> => new Promise((res) => (
|
|
|
|
setTimeout(() => res(data), 200 + Math.random() * 800)
|
|
|
|
))
|
|
|
|
|
2020-12-30 13:39:49 +00:00
|
|
|
export class Api extends EventEmitter {
|
|
|
|
private fetch: Fetcher
|
|
|
|
|
2021-01-18 10:42:56 +00:00
|
|
|
private personalNumber?: string
|
|
|
|
|
2021-03-11 08:58:55 +00:00
|
|
|
private headers: any
|
2020-12-30 13:39:49 +00:00
|
|
|
|
2021-03-11 08:58:55 +00:00
|
|
|
private cookieManager: CookieManager
|
2020-12-30 13:39:49 +00:00
|
|
|
|
|
|
|
public isLoggedIn: boolean = false
|
|
|
|
|
2021-01-06 22:46:45 +00:00
|
|
|
public isFake: boolean = false
|
|
|
|
|
2021-03-11 08:58:55 +00:00
|
|
|
constructor(fetch: Fetch, cookieManager: CookieManager, options?: FetcherOptions) {
|
2020-12-30 13:39:49 +00:00
|
|
|
super()
|
|
|
|
this.fetch = wrap(fetch, options)
|
2021-03-11 08:58:55 +00:00
|
|
|
this.cookieManager = cookieManager
|
|
|
|
this.headers = {}
|
2020-12-30 13:39:49 +00:00
|
|
|
}
|
|
|
|
|
2021-01-18 10:42:56 +00:00
|
|
|
getPersonalNumber() {
|
|
|
|
return this.personalNumber
|
|
|
|
}
|
|
|
|
|
2021-03-11 08:58:55 +00:00
|
|
|
async getSession(url: string, options: RequestInit = {}): Promise<RequestInit> {
|
|
|
|
const cookie = await this.cookieManager.getCookieString(url)
|
|
|
|
return {
|
|
|
|
...options,
|
2020-12-30 13:39:49 +00:00
|
|
|
headers: {
|
2021-03-11 08:58:55 +00:00
|
|
|
...this.headers,
|
|
|
|
...options.headers,
|
|
|
|
cookie,
|
2020-12-30 13:39:49 +00:00
|
|
|
},
|
|
|
|
}
|
2021-03-11 08:58:55 +00:00
|
|
|
}
|
2020-12-30 13:39:49 +00:00
|
|
|
|
2021-03-11 08:58:55 +00:00
|
|
|
async clearSession(): Promise<void> {
|
|
|
|
this.headers = {}
|
|
|
|
await this.cookieManager.clearAll()
|
|
|
|
}
|
|
|
|
|
|
|
|
addHeader(name: string, value: string): void {
|
|
|
|
this.headers[name] = value
|
2020-12-30 13:39:49 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
async login(personalNumber: string): Promise<LoginStatusChecker> {
|
2021-01-15 08:40:48 +00:00
|
|
|
if (personalNumber.endsWith('1212121212')) return this.fakeMode()
|
2021-01-06 22:46:45 +00:00
|
|
|
|
|
|
|
this.isFake = false
|
|
|
|
|
2020-12-30 13:39:49 +00:00
|
|
|
const ticketUrl = routes.login(personalNumber)
|
|
|
|
const ticketResponse = await this.fetch('auth-ticket', ticketUrl)
|
2021-03-01 17:08:03 +00:00
|
|
|
|
2021-03-01 17:31:47 +00:00
|
|
|
if (!ticketResponse.ok) {
|
2021-03-01 17:08:03 +00:00
|
|
|
throw new Error(`Server Error [${ticketResponse.status}] [${ticketResponse.statusText}] [${ticketUrl}]`)
|
|
|
|
}
|
2021-02-27 19:57:55 +00:00
|
|
|
|
2020-12-30 13:39:49 +00:00
|
|
|
const ticket: AuthTicket = await ticketResponse.json()
|
|
|
|
|
2021-01-18 10:42:56 +00:00
|
|
|
// login was initiated - store personal number
|
|
|
|
this.personalNumber = personalNumber
|
|
|
|
|
2020-12-30 13:39:49 +00:00
|
|
|
const status = checkStatus(this.fetch, ticket)
|
|
|
|
status.on('OK', async () => {
|
2021-03-11 08:58:55 +00:00
|
|
|
await this.retrieveSessionCookie()
|
|
|
|
await this.retrieveXsrfToken()
|
|
|
|
await this.retrieveApiKey()
|
|
|
|
|
|
|
|
this.isLoggedIn = true
|
|
|
|
this.emit('login')
|
2020-12-30 13:39:49 +00:00
|
|
|
})
|
2021-01-18 10:42:56 +00:00
|
|
|
status.on('ERROR', () => { this.personalNumber = undefined })
|
2020-12-30 13:39:49 +00:00
|
|
|
|
|
|
|
return status
|
|
|
|
}
|
|
|
|
|
2021-03-11 08:58:55 +00:00
|
|
|
async retrieveSessionCookie(): Promise<void> {
|
|
|
|
const url = routes.loginCookie
|
|
|
|
await this.fetch('login-cookie', url)
|
|
|
|
}
|
|
|
|
|
|
|
|
async retrieveXsrfToken(): Promise<void> {
|
|
|
|
const url = routes.hemPage
|
|
|
|
const session = await this.getSession(url)
|
|
|
|
const response = await this.fetch('hemPage', url, session)
|
|
|
|
const text = await response.text()
|
|
|
|
const doc = html.parse(decode(text))
|
|
|
|
const xsrfToken = doc.querySelector('input[name="__RequestVerificationToken"]').getAttribute('value') || ''
|
|
|
|
this.addHeader('X-XSRF-Token', xsrfToken)
|
|
|
|
}
|
|
|
|
|
|
|
|
async retrieveApiKey(): Promise<void> {
|
|
|
|
const url = routes.startBundle
|
|
|
|
const session = await this.getSession(url)
|
|
|
|
const response = await this.fetch('startBundle', url, session)
|
|
|
|
const text = await response.text()
|
|
|
|
|
|
|
|
const apiKeyRegex = /"API-Key": "([\w\d]+)"/gm
|
|
|
|
const apiKeyMatches = apiKeyRegex.exec(text)
|
|
|
|
const apiKey = apiKeyMatches && apiKeyMatches.length > 1 ? apiKeyMatches[1] : ''
|
|
|
|
|
|
|
|
this.addHeader('API-Key', apiKey)
|
|
|
|
}
|
|
|
|
|
|
|
|
async retrieveCdnUrl(): Promise<string> {
|
|
|
|
const url = routes.cdn
|
|
|
|
const session = await this.getSession(url)
|
|
|
|
const response = await this.fetch('cdn', url, session)
|
|
|
|
const cdnUrl = await response.text()
|
|
|
|
return cdnUrl
|
|
|
|
}
|
|
|
|
|
|
|
|
async retrieveAuthBody(): Promise<string> {
|
|
|
|
const url = routes.auth
|
|
|
|
const session = await this.getSession(url)
|
|
|
|
const response = await this.fetch('auth', url, session)
|
|
|
|
const authBody = await response.text()
|
|
|
|
return authBody
|
|
|
|
}
|
|
|
|
|
|
|
|
async retrieveAuthToken(url: string, authBody: string): Promise<string> {
|
|
|
|
const cdnHost = new URL(url).host
|
|
|
|
const session = await this.getSession(url, {
|
|
|
|
method: 'POST',
|
|
|
|
headers: {
|
|
|
|
Accept: 'text/plain',
|
|
|
|
Host: cdnHost,
|
|
|
|
Origin: 'https://etjanst.stockholm.se',
|
|
|
|
Referer: 'https://etjanst.stockholm.se/',
|
|
|
|
Connection: 'keep-alive',
|
|
|
|
},
|
|
|
|
body: authBody,
|
|
|
|
})
|
|
|
|
|
|
|
|
// Delete cookies from session and empty cookie manager
|
|
|
|
delete session.headers.cookie
|
|
|
|
const cookies = await this.cookieManager.getCookies(url)
|
|
|
|
this.cookieManager.clearAll()
|
|
|
|
|
|
|
|
// Perform request
|
|
|
|
const response = await this.fetch('createItem', url, session)
|
|
|
|
|
|
|
|
// Refill cookie manager
|
|
|
|
cookies.forEach((cookie) => {
|
|
|
|
this.cookieManager.setCookie(cookie, url)
|
|
|
|
})
|
|
|
|
|
|
|
|
if (!response.ok) {
|
|
|
|
throw new Error(`Server Error [${response.status}] [${response.statusText}] [${url}]`)
|
|
|
|
}
|
|
|
|
|
|
|
|
const authData = await response.json()
|
|
|
|
return authData.token
|
|
|
|
}
|
|
|
|
|
2021-01-06 22:46:45 +00:00
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2020-12-30 13:39:49 +00:00
|
|
|
async getUser(): Promise<User> {
|
2021-02-18 07:55:47 +00:00
|
|
|
if (this.isFake) return fakeResponse(fake.user())
|
2021-01-06 22:46:45 +00:00
|
|
|
|
2020-12-30 13:39:49 +00:00
|
|
|
const url = routes.user
|
2021-03-11 08:58:55 +00:00
|
|
|
const session = await this.getSession(url)
|
|
|
|
const response = await this.fetch('user', url, session)
|
2020-12-30 13:39:49 +00:00
|
|
|
const data = await response.json()
|
|
|
|
return parse.user(data)
|
|
|
|
}
|
|
|
|
|
|
|
|
async getChildren(): Promise<Child[]> {
|
2021-02-18 07:55:47 +00:00
|
|
|
if (this.isFake) return fakeResponse(fake.children())
|
2021-01-06 22:46:45 +00:00
|
|
|
|
2021-03-11 08:58:55 +00:00
|
|
|
const cdnUrl = await this.retrieveCdnUrl()
|
|
|
|
const authBody = await this.retrieveAuthBody()
|
|
|
|
const token = await this.retrieveAuthToken(cdnUrl, authBody)
|
2021-02-27 13:34:17 +00:00
|
|
|
|
2021-03-11 08:58:55 +00:00
|
|
|
const url = routes.children
|
|
|
|
const session = await this.getSession(url, {
|
2021-02-27 13:34:17 +00:00
|
|
|
headers: {
|
2021-03-01 17:31:47 +00:00
|
|
|
Accept: 'application/json;odata=verbose',
|
2021-03-11 08:58:55 +00:00
|
|
|
Auth: token,
|
2021-03-01 17:31:47 +00:00
|
|
|
Host: 'etjanst.stockholm.se',
|
|
|
|
Referer: 'https://etjanst.stockholm.se/Vardnadshavare/inloggad2/hem',
|
|
|
|
},
|
2021-02-26 23:01:03 +00:00
|
|
|
})
|
2021-03-11 08:58:55 +00:00
|
|
|
const response = await this.fetch('children', url, session)
|
2021-02-26 23:01:03 +00:00
|
|
|
|
2021-03-11 08:58:55 +00:00
|
|
|
console.log(session.headers)
|
|
|
|
console.log('children response', response)
|
|
|
|
if (!response.ok) {
|
|
|
|
throw new Error(`Server Error [${response.status}] [${response.statusText}] [${url}]`)
|
2021-03-01 17:08:03 +00:00
|
|
|
}
|
|
|
|
|
2021-03-11 08:58:55 +00:00
|
|
|
const data = await response.json()
|
2020-12-30 13:39:49 +00:00
|
|
|
return parse.children(data)
|
|
|
|
}
|
|
|
|
|
|
|
|
async getCalendar(child: Child): Promise<CalendarItem[]> {
|
2021-02-18 07:55:47 +00:00
|
|
|
if (this.isFake) return fakeResponse(fake.calendar(child))
|
2021-01-06 22:46:45 +00:00
|
|
|
|
2020-12-30 13:39:49 +00:00
|
|
|
const url = routes.calendar(child.id)
|
2021-03-11 08:58:55 +00:00
|
|
|
const session = await this.getSession(url)
|
|
|
|
const response = await this.fetch('calendar', url, session)
|
2020-12-30 13:39:49 +00:00
|
|
|
const data = await response.json()
|
|
|
|
return parse.calendar(data)
|
|
|
|
}
|
|
|
|
|
|
|
|
async getClassmates(child: Child): Promise<Classmate[]> {
|
2021-02-18 07:55:47 +00:00
|
|
|
if (this.isFake) return fakeResponse(fake.classmates(child))
|
2021-01-06 22:46:45 +00:00
|
|
|
|
2020-12-30 13:39:49 +00:00
|
|
|
const url = routes.classmates(child.sdsId)
|
2021-03-11 08:58:55 +00:00
|
|
|
const session = await this.getSession(url)
|
|
|
|
const response = await this.fetch('classmates', url, session)
|
2020-12-30 13:39:49 +00:00
|
|
|
const data = await response.json()
|
|
|
|
return parse.classmates(data)
|
|
|
|
}
|
|
|
|
|
2021-01-03 12:08:53 +00:00
|
|
|
async getSchedule(child: Child, from: DateTime, to: DateTime): Promise<ScheduleItem[]> {
|
2021-02-18 07:55:47 +00:00
|
|
|
if (this.isFake) return fakeResponse(fake.schedule(child))
|
2021-01-06 22:46:45 +00:00
|
|
|
|
2021-01-03 12:08:53 +00:00
|
|
|
const url = routes.schedule(child.sdsId, from.toISODate(), to.toISODate())
|
2021-03-11 08:58:55 +00:00
|
|
|
const session = await this.getSession(url)
|
|
|
|
const response = await this.fetch('schedule', url, session)
|
2020-12-30 13:39:49 +00:00
|
|
|
const data = await response.json()
|
|
|
|
return parse.schedule(data)
|
|
|
|
}
|
|
|
|
|
|
|
|
async getNews(child: Child): Promise<NewsItem[]> {
|
2021-02-18 07:55:47 +00:00
|
|
|
if (this.isFake) return fakeResponse(fake.news(child))
|
2021-01-06 22:46:45 +00:00
|
|
|
|
2020-12-30 13:39:49 +00:00
|
|
|
const url = routes.news(child.id)
|
2021-03-11 08:58:55 +00:00
|
|
|
const session = await this.getSession(url)
|
|
|
|
const response = await this.fetch('news', url, session)
|
2020-12-30 13:39:49 +00:00
|
|
|
const data = await response.json()
|
|
|
|
return parse.news(data)
|
|
|
|
}
|
|
|
|
|
2021-02-08 20:51:43 +00:00
|
|
|
async getNewsDetails(child: Child, item: NewsItem): Promise<any> {
|
2021-02-10 20:13:31 +00:00
|
|
|
if (this.isFake) {
|
2021-02-18 07:55:47 +00:00
|
|
|
return fakeResponse(fake.news(child).find((ni) => ni.id === item.id))
|
2021-02-10 20:13:31 +00:00
|
|
|
}
|
2021-02-08 20:51:43 +00:00
|
|
|
const url = routes.newsDetails(child.id, item.id)
|
2021-03-11 08:58:55 +00:00
|
|
|
const session = await this.getSession(url)
|
|
|
|
const response = await this.fetch(`news_${item.id}`, url, session)
|
2021-02-08 20:51:43 +00:00
|
|
|
const data = await response.json()
|
2021-02-12 11:19:48 +00:00
|
|
|
return parse.newsItemDetails(data)
|
2021-02-08 20:51:43 +00:00
|
|
|
}
|
|
|
|
|
2020-12-30 13:39:49 +00:00
|
|
|
async getMenu(child: Child): Promise<MenuItem[]> {
|
2021-02-18 07:55:47 +00:00
|
|
|
if (this.isFake) return fakeResponse(fake.menu(child))
|
2021-01-06 22:46:45 +00:00
|
|
|
|
2020-12-30 13:39:49 +00:00
|
|
|
const url = routes.menu(child.id)
|
2021-03-11 08:58:55 +00:00
|
|
|
const session = await this.getSession(url)
|
|
|
|
const response = await this.fetch('menu', url, session)
|
2020-12-30 13:39:49 +00:00
|
|
|
const data = await response.json()
|
|
|
|
return parse.menu(data)
|
|
|
|
}
|
|
|
|
|
|
|
|
async getNotifications(child: Child): Promise<Notification[]> {
|
2021-02-18 07:55:47 +00:00
|
|
|
if (this.isFake) return fakeResponse(fake.notifications(child))
|
2021-01-06 22:46:45 +00:00
|
|
|
|
2020-12-30 13:39:49 +00:00
|
|
|
const url = routes.notifications(child.sdsId)
|
2021-03-11 08:58:55 +00:00
|
|
|
const session = await this.getSession(url)
|
|
|
|
const response = await this.fetch('notifications', url, session)
|
2020-12-30 13:39:49 +00:00
|
|
|
const data = await response.json()
|
|
|
|
return parse.notifications(data)
|
|
|
|
}
|
|
|
|
|
|
|
|
async logout() {
|
2021-01-06 22:46:45 +00:00
|
|
|
this.isFake = false
|
2021-01-18 10:42:56 +00:00
|
|
|
this.personalNumber = undefined
|
2020-12-30 13:39:49 +00:00
|
|
|
this.isLoggedIn = false
|
|
|
|
this.emit('logout')
|
2021-03-11 08:58:55 +00:00
|
|
|
await this.clearSession()
|
2020-12-30 13:39:49 +00:00
|
|
|
}
|
|
|
|
}
|