mirror of
https://github.com/chylex/Discord-History-Tracker.git
synced 2024-11-25 14:42:44 +01:00
Compare commits
No commits in common. "f625a39b4d28e654ffefe148bd26ca8f3cc296bd" and "176a81e05551c7643f180fab02ce2c793b737e8d" have entirely different histories.
f625a39b4d
...
176a81e055
@ -33,6 +33,9 @@
|
||||
<Style Selector="TextBox:focus /template/ Border#PART_BorderElement">
|
||||
<Setter Property="BorderThickness" Value="1" />
|
||||
</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.Resources>
|
||||
<ControlTemplate x:Key="InlineDataValidationContentTemplate" TargetType="DataValidationErrors">
|
||||
|
@ -4,26 +4,12 @@ using Avalonia.Data.Converters;
|
||||
|
||||
namespace DHT.Desktop.Common {
|
||||
sealed class BytesValueConverter : IValueConverter {
|
||||
private sealed class Unit {
|
||||
private readonly string label;
|
||||
private readonly string numberFormat;
|
||||
|
||||
public Unit(string label, int decimalPlaces) {
|
||||
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 static readonly string[] Units = {
|
||||
"B",
|
||||
"kB",
|
||||
"MB",
|
||||
"GB",
|
||||
"TB"
|
||||
};
|
||||
|
||||
private const int Scale = 1000;
|
||||
@ -31,7 +17,13 @@ namespace DHT.Desktop.Common {
|
||||
private static string Convert(ulong size) {
|
||||
int power = size == 0L ? 0 : (int) Math.Log(size, Scale);
|
||||
int unit = power >= Units.Length ? Units.Length - 1 : power;
|
||||
return Units[unit].Format(unit == 0 ? size : size / Math.Pow(Scale, unit));
|
||||
if (unit == 0) {
|
||||
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) {
|
||||
|
@ -21,10 +21,10 @@
|
||||
<DebugType>none</DebugType>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Avalonia" Version="0.10.16" />
|
||||
<PackageReference Include="Avalonia.Controls.DataGrid" Version="0.10.16" />
|
||||
<PackageReference Include="Avalonia.Desktop" Version="0.10.16" />
|
||||
<PackageReference Include="Avalonia.Diagnostics" Version="0.10.16" Condition=" '$(Configuration)' == 'Debug' " />
|
||||
<PackageReference Include="Avalonia" Version="0.10.14" />
|
||||
<PackageReference Include="Avalonia.Controls.DataGrid" Version="0.10.14" />
|
||||
<PackageReference Include="Avalonia.Desktop" Version="0.10.14" />
|
||||
<PackageReference Include="Avalonia.Diagnostics" Version="0.10.14" Condition=" '$(Configuration)' == 'Debug' " />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Server\Server.csproj" />
|
||||
|
@ -11,11 +11,9 @@ using Avalonia.Controls;
|
||||
using DHT.Desktop.Common;
|
||||
using DHT.Desktop.Dialogs.Message;
|
||||
using DHT.Desktop.Main.Controls;
|
||||
using DHT.Desktop.Server;
|
||||
using DHT.Server.Data.Filters;
|
||||
using DHT.Server.Database;
|
||||
using DHT.Server.Database.Export;
|
||||
using DHT.Server.Database.Export.Strategy;
|
||||
using DHT.Utils.Models;
|
||||
using static DHT.Desktop.Program;
|
||||
|
||||
@ -57,7 +55,7 @@ namespace DHT.Desktop.Main.Pages {
|
||||
HasFilters = FilterModel.HasAnyFilters;
|
||||
}
|
||||
|
||||
private async Task WriteViewerFile(string path, IViewerExportStrategy strategy) {
|
||||
private async Task WriteViewerFile(string path) {
|
||||
const string ArchiveTag = "/*[ARCHIVE]*/";
|
||||
|
||||
string indexFile = await Resources.ReadTextAsync("Viewer/index.html");
|
||||
@ -70,7 +68,7 @@ namespace DHT.Desktop.Main.Pages {
|
||||
string jsonTempFile = path + ".tmp";
|
||||
|
||||
await using (var jsonStream = new FileStream(jsonTempFile, FileMode.Create, FileAccess.ReadWrite, FileShare.Read)) {
|
||||
await ViewerJsonExport.Generate(jsonStream, strategy, db, FilterModel.CreateFilter());
|
||||
await ViewerJsonExport.Generate(jsonStream, db, FilterModel.CreateFilter());
|
||||
|
||||
char[] jsonBuffer = new char[Math.Min(32768, jsonStream.Position)];
|
||||
jsonStream.Position = 0;
|
||||
@ -108,7 +106,7 @@ namespace DHT.Desktop.Main.Pages {
|
||||
TemporaryFiles.Add(fullPath);
|
||||
|
||||
Directory.CreateDirectory(rootPath);
|
||||
await WriteViewerFile(fullPath, new LiveViewerExportStrategy(ServerManager.Port, ServerManager.Token));
|
||||
await WriteViewerFile(fullPath);
|
||||
|
||||
Process.Start(new ProcessStartInfo(fullPath) { UseShellExecute = true });
|
||||
}
|
||||
@ -128,7 +126,7 @@ namespace DHT.Desktop.Main.Pages {
|
||||
|
||||
string? path = await dialog;
|
||||
if (!string.IsNullOrEmpty(path)) {
|
||||
await WriteViewerFile(path, StandaloneViewerExportStrategy.Instance);
|
||||
await WriteViewerFile(path);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -251,11 +251,6 @@ const STATE = (function() {
|
||||
mapped.type = attachment.content_type;
|
||||
}
|
||||
|
||||
if (attachment.width && attachment.height) {
|
||||
mapped.width = attachment.width;
|
||||
mapped.height = attachment.height;
|
||||
}
|
||||
|
||||
return mapped;
|
||||
});
|
||||
}
|
||||
|
@ -1,5 +1,9 @@
|
||||
#app-mount {
|
||||
height: calc(100% - 48px) !important;
|
||||
#app-mount div[class*="app-"] {
|
||||
margin-bottom: 48px !important;
|
||||
}
|
||||
|
||||
#app-mount div[class*="app-"] > div[class*="app-"] {
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
|
||||
#dht-ctrl {
|
||||
|
@ -78,12 +78,6 @@ 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 {
|
||||
setup() {
|
||||
templateChannelServer = new TEMPLATE([
|
||||
@ -129,7 +123,7 @@ const DISCORD = (function() {
|
||||
|
||||
// noinspection HtmlUnknownTarget
|
||||
templateAttachmentDownload = new TEMPLATE([
|
||||
"<a href='{url}' class='embed download'>Download {name}</a>"
|
||||
"<a href='{url}' class='embed download'>Download {filename}</a>"
|
||||
].join(""));
|
||||
|
||||
// noinspection HtmlUnknownTarget
|
||||
@ -182,8 +176,9 @@ const DISCORD = (function() {
|
||||
},
|
||||
|
||||
isImageAttachment(attachment) {
|
||||
const url = DOM.tryParseUrl(attachment.url);
|
||||
return url != null && isImageUrl(url);
|
||||
const dot = attachment.url.lastIndexOf(".");
|
||||
const ext = dot === -1 ? "" : attachment.url.substring(dot).toLowerCase();
|
||||
return ext === ".png" || ext === ".gif" || ext === ".jpg" || ext === ".jpeg";
|
||||
},
|
||||
|
||||
getChannelHTML(channel) { // noinspection FunctionWithInconsistentReturnsJS
|
||||
@ -240,14 +235,16 @@ const DISCORD = (function() {
|
||||
}
|
||||
|
||||
return value.map(attachment => {
|
||||
if (!DISCORD.isImageAttachment(attachment) || !SETTINGS.enableImagePreviews) {
|
||||
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 });
|
||||
if (this.isImageAttachment(attachment) && SETTINGS.enableImagePreviews) {
|
||||
return templateEmbedImage.apply({ url: attachment.url, src: attachment.url });
|
||||
}
|
||||
else {
|
||||
return templateEmbedImage.apply({ url: attachment.url, src: attachment.url });
|
||||
const sliced = attachment.url.split("/");
|
||||
|
||||
return templateAttachmentDownload.apply({
|
||||
"url": attachment.url,
|
||||
"filename": sliced[sliced.length - 1]
|
||||
});
|
||||
}
|
||||
}).join("");
|
||||
}
|
||||
|
@ -51,15 +51,4 @@ class DOM {
|
||||
const date = new Date(timestamp);
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -5,7 +5,5 @@ namespace DHT.Server.Data {
|
||||
public string? Type { get; internal init; }
|
||||
public string Url { get; internal init; }
|
||||
public ulong Size { get; internal init; }
|
||||
public int? Width { get; internal init; }
|
||||
public int? Height { get; internal init; }
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +0,0 @@
|
||||
namespace DHT.Server.Data {
|
||||
public readonly struct DownloadedAttachment {
|
||||
public string? Type { get; internal init; }
|
||||
public byte[] Data { get; internal init; }
|
||||
}
|
||||
}
|
@ -63,10 +63,6 @@ namespace DHT.Server.Database {
|
||||
return download;
|
||||
}
|
||||
|
||||
public DownloadedAttachment? GetDownloadedAttachment(string url) {
|
||||
return null;
|
||||
}
|
||||
|
||||
public void AddDownload(Data.Download download) {}
|
||||
|
||||
public void EnqueueDownloadItems(AttachmentFilter? filter = null) {}
|
||||
|
@ -1,7 +0,0 @@
|
||||
using DHT.Server.Data;
|
||||
|
||||
namespace DHT.Server.Database.Export.Strategy {
|
||||
public interface IViewerExportStrategy {
|
||||
string GetAttachmentUrl(Attachment attachment);
|
||||
}
|
||||
}
|
@ -1,18 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,13 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,4 +1,3 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
@ -6,14 +5,13 @@ using System.Text.Json;
|
||||
using System.Threading.Tasks;
|
||||
using DHT.Server.Data;
|
||||
using DHT.Server.Data.Filters;
|
||||
using DHT.Server.Database.Export.Strategy;
|
||||
using DHT.Utils.Logging;
|
||||
|
||||
namespace DHT.Server.Database.Export {
|
||||
public static class ViewerJsonExport {
|
||||
private static readonly Log Log = Log.ForType(typeof(ViewerJsonExport));
|
||||
|
||||
public static async Task Generate(Stream stream, IViewerExportStrategy strategy, IDatabaseFile db, MessageFilter? filter = null) {
|
||||
public static async Task Generate(Stream stream, IDatabaseFile db, MessageFilter? filter = null) {
|
||||
var perf = Log.Start();
|
||||
|
||||
var includedUserIds = new HashSet<ulong>();
|
||||
@ -43,7 +41,7 @@ namespace DHT.Server.Database.Export {
|
||||
|
||||
var value = new {
|
||||
meta = new { users, userindex, servers, channels },
|
||||
data = GenerateMessageList(includedMessages, userIndices, strategy)
|
||||
data = GenerateMessageList(includedMessages, userIndices)
|
||||
};
|
||||
|
||||
perf.Step("Generate value object");
|
||||
@ -140,7 +138,7 @@ namespace DHT.Server.Database.Export {
|
||||
return channels;
|
||||
}
|
||||
|
||||
private static object GenerateMessageList( List<Message> includedMessages, Dictionary<ulong, object> userIndices, IViewerExportStrategy strategy) {
|
||||
private static object GenerateMessageList(List<Message> includedMessages, Dictionary<ulong, object> userIndices) {
|
||||
var data = new Dictionary<string, Dictionary<string, object>>();
|
||||
|
||||
foreach (var grouping in includedMessages.GroupBy(static message => message.Channel)) {
|
||||
@ -166,18 +164,8 @@ namespace DHT.Server.Database.Export {
|
||||
}
|
||||
|
||||
if (!message.Attachments.IsEmpty) {
|
||||
obj["a"] = message.Attachments.Select(attachment => {
|
||||
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;
|
||||
obj["a"] = message.Attachments.Select(static attachment => new Dictionary<string, object> {
|
||||
{ "url", attachment.Url }
|
||||
}).ToArray();
|
||||
}
|
||||
|
||||
@ -200,7 +188,7 @@ namespace DHT.Server.Database.Export {
|
||||
r["a"] = reaction.EmojiFlags.HasFlag(EmojiFlags.Animated);
|
||||
r["c"] = reaction.Count;
|
||||
return r;
|
||||
}).ToArray();
|
||||
});
|
||||
}
|
||||
|
||||
channelData[message.Id.ToString()] = obj;
|
||||
|
@ -31,7 +31,6 @@ namespace DHT.Server.Database {
|
||||
void AddDownload(Data.Download download);
|
||||
List<Data.Download> GetDownloadsWithoutData();
|
||||
Data.Download GetDownloadWithData(Data.Download download);
|
||||
DownloadedAttachment? GetDownloadedAttachment(string url);
|
||||
|
||||
void EnqueueDownloadItems(AttachmentFilter? filter = null);
|
||||
List<DownloadItem> GetEnqueuedDownloadItems(int count);
|
||||
|
@ -6,7 +6,7 @@ using DHT.Utils.Logging;
|
||||
|
||||
namespace DHT.Server.Database.Sqlite {
|
||||
sealed class Schema {
|
||||
internal const int Version = 5;
|
||||
internal const int Version = 4;
|
||||
|
||||
private static readonly Log Log = Log.ForType<Schema>();
|
||||
|
||||
@ -79,9 +79,7 @@ namespace DHT.Server.Database.Sqlite {
|
||||
name TEXT NOT NULL,
|
||||
type TEXT,
|
||||
url TEXT NOT NULL,
|
||||
size INTEGER NOT NULL,
|
||||
width INTEGER,
|
||||
height INTEGER)");
|
||||
size INTEGER NOT NULL)");
|
||||
|
||||
Execute(@"CREATE TABLE embeds (
|
||||
message_id INTEGER NOT NULL,
|
||||
@ -161,12 +159,6 @@ namespace DHT.Server.Database.Sqlite {
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
@ -252,9 +252,7 @@ namespace DHT.Server.Database.Sqlite {
|
||||
("name", SqliteType.Text),
|
||||
("type", SqliteType.Text),
|
||||
("url", SqliteType.Text),
|
||||
("size", SqliteType.Integer),
|
||||
("width", SqliteType.Integer),
|
||||
("height", SqliteType.Integer)
|
||||
("size", SqliteType.Integer)
|
||||
});
|
||||
|
||||
using var embedCmd = conn.Insert("embeds", new[] {
|
||||
@ -309,8 +307,6 @@ namespace DHT.Server.Database.Sqlite {
|
||||
attachmentCmd.Set(":type", attachment.Type);
|
||||
attachmentCmd.Set(":url", attachment.Url);
|
||||
attachmentCmd.Set(":size", attachment.Size);
|
||||
attachmentCmd.Set(":width", attachment.Width);
|
||||
attachmentCmd.Set(":height", attachment.Height);
|
||||
attachmentCmd.ExecuteNonQuery();
|
||||
}
|
||||
}
|
||||
@ -474,28 +470,6 @@ 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) {
|
||||
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");
|
||||
@ -575,7 +549,7 @@ FROM downloads");
|
||||
var dict = new MultiDictionary<ulong, Attachment>();
|
||||
|
||||
using var conn = pool.Take();
|
||||
using var cmd = conn.Command("SELECT message_id, attachment_id, name, type, url, size, width, height FROM attachments");
|
||||
using var cmd = conn.Command("SELECT message_id, attachment_id, name, type, url, size FROM attachments");
|
||||
using var reader = cmd.ExecuteReader();
|
||||
|
||||
while (reader.Read()) {
|
||||
@ -586,9 +560,7 @@ FROM downloads");
|
||||
Name = reader.GetString(2),
|
||||
Type = reader.IsDBNull(3) ? null : reader.GetString(3),
|
||||
Url = reader.GetString(4),
|
||||
Size = reader.GetUint64(5),
|
||||
Width = reader.IsDBNull(6) ? null : reader.GetInt32(6),
|
||||
Height = reader.IsDBNull(7) ? null : reader.GetInt32(7)
|
||||
Size = reader.GetUint64(5)
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -45,21 +45,22 @@ namespace DHT.Server.Database.Sqlite.Utils {
|
||||
}
|
||||
|
||||
public ISqliteConnection Take() {
|
||||
while (true) {
|
||||
ThrowIfDisposed();
|
||||
PooledConnection? conn = null;
|
||||
|
||||
while (conn == null) {
|
||||
ThrowIfDisposed();
|
||||
lock (monitor) {
|
||||
if (free.TryTake(out var conn)) {
|
||||
if (free.TryTake(out conn, TimeSpan.FromMilliseconds(rand.Next(100, 200)))) {
|
||||
used.Add(conn);
|
||||
return conn;
|
||||
break;
|
||||
}
|
||||
else {
|
||||
Log.ForType<SqliteConnectionPool>().Warn("Thread " + Thread.CurrentThread.ManagedThreadId + " is starving for connections.");
|
||||
}
|
||||
}
|
||||
|
||||
Thread.Sleep(TimeSpan.FromMilliseconds(rand.Next(100, 200)));
|
||||
}
|
||||
|
||||
return conn;
|
||||
}
|
||||
|
||||
private void Return(PooledConnection conn) {
|
||||
|
@ -8,7 +8,6 @@ using DHT.Utils.Http;
|
||||
using DHT.Utils.Logging;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Http.Extensions;
|
||||
using Microsoft.Extensions.Primitives;
|
||||
|
||||
namespace DHT.Server.Endpoints {
|
||||
abstract class BaseEndpoint {
|
||||
@ -22,22 +21,26 @@ namespace DHT.Server.Endpoints {
|
||||
this.parameters = parameters;
|
||||
}
|
||||
|
||||
private async Task Handle(HttpContext ctx, StringValues token) {
|
||||
public async Task Handle(HttpContext ctx) {
|
||||
var request = ctx.Request;
|
||||
var response = ctx.Response;
|
||||
|
||||
Log.Info("Request: " + request.GetDisplayUrl() + " (" + request.ContentLength + " B)");
|
||||
|
||||
if (token.Count != 1 || token[0] != parameters.Token) {
|
||||
Log.Error("Token: " + (token.Count == 1 ? token[0] : "<missing>"));
|
||||
var requestToken = request.Headers["X-DHT-Token"];
|
||||
if (requestToken.Count != 1 || requestToken[0] != parameters.Token) {
|
||||
Log.Error("Token: " + (requestToken.Count == 1 ? requestToken[0] : "<missing>"));
|
||||
response.StatusCode = (int) HttpStatusCode.Forbidden;
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
response.StatusCode = (int) HttpStatusCode.OK;
|
||||
var output = await Respond(ctx);
|
||||
await output.WriteTo(response);
|
||||
var (statusCode, output) = await Respond(ctx);
|
||||
response.StatusCode = (int) statusCode;
|
||||
|
||||
if (output != null) {
|
||||
await response.WriteAsJsonAsync(output);
|
||||
}
|
||||
} catch (HttpException e) {
|
||||
Log.Error(e);
|
||||
response.StatusCode = (int) e.StatusCode;
|
||||
@ -48,15 +51,7 @@ namespace DHT.Server.Endpoints {
|
||||
}
|
||||
}
|
||||
|
||||
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 abstract Task<(HttpStatusCode, object?)> Respond(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.");
|
||||
|
@ -1,25 +0,0 @@
|
||||
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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -11,7 +11,7 @@ namespace DHT.Server.Endpoints {
|
||||
sealed class TrackChannelEndpoint : BaseEndpoint {
|
||||
public TrackChannelEndpoint(IDatabaseFile db, ServerParameters parameters) : base(db, parameters) {}
|
||||
|
||||
protected override async Task<IHttpOutput> Respond(HttpContext ctx) {
|
||||
protected override async Task<(HttpStatusCode, object?)> Respond(HttpContext ctx) {
|
||||
var root = await ReadJson(ctx);
|
||||
var server = ReadServer(root.RequireObject("server"), "server");
|
||||
var channel = ReadChannel(root.RequireObject("channel"), "channel", server.Id);
|
||||
@ -19,7 +19,7 @@ namespace DHT.Server.Endpoints {
|
||||
Db.AddServer(server);
|
||||
Db.AddChannel(channel);
|
||||
|
||||
return HttpOutput.None;
|
||||
return (HttpStatusCode.OK, null);
|
||||
}
|
||||
|
||||
private static Data.Server ReadServer(JsonElement json, string path) => new() {
|
||||
|
@ -17,7 +17,7 @@ namespace DHT.Server.Endpoints {
|
||||
sealed class TrackMessagesEndpoint : BaseEndpoint {
|
||||
public TrackMessagesEndpoint(IDatabaseFile db, ServerParameters parameters) : base(db, parameters) {}
|
||||
|
||||
protected override async Task<IHttpOutput> Respond(HttpContext ctx) {
|
||||
protected override async Task<(HttpStatusCode, object?)> Respond(HttpContext ctx) {
|
||||
var root = await ReadJson(ctx);
|
||||
|
||||
if (root.ValueKind != JsonValueKind.Array) {
|
||||
@ -39,7 +39,7 @@ namespace DHT.Server.Endpoints {
|
||||
|
||||
Db.AddMessages(messages);
|
||||
|
||||
return new HttpOutput.Json(anyNewMessages ? 1 : 0);
|
||||
return (HttpStatusCode.OK, anyNewMessages ? 1 : 0);
|
||||
}
|
||||
|
||||
private static Message ReadMessage(JsonElement json, string path) => new() {
|
||||
@ -61,9 +61,7 @@ namespace DHT.Server.Endpoints {
|
||||
Name = ele.RequireString("name", path),
|
||||
Type = ele.HasKey("type") ? ele.RequireString("type", path) : null,
|
||||
Url = ele.RequireString("url", 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
|
||||
Size = (ulong) ele.RequireLong("size", path)
|
||||
}).DistinctByKeyStable(static attachment => {
|
||||
// Some Discord messages have duplicate attachments with the same id for unknown reasons.
|
||||
return attachment.Id;
|
||||
|
@ -11,7 +11,7 @@ namespace DHT.Server.Endpoints {
|
||||
sealed class TrackUsersEndpoint : BaseEndpoint {
|
||||
public TrackUsersEndpoint(IDatabaseFile db, ServerParameters parameters) : base(db, parameters) {}
|
||||
|
||||
protected override async Task<IHttpOutput> Respond(HttpContext ctx) {
|
||||
protected override async Task<(HttpStatusCode, object?)> Respond(HttpContext ctx) {
|
||||
var root = await ReadJson(ctx);
|
||||
|
||||
if (root.ValueKind != JsonValueKind.Array) {
|
||||
@ -27,7 +27,7 @@ namespace DHT.Server.Endpoints {
|
||||
|
||||
Db.AddUsers(users);
|
||||
|
||||
return HttpOutput.None;
|
||||
return (HttpStatusCode.OK, null);
|
||||
}
|
||||
|
||||
private static User ReadUser(JsonElement json, string path) => new() {
|
||||
|
@ -20,7 +20,7 @@
|
||||
<FrameworkReference Include="Microsoft.AspNetCore.App" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Data.Sqlite" Version="6.0.7" />
|
||||
<PackageReference Include="Microsoft.Data.Sqlite" Version="6.0.5" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Utils\Utils.csproj" />
|
||||
|
@ -34,16 +34,13 @@ namespace DHT.Server.Service {
|
||||
app.UseCors();
|
||||
app.UseEndpoints(endpoints => {
|
||||
TrackChannelEndpoint trackChannel = new(db, parameters);
|
||||
endpoints.MapPost("/track-channel", async context => await trackChannel.HandlePost(context));
|
||||
endpoints.MapPost("/track-channel", async context => await trackChannel.Handle(context));
|
||||
|
||||
TrackUsersEndpoint trackUsers = new(db, parameters);
|
||||
endpoints.MapPost("/track-users", async context => await trackUsers.HandlePost(context));
|
||||
endpoints.MapPost("/track-users", async context => await trackUsers.Handle(context));
|
||||
|
||||
TrackMessagesEndpoint trackMessages = new(db, parameters);
|
||||
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));
|
||||
endpoints.MapPost("/track-messages", async context => await trackMessages.Handle(context));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -1,56 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,8 +0,0 @@
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
||||
namespace DHT.Utils.Http {
|
||||
public interface IHttpOutput {
|
||||
Task WriteTo(HttpResponse response);
|
||||
}
|
||||
}
|
@ -17,10 +17,7 @@
|
||||
<DebugType>none</DebugType>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<FrameworkReference Include="Microsoft.AspNetCore.App" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="JetBrains.Annotations" Version="2022.1.0" />
|
||||
<PackageReference Include="JetBrains.Annotations" Version="10.3.0" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Compile Include="..\Version.cs" Link="Version.cs" />
|
||||
|
BIN
app/empty.dht
BIN
app/empty.dht
Binary file not shown.
Loading…
Reference in New Issue
Block a user