using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text.Json; using System.Threading.Tasks; using Gott.Extensions; using Gott.Services; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing; namespace Gott.Endpoints; public static class SkillEndpoint { private static readonly Dictionary MimeTypes = new(StringComparer.OrdinalIgnoreCase) { { ".png", "image/png" }, { ".jpg", "image/jpeg" }, { ".jpeg", "image/jpeg" }, { ".gif", "image/gif" }, { ".webp", "image/webp" }, { ".svg", "image/svg+xml" }, { ".bmp", "image/bmp" }, { ".ico", "image/x-icon" }, { ".pdf", "application/pdf" }, { ".mp4", "video/mp4" }, { ".webm", "video/webm" }, { ".mp3", "audio/mpeg" }, { ".wav", "audio/wav" }, { ".ogg", "audio/ogg" }, }; public static void MapSkillEndpoints(this IEndpointRouteBuilder app) { // Skills (Dateisystem-basiert) app.MapGet("/api/skills", HandleListSkills); // Skill-Output-Dateien (Bilder etc.) app.MapGet("/api/skills/{slug}/output/{filename}", HandleSkillOutput); // Thumbnail (gecached) app.MapGet("/api/skills/{slug}/output/{filename}/thumb", HandleSkillOutputThumb); // Agent ↔ Skill Zuordnung app.MapGet("/api/agents/{agentId}/skills", HandleGetAgentSkills); app.MapPut("/api/agents/{agentId}/skills", HandleSetAgentSkills); app.MapPost("/api/agents/{agentId}/skills/{slug}", HandleAddAgentSkill); app.MapDelete("/api/agents/{agentId}/skills/{slug}", HandleRemoveAgentSkill); } private static async Task HandleListSkills(HttpContext ctx, SkillService skillService) { var skills = skillService.GetAllSkills(); await ctx.Response.WriteAsJsonAsync(skills); } private static async Task VerifyAgentOwner(HttpContext ctx, DatabaseService db, string agentId) { if (string.IsNullOrEmpty(agentId)) return false; var agent = await db.GetAgent(agentId); return agent != null && agent.UserId == ctx.GetUserId(); } private static async Task HandleGetAgentSkills(HttpContext ctx, DatabaseService db, SkillService skillService) { var agentId = ctx.Request.RouteValues["agentId"]?.ToString(); if (string.IsNullOrEmpty(agentId) || !await VerifyAgentOwner(ctx, db, agentId)) { ctx.Response.StatusCode = 400; return; } var slugs = await db.GetAgentSkills(agentId); var allSkills = skillService.GetAllSkills(); // Nur Skills zurückgeben die auch wirklich existieren var result = allSkills .Where(s => slugs.Contains(s.Slug)) .ToList(); await ctx.Response.WriteAsJsonAsync(result); } private static async Task HandleSetAgentSkills(HttpContext ctx, DatabaseService db) { var agentId = ctx.Request.RouteValues["agentId"]?.ToString(); if (string.IsNullOrEmpty(agentId) || !await VerifyAgentOwner(ctx, db, agentId)) { ctx.Response.StatusCode = 400; return; } var body = await JsonSerializer.DeserializeAsync(ctx.Request.Body); var slugs = body.TryGetProperty("skills", out var arr) ? arr.EnumerateArray().Select(e => e.GetString()).Where(s => !string.IsNullOrEmpty(s)).ToList() : new System.Collections.Generic.List(); await db.SetAgentSkills(agentId, slugs); var updated = await db.GetAgentSkills(agentId); await ctx.Response.WriteAsJsonAsync(updated); } private static async Task HandleAddAgentSkill(HttpContext ctx, DatabaseService db) { var agentId = ctx.Request.RouteValues["agentId"]?.ToString(); var slug = ctx.Request.RouteValues["slug"]?.ToString(); if (string.IsNullOrEmpty(agentId) || string.IsNullOrEmpty(slug) || !await VerifyAgentOwner(ctx, db, agentId)) { ctx.Response.StatusCode = 400; return; } await db.AddAgentSkill(agentId, slug); ctx.Response.StatusCode = 204; } private static async Task HandleRemoveAgentSkill(HttpContext ctx, DatabaseService db) { var agentId = ctx.Request.RouteValues["agentId"]?.ToString(); var slug = ctx.Request.RouteValues["slug"]?.ToString(); if (string.IsNullOrEmpty(agentId) || string.IsNullOrEmpty(slug) || !await VerifyAgentOwner(ctx, db, agentId)) { ctx.Response.StatusCode = 400; return; } await db.RemoveAgentSkill(agentId, slug); ctx.Response.StatusCode = 204; } private static async Task HandleSkillOutput(HttpContext ctx, SkillService skillService) { var slug = ctx.Request.RouteValues["slug"]?.ToString(); var filename = ctx.Request.RouteValues["filename"]?.ToString(); if (string.IsNullOrEmpty(slug) || string.IsNullOrEmpty(filename)) { ctx.Response.StatusCode = 400; return; } // Sicherheit: Keine Pfad-Traversals erlauben if (filename.Contains("..") || filename.Contains('/') || filename.Contains('\\')) { ctx.Response.StatusCode = 400; return; } var skill = skillService.GetSkill(slug); if (skill == null) { ctx.Response.StatusCode = 404; return; } var filePath = Path.Combine(skill.Path, "output", filename); if (!File.Exists(filePath)) { ctx.Response.StatusCode = 404; return; } var ext = Path.GetExtension(filename); var contentType = MimeTypes.GetValueOrDefault(ext, "application/octet-stream"); ctx.Response.ContentType = contentType; ctx.Response.Headers["Cache-Control"] = "private, max-age=3600"; await ctx.Response.SendFileAsync(filePath); } private static readonly HashSet ThumbExtensions = new(StringComparer.OrdinalIgnoreCase) { ".png", ".jpg", ".jpeg", ".gif", ".webp", ".bmp" }; private static async Task HandleSkillOutputThumb(HttpContext ctx, SkillService skillService) { var slug = ctx.Request.RouteValues["slug"]?.ToString(); var filename = ctx.Request.RouteValues["filename"]?.ToString(); if (string.IsNullOrEmpty(slug) || string.IsNullOrEmpty(filename)) { ctx.Response.StatusCode = 400; return; } if (filename.Contains("..") || filename.Contains('/') || filename.Contains('\\')) { ctx.Response.StatusCode = 400; return; } var ext = Path.GetExtension(filename); if (!ThumbExtensions.Contains(ext)) { ctx.Response.StatusCode = 400; await ctx.Response.WriteAsync("Thumbnails nur für Bilder verfügbar"); return; } var skill = skillService.GetSkill(slug); if (skill == null) { ctx.Response.StatusCode = 404; return; } var originalPath = Path.Combine(skill.Path, "output", filename); if (!File.Exists(originalPath)) { ctx.Response.StatusCode = 404; return; } var dataDir = Path.GetDirectoryName(skillService.SkillsDir); var cacheDir = Path.Combine(dataDir, "cache", "skills", slug); var thumbPath = await ImageService.GenerateThumbnail(originalPath, cacheDir); if (thumbPath != null) { ctx.Response.ContentType = "image/webp"; ctx.Response.Headers["Cache-Control"] = "private, max-age=86400"; await ctx.Response.SendFileAsync(thumbPath); } else { // Fallback: Original ausliefern ctx.Response.ContentType = MimeTypes.GetValueOrDefault(ext, "application/octet-stream"); ctx.Response.Headers["Cache-Control"] = "private, max-age=3600"; await ctx.Response.SendFileAsync(originalPath); } } }