Files
gott/Endpoints/SharedEndpoint.cs
T
Meleki be29984b25 feat: Einzelne Nachrichten als Link teilen
Backend:
- SharedMessageRecord Model + shared_messages DB-Tabelle
- SharedEndpoint: POST /api/messages/{id}/share, GET /api/shared/{token},
  GET /api/shared (Liste), DELETE /api/shared/{token}
- Auth-Ausnahme für GET /api/shared/{token} (öffentlich ohne Login)
- DB-Methoden: CreateSharedMessage, GetSharedMessage, GetChainMessagesUpTo

Frontend:
- Share-Button (fa-share-alt) an jeder Nachricht
- Kontextmenü: Einzelnachricht oder mit Thread-Kontext teilen
- Link wird automatisch in Zwischenablage kopiert
- Öffentliche Shared-Page (#shared/{token}) ohne Login
- Markdown-Rendering in der geteilten Ansicht
- CSS für Share-Button, Share-Menü und Shared-Page
2026-03-04 23:34:00 +01:00

181 lines
5.8 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;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json;
using System.Threading.Tasks;
using Gott.Extensions;
using Gott.Models;
using Gott.Services;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing;
namespace Gott.Endpoints;
public static class SharedEndpoint
{
public static void MapSharedEndpoints(this IEndpointRouteBuilder app)
{
// Authentifizierte Endpunkte
app.MapPost("/api/messages/{messageId}/share", HandleShareMessage);
app.MapGet("/api/shared", HandleListShares);
app.MapDelete("/api/shared/{token}", HandleDeleteShare);
// Öffentlicher Endpunkt (Auth-Ausnahme in Middleware)
app.MapGet("/api/shared/{token}", HandleGetShared);
}
private static async Task HandleShareMessage(HttpContext ctx, DatabaseService db)
{
var userId = ctx.GetUserId();
var messageIdStr = ctx.Request.RouteValues["messageId"]?.ToString();
if (!long.TryParse(messageIdStr, out var messageId))
{
ctx.Response.StatusCode = 400;
await ctx.Response.WriteAsJsonAsync(new { error = "Invalid messageId" });
return;
}
// Body lesen (optional: includeContext)
bool includeContext = false;
try
{
var body = await JsonSerializer.DeserializeAsync<JsonElement>(ctx.Request.Body);
if (body.TryGetProperty("includeContext", out var ic)) includeContext = ic.GetBoolean();
}
catch { /* kein Body oder ungültig – defaults verwenden */ }
// Nachricht existiert und gehört dem User?
var message = await db.GetMessage(messageId);
if (message == null)
{
ctx.Response.StatusCode = 404;
await ctx.Response.WriteAsJsonAsync(new { error = "Message not found" });
return;
}
// Chain holen → Session prüfen
var chain = await db.GetChain(message.ChainId);
if (chain == null)
{
ctx.Response.StatusCode = 404;
await ctx.Response.WriteAsJsonAsync(new { error = "Chain not found" });
return;
}
var session = await db.GetSession(chain.SessionId);
if (session == null || session.UserId != userId)
{
ctx.Response.StatusCode = 403;
await ctx.Response.WriteAsJsonAsync(new { error = "Not authorized" });
return;
}
var shared = await db.CreateSharedMessage(messageId, userId, includeContext);
await ctx.Response.WriteAsJsonAsync(new { token = shared.ShareToken });
}
private static async Task HandleGetShared(HttpContext ctx, DatabaseService db)
{
var token = ctx.Request.RouteValues["token"]?.ToString();
if (string.IsNullOrEmpty(token))
{
ctx.Response.StatusCode = 400;
return;
}
var shared = await db.GetSharedMessage(token);
if (shared == null)
{
ctx.Response.StatusCode = 404;
await ctx.Response.WriteAsJsonAsync(new { error = "Share not found" });
return;
}
// Ablaufdatum prüfen
if (!string.IsNullOrEmpty(shared.ExpiresAt) &&
DateTime.TryParse(shared.ExpiresAt, out var expires) &&
expires < DateTime.UtcNow)
{
ctx.Response.StatusCode = 410;
await ctx.Response.WriteAsJsonAsync(new { error = "Share expired" });
return;
}
var message = await db.GetMessage(shared.MessageId);
if (message == null)
{
ctx.Response.StatusCode = 404;
await ctx.Response.WriteAsJsonAsync(new { error = "Message not found" });
return;
}
// Chain-Info holen
var chain = await db.GetChain(message.ChainId);
if (shared.IncludeContext)
{
// Alle Messages der Chain bis zu dieser Nachricht laden
var contextMessages = await db.GetChainMessagesUpTo(message.ChainId, shared.MessageId);
// Nur text-Messages zurückgeben (keine tool_use/tool_result)
var textMessages = contextMessages
.Where(m => m.Type == "text" && m.DeletedOn == null)
.Select(m => new
{
m.Id,
m.Role,
m.Content,
m.CreatedAt
})
.ToList();
await ctx.Response.WriteAsJsonAsync(new
{
mode = "context",
messages = textMessages,
model = chain?.Model,
createdAt = shared.CreatedAt
});
}
else
{
// Einzelne Nachricht
await ctx.Response.WriteAsJsonAsync(new
{
mode = "single",
message = new
{
message.Id,
message.Role,
message.Content,
message.CreatedAt
},
model = chain?.Model,
createdAt = shared.CreatedAt
});
}
}
private static async Task HandleListShares(HttpContext ctx, DatabaseService db)
{
var userId = ctx.GetUserId();
var shares = await db.GetSharedMessagesByUser(userId);
await ctx.Response.WriteAsJsonAsync(shares);
}
private static async Task HandleDeleteShare(HttpContext ctx, DatabaseService db)
{
var userId = ctx.GetUserId();
var token = ctx.Request.RouteValues["token"]?.ToString();
if (string.IsNullOrEmpty(token))
{
ctx.Response.StatusCode = 400;
return;
}
await db.DeleteSharedMessage(token, userId);
ctx.Response.StatusCode = 204;
}
}