mirror of
https://github.com/chylex/Discord-History-Tracker.git
synced 2024-11-25 05:42:45 +01:00
Compare commits
2 Commits
0d5e01532e
...
4b1afa7aa3
Author | SHA1 | Date | |
---|---|---|---|
4b1afa7aa3 | |||
78d2e88d17 |
@ -36,18 +36,69 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
gui.scrollMessagesToTop();
|
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() {
|
async function loadData() {
|
||||||
try {
|
try {
|
||||||
const response = await fetch("/get-viewer-data?token=" + encodeURIComponent(window.DHT_SERVER_TOKEN) + "&session=" + encodeURIComponent(window.DHT_SERVER_SESSION), {
|
const metadataResponse = await fetchUrl("get-viewer-metadata", "application/json");
|
||||||
method: "GET",
|
const metadataJson = await metadataResponse.json();
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
const messagesResponse = await fetchUrl("get-viewer-messages", "application/x-ndjson");
|
||||||
},
|
const messages = {};
|
||||||
credentials: "omit",
|
|
||||||
redirect: "error",
|
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;
|
||||||
});
|
});
|
||||||
|
|
||||||
state.uploadFile(await response.json());
|
state.uploadFile(metadataJson, messages);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
alert("Could not load data, see console for details.");
|
alert("Could not load data, see console for details.");
|
||||||
|
@ -6,8 +6,7 @@ export default (function() {
|
|||||||
/**
|
/**
|
||||||
* @type {{}}
|
* @type {{}}
|
||||||
* @property {{}} users
|
* @property {{}} users
|
||||||
* @property {String[]} userindex
|
* @property {{}} servers
|
||||||
* @property {{}[]} servers
|
|
||||||
* @property {{}} channels
|
* @property {{}} channels
|
||||||
*/
|
*/
|
||||||
let loadedFileMeta;
|
let loadedFileMeta;
|
||||||
@ -20,20 +19,16 @@ export default (function() {
|
|||||||
let currentPage;
|
let currentPage;
|
||||||
let messagesPerPage;
|
let messagesPerPage;
|
||||||
|
|
||||||
const getUser = function(index) {
|
const getUser = function(id) {
|
||||||
return loadedFileMeta.users[loadedFileMeta.userindex[index]] || { "name": "<unknown>" };
|
return loadedFileMeta.users[id] || { "name": "<unknown>" };
|
||||||
};
|
|
||||||
|
|
||||||
const getUserId = function(index) {
|
|
||||||
return loadedFileMeta.userindex[index];
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const getUserList = function() {
|
const getUserList = function() {
|
||||||
return loadedFileMeta ? loadedFileMeta.users : [];
|
return loadedFileMeta ? loadedFileMeta.users : [];
|
||||||
};
|
};
|
||||||
|
|
||||||
const getServer = function(index) {
|
const getServer = function(id) {
|
||||||
return loadedFileMeta.servers[index] || { "name": "<unknown>", "type": "unknown" };
|
return loadedFileMeta.servers[id] || { "name": "<unknown>", "type": "unknown" };
|
||||||
};
|
};
|
||||||
|
|
||||||
const generateChannelHierarchy = function() {
|
const generateChannelHierarchy = function() {
|
||||||
@ -207,7 +202,7 @@ export default (function() {
|
|||||||
*/
|
*/
|
||||||
const message = messages[key];
|
const message = messages[key];
|
||||||
const user = getUser(message.u);
|
const user = getUser(message.u);
|
||||||
const avatar = user.avatar ? { id: getUserId(message.u), path: user.avatar } : null;
|
const avatar = user.avatar ? { id: message.u, path: user.avatar } : null;
|
||||||
|
|
||||||
const obj = {
|
const obj = {
|
||||||
user,
|
user,
|
||||||
@ -235,7 +230,7 @@ export default (function() {
|
|||||||
if ("r" in message) {
|
if ("r" in message) {
|
||||||
const replyMessage = getMessageById(message.r);
|
const replyMessage = getMessageById(message.r);
|
||||||
const replyUser = replyMessage ? getUser(replyMessage.u) : null;
|
const replyUser = replyMessage ? getUser(replyMessage.u) : null;
|
||||||
const replyAvatar = replyUser && replyUser.avatar ? { id: getUserId(replyMessage.u), path: replyUser.avatar } : null;
|
const replyAvatar = replyUser && replyUser.avatar ? { id: replyMessage.u, path: replyUser.avatar } : null;
|
||||||
|
|
||||||
obj["reply"] = replyMessage ? {
|
obj["reply"] = replyMessage ? {
|
||||||
"id": message.r,
|
"id": message.r,
|
||||||
@ -293,20 +288,17 @@ export default (function() {
|
|||||||
eventOnUsersRefreshed = callback;
|
eventOnUsersRefreshed = callback;
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
uploadFile(meta, data) {
|
||||||
* @param {{ meta, data }} file
|
|
||||||
*/
|
|
||||||
uploadFile(file) {
|
|
||||||
if (loadedFileMeta != null) {
|
if (loadedFileMeta != null) {
|
||||||
throw "A file is already loaded!";
|
throw "A file is already loaded!";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!file || typeof file.meta !== "object" || typeof file.data !== "object") {
|
if (typeof meta !== "object" || typeof data !== "object") {
|
||||||
throw "Invalid file format!";
|
throw "Invalid file format!";
|
||||||
}
|
}
|
||||||
|
|
||||||
loadedFileMeta = file.meta;
|
loadedFileMeta = meta;
|
||||||
loadedFileData = file.data;
|
loadedFileData = data;
|
||||||
loadedMessages = null;
|
loadedMessages = null;
|
||||||
|
|
||||||
selectedChannel = null;
|
selectedChannel = null;
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
namespace DHT.Server.Database.Export;
|
namespace DHT.Server.Database.Export;
|
||||||
|
|
||||||
readonly record struct Snowflake(ulong Id);
|
readonly record struct Snowflake(ulong Id) {
|
||||||
|
public static implicit operator Snowflake(ulong id) => new (id);
|
||||||
|
}
|
||||||
|
@ -3,14 +3,10 @@ using System.Text.Json.Serialization;
|
|||||||
|
|
||||||
namespace DHT.Server.Database.Export;
|
namespace DHT.Server.Database.Export;
|
||||||
|
|
||||||
sealed class ViewerJson {
|
static class ViewerJson {
|
||||||
public required JsonMeta Meta { get; init; }
|
|
||||||
public required Dictionary<Snowflake, Dictionary<Snowflake, JsonMessage>> Data { get; init; }
|
|
||||||
|
|
||||||
public sealed class JsonMeta {
|
public sealed class JsonMeta {
|
||||||
public required Dictionary<Snowflake, JsonUser> Users { get; init; }
|
public required Dictionary<Snowflake, JsonUser> Users { get; init; }
|
||||||
public required List<Snowflake> Userindex { get; init; }
|
public required Dictionary<Snowflake, JsonServer> Servers { get; init; }
|
||||||
public required List<JsonServer> Servers { get; init; }
|
|
||||||
public required Dictionary<Snowflake, JsonChannel> Channels { get; init; }
|
public required Dictionary<Snowflake, JsonChannel> Channels { get; init; }
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -30,7 +26,7 @@ sealed class ViewerJson {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public sealed class JsonChannel {
|
public sealed class JsonChannel {
|
||||||
public required int Server { get; init; }
|
public required Snowflake Server { get; init; }
|
||||||
public required string Name { get; init; }
|
public required string Name { get; init; }
|
||||||
|
|
||||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||||
@ -47,7 +43,9 @@ sealed class ViewerJson {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public sealed class JsonMessage {
|
public sealed class JsonMessage {
|
||||||
public required int U { get; init; }
|
public required Snowflake Id { get; init; }
|
||||||
|
public required Snowflake C { get; init; }
|
||||||
|
public required Snowflake U { get; init; }
|
||||||
public required long T { get; init; }
|
public required long T { get; init; }
|
||||||
|
|
||||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||||
|
@ -2,6 +2,7 @@ using System;
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
@ -14,106 +15,90 @@ namespace DHT.Server.Database.Export;
|
|||||||
static class ViewerJsonExport {
|
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, CancellationToken cancellationToken = default) {
|
public static async Task GetMetadata(Stream stream, IDatabaseFile db, MessageFilter? filter = null, CancellationToken cancellationToken = default) {
|
||||||
var perf = Log.Start();
|
var perf = Log.Start();
|
||||||
|
|
||||||
var includedUserIds = new HashSet<ulong>();
|
var includedChannels = new List<Channel>();
|
||||||
var includedChannelIds = new HashSet<ulong>();
|
|
||||||
var includedServerIds = new HashSet<ulong>();
|
var includedServerIds = new HashSet<ulong>();
|
||||||
|
|
||||||
var includedMessages = await db.Messages.Get(filter, cancellationToken).ToListAsync(cancellationToken);
|
var channelIdFilter = filter?.ChannelIds;
|
||||||
var includedChannels = new List<Channel>();
|
|
||||||
|
|
||||||
foreach (var message in includedMessages) {
|
|
||||||
includedUserIds.Add(message.Sender);
|
|
||||||
includedChannelIds.Add(message.Channel);
|
|
||||||
}
|
|
||||||
|
|
||||||
await foreach (var channel in db.Channels.Get(cancellationToken)) {
|
await foreach (var channel in db.Channels.Get(cancellationToken)) {
|
||||||
if (includedChannelIds.Contains(channel.Id)) {
|
if (channelIdFilter == null || channelIdFilter.Contains(channel.Id)) {
|
||||||
includedChannels.Add(channel);
|
includedChannels.Add(channel);
|
||||||
includedServerIds.Add(channel.Server);
|
includedServerIds.Add(channel.Server);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var (users, userIndex, userIndices) = await GenerateUserList(db, includedUserIds);
|
var users = await GenerateUserList(db, cancellationToken);
|
||||||
var (servers, serverIndices) = await GenerateServerList(db, includedServerIds);
|
var servers = await GenerateServerList(db, includedServerIds, cancellationToken);
|
||||||
var channels = GenerateChannelList(includedChannels, serverIndices);
|
var channels = GenerateChannelList(includedChannels);
|
||||||
|
|
||||||
|
var meta = new ViewerJson.JsonMeta {
|
||||||
|
Users = users,
|
||||||
|
Servers = servers,
|
||||||
|
Channels = channels
|
||||||
|
};
|
||||||
|
|
||||||
perf.Step("Collect database data");
|
perf.Step("Collect database data");
|
||||||
|
|
||||||
var value = new ViewerJson {
|
await JsonSerializer.SerializeAsync(stream, meta, ViewerJsonMetadataContext.Default.JsonMeta, cancellationToken);
|
||||||
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, cancellationToken);
|
|
||||||
|
|
||||||
perf.Step("Serialize to JSON");
|
perf.Step("Serialize to JSON");
|
||||||
perf.End();
|
perf.End();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static async Task<(Dictionary<Snowflake, ViewerJson.JsonUser> Users, List<Snowflake> UserIndex, Dictionary<ulong, int> UserIndices)> GenerateUserList(IDatabaseFile db, HashSet<ulong> userIds) {
|
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) {
|
||||||
var users = new Dictionary<Snowflake, ViewerJson.JsonUser>();
|
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()) {
|
await foreach (var user in db.Users.Get(cancellationToken)) {
|
||||||
var id = user.Id;
|
users[user.Id] = new ViewerJson.JsonUser {
|
||||||
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,
|
Name = user.Name,
|
||||||
Avatar = user.AvatarUrl,
|
Avatar = user.AvatarUrl,
|
||||||
Tag = user.Discriminator
|
Tag = user.Discriminator
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return (users, userIndex, userIndices);
|
return users;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static async Task<(List<ViewerJson.JsonServer> Servers, Dictionary<ulong, int> ServerIndices)> GenerateServerList(IDatabaseFile db, HashSet<ulong> serverIds) {
|
private static async Task<Dictionary<Snowflake, ViewerJson.JsonServer>> GenerateServerList(IDatabaseFile db, HashSet<ulong> serverIds, CancellationToken cancellationToken) {
|
||||||
var servers = new List<ViewerJson.JsonServer>();
|
var servers = new Dictionary<Snowflake, ViewerJson.JsonServer>();
|
||||||
var serverIndices = new Dictionary<ulong, int>();
|
|
||||||
|
|
||||||
await foreach (var server in db.Servers.Get()) {
|
await foreach (var server in db.Servers.Get(cancellationToken)) {
|
||||||
var id = server.Id;
|
if (!serverIds.Contains(server.Id)) {
|
||||||
if (!serverIds.Contains(id)) {
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
serverIndices[id] = servers.Count;
|
servers[server.Id] = new ViewerJson.JsonServer {
|
||||||
|
|
||||||
servers.Add(new ViewerJson.JsonServer {
|
|
||||||
Name = server.Name,
|
Name = server.Name,
|
||||||
Type = ServerTypes.ToJsonViewerString(server.Type)
|
Type = ServerTypes.ToJsonViewerString(server.Type)
|
||||||
});
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return (servers, serverIndices);
|
return servers;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Dictionary<Snowflake, ViewerJson.JsonChannel> GenerateChannelList(List<Channel> includedChannels, Dictionary<ulong, int> serverIndices) {
|
private static Dictionary<Snowflake, ViewerJson.JsonChannel> GenerateChannelList(List<Channel> includedChannels) {
|
||||||
var channels = new Dictionary<Snowflake, ViewerJson.JsonChannel>();
|
var channels = new Dictionary<Snowflake, ViewerJson.JsonChannel>();
|
||||||
|
|
||||||
foreach (var channel in includedChannels) {
|
foreach (var channel in includedChannels) {
|
||||||
var channelIdSnowflake = new Snowflake(channel.Id);
|
channels[channel.Id] = new ViewerJson.JsonChannel {
|
||||||
|
Server = channel.Server,
|
||||||
channels[channelIdSnowflake] = new ViewerJson.JsonChannel {
|
|
||||||
Server = serverIndices[channel.Server],
|
|
||||||
Name = channel.Name,
|
Name = channel.Name,
|
||||||
Parent = channel.ParentId?.ToString(),
|
Parent = channel.ParentId?.ToString(),
|
||||||
Position = channel.Position,
|
Position = channel.Position,
|
||||||
@ -125,51 +110,40 @@ static class ViewerJsonExport {
|
|||||||
return channels;
|
return channels;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Dictionary<Snowflake, Dictionary<Snowflake, ViewerJson.JsonMessage>> GenerateMessageList(List<Message> includedMessages, Dictionary<ulong, int> userIndices) {
|
private static async IAsyncEnumerable<ViewerJson.JsonMessage> GenerateMessageList(IDatabaseFile db, MessageFilter? filter, [EnumeratorCancellation] CancellationToken cancellationToken) {
|
||||||
var data = new Dictionary<Snowflake, Dictionary<Snowflake, ViewerJson.JsonMessage>>();
|
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(),
|
||||||
|
|
||||||
foreach (var grouping in includedMessages.GroupBy(static message => message.Channel)) {
|
A = message.Attachments.IsEmpty ? null : message.Attachments.Select(static attachment => {
|
||||||
var channelIdSnowflake = new Snowflake(grouping.Key);
|
var a = new ViewerJson.JsonMessageAttachment {
|
||||||
var channelData = new Dictionary<Snowflake, ViewerJson.JsonMessage>();
|
Url = attachment.DownloadUrl,
|
||||||
|
Name = Uri.TryCreate(attachment.NormalizedUrl, UriKind.Absolute, out var uri) ? Path.GetFileName(uri.LocalPath) : attachment.NormalizedUrl
|
||||||
|
};
|
||||||
|
|
||||||
foreach (var message in grouping) {
|
if (attachment is { Width: not null, Height: not null }) {
|
||||||
var messageIdSnowflake = new Snowflake(message.Id);
|
a.Width = attachment.Width;
|
||||||
|
a.Height = attachment.Height;
|
||||||
|
}
|
||||||
|
|
||||||
channelData[messageIdSnowflake] = new ViewerJson.JsonMessage {
|
return a;
|
||||||
U = userIndices[message.Sender],
|
}).ToArray(),
|
||||||
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 => {
|
E = message.Embeds.IsEmpty ? null : message.Embeds.Select(static embed => embed.Json).ToArray(),
|
||||||
var a = new ViewerJson.JsonMessageAttachment {
|
|
||||||
Url = attachment.DownloadUrl,
|
|
||||||
Name = Uri.TryCreate(attachment.NormalizedUrl, UriKind.Absolute, out var uri) ? Path.GetFileName(uri.LocalPath) : attachment.NormalizedUrl
|
|
||||||
};
|
|
||||||
|
|
||||||
if (attachment is { Width: not null, Height: not null }) {
|
Re = message.Reactions.IsEmpty ? null : message.Reactions.Select(static reaction => new ViewerJson.JsonMessageReaction {
|
||||||
a.Width = attachment.Width;
|
Id = reaction.EmojiId?.ToString(),
|
||||||
a.Height = attachment.Height;
|
N = reaction.EmojiName,
|
||||||
}
|
A = reaction.EmojiFlags.HasFlag(EmojiFlags.Animated),
|
||||||
|
C = reaction.Count
|
||||||
return a;
|
}).ToArray()
|
||||||
}).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()
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
data[channelIdSnowflake] = channelData;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return data;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
11
app/Server/Database/Export/ViewerJsonMessageContext.cs
Normal file
11
app/Server/Database/Export/ViewerJsonMessageContext.cs
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
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;
|
@ -7,5 +7,5 @@ namespace DHT.Server.Database.Export;
|
|||||||
PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase,
|
PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase,
|
||||||
GenerationMode = JsonSourceGenerationMode.Default
|
GenerationMode = JsonSourceGenerationMode.Default
|
||||||
)]
|
)]
|
||||||
[JsonSerializable(typeof(ViewerJson))]
|
[JsonSerializable(typeof(ViewerJson.JsonMeta))]
|
||||||
sealed partial class ViewerJsonContext : JsonSerializerContext;
|
sealed partial class ViewerJsonMetadataContext : JsonSerializerContext;
|
@ -14,7 +14,7 @@ public interface IServerRepository {
|
|||||||
|
|
||||||
Task<long> Count(CancellationToken cancellationToken = default);
|
Task<long> Count(CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
IAsyncEnumerable<Data.Server> Get();
|
IAsyncEnumerable<Data.Server> Get(CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
internal sealed class Dummy : IServerRepository {
|
internal sealed class Dummy : IServerRepository {
|
||||||
public IObservable<long> TotalCount { get; } = Observable.Return(0L);
|
public IObservable<long> TotalCount { get; } = Observable.Return(0L);
|
||||||
@ -27,7 +27,7 @@ public interface IServerRepository {
|
|||||||
return Task.FromResult(0L);
|
return Task.FromResult(0L);
|
||||||
}
|
}
|
||||||
|
|
||||||
public IAsyncEnumerable<Data.Server> Get() {
|
public IAsyncEnumerable<Data.Server> Get(CancellationToken cancellationToken) {
|
||||||
return AsyncEnumerable.Empty<Data.Server>();
|
return AsyncEnumerable.Empty<Data.Server>();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -15,7 +15,7 @@ public interface IUserRepository {
|
|||||||
|
|
||||||
Task<long> Count(CancellationToken cancellationToken = default);
|
Task<long> Count(CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
IAsyncEnumerable<User> Get();
|
IAsyncEnumerable<User> Get(CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
internal sealed class Dummy : IUserRepository {
|
internal sealed class Dummy : IUserRepository {
|
||||||
public IObservable<long> TotalCount { get; } = Observable.Return(0L);
|
public IObservable<long> TotalCount { get; } = Observable.Return(0L);
|
||||||
@ -28,7 +28,7 @@ public interface IUserRepository {
|
|||||||
return Task.FromResult(0L);
|
return Task.FromResult(0L);
|
||||||
}
|
}
|
||||||
|
|
||||||
public IAsyncEnumerable<User> Get() {
|
public IAsyncEnumerable<User> Get(CancellationToken cancellationToken) {
|
||||||
return AsyncEnumerable.Empty<User>();
|
return AsyncEnumerable.Empty<User>();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using DHT.Server.Data;
|
using DHT.Server.Data;
|
||||||
@ -46,13 +47,13 @@ sealed class SqliteServerRepository : BaseSqliteRepository, IServerRepository {
|
|||||||
return await conn.ExecuteReaderAsync("SELECT COUNT(*) FROM servers", static reader => reader?.GetInt64(0) ?? 0L, cancellationToken);
|
return await conn.ExecuteReaderAsync("SELECT COUNT(*) FROM servers", static reader => reader?.GetInt64(0) ?? 0L, cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async IAsyncEnumerable<Data.Server> Get() {
|
public async IAsyncEnumerable<Data.Server> Get([EnumeratorCancellation] CancellationToken cancellationToken) {
|
||||||
await using var conn = await pool.Take();
|
await using var conn = await pool.Take();
|
||||||
|
|
||||||
await using var cmd = conn.Command("SELECT id, name, type FROM servers");
|
await using var cmd = conn.Command("SELECT id, name, type FROM servers");
|
||||||
await using var reader = await cmd.ExecuteReaderAsync();
|
await using var reader = await cmd.ExecuteReaderAsync(cancellationToken);
|
||||||
|
|
||||||
while (await reader.ReadAsync()) {
|
while (await reader.ReadAsync(cancellationToken)) {
|
||||||
yield return new Data.Server {
|
yield return new Data.Server {
|
||||||
Id = reader.GetUint64(0),
|
Id = reader.GetUint64(0),
|
||||||
Name = reader.GetString(1),
|
Name = reader.GetString(1),
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using DHT.Server.Data;
|
using DHT.Server.Data;
|
||||||
@ -58,13 +59,13 @@ sealed class SqliteUserRepository : BaseSqliteRepository, IUserRepository {
|
|||||||
return await conn.ExecuteReaderAsync("SELECT COUNT(*) FROM users", static reader => reader?.GetInt64(0) ?? 0L, cancellationToken);
|
return await conn.ExecuteReaderAsync("SELECT COUNT(*) FROM users", static reader => reader?.GetInt64(0) ?? 0L, cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async IAsyncEnumerable<User> Get() {
|
public async IAsyncEnumerable<User> Get([EnumeratorCancellation] CancellationToken cancellationToken) {
|
||||||
await using var conn = await pool.Take();
|
await using var conn = await pool.Take();
|
||||||
|
|
||||||
await using var cmd = conn.Command("SELECT id, name, avatar_url, discriminator FROM users");
|
await using var cmd = conn.Command("SELECT id, name, avatar_url, discriminator FROM users");
|
||||||
await using var reader = await cmd.ExecuteReaderAsync();
|
await using var reader = await cmd.ExecuteReaderAsync(cancellationToken);
|
||||||
|
|
||||||
while (await reader.ReadAsync()) {
|
while (await reader.ReadAsync(cancellationToken)) {
|
||||||
yield return new User {
|
yield return new User {
|
||||||
Id = reader.GetUint64(0),
|
Id = reader.GetUint64(0),
|
||||||
Name = reader.GetString(1),
|
Name = reader.GetString(1),
|
||||||
|
@ -47,4 +47,13 @@ abstract class BaseEndpoint(IDatabaseFile db) {
|
|||||||
throw new HttpException(HttpStatusCode.UnsupportedMediaType, "This endpoint only accepts JSON.");
|
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,25 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Net;
|
|
||||||
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 DHT.Utils.Http;
|
|
||||||
using Microsoft.AspNetCore.Http;
|
|
||||||
|
|
||||||
namespace DHT.Server.Endpoints;
|
|
||||||
|
|
||||||
sealed class GetViewerDataEndpoint(IDatabaseFile db, ViewerSessions viewerSessions) : BaseEndpoint(db) {
|
|
||||||
protected override Task Respond(HttpRequest request, HttpResponse response, CancellationToken cancellationToken) {
|
|
||||||
if (!request.Query.TryGetValue("session", out var sessionIdValue) || sessionIdValue.Count != 1 || !Guid.TryParse(sessionIdValue[0], out Guid sessionId)) {
|
|
||||||
throw new HttpException(HttpStatusCode.BadRequest, "Invalid session ID.");
|
|
||||||
}
|
|
||||||
|
|
||||||
response.ContentType = MediaTypeNames.Application.Json;
|
|
||||||
|
|
||||||
var session = viewerSessions.Get(sessionId);
|
|
||||||
return ViewerJsonExport.Generate(response.Body, Db, session.MessageFilter, cancellationToken);
|
|
||||||
}
|
|
||||||
}
|
|
18
app/Server/Endpoints/GetViewerMessagesEndpoint.cs
Normal file
18
app/Server/Endpoints/GetViewerMessagesEndpoint.cs
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
19
app/Server/Endpoints/GetViewerMetadataEndpoint.cs
Normal file
19
app/Server/Endpoints/GetViewerMetadataEndpoint.cs
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
@ -51,7 +51,8 @@ sealed class Startup {
|
|||||||
app.UseRouting();
|
app.UseRouting();
|
||||||
app.UseEndpoints(endpoints => {
|
app.UseEndpoints(endpoints => {
|
||||||
endpoints.MapGet("/get-tracking-script", new GetTrackingScriptEndpoint(db, parameters, resources).Handle);
|
endpoints.MapGet("/get-tracking-script", new GetTrackingScriptEndpoint(db, parameters, resources).Handle);
|
||||||
endpoints.MapGet("/get-viewer-data", new GetViewerDataEndpoint(db, viewerSessions).Handle);
|
endpoints.MapGet("/get-viewer-metadata", new GetViewerMetadataEndpoint(db, viewerSessions).Handle);
|
||||||
|
endpoints.MapGet("/get-viewer-messages", new GetViewerMessagesEndpoint(db, viewerSessions).Handle);
|
||||||
endpoints.MapGet("/get-downloaded-file/{url}", new GetDownloadedFileEndpoint(db).Handle);
|
endpoints.MapGet("/get-downloaded-file/{url}", new GetDownloadedFileEndpoint(db).Handle);
|
||||||
endpoints.MapPost("/track-channel", new TrackChannelEndpoint(db).Handle);
|
endpoints.MapPost("/track-channel", new TrackChannelEndpoint(db).Handle);
|
||||||
endpoints.MapPost("/track-users", new TrackUsersEndpoint(db).Handle);
|
endpoints.MapPost("/track-users", new TrackUsersEndpoint(db).Handle);
|
||||||
|
Loading…
Reference in New Issue
Block a user