Untis API Integration
This commit is contained in:
@@ -0,0 +1,140 @@
|
||||
using System.Net;
|
||||
using System.Net.Http.Json;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace LehrerApp.Desktop.Services;
|
||||
|
||||
public sealed class WebUntisIntegrationException(string message) : Exception(message);
|
||||
|
||||
public sealed record UntisSchoolYearDto(int UntisId, string Name, int StartDate, int EndDate);
|
||||
public sealed record UntisClassDto(int UntisId, string Name, string? LongName);
|
||||
public sealed record UntisTeacherDto(int UntisId, string Name, string? ForeName, string? LongName, string? Title,
|
||||
bool Active, IReadOnlyList<int> DepartmentUntisIds)
|
||||
{
|
||||
public string DisplayName => string.IsNullOrWhiteSpace(LongName)
|
||||
? Name
|
||||
: $"{ForeName} {LongName} ({Name})".Trim();
|
||||
}
|
||||
public sealed record UntisEntityDto(int Id, string Name, int? OriginalId, string? OriginalName, string? ExternalKey);
|
||||
public sealed record UntisTimeUnitDto(string Name, int StartTime, int EndTime);
|
||||
public sealed record UntisTimeGridDayDto(int Day, IReadOnlyList<UntisTimeUnitDto> TimeUnits);
|
||||
public sealed record UntisTimetablePeriodDto(int Id, int Date, int StartTime, int EndTime, string? Code,
|
||||
string? ActivityType, string? Info, string? LessonText, string? SubstitutionText, string? StudentGroup,
|
||||
IReadOnlyList<UntisEntityDto> Classes, IReadOnlyList<UntisEntityDto> Teachers,
|
||||
IReadOnlyList<UntisEntityDto> Subjects, IReadOnlyList<UntisEntityDto> Rooms);
|
||||
public sealed record UntisStudentAddressDto(string? Email, string? Mobile, string? Phone, string? City,
|
||||
string? PostCode, string? Street);
|
||||
public sealed record UntisStudentDto(int UntisId, int ExternKey, string ClassName, string? Name, string? LongName,
|
||||
string? ForeName, string DisplayName, string? Gender, int? BirthDate, string? BirthDateRaw, int? EntryDate,
|
||||
string? EntryDateRaw, int? ExitDate, string? ExitDateRaw, string? Text, string? MedicalReportDuty,
|
||||
string? Schulpflicht, string? Majority, UntisStudentAddressDto Address, string? AttributeIL);
|
||||
public sealed record UntisStudentReportDto(int Count, string? ClassNameFilter, IReadOnlyList<UntisStudentDto> Students);
|
||||
public sealed record UntisStudentAbsenceDto(int StudentKey, int Date, int StartTime, int EndTime, int AbsentMinutes,
|
||||
bool Checked, string? AbsenceReason, string? ExcuseStatus, int? SubjectId, IReadOnlyList<int> TeacherIds,
|
||||
string? StudentGroup);
|
||||
public sealed record UntisStudentAbsenceReportDto(int StudentKey, int StartDate, int EndDate, int EntryCount,
|
||||
int AbsentMinutes, IReadOnlyList<UntisStudentAbsenceDto> Absences);
|
||||
|
||||
/// <summary>Authentifizierter Desktop-Client für die LehrerApp-API. Falls der Server neu gestartet
|
||||
/// wurde, baut er die nur im Server-RAM gehaltene WebUntis-Verbindung automatisch erneut auf.</summary>
|
||||
public sealed class WebUntisIntegrationService(HttpClient http, SyncSettingsService syncSettings,
|
||||
WebUntisSettingsService settings)
|
||||
{
|
||||
public bool IsAvailable => syncSettings.IsLoggedIn && !string.IsNullOrWhiteSpace(syncSettings.ServerUrl)
|
||||
&& settings.ApiIsConfigured;
|
||||
|
||||
public async Task ConnectAsync(WebUntisCredentials credentials, CancellationToken token = default)
|
||||
{
|
||||
using var response = await SendAsync(HttpMethod.Post, "/api/webuntis/connection", new
|
||||
{
|
||||
credentials.School,
|
||||
Host = string.IsNullOrWhiteSpace(credentials.Host) ? null : credentials.Host,
|
||||
credentials.Username,
|
||||
credentials.Password,
|
||||
}, token);
|
||||
await EnsureSuccessAsync(response);
|
||||
}
|
||||
|
||||
public async Task DisconnectAsync(CancellationToken token = default)
|
||||
{
|
||||
using var response = await SendAsync(HttpMethod.Delete, "/api/webuntis/connection", null, token);
|
||||
await EnsureSuccessAsync(response);
|
||||
}
|
||||
|
||||
public Task<IReadOnlyList<UntisSchoolYearDto>> GetSchoolYearsAsync(CancellationToken token = default) =>
|
||||
GetAsync<IReadOnlyList<UntisSchoolYearDto>>("/api/webuntis/schoolyears", token);
|
||||
|
||||
public Task<IReadOnlyList<UntisClassDto>> GetClassesAsync(int schoolYearId, CancellationToken token = default) =>
|
||||
GetAsync<IReadOnlyList<UntisClassDto>>($"/api/webuntis/classes?schoolyearId={schoolYearId}", token);
|
||||
|
||||
public Task<IReadOnlyList<UntisTeacherDto>> GetTeachersAsync(CancellationToken token = default) =>
|
||||
GetAsync<IReadOnlyList<UntisTeacherDto>>("/api/webuntis/teachers", token);
|
||||
|
||||
public Task<IReadOnlyList<UntisTimeGridDayDto>> GetTimeGridAsync(CancellationToken token = default) =>
|
||||
GetAsync<IReadOnlyList<UntisTimeGridDayDto>>("/api/webuntis/timegrid", token);
|
||||
|
||||
public Task<IReadOnlyList<UntisTimetablePeriodDto>> GetTimetableAsync(int teacherId, DateOnly start,
|
||||
DateOnly end, CancellationToken token = default) => GetAsync<IReadOnlyList<UntisTimetablePeriodDto>>(
|
||||
$"/api/webuntis/timetable?elementType=teacher&elementId={teacherId}&startDate={Date(start)}&endDate={Date(end)}", token);
|
||||
|
||||
public Task<UntisStudentReportDto> GetStudentsAsync(string className, CancellationToken token = default) =>
|
||||
GetAsync<UntisStudentReportDto>($"/api/webuntis/student-report?className={Uri.EscapeDataString(className)}", token);
|
||||
|
||||
public Task<UntisStudentAbsenceReportDto> GetAbsencesAsync(int studentKey, DateOnly start, DateOnly end,
|
||||
CancellationToken token = default) => GetAsync<UntisStudentAbsenceReportDto>(
|
||||
$"/api/webuntis/students/{studentKey}/absences?startDate={Date(start)}&endDate={Date(end)}", token);
|
||||
|
||||
private async Task<T> GetAsync<T>(string path, CancellationToken token)
|
||||
{
|
||||
await EnsureConnectedAsync(token);
|
||||
using var response = await SendAsync(HttpMethod.Get, path, null, token);
|
||||
await EnsureSuccessAsync(response);
|
||||
return await response.Content.ReadFromJsonAsync<T>(cancellationToken: token)
|
||||
?? throw new WebUntisIntegrationException("Der Server hat keine WebUntis-Daten zurückgegeben.");
|
||||
}
|
||||
|
||||
private async Task EnsureConnectedAsync(CancellationToken token)
|
||||
{
|
||||
var credentials = settings.GetApiCredentials()
|
||||
?? throw new WebUntisIntegrationException("Bitte zuerst die WebUntis-Anmeldedaten einrichten.");
|
||||
using var status = await SendAsync(HttpMethod.Get, "/api/webuntis/connection", null, token);
|
||||
await EnsureSuccessAsync(status);
|
||||
using var json = JsonDocument.Parse(await status.Content.ReadAsStringAsync(token));
|
||||
if (!json.RootElement.GetProperty("connected").GetBoolean()) await ConnectAsync(credentials, token);
|
||||
}
|
||||
|
||||
private async Task<HttpResponseMessage> SendAsync(HttpMethod method, string path, object? body,
|
||||
CancellationToken token)
|
||||
{
|
||||
if (!syncSettings.IsLoggedIn || string.IsNullOrWhiteSpace(syncSettings.ServerUrl))
|
||||
throw new WebUntisIntegrationException("Bitte zuerst unter „Synchronisation“ am LehrerApp-Server anmelden.");
|
||||
using var request = new HttpRequestMessage(method, new Uri(new Uri(syncSettings.ServerUrl), path));
|
||||
request.Headers.Authorization = new("Bearer", syncSettings.GetToken());
|
||||
if (body is not null) request.Content = JsonContent.Create(body);
|
||||
try { return await http.SendAsync(request, token); }
|
||||
catch (HttpRequestException) { throw new WebUntisIntegrationException("Der LehrerApp-Server ist nicht erreichbar."); }
|
||||
}
|
||||
|
||||
private static async Task EnsureSuccessAsync(HttpResponseMessage response)
|
||||
{
|
||||
if (response.IsSuccessStatusCode) return;
|
||||
if (response.StatusCode == HttpStatusCode.Unauthorized)
|
||||
throw new WebUntisIntegrationException("Die Server-Anmeldung ist abgelaufen. Bitte erneut anmelden.");
|
||||
if (response.StatusCode == HttpStatusCode.NotFound &&
|
||||
response.RequestMessage?.RequestUri?.AbsolutePath.StartsWith("/api/webuntis",
|
||||
StringComparison.OrdinalIgnoreCase) == true)
|
||||
throw new WebUntisIntegrationException(
|
||||
"Der konfigurierte LehrerApp-Server enthält die WebUntis-Integration noch nicht. " +
|
||||
"Bitte zuerst die aktuelle LehrerApp.Api auf dem Server bereitstellen.");
|
||||
try
|
||||
{
|
||||
using var json = JsonDocument.Parse(await response.Content.ReadAsStringAsync());
|
||||
if (json.RootElement.TryGetProperty("detail", out var detail) && !string.IsNullOrWhiteSpace(detail.GetString()))
|
||||
throw new WebUntisIntegrationException(detail.GetString()!);
|
||||
}
|
||||
catch (JsonException) { }
|
||||
throw new WebUntisIntegrationException("Der WebUntis-Abruf ist fehlgeschlagen.");
|
||||
}
|
||||
|
||||
private static int Date(DateOnly date) => date.Year * 10000 + date.Month * 100 + date.Day;
|
||||
}
|
||||
@@ -9,8 +9,12 @@ internal class WebUntisSettingsConfig
|
||||
public string? EncryptedIcalUrl { get; set; }
|
||||
public DateTime? LastSyncAt { get; set; }
|
||||
public string LastSyncStatus { get; set; } = "";
|
||||
public string? EncryptedApiCredentials { get; set; }
|
||||
public int? TeacherUntisId { get; set; }
|
||||
}
|
||||
|
||||
public sealed record WebUntisCredentials(string School, string Host, string Username, string Password);
|
||||
|
||||
/// <summary>
|
||||
/// Einstellungen für den WebUntis-iCal-Abgleich (Nutzer-Feedback, siehe TODO.md). Liegt wie
|
||||
/// AiSettingsService/SyncSettingsService in LehrerApp.Desktop statt LehrerApp.Core, da die
|
||||
@@ -30,6 +34,8 @@ public class WebUntisSettingsService
|
||||
|
||||
public bool Enabled => _config.Enabled;
|
||||
public bool IsConfigured => _config.EncryptedIcalUrl is not null;
|
||||
public bool ApiIsConfigured => _config.EncryptedApiCredentials is not null;
|
||||
public int? TeacherUntisId => _config.TeacherUntisId;
|
||||
public DateTime? LastSyncAt => _config.LastSyncAt;
|
||||
public string LastSyncStatus => _config.LastSyncStatus;
|
||||
|
||||
@@ -63,6 +69,29 @@ public class WebUntisSettingsService
|
||||
Save();
|
||||
}
|
||||
|
||||
public void SetApiCredentials(WebUntisCredentials credentials)
|
||||
{
|
||||
_config.EncryptedApiCredentials = SyncCrypto.EncryptObject(credentials, _urlKey);
|
||||
Save();
|
||||
}
|
||||
|
||||
public WebUntisCredentials? GetApiCredentials() => _config.EncryptedApiCredentials is null
|
||||
? null
|
||||
: SyncCrypto.DecryptObject<WebUntisCredentials>(_config.EncryptedApiCredentials, _urlKey);
|
||||
|
||||
public void ClearApiCredentials()
|
||||
{
|
||||
_config.EncryptedApiCredentials = null;
|
||||
_config.TeacherUntisId = null;
|
||||
Save();
|
||||
}
|
||||
|
||||
public void SetTeacherUntisId(int? teacherUntisId)
|
||||
{
|
||||
_config.TeacherUntisId = teacherUntisId;
|
||||
Save();
|
||||
}
|
||||
|
||||
public void SetLastSync(DateTime at, string status)
|
||||
{
|
||||
_config.LastSyncAt = at;
|
||||
|
||||
Reference in New Issue
Block a user