Select Git revision
notebook.ipynb
Code owners
Assign users and groups as approvers for specific file changes. Learn more.
fetch.dart 4.91 KiB
import 'dart:convert';
import 'package:flutter/foundation.dart';
import 'package:http/http.dart' as http;
import '../fetching/pageconfig.dart';
import '../fetching/timetable.dart';
import '../util/date_extension.dart';
import '../util/serializable.dart';
import 'class.dart';
import 'element_type.dart';
import 'lecture.dart';
typedef Cookie = (String key, String value);
typedef RequestOptions = (String path, String method, Json params);
typedef RequestResult = (Json json, List<Cookie> cookies, FetchStats stats);
class UntisFetch {
static const String apiBase = '/WebUntis/api/public';
static const String idHsReutlingen = '_aHMgcmV1dGxpbmdlbg==';
static const _sessionCookieNames = <String>{
'JSESSIONID',
'traceId',
};
final http.Client client = http.Client();
final Uri serverHost = Uri.https('poly.webuntis.com');
final List<Cookie> _sessionCookies = [];
late PageConfig<Class> allClasses = PageConfig(
this,
elementType: ElementType.classGroup,
convert: Class.new,
);
late PageConfig<Lecture> allLectures = PageConfig(
this,
elementType: ElementType.lecture,
convert: Lecture.new,
);
final Map<TimetableOptions, CacheableTimetable> _timetables = {};
CacheableTimetable timetableOf({
required ElementType type,
required int elementId,
required DateTime date,
}) {
final startOfWeek = date.atStartOfWeek();
final TimetableOptions options = (type, elementId, startOfWeek);
return _timetables.putIfAbsent(
options,
() => CacheableTimetable(this,
date: startOfWeek, type: type, elementId: elementId));
}
Uri _makeUri(RequestOptions options) {
final (String path, _, Json params) = options;
return serverHost.replace(
path: '$apiBase/$path',
queryParameters: params,
);
}
Future<http.StreamedResponse> _request(String method, Uri uri) async {
final request = http.Request(method, uri);
final cookies = <Cookie>[];
cookies.add(('schoolname', idHsReutlingen));
cookies.addAll(_sessionCookies);
request.headers['Set-Cookie'] =
cookies.map((cookie) => '${cookie.$1}=${cookie.$2}').join('; ');
final response = await client.send(request);
return response;
}
Future<RequestResult> _requestJson(RequestOptions options) async {
final (_, String method, _) = options;
final onStart = DateTime.now();
final response = await _request(method, _makeUri(options));
final cookieStrings =
response.headersSplitValues['Set-Cookie'] ?? <String>[];
final cookies = cookieStrings
.map((cookieString) => cookieString.split('='))
.map<Cookie>((parts) => (parts[0], parts[1]));
final resultCookies = <Cookie>[];
for (final cookie in cookies) {
if (_sessionCookieNames.contains(cookie.$1)) {
resultCookies.add(cookie);
}
}
final onDownloadStart = DateTime.now();
final responseString = await utf8.decodeStream(response.stream);
final onDownloadEnd = DateTime.now();
if (response.statusCode != 200) {
throw FetchError(response.statusCode, responseString);
}
try {
final bodyJson = jsonDecode(responseString);
final onDecodeEnd = DateTime.now();
final stats =
FetchStats(onStart, onDownloadStart, onDownloadEnd, onDecodeEnd);
return (bodyJson as Json, resultCookies, stats);
} on FormatException catch (_) {
throw FetchError(response.statusCode, responseString);
}
}
Future<Json> requestOptions(RequestOptions options) async {
final (_, String method, _) = options;
final uri = _makeUri(options);
final methodPadded = method.padRight(8);
print('$methodPadded $uri');
final (result, cookies, stats) = await compute(_requestJson, options);
_sessionCookies.addAll(cookies);
print('$stats');
return result;
}
Future<Json> request(
String path,
Json params, { String method = 'GET',
}) async {
final RequestOptions options = (path, method, params);
return await requestOptions(options);
}
}
class FetchStats {
final DateTime onStart;
final DateTime onDownloadStart;
final DateTime onDownloadEnd;
final DateTime onDecodeEnd;
FetchStats(
this.onStart,
this.onDownloadStart,
this.onDownloadEnd,
this.onDecodeEnd,
);
@override
String toString() {
final responseTime = onDownloadStart.difference(onStart);
final downloadTime = onDownloadEnd.difference(onDownloadStart);
final decodeTime = onDecodeEnd.difference(onDownloadEnd);
final timeStats = {
'Ping': responseTime,
'Download': downloadTime,
'JSON': decodeTime,
};
return timeStats.entries
.map((e) => '${e.key}: ${e.value.inMilliseconds}ms')
.join(', ');
}
}
class FetchError extends Error {
final int statusCode;
final String body;
FetchError(this.statusCode, this.body);
@override
String toString() => 'Server responded with status code $statusCode\n$body';
}