mirror of
https://github.com/chylex/Discord-History-Tracker.git
synced 2025-09-15 19:32:09 +02:00
Compare commits
1 Commits
d2934f4d6a
...
wip-viewer
Author | SHA1 | Date | |
---|---|---|---|
b660af4be0
|
@@ -45,17 +45,17 @@
|
||||
<Rectangle />
|
||||
<StackPanel Orientation="Vertical">
|
||||
<TextBlock Classes="label">Servers</TextBlock>
|
||||
<TextBlock Classes="value" Text="{Binding DatabaseStatistics.TotalServers, Mode=OneWay, Converter={StaticResource NumberValueConverter}}" />
|
||||
<TextBlock Classes="value" Text="{Binding DatabaseStatistics.TotalServers, Converter={StaticResource NumberValueConverter}}" />
|
||||
</StackPanel>
|
||||
<Rectangle />
|
||||
<StackPanel Orientation="Vertical">
|
||||
<TextBlock Classes="label">Channels</TextBlock>
|
||||
<TextBlock Classes="value" Text="{Binding DatabaseStatistics.TotalChannels, Mode=OneWay, Converter={StaticResource NumberValueConverter}}" />
|
||||
<TextBlock Classes="value" Text="{Binding DatabaseStatistics.TotalChannels, Converter={StaticResource NumberValueConverter}}" />
|
||||
</StackPanel>
|
||||
<Rectangle />
|
||||
<StackPanel Orientation="Vertical">
|
||||
<TextBlock Classes="label">Messages</TextBlock>
|
||||
<TextBlock Classes="value" Text="{Binding DatabaseStatistics.TotalMessages, Mode=OneWay, Converter={StaticResource NumberValueConverter}}" />
|
||||
<TextBlock Classes="value" Text="{Binding DatabaseStatistics.TotalMessages, Converter={StaticResource NumberValueConverter}}" />
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
|
||||
|
@@ -20,9 +20,9 @@ public sealed partial class MainWindow : Window {
|
||||
DataContext = new MainWindowModel(this, args);
|
||||
}
|
||||
|
||||
public async void OnClosed(object? sender, EventArgs e) {
|
||||
if (DataContext is MainWindowModel model) {
|
||||
await model.DisposeAsync();
|
||||
public void OnClosed(object? sender, EventArgs e) {
|
||||
if (DataContext is IDisposable disposable) {
|
||||
disposable.Dispose();
|
||||
}
|
||||
|
||||
foreach (var temporaryFile in ViewerPageModel.TemporaryFiles) {
|
||||
|
@@ -12,7 +12,7 @@ using DHT.Utils.Models;
|
||||
|
||||
namespace DHT.Desktop.Main;
|
||||
|
||||
sealed class MainWindowModel : BaseModel, IAsyncDisposable {
|
||||
sealed class MainWindowModel : BaseModel, IDisposable {
|
||||
private const string DefaultTitle = "Discord History Tracker";
|
||||
|
||||
public string Title { get; private set; } = DefaultTitle;
|
||||
@@ -75,7 +75,7 @@ sealed class MainWindowModel : BaseModel, IAsyncDisposable {
|
||||
if (e.PropertyName == nameof(welcomeScreenModel.Db)) {
|
||||
if (mainContentScreenModel != null) {
|
||||
mainContentScreenModel.DatabaseClosed -= MainContentScreenModelOnDatabaseClosed;
|
||||
await mainContentScreenModel.DisposeAsync();
|
||||
mainContentScreenModel.Dispose();
|
||||
}
|
||||
|
||||
db?.Dispose();
|
||||
@@ -107,13 +107,9 @@ sealed class MainWindowModel : BaseModel, IAsyncDisposable {
|
||||
welcomeScreenModel.CloseDatabase();
|
||||
}
|
||||
|
||||
public async ValueTask DisposeAsync() {
|
||||
public void Dispose() {
|
||||
welcomeScreenModel.Dispose();
|
||||
|
||||
if (mainContentScreenModel != null) {
|
||||
await mainContentScreenModel.DisposeAsync();
|
||||
}
|
||||
|
||||
mainContentScreenModel?.Dispose();
|
||||
db?.Dispose();
|
||||
db = null;
|
||||
}
|
||||
|
@@ -33,7 +33,7 @@
|
||||
<StackPanel Orientation="Vertical" Spacing="20">
|
||||
<DockPanel>
|
||||
<Button Command="{Binding OnClickToggleDownload}" Content="{Binding ToggleDownloadButtonText}" IsEnabled="{Binding IsToggleDownloadButtonEnabled}" DockPanel.Dock="Left" />
|
||||
<TextBlock Text="{Binding DownloadMessage}" MinWidth="100" Margin="10 0 0 0" VerticalAlignment="Center" TextAlignment="Right" DockPanel.Dock="Left" />
|
||||
<TextBlock Text="{Binding DownloadMessage}" Margin="10 0 0 0" VerticalAlignment="Center" DockPanel.Dock="Left" />
|
||||
<ProgressBar Value="{Binding DownloadProgress}" IsVisible="{Binding IsDownloading}" Margin="15 0" VerticalAlignment="Center" DockPanel.Dock="Right" />
|
||||
</DockPanel>
|
||||
<controls:AttachmentFilterPanel DataContext="{Binding FilterModel}" IsEnabled="{Binding !IsDownloading, RelativeSource={RelativeSource AncestorType=pages:AttachmentsPageModel}}" />
|
||||
@@ -42,8 +42,8 @@
|
||||
<DataGrid ItemsSource="{Binding StatisticsRows}" AutoGenerateColumns="False" CanUserReorderColumns="False" CanUserResizeColumns="False" CanUserSortColumns="False" IsReadOnly="True">
|
||||
<DataGrid.Columns>
|
||||
<DataGridTextColumn Header="State" Binding="{Binding State}" Width="*" />
|
||||
<DataGridTextColumn Header="Attachments" Binding="{Binding Items, Mode=OneWay, Converter={StaticResource NumberValueConverter}}" Width="*" CellStyleClasses="right" />
|
||||
<DataGridTextColumn Header="Size" Binding="{Binding Size, Mode=OneWay, Converter={StaticResource BytesValueConverter}}" Width="*" CellStyleClasses="right" />
|
||||
<DataGridTextColumn Header="Attachments" Binding="{Binding Items, Converter={StaticResource NumberValueConverter}}" Width="*" CellStyleClasses="right" />
|
||||
<DataGridTextColumn Header="Size" Binding="{Binding Size, Converter={StaticResource BytesValueConverter}}" Width="*" CellStyleClasses="right" />
|
||||
</DataGrid.Columns>
|
||||
</DataGrid>
|
||||
</Expander>
|
||||
|
@@ -2,7 +2,6 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Avalonia.Threading;
|
||||
using DHT.Desktop.Common;
|
||||
using DHT.Desktop.Main.Controls;
|
||||
@@ -16,17 +15,16 @@ using DHT.Utils.Tasks;
|
||||
|
||||
namespace DHT.Desktop.Main.Pages;
|
||||
|
||||
sealed class AttachmentsPageModel : BaseModel, IAsyncDisposable {
|
||||
sealed class AttachmentsPageModel : BaseModel, IDisposable {
|
||||
private static readonly DownloadItemFilter EnqueuedItemFilter = new() {
|
||||
IncludeStatuses = new HashSet<DownloadStatus> {
|
||||
DownloadStatus.Enqueued,
|
||||
DownloadStatus.Downloading
|
||||
DownloadStatus.Enqueued
|
||||
}
|
||||
};
|
||||
|
||||
private bool isThreadDownloadButtonEnabled = true;
|
||||
|
||||
public string ToggleDownloadButtonText => downloader == null ? "Start Downloading" : "Stop Downloading";
|
||||
public string ToggleDownloadButtonText => downloadThread == null ? "Start Downloading" : "Stop Downloading";
|
||||
|
||||
public bool IsToggleDownloadButtonEnabled {
|
||||
get => isThreadDownloadButtonEnabled;
|
||||
@@ -34,7 +32,7 @@ sealed class AttachmentsPageModel : BaseModel, IAsyncDisposable {
|
||||
}
|
||||
|
||||
public string DownloadMessage { get; set; } = "";
|
||||
public double DownloadProgress => totalItemsToDownloadCount is null or 0 ? 0.0 : 100.0 * doneItemsCount / totalItemsToDownloadCount.Value;
|
||||
public double DownloadProgress => allItemsCount is null or 0 ? 0.0 : 100.0 * doneItemsCount / allItemsCount.Value;
|
||||
|
||||
public AttachmentFilterPanelModel FilterModel { get; }
|
||||
|
||||
@@ -54,16 +52,15 @@ sealed class AttachmentsPageModel : BaseModel, IAsyncDisposable {
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsDownloading => downloader != null;
|
||||
public bool IsDownloading => downloadThread != null;
|
||||
public bool HasFailedDownloads => statisticsFailed.Items > 0;
|
||||
|
||||
private readonly IDatabaseFile db;
|
||||
private readonly AsyncValueComputer<DownloadStatusStatistics>.Single downloadStatisticsComputer;
|
||||
private BackgroundDownloader? downloader;
|
||||
private BackgroundDownloadThread? downloadThread;
|
||||
|
||||
private int doneItemsCount;
|
||||
private int initialFinishedCount;
|
||||
private int? totalItemsToDownloadCount;
|
||||
private int? allItemsCount;
|
||||
|
||||
public AttachmentsPageModel() : this(DummyDatabaseFile.Instance) {}
|
||||
|
||||
@@ -77,11 +74,11 @@ sealed class AttachmentsPageModel : BaseModel, IAsyncDisposable {
|
||||
db.Statistics.PropertyChanged += OnDbStatisticsChanged;
|
||||
}
|
||||
|
||||
public async ValueTask DisposeAsync() {
|
||||
public void Dispose() {
|
||||
db.Statistics.PropertyChanged -= OnDbStatisticsChanged;
|
||||
|
||||
FilterModel.Dispose();
|
||||
await DisposeDownloader();
|
||||
DisposeDownloadThread();
|
||||
}
|
||||
|
||||
private void OnDbStatisticsChanged(object? sender, PropertyChangedEventArgs e) {
|
||||
@@ -127,42 +124,44 @@ sealed class AttachmentsPageModel : BaseModel, IAsyncDisposable {
|
||||
OnPropertyChanged(nameof(HasFailedDownloads));
|
||||
}
|
||||
|
||||
totalItemsToDownloadCount = statisticsEnqueued.Items + statisticsDownloaded.Items + statisticsFailed.Items - initialFinishedCount;
|
||||
allItemsCount = doneItemsCount + statisticsEnqueued.Items;
|
||||
UpdateDownloadMessage();
|
||||
}
|
||||
|
||||
private void UpdateDownloadMessage() {
|
||||
DownloadMessage = IsDownloading ? doneItemsCount.Format() + " / " + (totalItemsToDownloadCount?.Format() ?? "?") : "";
|
||||
DownloadMessage = IsDownloading ? doneItemsCount.Format() + " / " + (allItemsCount?.Format() ?? "?") : "";
|
||||
|
||||
OnPropertyChanged(nameof(DownloadMessage));
|
||||
OnPropertyChanged(nameof(DownloadProgress));
|
||||
}
|
||||
|
||||
private void DownloaderOnOnItemFinished(object? sender, DownloadItem e) {
|
||||
private void DownloadThreadOnOnItemFinished(object? sender, DownloadItem e) {
|
||||
Interlocked.Increment(ref doneItemsCount);
|
||||
|
||||
|
||||
Dispatcher.UIThread.Invoke(UpdateDownloadMessage);
|
||||
downloadStatisticsComputer.Recompute();
|
||||
}
|
||||
|
||||
public async Task OnClickToggleDownload() {
|
||||
if (downloader == null) {
|
||||
initialFinishedCount = statisticsDownloaded.Items + statisticsFailed.Items;
|
||||
private void DownloadThreadOnOnServerStopped(object? sender, EventArgs e) {
|
||||
downloadStatisticsComputer.Recompute();
|
||||
IsToggleDownloadButtonEnabled = true;
|
||||
}
|
||||
|
||||
public void OnClickToggleDownload() {
|
||||
if (downloadThread == null) {
|
||||
EnqueueDownloadItems();
|
||||
downloader = new BackgroundDownloader(db);
|
||||
downloader.OnItemFinished += DownloaderOnOnItemFinished;
|
||||
downloadThread = new BackgroundDownloadThread(db);
|
||||
downloadThread.OnItemFinished += DownloadThreadOnOnItemFinished;
|
||||
downloadThread.OnServerStopped += DownloadThreadOnOnServerStopped;
|
||||
}
|
||||
else {
|
||||
IsToggleDownloadButtonEnabled = false;
|
||||
await DisposeDownloader();
|
||||
downloadStatisticsComputer.Recompute();
|
||||
IsToggleDownloadButtonEnabled = true;
|
||||
DisposeDownloadThread();
|
||||
|
||||
db.RemoveDownloadItems(EnqueuedItemFilter, FilterRemovalMode.RemoveMatching);
|
||||
|
||||
doneItemsCount = 0;
|
||||
initialFinishedCount = 0;
|
||||
totalItemsToDownloadCount = null;
|
||||
allItemsCount = null;
|
||||
UpdateDownloadMessage();
|
||||
}
|
||||
|
||||
@@ -174,7 +173,6 @@ sealed class AttachmentsPageModel : BaseModel, IAsyncDisposable {
|
||||
var allExceptFailedFilter = new DownloadItemFilter {
|
||||
IncludeStatuses = new HashSet<DownloadStatus> {
|
||||
DownloadStatus.Enqueued,
|
||||
DownloadStatus.Downloading,
|
||||
DownloadStatus.Success
|
||||
}
|
||||
};
|
||||
@@ -186,13 +184,13 @@ sealed class AttachmentsPageModel : BaseModel, IAsyncDisposable {
|
||||
}
|
||||
}
|
||||
|
||||
private async Task DisposeDownloader() {
|
||||
if (downloader != null) {
|
||||
downloader.OnItemFinished -= DownloaderOnOnItemFinished;
|
||||
await downloader.Stop();
|
||||
private void DisposeDownloadThread() {
|
||||
if (downloadThread != null) {
|
||||
downloadThread.OnItemFinished -= DownloadThreadOnOnItemFinished;
|
||||
downloadThread.StopThread();
|
||||
}
|
||||
|
||||
downloader = null;
|
||||
downloadThread = null;
|
||||
}
|
||||
|
||||
public sealed class StatisticsRow {
|
||||
|
@@ -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;
|
||||
|
@@ -11,7 +11,7 @@ using DHT.Utils.Logging;
|
||||
|
||||
namespace DHT.Desktop.Main.Screens;
|
||||
|
||||
sealed class MainContentScreenModel : IAsyncDisposable {
|
||||
sealed class MainContentScreenModel : IDisposable {
|
||||
private static readonly Log Log = Log.ForType<MainContentScreenModel>();
|
||||
|
||||
public DatabasePage DatabasePage { get; }
|
||||
@@ -35,7 +35,7 @@ sealed class MainContentScreenModel : IAsyncDisposable {
|
||||
public bool HasDebugPage => true;
|
||||
private DebugPageModel DebugPageModel { get; }
|
||||
#else
|
||||
public bool HasDebugPage => false;
|
||||
public bool HasDebugPage => false;
|
||||
#endif
|
||||
|
||||
public StatusBarModel StatusBarModel { get; }
|
||||
@@ -97,9 +97,9 @@ sealed class MainContentScreenModel : IAsyncDisposable {
|
||||
serverManager.Launch();
|
||||
}
|
||||
|
||||
public async ValueTask DisposeAsync() {
|
||||
public void Dispose() {
|
||||
ServerLauncher.ServerManagementExceptionCaught -= ServerLauncherOnServerManagementExceptionCaught;
|
||||
await AttachmentsPageModel.DisposeAsync();
|
||||
AttachmentsPageModel.Dispose();
|
||||
ViewerPageModel.Dispose();
|
||||
serverManager.Dispose();
|
||||
}
|
||||
|
@@ -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) {
|
||||
|
@@ -8,6 +8,5 @@ namespace DHT.Server.Data;
|
||||
public enum DownloadStatus {
|
||||
Enqueued = 0,
|
||||
GenericError = 1,
|
||||
Downloading = 2,
|
||||
Success = HttpStatusCode.OK
|
||||
}
|
||||
|
@@ -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();
|
||||
}
|
||||
|
||||
@@ -74,7 +74,7 @@ public sealed class DummyDatabaseFile : IDatabaseFile {
|
||||
|
||||
public void EnqueueDownloadItems(AttachmentFilter? filter = null) {}
|
||||
|
||||
public List<DownloadItem> PullEnqueuedDownloadItems(int count) {
|
||||
public List<DownloadItem> GetEnqueuedDownloadItems(int count) {
|
||||
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);
|
||||
|
||||
@@ -35,7 +35,7 @@ public interface IDatabaseFile : IDisposable {
|
||||
DownloadedAttachment? GetDownloadedAttachment(string url);
|
||||
|
||||
void EnqueueDownloadItems(AttachmentFilter? filter = null);
|
||||
List<DownloadItem> PullEnqueuedDownloadItems(int count);
|
||||
List<DownloadItem> GetEnqueuedDownloadItems(int count);
|
||||
void RemoveDownloadItems(DownloadItemFilter? filter, FilterRemovalMode mode);
|
||||
DownloadStatusStatistics GetDownloadStatusStatistics();
|
||||
|
||||
|
@@ -174,8 +174,8 @@ sealed class Schema {
|
||||
int processedUrls = -1;
|
||||
|
||||
await using (var updateCmd = conn.Command("UPDATE attachments SET download_url = url, url = :normalized_url WHERE attachment_id = :attachment_id")) {
|
||||
updateCmd.Add(":attachment_id", SqliteType.Integer);
|
||||
updateCmd.Add(":normalized_url", SqliteType.Text);
|
||||
updateCmd.Parameters.Add(":attachment_id", SqliteType.Integer);
|
||||
updateCmd.Parameters.Add(":normalized_url", SqliteType.Text);
|
||||
|
||||
foreach (var (attachmentId, normalizedUrl) in normalizedUrls) {
|
||||
if (++processedUrls % 1000 == 0) {
|
||||
@@ -235,8 +235,8 @@ sealed class Schema {
|
||||
tx = conn.BeginTransaction();
|
||||
|
||||
await using (var updateCmd = conn.Command("UPDATE downloads SET download_url = :download_url, url = :normalized_url WHERE url = :download_url")) {
|
||||
updateCmd.Add(":normalized_url", SqliteType.Text);
|
||||
updateCmd.Add(":download_url", SqliteType.Text);
|
||||
updateCmd.Parameters.Add(":normalized_url", SqliteType.Text);
|
||||
updateCmd.Parameters.Add(":download_url", SqliteType.Text);
|
||||
|
||||
foreach (var (normalizedUrl, downloadUrl) in normalizedUrlsToOriginalUrls) {
|
||||
if (++processedUrls % 100 == 0) {
|
||||
|
@@ -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),
|
||||
@@ -522,42 +522,25 @@ public sealed class SqliteDatabaseFile : IDatabaseFile {
|
||||
cmd.ExecuteNonQuery();
|
||||
}
|
||||
|
||||
public List<DownloadItem> PullEnqueuedDownloadItems(int count) {
|
||||
var found = new List<DownloadItem>();
|
||||
var pulled = new List<DownloadItem>();
|
||||
public List<DownloadItem> GetEnqueuedDownloadItems(int count) {
|
||||
var list = new List<DownloadItem>();
|
||||
|
||||
using var conn = pool.Take();
|
||||
using (var cmd = conn.Command("SELECT normalized_url, download_url, size FROM downloads WHERE status = :enqueued LIMIT :limit")) {
|
||||
cmd.AddAndSet(":enqueued", SqliteType.Integer, (int) DownloadStatus.Enqueued);
|
||||
cmd.AddAndSet(":limit", SqliteType.Integer, Math.Max(0, count));
|
||||
using var cmd = conn.Command("SELECT normalized_url, download_url, size FROM downloads WHERE status = :enqueued LIMIT :limit");
|
||||
cmd.AddAndSet(":enqueued", SqliteType.Integer, (int) DownloadStatus.Enqueued);
|
||||
cmd.AddAndSet(":limit", SqliteType.Integer, Math.Max(0, count));
|
||||
|
||||
using var reader = cmd.ExecuteReader();
|
||||
using var reader = cmd.ExecuteReader();
|
||||
|
||||
while (reader.Read()) {
|
||||
found.Add(new DownloadItem {
|
||||
NormalizedUrl = reader.GetString(0),
|
||||
DownloadUrl = reader.GetString(1),
|
||||
Size = reader.GetUint64(2),
|
||||
});
|
||||
}
|
||||
while (reader.Read()) {
|
||||
list.Add(new DownloadItem {
|
||||
NormalizedUrl = reader.GetString(0),
|
||||
DownloadUrl = reader.GetString(1),
|
||||
Size = reader.GetUint64(2),
|
||||
});
|
||||
}
|
||||
|
||||
if (found.Count != 0) {
|
||||
using var cmd = conn.Command("UPDATE downloads SET status = :downloading WHERE normalized_url = :normalized_url AND status = :enqueued");
|
||||
cmd.AddAndSet(":enqueued", SqliteType.Integer, (int) DownloadStatus.Enqueued);
|
||||
cmd.AddAndSet(":downloading", SqliteType.Integer, (int) DownloadStatus.Downloading);
|
||||
cmd.Add(":normalized_url", SqliteType.Text);
|
||||
|
||||
foreach (var item in found) {
|
||||
cmd.Set(":normalized_url", item.NormalizedUrl);
|
||||
|
||||
if (cmd.ExecuteNonQuery() == 1) {
|
||||
pulled.Add(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return pulled;
|
||||
return list;
|
||||
}
|
||||
|
||||
public void RemoveDownloadItems(DownloadItemFilter? filter, FilterRemovalMode mode) {
|
||||
@@ -579,16 +562,15 @@ public sealed class SqliteDatabaseFile : IDatabaseFile {
|
||||
static void LoadSuccessStatistics(ISqliteConnection conn, DownloadStatusStatistics result) {
|
||||
using var cmd = conn.Command("""
|
||||
SELECT
|
||||
IFNULL(SUM(CASE WHEN status IN (:enqueued, :downloading) THEN 1 ELSE 0 END), 0),
|
||||
IFNULL(SUM(CASE WHEN status IN (:enqueued, :downloading) THEN size ELSE 0 END), 0),
|
||||
IFNULL(SUM(CASE WHEN status = :enqueued THEN 1 ELSE 0 END), 0),
|
||||
IFNULL(SUM(CASE WHEN status = :enqueued THEN size ELSE 0 END), 0),
|
||||
IFNULL(SUM(CASE WHEN status = :success THEN 1 ELSE 0 END), 0),
|
||||
IFNULL(SUM(CASE WHEN status = :success THEN size ELSE 0 END), 0),
|
||||
IFNULL(SUM(CASE WHEN status NOT IN (:enqueued, :downloading) AND status != :success THEN 1 ELSE 0 END), 0),
|
||||
IFNULL(SUM(CASE WHEN status NOT IN (:enqueued, :downloading) AND status != :success THEN size ELSE 0 END), 0)
|
||||
IFNULL(SUM(CASE WHEN status != :enqueued AND status != :success THEN 1 ELSE 0 END), 0),
|
||||
IFNULL(SUM(CASE WHEN status != :enqueued AND status != :success THEN size ELSE 0 END), 0)
|
||||
FROM downloads
|
||||
""");
|
||||
cmd.AddAndSet(":enqueued", SqliteType.Integer, (int) DownloadStatus.Enqueued);
|
||||
cmd.AddAndSet(":downloading", SqliteType.Integer, (int) DownloadStatus.Downloading);
|
||||
cmd.AddAndSet(":success", SqliteType.Integer, (int) DownloadStatus.Success);
|
||||
|
||||
using var reader = cmd.ExecuteReader();
|
||||
|
@@ -62,10 +62,6 @@ static class SqliteExtensions {
|
||||
}
|
||||
}
|
||||
|
||||
public static void Add(this SqliteCommand cmd, string key, SqliteType type) {
|
||||
cmd.Parameters.Add(key, type);
|
||||
}
|
||||
|
||||
public static void AddAndSet(this SqliteCommand cmd, string key, SqliteType type, object? value) {
|
||||
cmd.Parameters.Add(key, type).Value = value ?? DBNull.Value;
|
||||
}
|
||||
|
130
app/Server/Download/BackgroundDownloadThread.cs
Normal file
130
app/Server/Download/BackgroundDownloadThread.cs
Normal file
@@ -0,0 +1,130 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
using DHT.Server.Database;
|
||||
using DHT.Utils.Logging;
|
||||
using DHT.Utils.Models;
|
||||
|
||||
namespace DHT.Server.Download;
|
||||
|
||||
public sealed class BackgroundDownloadThread : BaseModel {
|
||||
private static readonly Log Log = Log.ForType<BackgroundDownloadThread>();
|
||||
|
||||
public event EventHandler<DownloadItem>? OnItemFinished {
|
||||
add => parameters.OnItemFinished += value;
|
||||
remove => parameters.OnItemFinished -= value;
|
||||
}
|
||||
|
||||
public event EventHandler? OnServerStopped {
|
||||
add => parameters.OnServerStopped += value;
|
||||
remove => parameters.OnServerStopped -= value;
|
||||
}
|
||||
|
||||
private readonly CancellationTokenSource cancellationTokenSource;
|
||||
private readonly ThreadInstance.Parameters parameters;
|
||||
|
||||
public BackgroundDownloadThread(IDatabaseFile db) {
|
||||
this.cancellationTokenSource = new CancellationTokenSource();
|
||||
this.parameters = new ThreadInstance.Parameters(db, cancellationTokenSource);
|
||||
|
||||
var thread = new Thread(new ThreadInstance().Work) {
|
||||
Name = "DHT download thread"
|
||||
};
|
||||
|
||||
thread.Start(parameters);
|
||||
}
|
||||
|
||||
public void StopThread() {
|
||||
try {
|
||||
cancellationTokenSource.Cancel();
|
||||
} catch (ObjectDisposedException) {
|
||||
Log.Warn("Attempted to stop background download thread after the cancellation token has been disposed.");
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class ThreadInstance {
|
||||
private const int QueueSize = 32;
|
||||
|
||||
public sealed class Parameters {
|
||||
public event EventHandler<DownloadItem>? OnItemFinished;
|
||||
public event EventHandler? OnServerStopped;
|
||||
|
||||
public IDatabaseFile Db { get; }
|
||||
public CancellationTokenSource CancellationTokenSource { get; }
|
||||
|
||||
public Parameters(IDatabaseFile db, CancellationTokenSource cancellationTokenSource) {
|
||||
Db = db;
|
||||
CancellationTokenSource = cancellationTokenSource;
|
||||
}
|
||||
|
||||
public void FireOnItemFinished(DownloadItem item) {
|
||||
OnItemFinished?.Invoke(null, item);
|
||||
}
|
||||
|
||||
public void FireOnServerStopped() {
|
||||
OnServerStopped?.Invoke(null, EventArgs.Empty);
|
||||
}
|
||||
}
|
||||
|
||||
private readonly HttpClient client = new ();
|
||||
|
||||
public ThreadInstance() {
|
||||
client.DefaultRequestHeaders.UserAgent.ParseAdd("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.4844.51 Safari/537.36");
|
||||
}
|
||||
|
||||
public async void Work(object? obj) {
|
||||
var parameters = (Parameters) obj!;
|
||||
|
||||
var cancellationTokenSource = parameters.CancellationTokenSource;
|
||||
var cancellationToken = cancellationTokenSource.Token;
|
||||
|
||||
var db = parameters.Db;
|
||||
var queue = new ConcurrentQueue<DownloadItem>();
|
||||
|
||||
try {
|
||||
while (!cancellationToken.IsCancellationRequested) {
|
||||
FillQueue(db, queue, cancellationToken);
|
||||
|
||||
while (!cancellationToken.IsCancellationRequested && queue.TryDequeue(out var item)) {
|
||||
var downloadUrl = item.DownloadUrl;
|
||||
Log.Debug("Downloading " + downloadUrl + "...");
|
||||
|
||||
try {
|
||||
db.AddDownload(Data.Download.NewSuccess(item, await client.GetByteArrayAsync(downloadUrl, cancellationToken)));
|
||||
} catch (HttpRequestException e) {
|
||||
db.AddDownload(Data.Download.NewFailure(item, e.StatusCode, item.Size));
|
||||
Log.Error(e);
|
||||
} catch (Exception e) {
|
||||
db.AddDownload(Data.Download.NewFailure(item, null, item.Size));
|
||||
Log.Error(e);
|
||||
} finally {
|
||||
parameters.FireOnItemFinished(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (OperationCanceledException) {
|
||||
//
|
||||
} catch (ObjectDisposedException) {
|
||||
//
|
||||
} finally {
|
||||
cancellationTokenSource.Dispose();
|
||||
parameters.FireOnServerStopped();
|
||||
}
|
||||
}
|
||||
|
||||
private static void FillQueue(IDatabaseFile db, ConcurrentQueue<DownloadItem> queue, CancellationToken cancellationToken) {
|
||||
while (!cancellationToken.IsCancellationRequested && queue.IsEmpty) {
|
||||
var newItems = db.GetEnqueuedDownloadItems(QueueSize);
|
||||
if (newItems.Count == 0) {
|
||||
Thread.Sleep(TimeSpan.FromMilliseconds(50));
|
||||
}
|
||||
else {
|
||||
foreach (var item in newItems) {
|
||||
queue.Enqueue(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,107 +0,0 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
using System.Threading.Channels;
|
||||
using System.Threading.Tasks;
|
||||
using DHT.Server.Database;
|
||||
using DHT.Utils.Logging;
|
||||
using DHT.Utils.Models;
|
||||
using DHT.Utils.Tasks;
|
||||
|
||||
namespace DHT.Server.Download;
|
||||
|
||||
public sealed class BackgroundDownloader : BaseModel {
|
||||
private static readonly Log Log = Log.ForType<BackgroundDownloader>();
|
||||
|
||||
private const int DownloadTasks = 4;
|
||||
private const int QueueSize = 25;
|
||||
private const string UserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36";
|
||||
|
||||
public event EventHandler<DownloadItem>? OnItemFinished;
|
||||
|
||||
private readonly Channel<DownloadItem> downloadQueue = Channel.CreateBounded<DownloadItem>(new BoundedChannelOptions(QueueSize) {
|
||||
SingleReader = false,
|
||||
SingleWriter = true,
|
||||
AllowSynchronousContinuations = false,
|
||||
FullMode = BoundedChannelFullMode.Wait
|
||||
});
|
||||
|
||||
private readonly CancellationTokenSource cancellationTokenSource = new ();
|
||||
private readonly CancellationToken cancellationToken;
|
||||
|
||||
private readonly IDatabaseFile db;
|
||||
private readonly Task queueWriterTask;
|
||||
private readonly Task[] downloadTasks;
|
||||
|
||||
public BackgroundDownloader(IDatabaseFile db) {
|
||||
this.cancellationToken = cancellationTokenSource.Token;
|
||||
this.db = db;
|
||||
this.queueWriterTask = Task.Run(RunQueueWriterTask);
|
||||
this.downloadTasks = Enumerable.Range(1, DownloadTasks).Select(taskIndex => Task.Run(() => RunDownloadTask(taskIndex))).ToArray();
|
||||
}
|
||||
|
||||
private async Task RunQueueWriterTask() {
|
||||
while (await downloadQueue.Writer.WaitToWriteAsync(cancellationToken)) {
|
||||
var newItems = db.PullEnqueuedDownloadItems(QueueSize);
|
||||
if (newItems.Count == 0) {
|
||||
await Task.Delay(TimeSpan.FromMilliseconds(50), cancellationToken);
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach (var newItem in newItems) {
|
||||
await downloadQueue.Writer.WriteAsync(newItem, cancellationToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task RunDownloadTask(int taskIndex) {
|
||||
var log = Log.ForType<BackgroundDownloader>("Task " + taskIndex);
|
||||
|
||||
var client = new HttpClient();
|
||||
client.DefaultRequestHeaders.UserAgent.ParseAdd(UserAgent);
|
||||
client.Timeout = TimeSpan.FromSeconds(30);
|
||||
|
||||
while (!cancellationToken.IsCancellationRequested) {
|
||||
var item = await downloadQueue.Reader.ReadAsync(cancellationToken);
|
||||
log.Debug("Downloading " + item.DownloadUrl + "...");
|
||||
|
||||
try {
|
||||
var downloadedBytes = await client.GetByteArrayAsync(item.DownloadUrl, cancellationToken);
|
||||
db.AddDownload(Data.Download.NewSuccess(item, downloadedBytes));
|
||||
} catch (OperationCanceledException) {
|
||||
// Ignore.
|
||||
} catch (HttpRequestException e) {
|
||||
db.AddDownload(Data.Download.NewFailure(item, e.StatusCode, item.Size));
|
||||
log.Error(e);
|
||||
} catch (Exception e) {
|
||||
db.AddDownload(Data.Download.NewFailure(item, null, item.Size));
|
||||
log.Error(e);
|
||||
} finally {
|
||||
try {
|
||||
OnItemFinished?.Invoke(this, item);
|
||||
} catch (Exception e) {
|
||||
log.Error("Caught exception in event handler: " + e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async Task Stop() {
|
||||
try {
|
||||
await cancellationTokenSource.CancelAsync();
|
||||
} catch (Exception) {
|
||||
Log.Warn("Attempted to stop background download twice.");
|
||||
return;
|
||||
}
|
||||
|
||||
downloadQueue.Writer.Complete();
|
||||
|
||||
try {
|
||||
await queueWriterTask.WaitIgnoringCancellation();
|
||||
await Task.WhenAll(downloadTasks).WaitIgnoringCancellation();
|
||||
} finally {
|
||||
cancellationTokenSource.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
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;
|
||||
|
@@ -1,12 +0,0 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace DHT.Utils.Tasks;
|
||||
|
||||
public static class TaskExtensions {
|
||||
public static async Task WaitIgnoringCancellation(this Task task) {
|
||||
try {
|
||||
await task;
|
||||
} catch (OperationCanceledException) {}
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user