Compare commits

...

5 Commits

Author SHA1 Message Date
William Ryder f1c622ef60 wip: fix timetable and minor things 2023-10-06 12:29:06 +02:00
Christian Landgren 8385d30aaf fix: url for week data 2023-10-06 10:23:51 +02:00
Christian Landgren 618bdbf73c fix: handle errors in login flow correctly 2023-10-06 10:18:24 +02:00
William Ryder c4c52b9819 fix: wrong debug 2023-10-06 09:49:56 +02:00
William Ryder 9276db3c76 wip: fetch more data 2023-10-06 09:43:21 +02:00
5 changed files with 229 additions and 78 deletions

View File

@ -68,11 +68,14 @@ 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('news')
const news = await api.getNews(children[0])
/*
/*console.log('classmates')
const classmates = await api.getClassmates(children[0])
console.log(classmates)
@ -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) =>

View File

@ -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<User> {
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,35 +167,131 @@ 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<CalendarItem[]> {
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(
'get-week-data',
year.toString(),
isoWeek.toString()
)
console.log('fetching calendar', fetchUrl)
const overviewResponse = await this.fetch('get-week-data', fetchUrl, {
headers: {
'x-requested-with': 'XMLHttpRequest',
},
})
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([])
} catch (e) {
console.error('Error fetching overview', e)
return Promise.resolve([])
}
}
async getScheduledEvents(child: EtjanstChild): Promise<CalendarItem[]> {
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, {
@ -205,15 +301,44 @@ export class ApiAdmentum extends EventEmitter implements Api {
// },
// }).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<Classmate[]> {
// 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...')
}
@ -241,13 +366,24 @@ 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([])
}
async getNewsDetails(_child: EtjanstChild, item: NewsItem): Promise<any> {
return { ...item }
}
/*
/*
"data": {
"food_week": {
"id": 12846,
@ -268,24 +404,33 @@ export class ApiAdmentum extends EventEmitter implements Api {
*/
// eslint-disable-next-line @typescript-eslint/no-unused-vars
async getMenu(_child: EtjanstChild): Promise<MenuItem[]> {
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)
const menuResponseJson = await menuResponse.json()
console.log('menu response', menuResponseJson)
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([])
}
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) {
@ -314,13 +459,12 @@ export class ApiAdmentum extends EventEmitter implements Api {
year: number,
_lang: string
): Promise<TimetableEntry[]> {
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<void> {
@ -335,11 +479,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
@ -388,11 +531,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', () => {
@ -401,35 +543,38 @@ export class ApiAdmentum extends EventEmitter implements Api {
return statusChecker
}
async followRedirects(initialUrl: string): Promise<Response> {
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<LoginStatusChecker> {
this.isFake = true

View File

@ -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...')

View File

@ -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)
});
})

View File

@ -15,8 +15,13 @@ 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) =>
baseUrl + `overview?action=${action}&week=${week}&year=${year}`,
permission_groups: api + 'permission_groups',
primary_group_enrollments: api + 'primary_group_enrollments',
primary_group_municipality_statistics:
@ -34,14 +39,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`,