Compare commits

...

6 Commits

Author SHA1 Message Date
Christian Landgren ab9b3a5b42 feat: convert newsletter to markdown 2023-10-06 16:44:06 +02:00
William Ryder dd0f8f3cc4 feat: parse news data 2023-10-06 16:17:21 +02:00
William Ryder 959b17bbab Merge branch 'feat/larandegruppen' of https://github.com/kolplattformen/skolplattformen into feat/larandegruppen 2023-10-06 15:23:18 +02:00
William Ryder ef5e5601c2 feat: fetch week data 2023-10-06 15:17:16 +02:00
Christian Landgren b0764cf65a fix: correct name for fetch token 2023-10-06 14:51:11 +02:00
Christian Landgren 53ee3c1cad feat: get messages and canvas token 2023-10-06 14:50:27 +02:00
3 changed files with 226 additions and 121 deletions

View File

@ -29,7 +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, parseCalendarItem } from './parse/parsers'
import {
extractMvghostRequestBody,
parseTimetableData,
parseScheduleEventData,
parseBreaksData,
parseNewsData,
} from './parse/parsers'
import {
bankIdInitUrl,
bankIdCheckUrl,
@ -204,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(),
@ -216,94 +222,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<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, {
// 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<Classmate[]> {
// TODO: We could get this from the events a child is associated with...
@ -366,18 +303,26 @@ export class ApiAdmentum extends EventEmitter implements Api {
if (!this.isLoggedIn) {
throw new Error('Not logged in...')
}
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..
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()
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<any> {
@ -463,7 +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()
const timetableEntries = parseCalendarItem(calendarResponseJson)
const timetableEntries = parseTimetableData(calendarResponseJson)
return timetableEntries
}

View File

@ -1,51 +1,63 @@
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'
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
}
@ -71,22 +83,169 @@ export const parseScheduleEvent = (({
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,
/* 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 parseCalendarItem = (jsonData: any): any => {
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
}
/*
"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) => {
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: bodyText,
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,
'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,
@ -97,12 +256,12 @@ export const parseCalendarItem = (jsonData: any): any => {
dayOfWeek,
blockName: lesson.title || lesson.subject_name,
} as TimetableEntry)
});
})
})
} 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;
return timetableEntries
}
/*
@ -208,4 +367,4 @@ export const parseCalendarItem = (jsonData: any): any => {
"weeks_amount": 52,
"break_week": 27
}
*/
*/

View File

@ -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) =>