mirror of
https://github.com/chylex/Discord-History-Tracker.git
synced 2025-09-16 13:24:47 +02:00
Compare commits
1 Commits
v42.0
...
ebdabb3dd2
Author | SHA1 | Date | |
---|---|---|---|
ebdabb3dd2
|
@@ -1,7 +1,9 @@
|
||||
using System;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
using Avalonia.Controls;
|
||||
using DHT.Desktop.Main.Pages;
|
||||
using DHT.Utils.Logging;
|
||||
using JetBrains.Annotations;
|
||||
|
||||
@@ -41,5 +43,13 @@ public sealed partial class MainWindow : Window {
|
||||
Log.Error("Caught exception while disposing window: " + ex);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var temporaryFile in ViewerPageModel.TemporaryFiles) {
|
||||
try {
|
||||
File.Delete(temporaryFile);
|
||||
} catch (Exception) {
|
||||
// ignored
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -1,22 +1,30 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.ComponentModel;
|
||||
using System.Diagnostics;
|
||||
using System.Threading.Tasks;
|
||||
using System.Web;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Platform.Storage;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using DHT.Desktop.Common;
|
||||
using DHT.Desktop.Dialogs.File;
|
||||
using DHT.Desktop.Dialogs.Message;
|
||||
using DHT.Desktop.Dialogs.Progress;
|
||||
using DHT.Desktop.Main.Controls;
|
||||
using DHT.Desktop.Server;
|
||||
using DHT.Server;
|
||||
using DHT.Server.Data.Filters;
|
||||
using DHT.Server.Service.Viewer;
|
||||
|
||||
namespace DHT.Desktop.Main.Pages;
|
||||
|
||||
sealed partial class ViewerPageModel : ObservableObject, IDisposable {
|
||||
public static readonly ConcurrentBag<string> TemporaryFiles = [];
|
||||
|
||||
private static readonly FilePickerFileType[] ViewerFileTypes = [
|
||||
FileDialogs.CreateFilter("Discord History Viewer", ["html"])
|
||||
];
|
||||
|
||||
public bool DatabaseToolFilterModeKeep { get; set; } = true;
|
||||
public bool DatabaseToolFilterModeRemove { get; set; } = false;
|
||||
|
||||
@@ -51,9 +59,8 @@ sealed partial class ViewerPageModel : ObservableObject, IDisposable {
|
||||
try {
|
||||
string serverUrl = "http://127.0.0.1:" + ServerConfiguration.Port;
|
||||
string serverToken = ServerConfiguration.Token;
|
||||
string sessionId = state.ViewerSessions.Register(new ViewerSession(FilterModel.CreateFilter())).ToString();
|
||||
|
||||
Process.Start(new ProcessStartInfo(serverUrl + "/viewer/?token=" + HttpUtility.UrlEncode(serverToken) + "&session=" + HttpUtility.UrlEncode(sessionId)) {
|
||||
Process.Start(new ProcessStartInfo(serverUrl + "/viewer/?token=" + HttpUtility.UrlEncode(serverToken)) {
|
||||
UseShellExecute = true
|
||||
});
|
||||
} catch (Exception e) {
|
||||
|
@@ -2,8 +2,6 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="referrer" content="no-referrer">
|
||||
|
||||
<title>Discord Offline History</title>
|
||||
|
||||
<link rel="icon" href="favicon.ico">
|
||||
@@ -15,9 +13,7 @@
|
||||
<link rel="stylesheet" href="styles/modal.css">
|
||||
|
||||
<script type="text/javascript">
|
||||
const query = new URLSearchParams(location.search);
|
||||
window.DHT_SERVER_TOKEN = query.get("token");
|
||||
window.DHT_SERVER_SESSION = query.get("session");
|
||||
window.DHT_SERVER_TOKEN = new URLSearchParams(location.search).get("token");
|
||||
</script>
|
||||
<script type="module" src="scripts/bootstrap.mjs"></script>
|
||||
</head>
|
||||
|
@@ -36,69 +36,19 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
gui.scrollMessagesToTop();
|
||||
});
|
||||
|
||||
async function fetchUrl(path, contentType) {
|
||||
const response = await fetch("/" + path + "?token=" + encodeURIComponent(window.DHT_SERVER_TOKEN) + "&session=" + encodeURIComponent(window.DHT_SERVER_SESSION), {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": contentType,
|
||||
},
|
||||
credentials: "omit",
|
||||
redirect: "error",
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw "Unexpected response status: " + response.statusText;
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
async function processLines(response, callback) {
|
||||
let body = "";
|
||||
|
||||
for await (const chunk of response.body.pipeThrough(new TextDecoderStream("utf-8"))) {
|
||||
body += chunk;
|
||||
|
||||
let startIndex = 0;
|
||||
|
||||
while (true) {
|
||||
const endIndex = body.indexOf("\n", startIndex);
|
||||
if (endIndex === -1) {
|
||||
break;
|
||||
}
|
||||
|
||||
callback(body.substring(startIndex, endIndex));
|
||||
startIndex = endIndex + 1;
|
||||
}
|
||||
|
||||
body = body.substring(startIndex);
|
||||
}
|
||||
|
||||
if (body !== "") {
|
||||
callback(body);
|
||||
}
|
||||
}
|
||||
|
||||
async function loadData() {
|
||||
try {
|
||||
const metadataResponse = await fetchUrl("get-viewer-metadata", "application/json");
|
||||
const metadataJson = await metadataResponse.json();
|
||||
|
||||
const messagesResponse = await fetchUrl("get-viewer-messages", "application/x-ndjson");
|
||||
const messages = {};
|
||||
|
||||
await processLines(messagesResponse, line => {
|
||||
const message = JSON.parse(line);
|
||||
const channel = message.c;
|
||||
|
||||
const channelMessages = messages[channel] || (messages[channel] = {});
|
||||
channelMessages[message.id] = message;
|
||||
|
||||
delete message.id;
|
||||
delete message.c;
|
||||
const response = await fetch("/get-viewer-data?token=" + encodeURIComponent(window.DHT_SERVER_TOKEN), {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
credentials: "omit",
|
||||
redirect: "error",
|
||||
});
|
||||
|
||||
state.uploadFile(metadataJson, messages);
|
||||
const json = await response.json();
|
||||
state.uploadFile(json);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
alert("Could not load data, see console for details.");
|
||||
|
@@ -246,10 +246,10 @@ export default (function() {
|
||||
return templateEmbedUnsupported.apply(embed);
|
||||
}
|
||||
else if ("image" in embed && embed.image.url) {
|
||||
return getImageEmbed(embed.url, embed.image);
|
||||
return getImageEmbed(fileUrlProcessor(embed.url), embed.image);
|
||||
}
|
||||
else if ("thumbnail" in embed && embed.thumbnail.url) {
|
||||
return getImageEmbed(embed.url, embed.thumbnail);
|
||||
return getImageEmbed(fileUrlProcessor(embed.url), embed.thumbnail);
|
||||
}
|
||||
else if ("title" in embed && "description" in embed) {
|
||||
return templateEmbedRich.apply(embed);
|
||||
|
@@ -5,7 +5,7 @@ import discord from "./discord.mjs";
|
||||
// ------------------------
|
||||
|
||||
const filter = {
|
||||
byUser: ((user) => message => message.u === user),
|
||||
byUser: ((userindex) => message => message.u === userindex),
|
||||
byTime: ((timeStart, timeEnd) => message => message.t >= timeStart && message.t <= timeEnd),
|
||||
byContents: ((substr) => message => ("m" in message ? message.m : "").indexOf(substr) !== -1),
|
||||
byRegex: ((regex) => message => regex.test("m" in message ? message.m : "")),
|
||||
|
@@ -6,7 +6,8 @@ export default (function() {
|
||||
/**
|
||||
* @type {{}}
|
||||
* @property {{}} users
|
||||
* @property {{}} servers
|
||||
* @property {String[]} userindex
|
||||
* @property {{}[]} servers
|
||||
* @property {{}} channels
|
||||
*/
|
||||
let loadedFileMeta;
|
||||
@@ -19,16 +20,20 @@ export default (function() {
|
||||
let currentPage;
|
||||
let messagesPerPage;
|
||||
|
||||
const getUser = function(id) {
|
||||
return loadedFileMeta.users[id] || { "name": "<unknown>" };
|
||||
const getUser = function(index) {
|
||||
return loadedFileMeta.users[loadedFileMeta.userindex[index]] || { "name": "<unknown>" };
|
||||
};
|
||||
|
||||
const getUserId = function(index) {
|
||||
return loadedFileMeta.userindex[index];
|
||||
};
|
||||
|
||||
const getUserList = function() {
|
||||
return loadedFileMeta ? loadedFileMeta.users : [];
|
||||
};
|
||||
|
||||
const getServer = function(id) {
|
||||
return loadedFileMeta.servers[id] || { "name": "<unknown>", "type": "unknown" };
|
||||
const getServer = function(index) {
|
||||
return loadedFileMeta.servers[index] || { "name": "<unknown>", "type": "unknown" };
|
||||
};
|
||||
|
||||
const generateChannelHierarchy = function() {
|
||||
@@ -202,7 +207,7 @@ export default (function() {
|
||||
*/
|
||||
const message = messages[key];
|
||||
const user = getUser(message.u);
|
||||
const avatar = user.avatar ? { id: message.u, path: user.avatar } : null;
|
||||
const avatar = user.avatar ? { id: getUserId(message.u), path: user.avatar } : null;
|
||||
|
||||
const obj = {
|
||||
user,
|
||||
@@ -230,7 +235,7 @@ export default (function() {
|
||||
if ("r" in message) {
|
||||
const replyMessage = getMessageById(message.r);
|
||||
const replyUser = replyMessage ? getUser(replyMessage.u) : null;
|
||||
const replyAvatar = replyUser && replyUser.avatar ? { id: replyMessage.u, path: replyUser.avatar } : null;
|
||||
const replyAvatar = replyUser && replyUser.avatar ? { id: getUserId(replyMessage.u), path: replyUser.avatar } : null;
|
||||
|
||||
obj["reply"] = replyMessage ? {
|
||||
"id": message.r,
|
||||
@@ -288,17 +293,20 @@ export default (function() {
|
||||
eventOnUsersRefreshed = callback;
|
||||
},
|
||||
|
||||
uploadFile(meta, data) {
|
||||
/**
|
||||
* @param {{ meta, data }} file
|
||||
*/
|
||||
uploadFile(file) {
|
||||
if (loadedFileMeta != null) {
|
||||
throw "A file is already loaded!";
|
||||
}
|
||||
|
||||
if (typeof meta !== "object" || typeof data !== "object") {
|
||||
if (!file || typeof file.meta !== "object" || typeof file.data !== "object") {
|
||||
throw "Invalid file format!";
|
||||
}
|
||||
|
||||
loadedFileMeta = meta;
|
||||
loadedFileData = data;
|
||||
loadedFileMeta = file.meta;
|
||||
loadedFileData = file.data;
|
||||
loadedMessages = null;
|
||||
|
||||
selectedChannel = null;
|
||||
@@ -411,7 +419,7 @@ export default (function() {
|
||||
setActiveFilter(filter) {
|
||||
switch (filter ? filter.type : "") {
|
||||
case "user":
|
||||
filterFunction = processor.FILTER.byUser(filter.value);
|
||||
filterFunction = processor.FILTER.byUser(loadedFileMeta.userindex.indexOf(filter.value));
|
||||
break;
|
||||
|
||||
case "contents":
|
||||
|
@@ -1,5 +1,3 @@
|
||||
namespace DHT.Server.Database.Export;
|
||||
|
||||
readonly record struct Snowflake(ulong Id) {
|
||||
public static implicit operator Snowflake(ulong id) => new (id);
|
||||
}
|
||||
readonly record struct Snowflake(ulong Id);
|
||||
|
@@ -3,10 +3,14 @@ using System.Text.Json.Serialization;
|
||||
|
||||
namespace DHT.Server.Database.Export;
|
||||
|
||||
static class ViewerJson {
|
||||
sealed class ViewerJson {
|
||||
public required JsonMeta Meta { get; init; }
|
||||
public required Dictionary<Snowflake, Dictionary<Snowflake, JsonMessage>> Data { get; init; }
|
||||
|
||||
public sealed class JsonMeta {
|
||||
public required Dictionary<Snowflake, JsonUser> Users { get; init; }
|
||||
public required Dictionary<Snowflake, JsonServer> Servers { get; init; }
|
||||
public required List<Snowflake> Userindex { get; init; }
|
||||
public required List<JsonServer> Servers { get; init; }
|
||||
public required Dictionary<Snowflake, JsonChannel> Channels { get; init; }
|
||||
}
|
||||
|
||||
@@ -26,7 +30,7 @@ static class ViewerJson {
|
||||
}
|
||||
|
||||
public sealed class JsonChannel {
|
||||
public required Snowflake Server { get; init; }
|
||||
public required int Server { get; init; }
|
||||
public required string Name { get; init; }
|
||||
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
@@ -43,9 +47,7 @@ static class ViewerJson {
|
||||
}
|
||||
|
||||
public sealed class JsonMessage {
|
||||
public required Snowflake Id { get; init; }
|
||||
public required Snowflake C { get; init; }
|
||||
public required Snowflake U { get; init; }
|
||||
public required int U { get; init; }
|
||||
public required long T { get; init; }
|
||||
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
|
@@ -7,5 +7,5 @@ namespace DHT.Server.Database.Export;
|
||||
PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase,
|
||||
GenerationMode = JsonSourceGenerationMode.Default
|
||||
)]
|
||||
[JsonSerializable(typeof(ViewerJson.JsonMeta))]
|
||||
sealed partial class ViewerJsonMetadataContext : JsonSerializerContext;
|
||||
[JsonSerializable(typeof(ViewerJson))]
|
||||
sealed partial class ViewerJsonContext : JsonSerializerContext;
|
@@ -2,9 +2,7 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using DHT.Server.Data;
|
||||
using DHT.Server.Data.Filters;
|
||||
@@ -15,90 +13,106 @@ namespace DHT.Server.Database.Export;
|
||||
static class ViewerJsonExport {
|
||||
private static readonly Log Log = Log.ForType(typeof(ViewerJsonExport));
|
||||
|
||||
public static async Task GetMetadata(Stream stream, IDatabaseFile db, MessageFilter? filter = null, CancellationToken cancellationToken = default) {
|
||||
public static async Task Generate(Stream stream, IDatabaseFile db, MessageFilter? filter = null) {
|
||||
var perf = Log.Start();
|
||||
|
||||
var includedChannels = new List<Channel>();
|
||||
var includedUserIds = new HashSet<ulong>();
|
||||
var includedChannelIds = new HashSet<ulong>();
|
||||
var includedServerIds = new HashSet<ulong>();
|
||||
|
||||
var channelIdFilter = filter?.ChannelIds;
|
||||
var includedMessages = await db.Messages.Get(filter).ToListAsync();
|
||||
var includedChannels = new List<Channel>();
|
||||
|
||||
await foreach (var channel in db.Channels.Get(cancellationToken)) {
|
||||
if (channelIdFilter == null || channelIdFilter.Contains(channel.Id)) {
|
||||
foreach (var message in includedMessages) {
|
||||
includedUserIds.Add(message.Sender);
|
||||
includedChannelIds.Add(message.Channel);
|
||||
}
|
||||
|
||||
await foreach (var channel in db.Channels.Get()) {
|
||||
if (includedChannelIds.Contains(channel.Id)) {
|
||||
includedChannels.Add(channel);
|
||||
includedServerIds.Add(channel.Server);
|
||||
}
|
||||
}
|
||||
|
||||
var users = await GenerateUserList(db, cancellationToken);
|
||||
var servers = await GenerateServerList(db, includedServerIds, cancellationToken);
|
||||
var channels = GenerateChannelList(includedChannels);
|
||||
|
||||
var meta = new ViewerJson.JsonMeta {
|
||||
Users = users,
|
||||
Servers = servers,
|
||||
Channels = channels
|
||||
};
|
||||
var (users, userIndex, userIndices) = await GenerateUserList(db, includedUserIds);
|
||||
var (servers, serverIndices) = await GenerateServerList(db, includedServerIds);
|
||||
var channels = GenerateChannelList(includedChannels, serverIndices);
|
||||
|
||||
perf.Step("Collect database data");
|
||||
|
||||
await JsonSerializer.SerializeAsync(stream, meta, ViewerJsonMetadataContext.Default.JsonMeta, cancellationToken);
|
||||
var value = new ViewerJson {
|
||||
Meta = new ViewerJson.JsonMeta {
|
||||
Users = users,
|
||||
Userindex = userIndex,
|
||||
Servers = servers,
|
||||
Channels = channels
|
||||
},
|
||||
Data = GenerateMessageList(includedMessages, userIndices)
|
||||
};
|
||||
|
||||
perf.Step("Generate value object");
|
||||
|
||||
await JsonSerializer.SerializeAsync(stream, value, ViewerJsonContext.Default.ViewerJson);
|
||||
|
||||
perf.Step("Serialize to JSON");
|
||||
perf.End();
|
||||
}
|
||||
|
||||
public static async Task GetMessages(Stream stream, IDatabaseFile db, MessageFilter? filter = null, CancellationToken cancellationToken = default) {
|
||||
var perf = Log.Start();
|
||||
|
||||
ReadOnlyMemory<byte> newLine = "\n"u8.ToArray();
|
||||
|
||||
await foreach(var message in GenerateMessageList(db, filter, cancellationToken)) {
|
||||
await JsonSerializer.SerializeAsync(stream, message, ViewerJsonMessageContext.Default.JsonMessage, cancellationToken);
|
||||
await stream.WriteAsync(newLine, cancellationToken);
|
||||
}
|
||||
|
||||
perf.Step("Generate and serialize messages to JSON");
|
||||
perf.End();
|
||||
}
|
||||
|
||||
private static async Task<Dictionary<Snowflake, ViewerJson.JsonUser>> GenerateUserList(IDatabaseFile db, CancellationToken cancellationToken) {
|
||||
private static async Task<(Dictionary<Snowflake, ViewerJson.JsonUser> Users, List<Snowflake> UserIndex, Dictionary<ulong, int> UserIndices)> GenerateUserList(IDatabaseFile db, HashSet<ulong> userIds) {
|
||||
var users = new Dictionary<Snowflake, ViewerJson.JsonUser>();
|
||||
var userIndex = new List<Snowflake>();
|
||||
var userIndices = new Dictionary<ulong, int>();
|
||||
|
||||
await foreach (var user in db.Users.Get(cancellationToken)) {
|
||||
users[user.Id] = new ViewerJson.JsonUser {
|
||||
await foreach (var user in db.Users.Get()) {
|
||||
var id = user.Id;
|
||||
if (!userIds.Contains(id)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
var idSnowflake = new Snowflake(id);
|
||||
userIndices[id] = users.Count;
|
||||
userIndex.Add(idSnowflake);
|
||||
|
||||
users[idSnowflake] = new ViewerJson.JsonUser {
|
||||
Name = user.Name,
|
||||
Avatar = user.AvatarUrl,
|
||||
Tag = user.Discriminator
|
||||
};
|
||||
}
|
||||
|
||||
return users;
|
||||
return (users, userIndex, userIndices);
|
||||
}
|
||||
|
||||
private static async Task<Dictionary<Snowflake, ViewerJson.JsonServer>> GenerateServerList(IDatabaseFile db, HashSet<ulong> serverIds, CancellationToken cancellationToken) {
|
||||
var servers = new Dictionary<Snowflake, ViewerJson.JsonServer>();
|
||||
private static async Task<(List<ViewerJson.JsonServer> Servers, Dictionary<ulong, int> ServerIndices)> GenerateServerList(IDatabaseFile db, HashSet<ulong> serverIds) {
|
||||
var servers = new List<ViewerJson.JsonServer>();
|
||||
var serverIndices = new Dictionary<ulong, int>();
|
||||
|
||||
await foreach (var server in db.Servers.Get(cancellationToken)) {
|
||||
if (!serverIds.Contains(server.Id)) {
|
||||
await foreach (var server in db.Servers.Get()) {
|
||||
var id = server.Id;
|
||||
if (!serverIds.Contains(id)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
servers[server.Id] = new ViewerJson.JsonServer {
|
||||
serverIndices[id] = servers.Count;
|
||||
|
||||
servers.Add(new ViewerJson.JsonServer {
|
||||
Name = server.Name,
|
||||
Type = ServerTypes.ToJsonViewerString(server.Type)
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
return servers;
|
||||
return (servers, serverIndices);
|
||||
}
|
||||
|
||||
private static Dictionary<Snowflake, ViewerJson.JsonChannel> GenerateChannelList(List<Channel> includedChannels) {
|
||||
private static Dictionary<Snowflake, ViewerJson.JsonChannel> GenerateChannelList(List<Channel> includedChannels, Dictionary<ulong, int> serverIndices) {
|
||||
var channels = new Dictionary<Snowflake, ViewerJson.JsonChannel>();
|
||||
|
||||
foreach (var channel in includedChannels) {
|
||||
channels[channel.Id] = new ViewerJson.JsonChannel {
|
||||
Server = channel.Server,
|
||||
var channelIdSnowflake = new Snowflake(channel.Id);
|
||||
|
||||
channels[channelIdSnowflake] = new ViewerJson.JsonChannel {
|
||||
Server = serverIndices[channel.Server],
|
||||
Name = channel.Name,
|
||||
Parent = channel.ParentId?.ToString(),
|
||||
Position = channel.Position,
|
||||
@@ -110,40 +124,51 @@ static class ViewerJsonExport {
|
||||
return channels;
|
||||
}
|
||||
|
||||
private static async IAsyncEnumerable<ViewerJson.JsonMessage> GenerateMessageList(IDatabaseFile db, MessageFilter? filter, [EnumeratorCancellation] CancellationToken cancellationToken) {
|
||||
await foreach (var message in db.Messages.Get(filter, cancellationToken)) {
|
||||
yield return new ViewerJson.JsonMessage {
|
||||
Id = message.Id,
|
||||
C = message.Channel,
|
||||
U = message.Sender,
|
||||
T = message.Timestamp,
|
||||
M = string.IsNullOrEmpty(message.Text) ? null : message.Text,
|
||||
Te = message.EditTimestamp,
|
||||
R = message.RepliedToId?.ToString(),
|
||||
private static Dictionary<Snowflake, Dictionary<Snowflake, ViewerJson.JsonMessage>> GenerateMessageList(List<Message> includedMessages, Dictionary<ulong, int> userIndices) {
|
||||
var data = new Dictionary<Snowflake, Dictionary<Snowflake, ViewerJson.JsonMessage>>();
|
||||
|
||||
A = message.Attachments.IsEmpty ? null : message.Attachments.Select(static attachment => {
|
||||
var a = new ViewerJson.JsonMessageAttachment {
|
||||
Url = attachment.DownloadUrl,
|
||||
Name = Uri.TryCreate(attachment.NormalizedUrl, UriKind.Absolute, out var uri) ? Path.GetFileName(uri.LocalPath) : attachment.NormalizedUrl
|
||||
};
|
||||
foreach (var grouping in includedMessages.GroupBy(static message => message.Channel)) {
|
||||
var channelIdSnowflake = new Snowflake(grouping.Key);
|
||||
var channelData = new Dictionary<Snowflake, ViewerJson.JsonMessage>();
|
||||
|
||||
if (attachment is { Width: not null, Height: not null }) {
|
||||
a.Width = attachment.Width;
|
||||
a.Height = attachment.Height;
|
||||
}
|
||||
foreach (var message in grouping) {
|
||||
var messageIdSnowflake = new Snowflake(message.Id);
|
||||
|
||||
channelData[messageIdSnowflake] = new ViewerJson.JsonMessage {
|
||||
U = userIndices[message.Sender],
|
||||
T = message.Timestamp,
|
||||
M = string.IsNullOrEmpty(message.Text) ? null : message.Text,
|
||||
Te = message.EditTimestamp,
|
||||
R = message.RepliedToId?.ToString(),
|
||||
|
||||
A = message.Attachments.IsEmpty ? null : message.Attachments.Select(static attachment => {
|
||||
var a = new ViewerJson.JsonMessageAttachment {
|
||||
Url = attachment.DownloadUrl,
|
||||
Name = Uri.TryCreate(attachment.NormalizedUrl, UriKind.Absolute, out var uri) ? Path.GetFileName(uri.LocalPath) : attachment.NormalizedUrl
|
||||
};
|
||||
|
||||
return a;
|
||||
}).ToArray(),
|
||||
if (attachment is { Width: not null, Height: not null }) {
|
||||
a.Width = attachment.Width;
|
||||
a.Height = attachment.Height;
|
||||
}
|
||||
|
||||
E = message.Embeds.IsEmpty ? null : message.Embeds.Select(static embed => embed.Json).ToArray(),
|
||||
return a;
|
||||
}).ToArray(),
|
||||
|
||||
E = message.Embeds.IsEmpty ? null : message.Embeds.Select(static embed => embed.Json).ToArray(),
|
||||
|
||||
Re = message.Reactions.IsEmpty ? null : message.Reactions.Select(static reaction => new ViewerJson.JsonMessageReaction {
|
||||
Id = reaction.EmojiId?.ToString(),
|
||||
N = reaction.EmojiName,
|
||||
A = reaction.EmojiFlags.HasFlag(EmojiFlags.Animated),
|
||||
C = reaction.Count
|
||||
}).ToArray()
|
||||
};
|
||||
}
|
||||
|
||||
Re = message.Reactions.IsEmpty ? null : message.Reactions.Select(static reaction => new ViewerJson.JsonMessageReaction {
|
||||
Id = reaction.EmojiId?.ToString(),
|
||||
N = reaction.EmojiName,
|
||||
A = reaction.EmojiFlags.HasFlag(EmojiFlags.Animated),
|
||||
C = reaction.Count
|
||||
}).ToArray()
|
||||
};
|
||||
data[channelIdSnowflake] = channelData;
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
@@ -1,11 +0,0 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace DHT.Server.Database.Export;
|
||||
|
||||
[JsonSourceGenerationOptions(
|
||||
Converters = [typeof(SnowflakeJsonSerializer)],
|
||||
PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase,
|
||||
GenerationMode = JsonSourceGenerationMode.Default
|
||||
)]
|
||||
[JsonSerializable(typeof(ViewerJson.JsonMessage))]
|
||||
sealed partial class ViewerJsonMessageContext : JsonSerializerContext;
|
@@ -15,7 +15,7 @@ public interface IChannelRepository {
|
||||
|
||||
Task<long> Count(CancellationToken cancellationToken = default);
|
||||
|
||||
IAsyncEnumerable<Channel> Get(CancellationToken cancellationToken = default);
|
||||
IAsyncEnumerable<Channel> Get();
|
||||
|
||||
internal sealed class Dummy : IChannelRepository {
|
||||
public IObservable<long> TotalCount { get; } = Observable.Return(0L);
|
||||
@@ -28,7 +28,7 @@ public interface IChannelRepository {
|
||||
return Task.FromResult(0L);
|
||||
}
|
||||
|
||||
public IAsyncEnumerable<Channel> Get(CancellationToken cancellationToken) {
|
||||
public IAsyncEnumerable<Channel> Get() {
|
||||
return AsyncEnumerable.Empty<Channel>();
|
||||
}
|
||||
}
|
||||
|
@@ -24,7 +24,7 @@ public interface IDownloadRepository {
|
||||
|
||||
Task<bool> GetDownloadData(string normalizedUrl, Func<Stream, Task> dataProcessor);
|
||||
|
||||
Task<bool> GetSuccessfulDownloadWithData(string normalizedUrl, Func<Data.Download, Stream, CancellationToken, Task> dataProcessor, CancellationToken cancellationToken = default);
|
||||
Task<bool> GetSuccessfulDownloadWithData(string normalizedUrl, Func<Data.Download, Stream, Task> dataProcessor);
|
||||
|
||||
IAsyncEnumerable<DownloadItem> PullPendingDownloadItems(int count, DownloadItemFilter filter, CancellationToken cancellationToken = default);
|
||||
|
||||
@@ -55,7 +55,7 @@ public interface IDownloadRepository {
|
||||
return Task.FromResult(false);
|
||||
}
|
||||
|
||||
public Task<bool> GetSuccessfulDownloadWithData(string normalizedUrl, Func<Data.Download, Stream, CancellationToken, Task> dataProcessor, CancellationToken cancellationToken) {
|
||||
public Task<bool> GetSuccessfulDownloadWithData(string normalizedUrl, Func<Data.Download, Stream, Task> dataProcessor) {
|
||||
return Task.FromResult(false);
|
||||
}
|
||||
|
||||
|
@@ -16,7 +16,7 @@ public interface IMessageRepository {
|
||||
|
||||
Task<long> Count(MessageFilter? filter = null, CancellationToken cancellationToken = default);
|
||||
|
||||
IAsyncEnumerable<Message> Get(MessageFilter? filter = null, CancellationToken cancellationToken = default);
|
||||
IAsyncEnumerable<Message> Get(MessageFilter? filter = null);
|
||||
|
||||
IAsyncEnumerable<ulong> GetIds(MessageFilter? filter = null);
|
||||
|
||||
@@ -33,7 +33,7 @@ public interface IMessageRepository {
|
||||
return Task.FromResult(0L);
|
||||
}
|
||||
|
||||
public IAsyncEnumerable<Message> Get(MessageFilter? filter, CancellationToken cancellationToken) {
|
||||
public IAsyncEnumerable<Message> Get(MessageFilter? filter) {
|
||||
return AsyncEnumerable.Empty<Message>();
|
||||
}
|
||||
|
||||
|
@@ -14,7 +14,7 @@ public interface IServerRepository {
|
||||
|
||||
Task<long> Count(CancellationToken cancellationToken = default);
|
||||
|
||||
IAsyncEnumerable<Data.Server> Get(CancellationToken cancellationToken = default);
|
||||
IAsyncEnumerable<Data.Server> Get();
|
||||
|
||||
internal sealed class Dummy : IServerRepository {
|
||||
public IObservable<long> TotalCount { get; } = Observable.Return(0L);
|
||||
@@ -27,7 +27,7 @@ public interface IServerRepository {
|
||||
return Task.FromResult(0L);
|
||||
}
|
||||
|
||||
public IAsyncEnumerable<Data.Server> Get(CancellationToken cancellationToken) {
|
||||
public IAsyncEnumerable<Data.Server> Get() {
|
||||
return AsyncEnumerable.Empty<Data.Server>();
|
||||
}
|
||||
}
|
||||
|
@@ -15,7 +15,7 @@ public interface IUserRepository {
|
||||
|
||||
Task<long> Count(CancellationToken cancellationToken = default);
|
||||
|
||||
IAsyncEnumerable<User> Get(CancellationToken cancellationToken = default);
|
||||
IAsyncEnumerable<User> Get();
|
||||
|
||||
internal sealed class Dummy : IUserRepository {
|
||||
public IObservable<long> TotalCount { get; } = Observable.Return(0L);
|
||||
@@ -28,7 +28,7 @@ public interface IUserRepository {
|
||||
return Task.FromResult(0L);
|
||||
}
|
||||
|
||||
public IAsyncEnumerable<User> Get(CancellationToken cancellationToken) {
|
||||
public IAsyncEnumerable<User> Get() {
|
||||
return AsyncEnumerable.Empty<User>();
|
||||
}
|
||||
}
|
||||
|
@@ -1,5 +1,4 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using DHT.Server.Data;
|
||||
@@ -55,13 +54,13 @@ sealed class SqliteChannelRepository : BaseSqliteRepository, IChannelRepository
|
||||
return await conn.ExecuteReaderAsync("SELECT COUNT(*) FROM channels", static reader => reader?.GetInt64(0) ?? 0L, cancellationToken);
|
||||
}
|
||||
|
||||
public async IAsyncEnumerable<Channel> Get([EnumeratorCancellation] CancellationToken cancellationToken) {
|
||||
public async IAsyncEnumerable<Channel> Get() {
|
||||
await using var conn = await pool.Take();
|
||||
|
||||
await using var cmd = conn.Command("SELECT id, server, name, parent_id, position, topic, nsfw FROM channels");
|
||||
await using var reader = await cmd.ExecuteReaderAsync(cancellationToken);
|
||||
await using var reader = await cmd.ExecuteReaderAsync();
|
||||
|
||||
while (await reader.ReadAsync(cancellationToken)) {
|
||||
while (await reader.ReadAsync()) {
|
||||
yield return new Channel {
|
||||
Id = reader.GetUint64(0),
|
||||
Server = reader.GetUint64(1),
|
||||
|
@@ -210,7 +210,7 @@ sealed class SqliteDownloadRepository(SqliteConnectionPool pool) : BaseSqliteRep
|
||||
return true;
|
||||
}
|
||||
|
||||
public async Task<bool> GetSuccessfulDownloadWithData(string normalizedUrl, Func<Data.Download, Stream, CancellationToken, Task> dataProcessor, CancellationToken cancellationToken) {
|
||||
public async Task<bool> GetSuccessfulDownloadWithData(string normalizedUrl, Func<Data.Download, Stream, Task> dataProcessor) {
|
||||
await using var conn = await pool.Take();
|
||||
|
||||
await using var cmd = conn.Command(
|
||||
@@ -228,8 +228,8 @@ sealed class SqliteDownloadRepository(SqliteConnectionPool pool) : BaseSqliteRep
|
||||
string? type;
|
||||
long rowid;
|
||||
|
||||
await using (var reader = await cmd.ExecuteReaderAsync(cancellationToken)) {
|
||||
if (!await reader.ReadAsync(cancellationToken)) {
|
||||
await using (var reader = await cmd.ExecuteReaderAsync()) {
|
||||
if (!await reader.ReadAsync()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -239,7 +239,7 @@ sealed class SqliteDownloadRepository(SqliteConnectionPool pool) : BaseSqliteRep
|
||||
}
|
||||
|
||||
await using (var blob = new SqliteBlob(conn.InnerConnection, "download_blobs", "blob", rowid, readOnly: true)) {
|
||||
await dataProcessor(new Data.Download(normalizedUrl, downloadUrl, DownloadStatus.Success, type, (ulong) blob.Length), blob, cancellationToken);
|
||||
await dataProcessor(new Data.Download(normalizedUrl, downloadUrl, DownloadStatus.Success, type, (ulong) blob.Length), blob);
|
||||
}
|
||||
|
||||
return true;
|
||||
|
@@ -1,7 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using DHT.Server.Data;
|
||||
@@ -214,7 +213,7 @@ sealed class SqliteMessageRepository : BaseSqliteRepository, IMessageRepository
|
||||
}
|
||||
}
|
||||
|
||||
public async IAsyncEnumerable<Message> Get(MessageFilter? filter, [EnumeratorCancellation] CancellationToken cancellationToken) {
|
||||
public async IAsyncEnumerable<Message> Get(MessageFilter? filter) {
|
||||
await using var conn = await pool.Take();
|
||||
|
||||
const string AttachmentSql =
|
||||
@@ -270,9 +269,9 @@ sealed class SqliteMessageRepository : BaseSqliteRepository, IMessageRepository
|
||||
"""
|
||||
);
|
||||
|
||||
await using var reader = await messageCmd.ExecuteReaderAsync(cancellationToken);
|
||||
await using var reader = await messageCmd.ExecuteReaderAsync();
|
||||
|
||||
while (await reader.ReadAsync(cancellationToken)) {
|
||||
while (await reader.ReadAsync()) {
|
||||
ulong messageId = reader.GetUint64(0);
|
||||
|
||||
yield return new Message {
|
||||
|
@@ -1,5 +1,4 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using DHT.Server.Data;
|
||||
@@ -47,13 +46,13 @@ sealed class SqliteServerRepository : BaseSqliteRepository, IServerRepository {
|
||||
return await conn.ExecuteReaderAsync("SELECT COUNT(*) FROM servers", static reader => reader?.GetInt64(0) ?? 0L, cancellationToken);
|
||||
}
|
||||
|
||||
public async IAsyncEnumerable<Data.Server> Get([EnumeratorCancellation] CancellationToken cancellationToken) {
|
||||
public async IAsyncEnumerable<Data.Server> Get() {
|
||||
await using var conn = await pool.Take();
|
||||
|
||||
await using var cmd = conn.Command("SELECT id, name, type FROM servers");
|
||||
await using var reader = await cmd.ExecuteReaderAsync(cancellationToken);
|
||||
await using var reader = await cmd.ExecuteReaderAsync();
|
||||
|
||||
while (await reader.ReadAsync(cancellationToken)) {
|
||||
while (await reader.ReadAsync()) {
|
||||
yield return new Data.Server {
|
||||
Id = reader.GetUint64(0),
|
||||
Name = reader.GetString(1),
|
||||
|
@@ -1,5 +1,4 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using DHT.Server.Data;
|
||||
@@ -59,13 +58,13 @@ sealed class SqliteUserRepository : BaseSqliteRepository, IUserRepository {
|
||||
return await conn.ExecuteReaderAsync("SELECT COUNT(*) FROM users", static reader => reader?.GetInt64(0) ?? 0L, cancellationToken);
|
||||
}
|
||||
|
||||
public async IAsyncEnumerable<User> Get([EnumeratorCancellation] CancellationToken cancellationToken) {
|
||||
public async IAsyncEnumerable<User> Get() {
|
||||
await using var conn = await pool.Take();
|
||||
|
||||
await using var cmd = conn.Command("SELECT id, name, avatar_url, discriminator FROM users");
|
||||
await using var reader = await cmd.ExecuteReaderAsync(cancellationToken);
|
||||
await using var reader = await cmd.ExecuteReaderAsync();
|
||||
|
||||
while (await reader.ReadAsync(cancellationToken)) {
|
||||
while (await reader.ReadAsync()) {
|
||||
yield return new User {
|
||||
Id = reader.GetUint64(0),
|
||||
Name = reader.GetString(1),
|
||||
|
@@ -1,7 +1,6 @@
|
||||
using System;
|
||||
using System.Net;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using DHT.Server.Database;
|
||||
using DHT.Utils.Http;
|
||||
@@ -20,9 +19,7 @@ abstract class BaseEndpoint(IDatabaseFile db) {
|
||||
|
||||
try {
|
||||
response.StatusCode = (int) HttpStatusCode.OK;
|
||||
await Respond(ctx.Request, response, ctx.RequestAborted);
|
||||
} catch (OperationCanceledException) {
|
||||
throw;
|
||||
await Respond(ctx.Request, response);
|
||||
} catch (HttpException e) {
|
||||
Log.Error(e);
|
||||
response.StatusCode = (int) e.StatusCode;
|
||||
@@ -38,7 +35,7 @@ abstract class BaseEndpoint(IDatabaseFile db) {
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract Task Respond(HttpRequest request, HttpResponse response, CancellationToken cancellationToken);
|
||||
protected abstract Task Respond(HttpRequest request, HttpResponse response);
|
||||
|
||||
protected static async Task<JsonElement> ReadJson(HttpRequest request) {
|
||||
try {
|
||||
@@ -47,13 +44,4 @@ abstract class BaseEndpoint(IDatabaseFile db) {
|
||||
throw new HttpException(HttpStatusCode.UnsupportedMediaType, "This endpoint only accepts JSON.");
|
||||
}
|
||||
}
|
||||
|
||||
protected static Guid GetSessionId(HttpRequest request) {
|
||||
if (request.Query.TryGetValue("session", out var sessionIdValue) && sessionIdValue.Count == 1 && Guid.TryParse(sessionIdValue[0], out Guid sessionId)) {
|
||||
return sessionId;
|
||||
}
|
||||
else {
|
||||
throw new HttpException(HttpStatusCode.BadRequest, "Invalid session ID.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -1,7 +1,4 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Net;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using DHT.Server.Database;
|
||||
using DHT.Server.Download;
|
||||
@@ -11,16 +8,12 @@ using Microsoft.AspNetCore.Http;
|
||||
namespace DHT.Server.Endpoints;
|
||||
|
||||
sealed class GetDownloadedFileEndpoint(IDatabaseFile db) : BaseEndpoint(db) {
|
||||
protected override async Task Respond(HttpRequest request, HttpResponse response, CancellationToken cancellationToken) {
|
||||
protected override async Task Respond(HttpRequest request, HttpResponse response) {
|
||||
string url = WebUtility.UrlDecode((string) request.RouteValues["url"]!);
|
||||
string normalizedUrl = DiscordCdn.NormalizeUrl(url);
|
||||
|
||||
if (!await Db.Downloads.GetSuccessfulDownloadWithData(normalizedUrl, WriteDataTo(response), cancellationToken)) {
|
||||
if (!await Db.Downloads.GetSuccessfulDownloadWithData(normalizedUrl, (download, stream) => response.WriteStreamAsync(download.Type, download.Size, stream))) {
|
||||
response.Redirect(url, permanent: false);
|
||||
}
|
||||
}
|
||||
|
||||
private static Func<Data.Download, Stream, CancellationToken, Task> WriteDataTo(HttpResponse response) {
|
||||
return (download, stream, cancellationToken) => response.WriteStreamAsync(download.Type, download.Size, stream, cancellationToken);
|
||||
}
|
||||
}
|
||||
|
@@ -1,5 +1,4 @@
|
||||
using System.Net.Mime;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Web;
|
||||
using DHT.Server.Database;
|
||||
@@ -11,7 +10,7 @@ using Microsoft.AspNetCore.Http;
|
||||
namespace DHT.Server.Endpoints;
|
||||
|
||||
sealed class GetTrackingScriptEndpoint(IDatabaseFile db, ServerParameters parameters, ResourceLoader resources) : BaseEndpoint(db) {
|
||||
protected override async Task Respond(HttpRequest request, HttpResponse response, CancellationToken cancellationToken) {
|
||||
protected override async Task Respond(HttpRequest request, HttpResponse response) {
|
||||
string bootstrap = await resources.ReadTextAsync("Tracker/bootstrap.js");
|
||||
string script = bootstrap.Replace("= 0; /*[PORT]*/", "= " + parameters.Port + ";")
|
||||
.Replace("/*[TOKEN]*/", HttpUtility.JavaScriptStringEncode(parameters.Token))
|
||||
@@ -21,6 +20,6 @@ sealed class GetTrackingScriptEndpoint(IDatabaseFile db, ServerParameters parame
|
||||
.Replace("/*[DEBUGGER]*/", request.Query.ContainsKey("debug") ? "debugger;" : "");
|
||||
|
||||
response.Headers.Append("X-DHT", "1");
|
||||
await response.WriteTextAsync(MediaTypeNames.Text.JavaScript, script, cancellationToken);
|
||||
await response.WriteTextAsync(MediaTypeNames.Text.JavaScript, script);
|
||||
}
|
||||
}
|
||||
|
@@ -1,18 +0,0 @@
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using DHT.Server.Database;
|
||||
using DHT.Server.Database.Export;
|
||||
using DHT.Server.Service.Viewer;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
||||
namespace DHT.Server.Endpoints;
|
||||
|
||||
sealed class GetViewerMessagesEndpoint(IDatabaseFile db, ViewerSessions viewerSessions) : BaseEndpoint(db) {
|
||||
protected override Task Respond(HttpRequest request, HttpResponse response, CancellationToken cancellationToken) {
|
||||
var sessionId = GetSessionId(request);
|
||||
var session = viewerSessions.Get(sessionId);
|
||||
|
||||
response.ContentType = "application/x-ndjson";
|
||||
return ViewerJsonExport.GetMessages(response.Body, Db, session.MessageFilter, cancellationToken);
|
||||
}
|
||||
}
|
@@ -1,19 +0,0 @@
|
||||
using System.Net.Mime;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using DHT.Server.Database;
|
||||
using DHT.Server.Database.Export;
|
||||
using DHT.Server.Service.Viewer;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
||||
namespace DHT.Server.Endpoints;
|
||||
|
||||
sealed class GetViewerMetadataEndpoint(IDatabaseFile db, ViewerSessions viewerSessions) : BaseEndpoint(db) {
|
||||
protected override Task Respond(HttpRequest request, HttpResponse response, CancellationToken cancellationToken) {
|
||||
var sessionId = GetSessionId(request);
|
||||
var session = viewerSessions.Get(sessionId);
|
||||
|
||||
response.ContentType = MediaTypeNames.Application.Json;
|
||||
return ViewerJsonExport.GetMetadata(response.Body, Db, session.MessageFilter, cancellationToken);
|
||||
}
|
||||
}
|
@@ -1,6 +1,5 @@
|
||||
using System.Net;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using DHT.Server.Data;
|
||||
using DHT.Server.Database;
|
||||
@@ -10,7 +9,7 @@ using Microsoft.AspNetCore.Http;
|
||||
namespace DHT.Server.Endpoints;
|
||||
|
||||
sealed class TrackChannelEndpoint(IDatabaseFile db) : BaseEndpoint(db) {
|
||||
protected override async Task Respond(HttpRequest request, HttpResponse response, CancellationToken cancellationToken) {
|
||||
protected override async Task Respond(HttpRequest request, HttpResponse response) {
|
||||
var root = await ReadJson(request);
|
||||
var server = ReadServer(root.RequireObject("server"), "server");
|
||||
var channel = ReadChannel(root.RequireObject("channel"), "channel", server.Id);
|
||||
|
@@ -4,7 +4,6 @@ using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using DHT.Server.Data;
|
||||
using DHT.Server.Data.Filters;
|
||||
@@ -20,7 +19,7 @@ sealed class TrackMessagesEndpoint(IDatabaseFile db) : BaseEndpoint(db) {
|
||||
private const string HasNewMessages = "1";
|
||||
private const string NoNewMessages = "0";
|
||||
|
||||
protected override async Task Respond(HttpRequest request, HttpResponse response, CancellationToken cancellationToken) {
|
||||
protected override async Task Respond(HttpRequest request, HttpResponse response) {
|
||||
var root = await ReadJson(request);
|
||||
|
||||
if (root.ValueKind != JsonValueKind.Array) {
|
||||
@@ -38,11 +37,11 @@ sealed class TrackMessagesEndpoint(IDatabaseFile db) : BaseEndpoint(db) {
|
||||
}
|
||||
|
||||
var addedMessageFilter = new MessageFilter { MessageIds = addedMessageIds };
|
||||
bool anyNewMessages = await Db.Messages.Count(addedMessageFilter, CancellationToken.None) < addedMessageIds.Count;
|
||||
bool anyNewMessages = await Db.Messages.Count(addedMessageFilter) < addedMessageIds.Count;
|
||||
|
||||
await Db.Messages.Add(messages);
|
||||
|
||||
await response.WriteTextAsync(anyNewMessages ? HasNewMessages : NoNewMessages, cancellationToken);
|
||||
await response.WriteTextAsync(anyNewMessages ? HasNewMessages : NoNewMessages);
|
||||
}
|
||||
|
||||
private static Message ReadMessage(JsonElement json, string path) => new () {
|
||||
|
@@ -1,6 +1,5 @@
|
||||
using System.Net;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using DHT.Server.Data;
|
||||
using DHT.Server.Database;
|
||||
@@ -10,7 +9,7 @@ using Microsoft.AspNetCore.Http;
|
||||
namespace DHT.Server.Endpoints;
|
||||
|
||||
sealed class TrackUsersEndpoint(IDatabaseFile db) : BaseEndpoint(db) {
|
||||
protected override async Task Respond(HttpRequest request, HttpResponse response, CancellationToken cancellationToken) {
|
||||
protected override async Task Respond(HttpRequest request, HttpResponse response) {
|
||||
var root = await ReadJson(request);
|
||||
|
||||
if (root.ValueKind != JsonValueKind.Array) {
|
||||
|
@@ -16,13 +16,13 @@ sealed class ViewerEndpoint(IDatabaseFile db, ResourceLoader resources) : BaseEn
|
||||
private readonly Dictionary<string, byte[]?> cache = new ();
|
||||
private readonly SemaphoreSlim cacheSemaphore = new (1);
|
||||
|
||||
protected override async Task Respond(HttpRequest request, HttpResponse response, CancellationToken cancellationToken) {
|
||||
protected override async Task Respond(HttpRequest request, HttpResponse response) {
|
||||
string path = (string?) request.RouteValues["path"] ?? "index.html";
|
||||
string resourcePath = "Viewer/" + path;
|
||||
|
||||
byte[]? resourceBytes;
|
||||
|
||||
await cacheSemaphore.WaitAsync(cancellationToken);
|
||||
await cacheSemaphore.WaitAsync();
|
||||
try {
|
||||
if (!cache.TryGetValue(resourcePath, out resourceBytes)) {
|
||||
cache[resourcePath] = resourceBytes = await resources.ReadBytesAsyncIfExists(resourcePath);
|
||||
@@ -36,7 +36,7 @@ sealed class ViewerEndpoint(IDatabaseFile db, ResourceLoader resources) : BaseEn
|
||||
}
|
||||
else {
|
||||
var contentType = ContentTypeProvider.TryGetContentType(path, out string? type) ? type : null;
|
||||
await response.WriteFileAsync(contentType, resourceBytes, cancellationToken);
|
||||
await response.WriteFileAsync(contentType, resourceBytes);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -1,4 +1,3 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Threading.Tasks;
|
||||
using DHT.Utils.Logging;
|
||||
@@ -7,34 +6,24 @@ using Microsoft.AspNetCore.Http.Extensions;
|
||||
|
||||
namespace DHT.Server.Service.Middlewares;
|
||||
|
||||
sealed class ServerLoggingMiddleware(RequestDelegate next) {
|
||||
sealed class ServerLoggingMiddleware {
|
||||
private static readonly Log Log = Log.ForType<ServerLoggingMiddleware>();
|
||||
|
||||
private readonly RequestDelegate next;
|
||||
|
||||
public ServerLoggingMiddleware(RequestDelegate next) {
|
||||
this.next = next;
|
||||
}
|
||||
|
||||
public async Task InvokeAsync(HttpContext context) {
|
||||
var stopwatch = Stopwatch.StartNew();
|
||||
try {
|
||||
await next(context);
|
||||
} catch (OperationCanceledException) {
|
||||
OnFinished(stopwatch, context);
|
||||
throw;
|
||||
}
|
||||
|
||||
OnFinished(stopwatch, context);
|
||||
}
|
||||
|
||||
private static void OnFinished(Stopwatch stopwatch, HttpContext context) {
|
||||
await next(context);
|
||||
stopwatch.Stop();
|
||||
|
||||
|
||||
var request = context.Request;
|
||||
var requestLength = request.ContentLength ?? 0L;
|
||||
var responseStatus = context.Response.StatusCode;
|
||||
var elapsedMs = stopwatch.ElapsedMilliseconds;
|
||||
|
||||
if (context.RequestAborted.IsCancellationRequested) {
|
||||
Log.Debug("Request to " + request.GetEncodedPathAndQuery() + " (" + requestLength + " B) was cancelled after " + elapsedMs + " ms");
|
||||
}
|
||||
else {
|
||||
var responseStatus = context.Response.StatusCode;
|
||||
Log.Debug("Request to " + request.GetEncodedPathAndQuery() + " (" + requestLength + " B) returned " + responseStatus + ", took " + elapsedMs + " ms");
|
||||
}
|
||||
Log.Debug("Request to " + request.GetEncodedPathAndQuery() + " (" + requestLength + " B) returned " + responseStatus + ", took " + elapsedMs + " ms");
|
||||
}
|
||||
}
|
||||
|
@@ -3,7 +3,6 @@ using System.Reflection;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using DHT.Server.Database;
|
||||
using DHT.Server.Service.Viewer;
|
||||
using DHT.Utils.Logging;
|
||||
using DHT.Utils.Resources;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
@@ -28,12 +27,10 @@ public sealed class ServerManager {
|
||||
}
|
||||
|
||||
private readonly IDatabaseFile db;
|
||||
private readonly ViewerSessions viewerSessions;
|
||||
private readonly SemaphoreSlim semaphore = new (1, 1);
|
||||
|
||||
internal ServerManager(IDatabaseFile db, ViewerSessions viewerSessions) {
|
||||
internal ServerManager(IDatabaseFile db) {
|
||||
this.db = db;
|
||||
this.viewerSessions = viewerSessions;
|
||||
}
|
||||
|
||||
public async Task Start(ushort port, string token) {
|
||||
@@ -63,7 +60,6 @@ public sealed class ServerManager {
|
||||
services.AddSingleton(typeof(IDatabaseFile), db);
|
||||
services.AddSingleton(typeof(ServerParameters), new ServerParameters(port, token));
|
||||
services.AddSingleton(typeof(ResourceLoader), new ResourceLoader(Assembly.GetExecutingAssembly()));
|
||||
services.AddSingleton(typeof(ViewerSessions), viewerSessions);
|
||||
}
|
||||
|
||||
void SetKestrelOptions(KestrelServerOptions options) {
|
||||
|
@@ -3,7 +3,6 @@ using System.Text.Json.Serialization;
|
||||
using DHT.Server.Database;
|
||||
using DHT.Server.Endpoints;
|
||||
using DHT.Server.Service.Middlewares;
|
||||
using DHT.Server.Service.Viewer;
|
||||
using DHT.Utils.Resources;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Http.Json;
|
||||
@@ -35,7 +34,7 @@ sealed class Startup {
|
||||
}
|
||||
|
||||
[SuppressMessage("ReSharper", "UnusedMember.Global")]
|
||||
public void Configure(IApplicationBuilder app, IHostApplicationLifetime lifetime, IDatabaseFile db, ServerParameters parameters, ResourceLoader resources, ViewerSessions viewerSessions) {
|
||||
public void Configure(IApplicationBuilder app, IHostApplicationLifetime lifetime, IDatabaseFile db, ServerParameters parameters, ResourceLoader resources) {
|
||||
app.UseMiddleware<ServerLoggingMiddleware>();
|
||||
app.UseCors();
|
||||
|
||||
@@ -51,8 +50,7 @@ sealed class Startup {
|
||||
app.UseRouting();
|
||||
app.UseEndpoints(endpoints => {
|
||||
endpoints.MapGet("/get-tracking-script", new GetTrackingScriptEndpoint(db, parameters, resources).Handle);
|
||||
endpoints.MapGet("/get-viewer-metadata", new GetViewerMetadataEndpoint(db, viewerSessions).Handle);
|
||||
endpoints.MapGet("/get-viewer-messages", new GetViewerMessagesEndpoint(db, viewerSessions).Handle);
|
||||
endpoints.MapGet("/get-viewer-data", new GetViewerDataEndpoint(db).Handle);
|
||||
endpoints.MapGet("/get-downloaded-file/{url}", new GetDownloadedFileEndpoint(db).Handle);
|
||||
endpoints.MapPost("/track-channel", new TrackChannelEndpoint(db).Handle);
|
||||
endpoints.MapPost("/track-users", new TrackUsersEndpoint(db).Handle);
|
||||
|
@@ -1,5 +0,0 @@
|
||||
using DHT.Server.Data.Filters;
|
||||
|
||||
namespace DHT.Server.Service.Viewer;
|
||||
|
||||
public readonly record struct ViewerSession(MessageFilter? MessageFilter);
|
@@ -1,35 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace DHT.Server.Service.Viewer;
|
||||
|
||||
public sealed class ViewerSessions : IDisposable {
|
||||
private readonly Dictionary<Guid, ViewerSession> sessions = new ();
|
||||
private bool isDisposed = false;
|
||||
|
||||
public Guid Register(ViewerSession session) {
|
||||
Guid guid = Guid.NewGuid();
|
||||
|
||||
lock (this) {
|
||||
ObjectDisposedException.ThrowIf(isDisposed, this);
|
||||
sessions[guid] = session;
|
||||
}
|
||||
|
||||
return guid;
|
||||
}
|
||||
|
||||
internal ViewerSession Get(Guid guid) {
|
||||
lock (this) {
|
||||
return sessions.GetValueOrDefault(guid);
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose() {
|
||||
lock (this) {
|
||||
if (!isDisposed) {
|
||||
isDisposed = true;
|
||||
sessions.Clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -3,29 +3,19 @@ using System.Threading.Tasks;
|
||||
using DHT.Server.Database;
|
||||
using DHT.Server.Download;
|
||||
using DHT.Server.Service;
|
||||
using DHT.Server.Service.Viewer;
|
||||
|
||||
namespace DHT.Server;
|
||||
|
||||
public sealed class State : IAsyncDisposable {
|
||||
public sealed class State(IDatabaseFile db, int? concurrentDownloads) : IAsyncDisposable {
|
||||
public static State Dummy { get; } = new (DummyDatabaseFile.Instance, null);
|
||||
|
||||
public IDatabaseFile Db { get; }
|
||||
public Downloader Downloader { get; }
|
||||
public ViewerSessions ViewerSessions { get; }
|
||||
public ServerManager Server { get; }
|
||||
|
||||
public State(IDatabaseFile db, int? concurrentDownloads) {
|
||||
Db = db;
|
||||
Downloader = new Downloader(db, concurrentDownloads);
|
||||
ViewerSessions = new ViewerSessions();
|
||||
Server = new ServerManager(db, ViewerSessions);
|
||||
}
|
||||
|
||||
public IDatabaseFile Db { get; } = db;
|
||||
public Downloader Downloader { get; } = new (db, concurrentDownloads);
|
||||
public ServerManager Server { get; } = new (db);
|
||||
|
||||
public async ValueTask DisposeAsync() {
|
||||
await Downloader.Stop();
|
||||
await Server.Stop();
|
||||
await Db.DisposeAsync();
|
||||
ViewerSessions.Dispose();
|
||||
}
|
||||
}
|
||||
|
@@ -1,34 +1,33 @@
|
||||
using System.IO;
|
||||
using System.Net.Mime;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
||||
namespace DHT.Utils.Http;
|
||||
|
||||
public static class HttpExtensions {
|
||||
public static Task WriteTextAsync(this HttpResponse response, string text, CancellationToken cancellationToken) {
|
||||
return WriteTextAsync(response, MediaTypeNames.Text.Plain, text, cancellationToken);
|
||||
public static Task WriteTextAsync(this HttpResponse response, string text) {
|
||||
return WriteTextAsync(response, MediaTypeNames.Text.Plain, text);
|
||||
}
|
||||
|
||||
public static async Task WriteTextAsync(this HttpResponse response, string contentType, string text, CancellationToken cancellationToken) {
|
||||
public static async Task WriteTextAsync(this HttpResponse response, string contentType, string text) {
|
||||
response.ContentType = contentType;
|
||||
await response.StartAsync(cancellationToken);
|
||||
await response.WriteAsync(text, Encoding.UTF8, cancellationToken);
|
||||
await response.StartAsync();
|
||||
await response.WriteAsync(text, Encoding.UTF8);
|
||||
}
|
||||
|
||||
public static async Task WriteFileAsync(this HttpResponse response, string? contentType, byte[] bytes, CancellationToken cancellationToken) {
|
||||
public static async Task WriteFileAsync(this HttpResponse response, string? contentType, byte[] bytes) {
|
||||
response.ContentType = contentType ?? string.Empty;
|
||||
response.ContentLength = bytes.Length;
|
||||
await response.StartAsync(cancellationToken);
|
||||
await response.Body.WriteAsync(bytes, cancellationToken);
|
||||
await response.StartAsync();
|
||||
await response.Body.WriteAsync(bytes);
|
||||
}
|
||||
|
||||
public static async Task WriteStreamAsync(this HttpResponse response, string? contentType, ulong? contentLength, Stream source, CancellationToken cancellationToken) {
|
||||
public static async Task WriteStreamAsync(this HttpResponse response, string? contentType, ulong? contentLength, Stream source) {
|
||||
response.ContentType = contentType ?? string.Empty;
|
||||
response.ContentLength = (long?) contentLength;
|
||||
await response.StartAsync(cancellationToken);
|
||||
await source.CopyToAsync(response.Body, cancellationToken);
|
||||
await response.StartAsync();
|
||||
await source.CopyToAsync(response.Body);
|
||||
}
|
||||
}
|
||||
|
@@ -8,5 +8,5 @@ using DHT.Utils;
|
||||
namespace DHT.Utils;
|
||||
|
||||
static class Version {
|
||||
public const string Tag = "42.0.0.0";
|
||||
public const string Tag = "41.2.0.0";
|
||||
}
|
||||
|
Reference in New Issue
Block a user