1
0
mirror of https://github.com/chylex/Discord-History-Tracker.git synced 2024-10-18 02:42:53 +02:00

Compare commits

...

10 Commits

30 changed files with 307 additions and 85 deletions

View File

@ -33,9 +33,6 @@
<Style Selector="TextBox:focus /template/ Border#PART_BorderElement"> <Style Selector="TextBox:focus /template/ Border#PART_BorderElement">
<Setter Property="BorderThickness" Value="1" /> <Setter Property="BorderThickness" Value="1" />
</Style> </Style>
<Style Selector="TextBox:disabled"><!-- TODO bug in Avalonia (https://github.com/AvaloniaUI/Avalonia/pull/7792) -->
<Setter Property="Foreground" Value="{DynamicResource TextControlForegroundDisabled}" />
</Style>
<Style Selector="TextBox:error DataValidationErrors"> <Style Selector="TextBox:error DataValidationErrors">
<Style.Resources> <Style.Resources>
<ControlTemplate x:Key="InlineDataValidationContentTemplate" TargetType="DataValidationErrors"> <ControlTemplate x:Key="InlineDataValidationContentTemplate" TargetType="DataValidationErrors">

View File

@ -4,12 +4,26 @@ using Avalonia.Data.Converters;
namespace DHT.Desktop.Common { namespace DHT.Desktop.Common {
sealed class BytesValueConverter : IValueConverter { sealed class BytesValueConverter : IValueConverter {
private static readonly string[] Units = { private sealed class Unit {
"B", private readonly string label;
"kB", private readonly string numberFormat;
"MB",
"GB", public Unit(string label, int decimalPlaces) {
"TB" this.label = label;
this.numberFormat = "{0:n" + decimalPlaces + "}";
}
public string Format(double size) {
return string.Format(Program.Culture, numberFormat, size) + " " + label;
}
}
private static readonly Unit[] Units = {
new ("B", decimalPlaces: 0),
new ("kB", decimalPlaces: 0),
new ("MB", decimalPlaces: 1),
new ("GB", decimalPlaces: 1),
new ("TB", decimalPlaces: 1)
}; };
private const int Scale = 1000; private const int Scale = 1000;
@ -17,13 +31,7 @@ namespace DHT.Desktop.Common {
private static string Convert(ulong size) { private static string Convert(ulong size) {
int power = size == 0L ? 0 : (int) Math.Log(size, Scale); int power = size == 0L ? 0 : (int) Math.Log(size, Scale);
int unit = power >= Units.Length ? Units.Length - 1 : power; int unit = power >= Units.Length ? Units.Length - 1 : power;
if (unit == 0) { return Units[unit].Format(unit == 0 ? size : size / Math.Pow(Scale, unit));
return string.Format(Program.Culture, "{0:n0}", size) + " " + Units[unit];
}
else {
double humanReadableSize = size / Math.Pow(Scale, unit);
return string.Format(Program.Culture, "{0:n0}", humanReadableSize) + " " + Units[unit];
}
} }
public object Convert(object? value, Type targetType, object? parameter, CultureInfo culture) { public object Convert(object? value, Type targetType, object? parameter, CultureInfo culture) {

View File

@ -21,10 +21,10 @@
<DebugType>none</DebugType> <DebugType>none</DebugType>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Avalonia" Version="0.10.14" /> <PackageReference Include="Avalonia" Version="0.10.16" />
<PackageReference Include="Avalonia.Controls.DataGrid" Version="0.10.14" /> <PackageReference Include="Avalonia.Controls.DataGrid" Version="0.10.16" />
<PackageReference Include="Avalonia.Desktop" Version="0.10.14" /> <PackageReference Include="Avalonia.Desktop" Version="0.10.16" />
<PackageReference Include="Avalonia.Diagnostics" Version="0.10.14" Condition=" '$(Configuration)' == 'Debug' " /> <PackageReference Include="Avalonia.Diagnostics" Version="0.10.16" Condition=" '$(Configuration)' == 'Debug' " />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\Server\Server.csproj" /> <ProjectReference Include="..\Server\Server.csproj" />

View File

@ -11,9 +11,11 @@ using Avalonia.Controls;
using DHT.Desktop.Common; using DHT.Desktop.Common;
using DHT.Desktop.Dialogs.Message; using DHT.Desktop.Dialogs.Message;
using DHT.Desktop.Main.Controls; using DHT.Desktop.Main.Controls;
using DHT.Desktop.Server;
using DHT.Server.Data.Filters; using DHT.Server.Data.Filters;
using DHT.Server.Database; using DHT.Server.Database;
using DHT.Server.Database.Export; using DHT.Server.Database.Export;
using DHT.Server.Database.Export.Strategy;
using DHT.Utils.Models; using DHT.Utils.Models;
using static DHT.Desktop.Program; using static DHT.Desktop.Program;
@ -55,7 +57,7 @@ namespace DHT.Desktop.Main.Pages {
HasFilters = FilterModel.HasAnyFilters; HasFilters = FilterModel.HasAnyFilters;
} }
private async Task WriteViewerFile(string path) { private async Task WriteViewerFile(string path, IViewerExportStrategy strategy) {
const string ArchiveTag = "/*[ARCHIVE]*/"; const string ArchiveTag = "/*[ARCHIVE]*/";
string indexFile = await Resources.ReadTextAsync("Viewer/index.html"); string indexFile = await Resources.ReadTextAsync("Viewer/index.html");
@ -68,7 +70,7 @@ namespace DHT.Desktop.Main.Pages {
string jsonTempFile = path + ".tmp"; string jsonTempFile = path + ".tmp";
await using (var jsonStream = new FileStream(jsonTempFile, FileMode.Create, FileAccess.ReadWrite, FileShare.Read)) { await using (var jsonStream = new FileStream(jsonTempFile, FileMode.Create, FileAccess.ReadWrite, FileShare.Read)) {
await ViewerJsonExport.Generate(jsonStream, db, FilterModel.CreateFilter()); await ViewerJsonExport.Generate(jsonStream, strategy, db, FilterModel.CreateFilter());
char[] jsonBuffer = new char[Math.Min(32768, jsonStream.Position)]; char[] jsonBuffer = new char[Math.Min(32768, jsonStream.Position)];
jsonStream.Position = 0; jsonStream.Position = 0;
@ -106,7 +108,7 @@ namespace DHT.Desktop.Main.Pages {
TemporaryFiles.Add(fullPath); TemporaryFiles.Add(fullPath);
Directory.CreateDirectory(rootPath); Directory.CreateDirectory(rootPath);
await WriteViewerFile(fullPath); await WriteViewerFile(fullPath, new LiveViewerExportStrategy(ServerManager.Port, ServerManager.Token));
Process.Start(new ProcessStartInfo(fullPath) { UseShellExecute = true }); Process.Start(new ProcessStartInfo(fullPath) { UseShellExecute = true });
} }
@ -126,7 +128,7 @@ namespace DHT.Desktop.Main.Pages {
string? path = await dialog; string? path = await dialog;
if (!string.IsNullOrEmpty(path)) { if (!string.IsNullOrEmpty(path)) {
await WriteViewerFile(path); await WriteViewerFile(path, StandaloneViewerExportStrategy.Instance);
} }
} }

View File

@ -251,6 +251,11 @@ const STATE = (function() {
mapped.type = attachment.content_type; mapped.type = attachment.content_type;
} }
if (attachment.width && attachment.height) {
mapped.width = attachment.width;
mapped.height = attachment.height;
}
return mapped; return mapped;
}); });
} }

View File

@ -1,9 +1,5 @@
#app-mount div[class*="app-"] { #app-mount {
margin-bottom: 48px !important; height: calc(100% - 48px) !important;
}
#app-mount div[class*="app-"] > div[class*="app-"] {
margin-bottom: 0 !important;
} }
#dht-ctrl { #dht-ctrl {

View File

@ -78,6 +78,12 @@ const DISCORD = (function() {
} }
}; };
const isImageUrl = function(url) {
const dot = url.pathname.lastIndexOf(".");
const ext = dot === -1 ? "" : url.pathname.substring(dot).toLowerCase();
return ext === ".png" || ext === ".gif" || ext === ".jpg" || ext === ".jpeg";
};
return { return {
setup() { setup() {
templateChannelServer = new TEMPLATE([ templateChannelServer = new TEMPLATE([
@ -123,7 +129,7 @@ const DISCORD = (function() {
// noinspection HtmlUnknownTarget // noinspection HtmlUnknownTarget
templateAttachmentDownload = new TEMPLATE([ templateAttachmentDownload = new TEMPLATE([
"<a href='{url}' class='embed download'>Download {filename}</a>" "<a href='{url}' class='embed download'>Download {name}</a>"
].join("")); ].join(""));
// noinspection HtmlUnknownTarget // noinspection HtmlUnknownTarget
@ -176,9 +182,8 @@ const DISCORD = (function() {
}, },
isImageAttachment(attachment) { isImageAttachment(attachment) {
const dot = attachment.url.lastIndexOf("."); const url = DOM.tryParseUrl(attachment.url);
const ext = dot === -1 ? "" : attachment.url.substring(dot).toLowerCase(); return url != null && isImageUrl(url);
return ext === ".png" || ext === ".gif" || ext === ".jpg" || ext === ".jpeg";
}, },
getChannelHTML(channel) { // noinspection FunctionWithInconsistentReturnsJS getChannelHTML(channel) { // noinspection FunctionWithInconsistentReturnsJS
@ -235,16 +240,14 @@ const DISCORD = (function() {
} }
return value.map(attachment => { return value.map(attachment => {
if (this.isImageAttachment(attachment) && SETTINGS.enableImagePreviews) { if (!DISCORD.isImageAttachment(attachment) || !SETTINGS.enableImagePreviews) {
return templateEmbedImage.apply({ url: attachment.url, src: attachment.url }); return templateAttachmentDownload.apply(attachment);
}
else if ("width" in attachment && "height" in attachment) {
return templateEmbedImageWithSize.apply({ url: attachment.url, src: attachment.url, width: attachment.width, height: attachment.height });
} }
else { else {
const sliced = attachment.url.split("/"); return templateEmbedImage.apply({ url: attachment.url, src: attachment.url });
return templateAttachmentDownload.apply({
"url": attachment.url,
"filename": sliced[sliced.length - 1]
});
} }
}).join(""); }).join("");
} }

View File

@ -51,4 +51,15 @@ class DOM {
const date = new Date(timestamp); const date = new Date(timestamp);
return date.toLocaleDateString() + ", " + date.toLocaleTimeString(); return date.toLocaleDateString() + ", " + date.toLocaleTimeString();
}; };
/**
* Parses a url string into a URL object and returns it. If the parsing fails, returns null.
*/
static tryParseUrl(url) {
try {
return new URL(url);
} catch (ignore) {
return null;
}
}
} }

View File

@ -5,5 +5,7 @@ namespace DHT.Server.Data {
public string? Type { get; internal init; } public string? Type { get; internal init; }
public string Url { get; internal init; } public string Url { get; internal init; }
public ulong Size { get; internal init; } public ulong Size { get; internal init; }
public int? Width { get; internal init; }
public int? Height { get; internal init; }
} }
} }

View File

@ -0,0 +1,6 @@
namespace DHT.Server.Data {
public readonly struct DownloadedAttachment {
public string? Type { get; internal init; }
public byte[] Data { get; internal init; }
}
}

View File

@ -63,6 +63,10 @@ namespace DHT.Server.Database {
return download; return download;
} }
public DownloadedAttachment? GetDownloadedAttachment(string url) {
return null;
}
public void AddDownload(Data.Download download) {} public void AddDownload(Data.Download download) {}
public void EnqueueDownloadItems(AttachmentFilter? filter = null) {} public void EnqueueDownloadItems(AttachmentFilter? filter = null) {}

View File

@ -0,0 +1,7 @@
using DHT.Server.Data;
namespace DHT.Server.Database.Export.Strategy {
public interface IViewerExportStrategy {
string GetAttachmentUrl(Attachment attachment);
}
}

View File

@ -0,0 +1,18 @@
using System.Net;
using DHT.Server.Data;
namespace DHT.Server.Database.Export.Strategy {
public sealed class LiveViewerExportStrategy : IViewerExportStrategy {
private readonly string safePort;
private readonly string safeToken;
public LiveViewerExportStrategy(ushort port, string token) {
this.safePort = port.ToString();
this.safeToken = WebUtility.UrlEncode(token);
}
public string GetAttachmentUrl(Attachment attachment) {
return "http://127.0.0.1:" + safePort + "/get-attachment/" + WebUtility.UrlEncode(attachment.Url) + "?token=" + safeToken;
}
}
}

View File

@ -0,0 +1,13 @@
using DHT.Server.Data;
namespace DHT.Server.Database.Export.Strategy {
public sealed class StandaloneViewerExportStrategy : IViewerExportStrategy {
public static StandaloneViewerExportStrategy Instance { get; } = new ();
private StandaloneViewerExportStrategy() {}
public string GetAttachmentUrl(Attachment attachment) {
return attachment.Url;
}
}
}

View File

@ -1,3 +1,4 @@
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
@ -5,13 +6,14 @@ using System.Text.Json;
using System.Threading.Tasks; using System.Threading.Tasks;
using DHT.Server.Data; using DHT.Server.Data;
using DHT.Server.Data.Filters; using DHT.Server.Data.Filters;
using DHT.Server.Database.Export.Strategy;
using DHT.Utils.Logging; using DHT.Utils.Logging;
namespace DHT.Server.Database.Export { namespace DHT.Server.Database.Export {
public static class ViewerJsonExport { public static class ViewerJsonExport {
private static readonly Log Log = Log.ForType(typeof(ViewerJsonExport)); private static readonly Log Log = Log.ForType(typeof(ViewerJsonExport));
public static async Task Generate(Stream stream, IDatabaseFile db, MessageFilter? filter = null) { public static async Task Generate(Stream stream, IViewerExportStrategy strategy, IDatabaseFile db, MessageFilter? filter = null) {
var perf = Log.Start(); var perf = Log.Start();
var includedUserIds = new HashSet<ulong>(); var includedUserIds = new HashSet<ulong>();
@ -41,7 +43,7 @@ namespace DHT.Server.Database.Export {
var value = new { var value = new {
meta = new { users, userindex, servers, channels }, meta = new { users, userindex, servers, channels },
data = GenerateMessageList(includedMessages, userIndices) data = GenerateMessageList(includedMessages, userIndices, strategy)
}; };
perf.Step("Generate value object"); perf.Step("Generate value object");
@ -138,7 +140,7 @@ namespace DHT.Server.Database.Export {
return channels; return channels;
} }
private static object GenerateMessageList(List<Message> includedMessages, Dictionary<ulong, object> userIndices) { private static object GenerateMessageList( List<Message> includedMessages, Dictionary<ulong, object> userIndices, IViewerExportStrategy strategy) {
var data = new Dictionary<string, Dictionary<string, object>>(); var data = new Dictionary<string, Dictionary<string, object>>();
foreach (var grouping in includedMessages.GroupBy(static message => message.Channel)) { foreach (var grouping in includedMessages.GroupBy(static message => message.Channel)) {
@ -164,8 +166,18 @@ namespace DHT.Server.Database.Export {
} }
if (!message.Attachments.IsEmpty) { if (!message.Attachments.IsEmpty) {
obj["a"] = message.Attachments.Select(static attachment => new Dictionary<string, object> { obj["a"] = message.Attachments.Select(attachment => {
{ "url", attachment.Url } var a = new Dictionary<string, object> {
{ "url", strategy.GetAttachmentUrl(attachment) },
{ "name", Uri.TryCreate(attachment.Url, UriKind.Absolute, out var uri) ? Path.GetFileName(uri.LocalPath) : attachment.Url }
};
if (attachment.Width != null && attachment.Height != null) {
a["width"] = attachment.Width;
a["height"] = attachment.Height;
}
return a;
}).ToArray(); }).ToArray();
} }
@ -188,7 +200,7 @@ namespace DHT.Server.Database.Export {
r["a"] = reaction.EmojiFlags.HasFlag(EmojiFlags.Animated); r["a"] = reaction.EmojiFlags.HasFlag(EmojiFlags.Animated);
r["c"] = reaction.Count; r["c"] = reaction.Count;
return r; return r;
}); }).ToArray();
} }
channelData[message.Id.ToString()] = obj; channelData[message.Id.ToString()] = obj;

View File

@ -31,6 +31,7 @@ namespace DHT.Server.Database {
void AddDownload(Data.Download download); void AddDownload(Data.Download download);
List<Data.Download> GetDownloadsWithoutData(); List<Data.Download> GetDownloadsWithoutData();
Data.Download GetDownloadWithData(Data.Download download); Data.Download GetDownloadWithData(Data.Download download);
DownloadedAttachment? GetDownloadedAttachment(string url);
void EnqueueDownloadItems(AttachmentFilter? filter = null); void EnqueueDownloadItems(AttachmentFilter? filter = null);
List<DownloadItem> GetEnqueuedDownloadItems(int count); List<DownloadItem> GetEnqueuedDownloadItems(int count);

View File

@ -6,7 +6,7 @@ using DHT.Utils.Logging;
namespace DHT.Server.Database.Sqlite { namespace DHT.Server.Database.Sqlite {
sealed class Schema { sealed class Schema {
internal const int Version = 4; internal const int Version = 5;
private static readonly Log Log = Log.ForType<Schema>(); private static readonly Log Log = Log.ForType<Schema>();
@ -79,7 +79,9 @@ namespace DHT.Server.Database.Sqlite {
name TEXT NOT NULL, name TEXT NOT NULL,
type TEXT, type TEXT,
url TEXT NOT NULL, url TEXT NOT NULL,
size INTEGER NOT NULL)"); size INTEGER NOT NULL,
width INTEGER,
height INTEGER)");
Execute(@"CREATE TABLE embeds ( Execute(@"CREATE TABLE embeds (
message_id INTEGER NOT NULL, message_id INTEGER NOT NULL,
@ -159,6 +161,12 @@ namespace DHT.Server.Database.Sqlite {
perf.Step("Upgrade to version 4"); perf.Step("Upgrade to version 4");
} }
if (dbVersion <= 4) {
Execute("ALTER TABLE attachments ADD width INTEGER");
Execute("ALTER TABLE attachments ADD height INTEGER");
perf.Step("Upgrade to version 5");
}
perf.End(); perf.End();
} }
} }

View File

@ -252,7 +252,9 @@ namespace DHT.Server.Database.Sqlite {
("name", SqliteType.Text), ("name", SqliteType.Text),
("type", SqliteType.Text), ("type", SqliteType.Text),
("url", SqliteType.Text), ("url", SqliteType.Text),
("size", SqliteType.Integer) ("size", SqliteType.Integer),
("width", SqliteType.Integer),
("height", SqliteType.Integer)
}); });
using var embedCmd = conn.Insert("embeds", new[] { using var embedCmd = conn.Insert("embeds", new[] {
@ -307,6 +309,8 @@ namespace DHT.Server.Database.Sqlite {
attachmentCmd.Set(":type", attachment.Type); attachmentCmd.Set(":type", attachment.Type);
attachmentCmd.Set(":url", attachment.Url); attachmentCmd.Set(":url", attachment.Url);
attachmentCmd.Set(":size", attachment.Size); attachmentCmd.Set(":size", attachment.Size);
attachmentCmd.Set(":width", attachment.Width);
attachmentCmd.Set(":height", attachment.Height);
attachmentCmd.ExecuteNonQuery(); attachmentCmd.ExecuteNonQuery();
} }
} }
@ -470,6 +474,28 @@ LEFT JOIN replied_to rt ON m.message_id = rt.message_id" + filter.GenerateWhereC
} }
} }
public DownloadedAttachment? GetDownloadedAttachment(string url) {
using var conn = pool.Take();
using var cmd = conn.Command(@"
SELECT a.type, d.blob FROM downloads d
LEFT JOIN attachments a ON d.url = a.url
WHERE d.url = :url AND d.status = :success AND d.blob IS NOT NULL");
cmd.AddAndSet(":url", SqliteType.Text, url);
cmd.AddAndSet(":success", SqliteType.Integer, (int) DownloadStatus.Success);
using var reader = cmd.ExecuteReader();
if (!reader.Read()) {
return null;
}
return new DownloadedAttachment {
Type = reader.IsDBNull(0) ? null : reader.GetString(0),
Data = (byte[]) reader["blob"]
};
}
public void EnqueueDownloadItems(AttachmentFilter? filter = null) { public void EnqueueDownloadItems(AttachmentFilter? filter = null) {
using var conn = pool.Take(); using var conn = pool.Take();
using var cmd = conn.Command("INSERT INTO downloads (url, status, size) SELECT a.url, :enqueued, MAX(a.size) FROM attachments a" + filter.GenerateWhereClause("a") + " GROUP BY a.url"); using var cmd = conn.Command("INSERT INTO downloads (url, status, size) SELECT a.url, :enqueued, MAX(a.size) FROM attachments a" + filter.GenerateWhereClause("a") + " GROUP BY a.url");
@ -549,7 +575,7 @@ FROM downloads");
var dict = new MultiDictionary<ulong, Attachment>(); var dict = new MultiDictionary<ulong, Attachment>();
using var conn = pool.Take(); using var conn = pool.Take();
using var cmd = conn.Command("SELECT message_id, attachment_id, name, type, url, size FROM attachments"); using var cmd = conn.Command("SELECT message_id, attachment_id, name, type, url, size, width, height FROM attachments");
using var reader = cmd.ExecuteReader(); using var reader = cmd.ExecuteReader();
while (reader.Read()) { while (reader.Read()) {
@ -560,7 +586,9 @@ FROM downloads");
Name = reader.GetString(2), Name = reader.GetString(2),
Type = reader.IsDBNull(3) ? null : reader.GetString(3), Type = reader.IsDBNull(3) ? null : reader.GetString(3),
Url = reader.GetString(4), Url = reader.GetString(4),
Size = reader.GetUint64(5) Size = reader.GetUint64(5),
Width = reader.IsDBNull(6) ? null : reader.GetInt32(6),
Height = reader.IsDBNull(7) ? null : reader.GetInt32(7)
}); });
} }

View File

@ -45,22 +45,21 @@ namespace DHT.Server.Database.Sqlite.Utils {
} }
public ISqliteConnection Take() { public ISqliteConnection Take() {
PooledConnection? conn = null; while (true) {
while (conn == null) {
ThrowIfDisposed(); ThrowIfDisposed();
lock (monitor) { lock (monitor) {
if (free.TryTake(out conn, TimeSpan.FromMilliseconds(rand.Next(100, 200)))) { if (free.TryTake(out var conn)) {
used.Add(conn); used.Add(conn);
break; return conn;
} }
else { else {
Log.ForType<SqliteConnectionPool>().Warn("Thread " + Thread.CurrentThread.ManagedThreadId + " is starving for connections."); Log.ForType<SqliteConnectionPool>().Warn("Thread " + Thread.CurrentThread.ManagedThreadId + " is starving for connections.");
} }
} }
}
return conn; Thread.Sleep(TimeSpan.FromMilliseconds(rand.Next(100, 200)));
}
} }
private void Return(PooledConnection conn) { private void Return(PooledConnection conn) {

View File

@ -8,6 +8,7 @@ using DHT.Utils.Http;
using DHT.Utils.Logging; using DHT.Utils.Logging;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Extensions; using Microsoft.AspNetCore.Http.Extensions;
using Microsoft.Extensions.Primitives;
namespace DHT.Server.Endpoints { namespace DHT.Server.Endpoints {
abstract class BaseEndpoint { abstract class BaseEndpoint {
@ -21,26 +22,22 @@ namespace DHT.Server.Endpoints {
this.parameters = parameters; this.parameters = parameters;
} }
public async Task Handle(HttpContext ctx) { private async Task Handle(HttpContext ctx, StringValues token) {
var request = ctx.Request; var request = ctx.Request;
var response = ctx.Response; var response = ctx.Response;
Log.Info("Request: " + request.GetDisplayUrl() + " (" + request.ContentLength + " B)"); Log.Info("Request: " + request.GetDisplayUrl() + " (" + request.ContentLength + " B)");
var requestToken = request.Headers["X-DHT-Token"]; if (token.Count != 1 || token[0] != parameters.Token) {
if (requestToken.Count != 1 || requestToken[0] != parameters.Token) { Log.Error("Token: " + (token.Count == 1 ? token[0] : "<missing>"));
Log.Error("Token: " + (requestToken.Count == 1 ? requestToken[0] : "<missing>"));
response.StatusCode = (int) HttpStatusCode.Forbidden; response.StatusCode = (int) HttpStatusCode.Forbidden;
return; return;
} }
try { try {
var (statusCode, output) = await Respond(ctx); response.StatusCode = (int) HttpStatusCode.OK;
response.StatusCode = (int) statusCode; var output = await Respond(ctx);
await output.WriteTo(response);
if (output != null) {
await response.WriteAsJsonAsync(output);
}
} catch (HttpException e) { } catch (HttpException e) {
Log.Error(e); Log.Error(e);
response.StatusCode = (int) e.StatusCode; response.StatusCode = (int) e.StatusCode;
@ -51,7 +48,15 @@ namespace DHT.Server.Endpoints {
} }
} }
protected abstract Task<(HttpStatusCode, object?)> Respond(HttpContext ctx); public async Task HandleGet(HttpContext ctx) {
await Handle(ctx, ctx.Request.Query["token"]);
}
public async Task HandlePost(HttpContext ctx) {
await Handle(ctx, ctx.Request.Headers["X-DHT-Token"]);
}
protected abstract Task<IHttpOutput> Respond(HttpContext ctx);
protected static async Task<JsonElement> ReadJson(HttpContext ctx) { protected static async Task<JsonElement> ReadJson(HttpContext ctx) {
return await ctx.Request.ReadFromJsonAsync<JsonElement?>() ?? throw new HttpException(HttpStatusCode.UnsupportedMediaType, "This endpoint only accepts JSON."); return await ctx.Request.ReadFromJsonAsync<JsonElement?>() ?? throw new HttpException(HttpStatusCode.UnsupportedMediaType, "This endpoint only accepts JSON.");

View File

@ -0,0 +1,25 @@
using System.Net;
using System.Threading.Tasks;
using DHT.Server.Data;
using DHT.Server.Database;
using DHT.Server.Service;
using DHT.Utils.Http;
using Microsoft.AspNetCore.Http;
namespace DHT.Server.Endpoints {
sealed class GetAttachmentEndpoint : BaseEndpoint {
public GetAttachmentEndpoint(IDatabaseFile db, ServerParameters parameters) : base(db, parameters) {}
protected override Task<IHttpOutput> Respond(HttpContext ctx) {
string attachmentUrl = WebUtility.UrlDecode((string) ctx.Request.RouteValues["url"]!);
DownloadedAttachment? maybeDownloadedAttachment = Db.GetDownloadedAttachment(attachmentUrl);
if (maybeDownloadedAttachment is {} downloadedAttachment) {
return Task.FromResult<IHttpOutput>(new HttpOutput.File(downloadedAttachment.Type, downloadedAttachment.Data));
}
else {
return Task.FromResult<IHttpOutput>(new HttpOutput.Redirect(attachmentUrl, permanent: false));
}
}
}
}

View File

@ -11,7 +11,7 @@ namespace DHT.Server.Endpoints {
sealed class TrackChannelEndpoint : BaseEndpoint { sealed class TrackChannelEndpoint : BaseEndpoint {
public TrackChannelEndpoint(IDatabaseFile db, ServerParameters parameters) : base(db, parameters) {} public TrackChannelEndpoint(IDatabaseFile db, ServerParameters parameters) : base(db, parameters) {}
protected override async Task<(HttpStatusCode, object?)> Respond(HttpContext ctx) { protected override async Task<IHttpOutput> Respond(HttpContext ctx) {
var root = await ReadJson(ctx); var root = await ReadJson(ctx);
var server = ReadServer(root.RequireObject("server"), "server"); var server = ReadServer(root.RequireObject("server"), "server");
var channel = ReadChannel(root.RequireObject("channel"), "channel", server.Id); var channel = ReadChannel(root.RequireObject("channel"), "channel", server.Id);
@ -19,7 +19,7 @@ namespace DHT.Server.Endpoints {
Db.AddServer(server); Db.AddServer(server);
Db.AddChannel(channel); Db.AddChannel(channel);
return (HttpStatusCode.OK, null); return HttpOutput.None;
} }
private static Data.Server ReadServer(JsonElement json, string path) => new() { private static Data.Server ReadServer(JsonElement json, string path) => new() {

View File

@ -17,7 +17,7 @@ namespace DHT.Server.Endpoints {
sealed class TrackMessagesEndpoint : BaseEndpoint { sealed class TrackMessagesEndpoint : BaseEndpoint {
public TrackMessagesEndpoint(IDatabaseFile db, ServerParameters parameters) : base(db, parameters) {} public TrackMessagesEndpoint(IDatabaseFile db, ServerParameters parameters) : base(db, parameters) {}
protected override async Task<(HttpStatusCode, object?)> Respond(HttpContext ctx) { protected override async Task<IHttpOutput> Respond(HttpContext ctx) {
var root = await ReadJson(ctx); var root = await ReadJson(ctx);
if (root.ValueKind != JsonValueKind.Array) { if (root.ValueKind != JsonValueKind.Array) {
@ -39,7 +39,7 @@ namespace DHT.Server.Endpoints {
Db.AddMessages(messages); Db.AddMessages(messages);
return (HttpStatusCode.OK, anyNewMessages ? 1 : 0); return new HttpOutput.Json(anyNewMessages ? 1 : 0);
} }
private static Message ReadMessage(JsonElement json, string path) => new() { private static Message ReadMessage(JsonElement json, string path) => new() {
@ -61,7 +61,9 @@ namespace DHT.Server.Endpoints {
Name = ele.RequireString("name", path), Name = ele.RequireString("name", path),
Type = ele.HasKey("type") ? ele.RequireString("type", path) : null, Type = ele.HasKey("type") ? ele.RequireString("type", path) : null,
Url = ele.RequireString("url", path), Url = ele.RequireString("url", path),
Size = (ulong) ele.RequireLong("size", path) Size = (ulong) ele.RequireLong("size", path),
Width = ele.HasKey("width") ? ele.RequireInt("width", path) : null,
Height = ele.HasKey("height") ? ele.RequireInt("height", path) : null
}).DistinctByKeyStable(static attachment => { }).DistinctByKeyStable(static attachment => {
// Some Discord messages have duplicate attachments with the same id for unknown reasons. // Some Discord messages have duplicate attachments with the same id for unknown reasons.
return attachment.Id; return attachment.Id;

View File

@ -11,7 +11,7 @@ namespace DHT.Server.Endpoints {
sealed class TrackUsersEndpoint : BaseEndpoint { sealed class TrackUsersEndpoint : BaseEndpoint {
public TrackUsersEndpoint(IDatabaseFile db, ServerParameters parameters) : base(db, parameters) {} public TrackUsersEndpoint(IDatabaseFile db, ServerParameters parameters) : base(db, parameters) {}
protected override async Task<(HttpStatusCode, object?)> Respond(HttpContext ctx) { protected override async Task<IHttpOutput> Respond(HttpContext ctx) {
var root = await ReadJson(ctx); var root = await ReadJson(ctx);
if (root.ValueKind != JsonValueKind.Array) { if (root.ValueKind != JsonValueKind.Array) {
@ -27,7 +27,7 @@ namespace DHT.Server.Endpoints {
Db.AddUsers(users); Db.AddUsers(users);
return (HttpStatusCode.OK, null); return HttpOutput.None;
} }
private static User ReadUser(JsonElement json, string path) => new() { private static User ReadUser(JsonElement json, string path) => new() {

View File

@ -20,7 +20,7 @@
<FrameworkReference Include="Microsoft.AspNetCore.App" /> <FrameworkReference Include="Microsoft.AspNetCore.App" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.Data.Sqlite" Version="6.0.5" /> <PackageReference Include="Microsoft.Data.Sqlite" Version="6.0.7" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\Utils\Utils.csproj" /> <ProjectReference Include="..\Utils\Utils.csproj" />

View File

@ -34,13 +34,16 @@ namespace DHT.Server.Service {
app.UseCors(); app.UseCors();
app.UseEndpoints(endpoints => { app.UseEndpoints(endpoints => {
TrackChannelEndpoint trackChannel = new(db, parameters); TrackChannelEndpoint trackChannel = new(db, parameters);
endpoints.MapPost("/track-channel", async context => await trackChannel.Handle(context)); endpoints.MapPost("/track-channel", async context => await trackChannel.HandlePost(context));
TrackUsersEndpoint trackUsers = new(db, parameters); TrackUsersEndpoint trackUsers = new(db, parameters);
endpoints.MapPost("/track-users", async context => await trackUsers.Handle(context)); endpoints.MapPost("/track-users", async context => await trackUsers.HandlePost(context));
TrackMessagesEndpoint trackMessages = new(db, parameters); TrackMessagesEndpoint trackMessages = new(db, parameters);
endpoints.MapPost("/track-messages", async context => await trackMessages.Handle(context)); endpoints.MapPost("/track-messages", async context => await trackMessages.HandlePost(context));
GetAttachmentEndpoint getAttachment = new(db, parameters);
endpoints.MapGet("/get-attachment/{url}", async context => await getAttachment.HandleGet(context));
}); });
} }
} }

View File

@ -0,0 +1,56 @@
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
namespace DHT.Utils.Http {
public static class HttpOutput {
public static IHttpOutput None { get; } = new NoneImpl();
private sealed class NoneImpl : IHttpOutput {
public Task WriteTo(HttpResponse response) {
return Task.CompletedTask;
}
}
public sealed class Json : IHttpOutput {
private readonly object? obj;
public Json(object? obj) {
this.obj = obj;
}
public Task WriteTo(HttpResponse response) {
return response.WriteAsJsonAsync(obj);
}
}
public sealed class File : IHttpOutput {
private readonly string? contentType;
private readonly byte[] bytes;
public File(string? contentType, byte[] bytes) {
this.contentType = contentType;
this.bytes = bytes;
}
public async Task WriteTo(HttpResponse response) {
response.ContentType = contentType ?? string.Empty;
await response.Body.WriteAsync(bytes);
}
}
public sealed class Redirect : IHttpOutput {
private readonly string url;
private readonly bool permanent;
public Redirect(string url, bool permanent) {
this.url = url;
this.permanent = permanent;
}
public Task WriteTo(HttpResponse response) {
response.Redirect(url, permanent);
return Task.CompletedTask;
}
}
}
}

View File

@ -0,0 +1,8 @@
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
namespace DHT.Utils.Http {
public interface IHttpOutput {
Task WriteTo(HttpResponse response);
}
}

View File

@ -17,7 +17,10 @@
<DebugType>none</DebugType> <DebugType>none</DebugType>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="JetBrains.Annotations" Version="10.3.0" /> <FrameworkReference Include="Microsoft.AspNetCore.App" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="JetBrains.Annotations" Version="2022.1.0" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Compile Include="..\Version.cs" Link="Version.cs" /> <Compile Include="..\Version.cs" Link="Version.cs" />

Binary file not shown.