Files
LehrerApp/LehrerApp.WebUntis/WebUntisOpenPeriods.cs
T
admin 2ec8adac61
CI / build-and-test (push) Canceled after 0s
Webuntis stunden check
2026-09-08 23:59:07 +02:00

114 lines
6.2 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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");
}