mirror of
https://github.com/chylex/Discord-History-Tracker.git
synced 2025-04-16 16:32:18 +03:00
WIP
This commit is contained in:
parent
3d9d6a454a
commit
b660af4be0
app
Desktop/Main/Pages
Resources/Viewer
Server
Utils/Http
@ -65,6 +65,8 @@ sealed class ViewerPageModel : BaseModel, IDisposable {
|
||||
string indexFile = await Resources.ReadTextAsync("Viewer/index.html");
|
||||
string viewerTemplate = indexFile.Replace("/*[JS]*/", await Resources.ReadJoinedAsync("Viewer/scripts/", '\n'))
|
||||
.Replace("/*[CSS]*/", await Resources.ReadJoinedAsync("Viewer/styles/", '\n'));
|
||||
|
||||
viewerTemplate = strategy.ProcessViewerTemplate(viewerTemplate);
|
||||
|
||||
int viewerArchiveTagStart = viewerTemplate.IndexOf(ArchiveTag);
|
||||
int viewerArchiveTagEnd = viewerArchiveTagStart + ArchiveTag.Length;
|
||||
|
@ -6,6 +6,8 @@
|
||||
|
||||
<script type="text/javascript">
|
||||
window.DHT_EMBEDDED = "/*[ARCHIVE]*/";
|
||||
window.DHT_SERVER_URL = "/*[SERVER_URL]*/";
|
||||
window.DHT_SERVER_TOKEN = "/*[SERVER_TOKEN]*/";
|
||||
/*[JS]*/
|
||||
</script>
|
||||
<style>
|
||||
|
@ -182,15 +182,32 @@ const STATE = (function() {
|
||||
return null;
|
||||
};
|
||||
|
||||
const getMessageList = function() {
|
||||
const getMessageList = async function(abortSignal) {
|
||||
if (!loadedMessages) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const messages = getMessages(selectedChannel);
|
||||
const startIndex = messagesPerPage * (root.getCurrentPage() - 1);
|
||||
const slicedMessages = loadedMessages.slice(startIndex, !messagesPerPage ? undefined : startIndex + messagesPerPage);
|
||||
|
||||
return loadedMessages.slice(startIndex, !messagesPerPage ? undefined : startIndex + messagesPerPage).map(key => {
|
||||
let messageTexts = null;
|
||||
|
||||
if (window.DHT_SERVER_URL !== null) {
|
||||
const messageIds = new Set(slicedMessages);
|
||||
|
||||
for (const key of slicedMessages) {
|
||||
const message = messages[key];
|
||||
|
||||
if ("r" in message) {
|
||||
messageIds.add(message.r);
|
||||
}
|
||||
}
|
||||
|
||||
messageTexts = await getMessageTextsFromServer(messageIds, abortSignal);
|
||||
}
|
||||
|
||||
return slicedMessages.map(key => {
|
||||
/**
|
||||
* @type {{}}
|
||||
* @property {Number} u
|
||||
@ -216,6 +233,9 @@ const STATE = (function() {
|
||||
if ("m" in message) {
|
||||
obj["contents"] = message.m;
|
||||
}
|
||||
else if (messageTexts && key in messageTexts) {
|
||||
obj["contents"] = messageTexts[key];
|
||||
}
|
||||
|
||||
if ("e" in message) {
|
||||
obj["embeds"] = message.e.map(embed => JSON.parse(embed));
|
||||
@ -230,15 +250,16 @@ const STATE = (function() {
|
||||
}
|
||||
|
||||
if ("r" in message) {
|
||||
const replyMessage = getMessageById(message.r);
|
||||
const replyId = message.r;
|
||||
const replyMessage = getMessageById(replyId);
|
||||
const replyUser = replyMessage ? getUser(replyMessage.u) : null;
|
||||
const replyAvatar = replyUser && replyUser.avatar ? { id: getUserId(replyMessage.u), path: replyUser.avatar } : null;
|
||||
|
||||
obj["reply"] = replyMessage ? {
|
||||
"id": message.r,
|
||||
"id": replyId,
|
||||
"user": replyUser,
|
||||
"avatar": replyAvatar,
|
||||
"contents": replyMessage.m
|
||||
"contents": messageTexts != null && replyId in messageTexts ? messageTexts[replyId] : replyMessage.m,
|
||||
} : null;
|
||||
}
|
||||
|
||||
@ -250,9 +271,35 @@ const STATE = (function() {
|
||||
});
|
||||
};
|
||||
|
||||
const getMessageTextsFromServer = async function(messageIds, abortSignal) {
|
||||
let idParams = "";
|
||||
|
||||
for (const messageId of messageIds) {
|
||||
idParams += "id=" + encodeURIComponent(messageId) + "&";
|
||||
}
|
||||
|
||||
const response = await fetch(DHT_SERVER_URL + "/get-messages?" + idParams + "token=" + encodeURIComponent(DHT_SERVER_TOKEN), {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
credentials: "omit",
|
||||
redirect: "error",
|
||||
signal: abortSignal
|
||||
});
|
||||
|
||||
if (response.status === 200) {
|
||||
return response.json();
|
||||
}
|
||||
else {
|
||||
throw new Error("Server returned status " + response.status + " " + response.statusText);
|
||||
}
|
||||
};
|
||||
|
||||
let eventOnUsersRefreshed;
|
||||
let eventOnChannelsRefreshed;
|
||||
let eventOnMessagesRefreshed;
|
||||
let messageLoaderAborter = null;
|
||||
|
||||
const triggerUsersRefreshed = function() {
|
||||
eventOnUsersRefreshed && eventOnUsersRefreshed(getUserList());
|
||||
@ -263,7 +310,22 @@ const STATE = (function() {
|
||||
};
|
||||
|
||||
const triggerMessagesRefreshed = function() {
|
||||
eventOnMessagesRefreshed && eventOnMessagesRefreshed(getMessageList());
|
||||
if (!eventOnMessagesRefreshed) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (messageLoaderAborter != null) {
|
||||
messageLoaderAborter.abort();
|
||||
}
|
||||
|
||||
const aborter = new AbortController();
|
||||
messageLoaderAborter = aborter;
|
||||
|
||||
getMessageList(aborter.signal).then(eventOnMessagesRefreshed).finally(() => {
|
||||
if (messageLoaderAborter === aborter) {
|
||||
messageLoaderAborter = null;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const getFilteredMessageKeys = function(channel) {
|
||||
|
@ -44,7 +44,7 @@ public sealed class DummyDatabaseFile : IDatabaseFile {
|
||||
return 0;
|
||||
}
|
||||
|
||||
public List<Message> GetMessages(MessageFilter? filter = null) {
|
||||
public List<Message> GetMessages(MessageFilter? filter = null, bool includeText = true) {
|
||||
return new();
|
||||
}
|
||||
|
||||
|
@ -3,5 +3,7 @@ using DHT.Server.Data;
|
||||
namespace DHT.Server.Database.Export.Strategy;
|
||||
|
||||
public interface IViewerExportStrategy {
|
||||
bool IncludeMessageText { get; }
|
||||
string ProcessViewerTemplate(string template);
|
||||
string GetAttachmentUrl(Attachment attachment);
|
||||
}
|
||||
|
@ -12,6 +12,13 @@ public sealed class LiveViewerExportStrategy : IViewerExportStrategy {
|
||||
this.safeToken = WebUtility.UrlEncode(token);
|
||||
}
|
||||
|
||||
public bool IncludeMessageText => false;
|
||||
|
||||
public string ProcessViewerTemplate(string template) {
|
||||
return template.Replace("/*[SERVER_URL]*/", "http://127.0.0.1:" + safePort)
|
||||
.Replace("/*[SERVER_TOKEN]*/", WebUtility.UrlEncode(safeToken));
|
||||
}
|
||||
|
||||
public string GetAttachmentUrl(Attachment attachment) {
|
||||
return "http://127.0.0.1:" + safePort + "/get-attachment/" + WebUtility.UrlEncode(attachment.NormalizedUrl) + "?token=" + safeToken;
|
||||
}
|
||||
|
@ -7,6 +7,13 @@ public sealed class StandaloneViewerExportStrategy : IViewerExportStrategy {
|
||||
|
||||
private StandaloneViewerExportStrategy() {}
|
||||
|
||||
public bool IncludeMessageText => true;
|
||||
|
||||
public string ProcessViewerTemplate(string template) {
|
||||
return template.Replace("\"/*[SERVER_URL]*/\"", "null")
|
||||
.Replace("\"/*[SERVER_TOKEN]*/\"", "null");
|
||||
}
|
||||
|
||||
public string GetAttachmentUrl(Attachment attachment) {
|
||||
// The normalized URL will not load files from Discord CDN once the time limit is enforced.
|
||||
|
||||
|
@ -21,7 +21,7 @@ public static class ViewerJsonExport {
|
||||
var includedChannelIds = new HashSet<ulong>();
|
||||
var includedServerIds = new HashSet<ulong>();
|
||||
|
||||
var includedMessages = db.GetMessages(filter);
|
||||
var includedMessages = db.GetMessages(filter, strategy.IncludeMessageText);
|
||||
var includedChannels = new List<Channel>();
|
||||
|
||||
foreach (var message in includedMessages) {
|
||||
|
@ -23,7 +23,7 @@ public interface IDatabaseFile : IDisposable {
|
||||
|
||||
void AddMessages(Message[] messages);
|
||||
int CountMessages(MessageFilter? filter = null);
|
||||
List<Message> GetMessages(MessageFilter? filter = null);
|
||||
List<Message> GetMessages(MessageFilter? filter = null, bool includeText = true);
|
||||
HashSet<ulong> GetMessageIds(MessageFilter? filter = null);
|
||||
void RemoveMessages(MessageFilter filter, FilterRemovalMode mode);
|
||||
|
||||
|
@ -360,7 +360,7 @@ public sealed class SqliteDatabaseFile : IDatabaseFile {
|
||||
return reader.Read() ? reader.GetInt32(0) : 0;
|
||||
}
|
||||
|
||||
public List<Message> GetMessages(MessageFilter? filter = null) {
|
||||
public List<Message> GetMessages(MessageFilter? filter = null, bool includeText = true) {
|
||||
var perf = log.Start();
|
||||
var list = new List<Message>();
|
||||
|
||||
@ -370,7 +370,7 @@ public sealed class SqliteDatabaseFile : IDatabaseFile {
|
||||
|
||||
using var conn = pool.Take();
|
||||
using var cmd = conn.Command($"""
|
||||
SELECT m.message_id, m.sender_id, m.channel_id, m.text, m.timestamp, et.edit_timestamp, rt.replied_to_id
|
||||
SELECT m.message_id, m.sender_id, m.channel_id, {(includeText ? "m.text" : "NULL")}, m.timestamp, et.edit_timestamp, rt.replied_to_id
|
||||
FROM messages m
|
||||
LEFT JOIN edit_timestamps et ON m.message_id = et.message_id
|
||||
LEFT JOIN replied_to rt ON m.message_id = rt.message_id
|
||||
@ -385,7 +385,7 @@ public sealed class SqliteDatabaseFile : IDatabaseFile {
|
||||
Id = id,
|
||||
Sender = reader.GetUint64(1),
|
||||
Channel = reader.GetUint64(2),
|
||||
Text = reader.GetString(3),
|
||||
Text = includeText ? reader.GetString(3) : string.Empty,
|
||||
Timestamp = reader.GetInt64(4),
|
||||
EditTimestamp = reader.IsDBNull(5) ? null : reader.GetInt64(5),
|
||||
RepliedToId = reader.IsDBNull(6) ? null : reader.GetUint64(6),
|
||||
|
34
app/Server/Endpoints/GetMessagesEndpoint.cs
Normal file
34
app/Server/Endpoints/GetMessagesEndpoint.cs
Normal file
@ -0,0 +1,34 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Threading.Tasks;
|
||||
using DHT.Server.Data.Filters;
|
||||
using DHT.Server.Database;
|
||||
using DHT.Utils.Http;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using GetMessagesJsonContext = DHT.Server.Endpoints.Responses.GetMessagesJsonContext;
|
||||
|
||||
namespace DHT.Server.Endpoints;
|
||||
|
||||
sealed class GetMessagesEndpoint : BaseEndpoint {
|
||||
public GetMessagesEndpoint(IDatabaseFile db) : base(db) {}
|
||||
|
||||
protected override Task<IHttpOutput> Respond(HttpContext ctx) {
|
||||
HashSet<ulong> messageIdSet;
|
||||
try {
|
||||
var messageIds = ctx.Request.Query["id"];
|
||||
messageIdSet = messageIds.Select(ulong.Parse!).ToHashSet();
|
||||
} catch (Exception) {
|
||||
throw new HttpException(HttpStatusCode.BadRequest, "Invalid message ids.");
|
||||
}
|
||||
|
||||
var messageFilter = new MessageFilter {
|
||||
MessageIds = messageIdSet
|
||||
};
|
||||
|
||||
var messages = Db.GetMessages(messageFilter).ToDictionary(static message => message.Id, static message => message.Text);
|
||||
var response = new HttpOutput.Json<Dictionary<ulong, string>>(messages, GetMessagesJsonContext.Default.DictionaryUInt64String);
|
||||
return Task.FromResult<IHttpOutput>(response);
|
||||
}
|
||||
}
|
8
app/Server/Endpoints/Responses/GetMessagesJsonContext.cs
Normal file
8
app/Server/Endpoints/Responses/GetMessagesJsonContext.cs
Normal file
@ -0,0 +1,8 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace DHT.Server.Endpoints.Responses;
|
||||
|
||||
[JsonSourceGenerationOptions(PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase, GenerationMode = JsonSourceGenerationMode.Default)]
|
||||
[JsonSerializable(typeof(Dictionary<ulong, string>))]
|
||||
sealed partial class GetMessagesJsonContext : JsonSerializerContext {}
|
@ -16,6 +16,7 @@ sealed class Startup {
|
||||
"https://ptb.discord.com",
|
||||
"https://canary.discord.com",
|
||||
"https://discordapp.com",
|
||||
"null" // For file:// protocol in the Viewer
|
||||
};
|
||||
|
||||
public void ConfigureServices(IServiceCollection services) {
|
||||
@ -41,6 +42,7 @@ sealed class Startup {
|
||||
|
||||
app.UseEndpoints(endpoints => {
|
||||
endpoints.MapGet("/get-tracking-script", new GetTrackingScriptEndpoint(db, parameters).Handle);
|
||||
endpoints.MapGet("/get-messages", new GetMessagesEndpoint(db).Handle);
|
||||
endpoints.MapGet("/get-attachment/{url}", new GetAttachmentEndpoint(db).Handle);
|
||||
endpoints.MapPost("/track-channel", new TrackChannelEndpoint(db).Handle);
|
||||
endpoints.MapPost("/track-users", new TrackUsersEndpoint(db).Handle);
|
||||
|
@ -1,4 +1,5 @@
|
||||
using System.Text;
|
||||
using System.Text.Json.Serialization.Metadata;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
||||
@ -25,6 +26,20 @@ public static class HttpOutput {
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class Json<TValue> : IHttpOutput {
|
||||
private readonly TValue value;
|
||||
private readonly JsonTypeInfo<TValue> typeInfo;
|
||||
|
||||
public Json(TValue value, JsonTypeInfo<TValue> typeInfo) {
|
||||
this.value = value;
|
||||
this.typeInfo = typeInfo;
|
||||
}
|
||||
|
||||
public Task WriteTo(HttpResponse response) {
|
||||
return response.WriteAsJsonAsync(value, typeInfo);
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class File : IHttpOutput {
|
||||
private readonly string? contentType;
|
||||
private readonly byte[] bytes;
|
||||
|
Loading…
x
Reference in New Issue
Block a user