@@ -5,7 +5,7 @@ using System.Text.Json;
|
||||
|
||||
namespace LehrerApp.WebUntis;
|
||||
|
||||
public sealed class WebUntisClient : IAsyncDisposable
|
||||
public sealed partial class WebUntisClient : IAsyncDisposable
|
||||
{
|
||||
private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web);
|
||||
private static readonly UTF8Encoding StrictUtf8 = new(false, true);
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
using System.Net.Http.Headers;
|
||||
using System.Net.Http.Json;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace LehrerApp.WebUntis;
|
||||
|
||||
public sealed record UntisOpenElement(int Id, string Name)
|
||||
{
|
||||
public override string ToString() => Name;
|
||||
}
|
||||
public sealed record UntisOpenPeriodsMeta(
|
||||
IReadOnlyList<UntisOpenElement> Teachers, IReadOnlyList<UntisOpenElement> Classes,
|
||||
IReadOnlyList<int> MyClassIds, DateOnly SchoolYearStart, DateOnly SchoolYearEnd,
|
||||
int? OwnTeacherId);
|
||||
public sealed record UntisOpenPeriod(int Id, DateTime Start, DateTime End, int Hour,
|
||||
string Classes, string Teachers, string Subject, bool TopicMissing, bool AttendanceMissing)
|
||||
{
|
||||
public string When => $"{Start:dd.MM.yyyy} · {Hour}. Stunde · {Start:HH:mm}–{End:HH:mm}";
|
||||
public string Missing => (TopicMissing, AttendanceMissing) switch
|
||||
{
|
||||
(true, true) => "Thema und Anwesenheitskontrolle fehlen",
|
||||
(true, false) => "Thema fehlt",
|
||||
(false, true) => "Anwesenheitskontrolle fehlt",
|
||||
_ => "Von WebUntis als offen gemeldet"
|
||||
};
|
||||
}
|
||||
|
||||
public sealed partial class WebUntisClient
|
||||
{
|
||||
private const string OpenPeriodsPath = "/WebUntis/api/rest/view/v1/classreg/open-periods";
|
||||
|
||||
public Task<UntisOpenPeriodsMeta> GetOpenPeriodsMetaAsync(int schoolYearId, CancellationToken token) =>
|
||||
WithSessionAsync(async session =>
|
||||
{
|
||||
var json = await OpenPeriodsRequestAsync(session, schoolYearId, null, token);
|
||||
var year = json.GetProperty("schoolYear");
|
||||
return new UntisOpenPeriodsMeta(OpenElements(json.GetProperty("teachers")),
|
||||
OpenElements(json.GetProperty("classes")),
|
||||
json.GetProperty("myClassIds").EnumerateArray().Select(x => x.GetInt32()).ToArray(),
|
||||
year.GetProperty("start").GetDateOnly(), year.GetProperty("end").GetDateOnly(),
|
||||
_myTeacherUntisId);
|
||||
}, token);
|
||||
|
||||
public Task<IReadOnlyList<UntisOpenPeriod>> GetOpenPeriodsAsync(int schoolYearId, int? teacherId,
|
||||
int? classId, DateOnly start, DateOnly end, CancellationToken token)
|
||||
{
|
||||
if ((teacherId is null) == (classId is null) || teacherId is <= 0 || classId is <= 0)
|
||||
throw new ArgumentException("Genau eine gültige Lehrer- oder Klassen-ID ist erforderlich.");
|
||||
if (start > end) throw new ArgumentException("Der Beginn muss vor dem Ende liegen.");
|
||||
var body = new Dictionary<string, object>
|
||||
{
|
||||
[teacherId.HasValue ? "teacherId" : "classId"] = teacherId ?? classId!.Value,
|
||||
["filter"] = "TOPIC_OR_ABSENCE_OPEN",
|
||||
["dateRange"] = new { start = start.ToString("yyyy-MM-dd"), end = end.ToString("yyyy-MM-dd") }
|
||||
};
|
||||
return WithSessionAsync(async session => ParseOpenPeriods(
|
||||
await OpenPeriodsRequestAsync(session, schoolYearId, body, token)), token);
|
||||
}
|
||||
|
||||
public static IReadOnlyList<UntisOpenPeriod> ParseOpenPeriods(JsonElement json) =>
|
||||
json.GetProperty("periods").EnumerateArray().Select(row =>
|
||||
{
|
||||
var p = row.GetProperty("period");
|
||||
var range = p.GetProperty("dtRange");
|
||||
return new UntisOpenPeriod(p.GetProperty("id").GetInt32(),
|
||||
range.GetProperty("start").GetDateTime(), range.GetProperty("end").GetDateTime(),
|
||||
p.GetProperty("hr").GetInt32(),
|
||||
string.Join(", ", OpenElements(p.GetProperty("classes")).Select(x => x.Name)),
|
||||
string.Join(", ", OpenElements(p.GetProperty("teachers")).Select(x => x.Name)),
|
||||
p.GetProperty("subject").GetProperty("el").GetProperty("name").GetString() ?? "",
|
||||
row.GetProperty("topicNeeded").GetBoolean() &&
|
||||
string.IsNullOrWhiteSpace(row.GetProperty("topicShort").GetString()),
|
||||
row.GetProperty("absCheckNeeded").GetBoolean() && !row.GetProperty("absChecked").GetBoolean());
|
||||
}).OrderBy(x => x.Start).ToArray();
|
||||
|
||||
private static IReadOnlyList<UntisOpenElement> OpenElements(JsonElement array) => array.EnumerateArray()
|
||||
.Select(x => x.GetProperty("el"))
|
||||
.Select(x => new UntisOpenElement(x.GetProperty("id").GetInt32(),
|
||||
x.GetProperty("nameShort").GetString() ?? x.GetProperty("name").GetString() ?? ""))
|
||||
.ToArray();
|
||||
|
||||
private async Task<JsonElement> OpenPeriodsRequestAsync(string session, int schoolYearId,
|
||||
object? body, CancellationToken token)
|
||||
{
|
||||
// The REST UI uses a bearer token issued for the existing WebUntis session.
|
||||
using var tokenRequest = ReportRequest($"https://{GetConfiguration().Host}/WebUntis/api/token/new", session, true);
|
||||
using var tokenResponse = await SendAsync(tokenRequest, TimeSpan.FromSeconds(20), token);
|
||||
if (!tokenResponse.IsSuccessStatusCode)
|
||||
throw new WebUntisException($"WebUntis-Token konnte nicht abgerufen werden: HTTP {(int)tokenResponse.StatusCode}.");
|
||||
var bearer = (await tokenResponse.Content.ReadAsStringAsync(token)).Trim();
|
||||
if (bearer.StartsWith('"')) bearer = JsonSerializer.Deserialize<string>(bearer) ?? "";
|
||||
if (string.IsNullOrWhiteSpace(bearer) ||
|
||||
bearer.Any(c => !char.IsAsciiLetterOrDigit(c) && c is not '-' and not '_' and not '.'))
|
||||
throw new WebUntisException("WebUntis hat keinen gültigen Zugriffstoken geliefert.");
|
||||
using var request = ReportRequest($"https://{GetConfiguration().Host}{OpenPeriodsPath}{(body is null ? "/meta" : "")}", session, true);
|
||||
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", bearer);
|
||||
request.Headers.Add("x-webuntis-api-school-year-id", schoolYearId.ToString());
|
||||
if (body is not null)
|
||||
{
|
||||
request.Method = HttpMethod.Post;
|
||||
request.Content = JsonContent.Create(body, options: JsonOptions);
|
||||
}
|
||||
using var response = await SendAsync(request, TimeSpan.FromSeconds(30), token);
|
||||
if (!response.IsSuccessStatusCode)
|
||||
throw new WebUntisException($"Offene Stunden konnten nicht abgerufen werden: HTTP {(int)response.StatusCode}.");
|
||||
return await ReadJsonAsync(response, token);
|
||||
}
|
||||
}
|
||||
|
||||
internal static class OpenPeriodJsonDates
|
||||
{
|
||||
public static DateOnly GetDateOnly(this JsonElement value) => DateOnly.ParseExact(value.GetString()!, "yyyy-MM-dd");
|
||||
}
|
||||
Reference in New Issue
Block a user