mirror of
https://github.com/chylex/Discord-History-Tracker.git
synced 2025-07-15 23:51:01 +03:00
Compare commits
6 Commits
58259c0bb4
...
8f5f6065d8
Author | SHA1 | Date | |
---|---|---|---|
|
8f5f6065d8 | ||
|
ad299bf762 | ||
|
f70bbd53d9 | ||
|
ae821f738e | ||
|
ab7f5d0a41 | ||
|
1bddde7ccd |
@ -14,7 +14,7 @@ using DHT.Server.Database;
|
||||
using DHT.Utils.Models;
|
||||
|
||||
namespace DHT.Desktop.Main.Controls {
|
||||
sealed class FilterPanelModel : BaseModel {
|
||||
sealed class FilterPanelModel : BaseModel, IDisposable {
|
||||
private static readonly HashSet<string> FilterProperties = new () {
|
||||
nameof(FilterByDate),
|
||||
nameof(StartDate),
|
||||
@ -103,6 +103,10 @@ namespace DHT.Desktop.Main.Controls {
|
||||
db.Statistics.PropertyChanged += OnDbStatisticsChanged;
|
||||
}
|
||||
|
||||
public void Dispose() {
|
||||
db.Statistics.PropertyChanged -= OnDbStatisticsChanged;
|
||||
}
|
||||
|
||||
private void OnPropertyChanged(object? sender, PropertyChangedEventArgs e) {
|
||||
if (e.PropertyName != null && FilterProperties.Contains(e.PropertyName)) {
|
||||
FilterPropertyChanged?.Invoke(sender, e);
|
||||
|
@ -56,6 +56,7 @@ namespace DHT.Desktop.Main {
|
||||
|
||||
public void Dispose() {
|
||||
TrackingPageModel.Dispose();
|
||||
ViewerPageModel.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -17,7 +17,6 @@
|
||||
</Design.DataContext>
|
||||
|
||||
<Panel>
|
||||
<ContentPresenter Content="{Binding WelcomeScreen}" IsVisible="{Binding ShowWelcomeScreen}" />
|
||||
<ContentPresenter Content="{Binding MainContentScreen}" IsVisible="{Binding ShowMainContentScreen}" />
|
||||
<ContentPresenter Content="{Binding CurrentScreen}" />
|
||||
</Panel>
|
||||
</Window>
|
||||
|
@ -15,14 +15,13 @@ namespace DHT.Desktop.Main {
|
||||
|
||||
public string Title { get; private set; } = DefaultTitle;
|
||||
|
||||
public WelcomeScreen WelcomeScreen { get; }
|
||||
private WelcomeScreenModel WelcomeScreenModel { get; }
|
||||
public UserControl CurrentScreen { get; private set; }
|
||||
|
||||
private readonly WelcomeScreen welcomeScreen;
|
||||
private readonly WelcomeScreenModel welcomeScreenModel;
|
||||
|
||||
public MainContentScreen? MainContentScreen { get; private set; }
|
||||
private MainContentScreenModel? MainContentScreenModel { get; set; }
|
||||
|
||||
public bool ShowWelcomeScreen => db == null;
|
||||
public bool ShowMainContentScreen => db != null;
|
||||
private MainContentScreen? mainContentScreen;
|
||||
private MainContentScreenModel? mainContentScreenModel;
|
||||
|
||||
private readonly Window window;
|
||||
|
||||
@ -34,10 +33,11 @@ namespace DHT.Desktop.Main {
|
||||
public MainWindowModel(Window window, Arguments args) {
|
||||
this.window = window;
|
||||
|
||||
WelcomeScreenModel = new WelcomeScreenModel(window);
|
||||
WelcomeScreen = new WelcomeScreen { DataContext = WelcomeScreenModel };
|
||||
welcomeScreenModel = new WelcomeScreenModel(window);
|
||||
welcomeScreen = new WelcomeScreen { DataContext = welcomeScreenModel };
|
||||
CurrentScreen = welcomeScreen;
|
||||
|
||||
WelcomeScreenModel.PropertyChanged += WelcomeScreenModelOnPropertyChanged;
|
||||
welcomeScreenModel.PropertyChanged += WelcomeScreenModelOnPropertyChanged;
|
||||
|
||||
var dbFile = args.DatabaseFile;
|
||||
if (!string.IsNullOrWhiteSpace(dbFile)) {
|
||||
@ -50,7 +50,7 @@ namespace DHT.Desktop.Main {
|
||||
}
|
||||
|
||||
if (File.Exists(dbFile)) {
|
||||
await WelcomeScreenModel.OpenOrCreateDatabaseFromPath(dbFile);
|
||||
await welcomeScreenModel.OpenOrCreateDatabaseFromPath(dbFile);
|
||||
}
|
||||
else {
|
||||
await Dialog.ShowOk(window, "Database Error", "Database file not found:\n" + dbFile);
|
||||
@ -70,31 +70,31 @@ namespace DHT.Desktop.Main {
|
||||
}
|
||||
|
||||
private async void WelcomeScreenModelOnPropertyChanged(object? sender, PropertyChangedEventArgs e) {
|
||||
if (e.PropertyName == nameof(WelcomeScreenModel.Db)) {
|
||||
if (MainContentScreenModel != null) {
|
||||
MainContentScreenModel.DatabaseClosed -= MainContentScreenModelOnDatabaseClosed;
|
||||
MainContentScreenModel.Dispose();
|
||||
if (e.PropertyName == nameof(welcomeScreenModel.Db)) {
|
||||
if (mainContentScreenModel != null) {
|
||||
mainContentScreenModel.DatabaseClosed -= MainContentScreenModelOnDatabaseClosed;
|
||||
mainContentScreenModel.Dispose();
|
||||
}
|
||||
|
||||
db?.Dispose();
|
||||
db = WelcomeScreenModel.Db;
|
||||
db = welcomeScreenModel.Db;
|
||||
|
||||
if (db == null) {
|
||||
Title = DefaultTitle;
|
||||
MainContentScreenModel = null;
|
||||
MainContentScreen = null;
|
||||
mainContentScreenModel = null;
|
||||
mainContentScreen = null;
|
||||
CurrentScreen = welcomeScreen;
|
||||
}
|
||||
else {
|
||||
Title = Path.GetFileName(db.Path) + " - " + DefaultTitle;
|
||||
MainContentScreenModel = new MainContentScreenModel(window, db);
|
||||
await MainContentScreenModel.Initialize();
|
||||
MainContentScreenModel.DatabaseClosed += MainContentScreenModelOnDatabaseClosed;
|
||||
MainContentScreen = new MainContentScreen { DataContext = MainContentScreenModel };
|
||||
OnPropertyChanged(nameof(MainContentScreen));
|
||||
mainContentScreenModel = new MainContentScreenModel(window, db);
|
||||
await mainContentScreenModel.Initialize();
|
||||
mainContentScreenModel.DatabaseClosed += MainContentScreenModelOnDatabaseClosed;
|
||||
mainContentScreen = new MainContentScreen { DataContext = mainContentScreenModel };
|
||||
CurrentScreen = mainContentScreen;
|
||||
}
|
||||
|
||||
OnPropertyChanged(nameof(ShowWelcomeScreen));
|
||||
OnPropertyChanged(nameof(ShowMainContentScreen));
|
||||
OnPropertyChanged(nameof(CurrentScreen));
|
||||
OnPropertyChanged(nameof(Title));
|
||||
|
||||
window.Focus();
|
||||
@ -102,12 +102,12 @@ namespace DHT.Desktop.Main {
|
||||
}
|
||||
|
||||
private void MainContentScreenModelOnDatabaseClosed(object? sender, EventArgs e) {
|
||||
WelcomeScreenModel.CloseDatabase();
|
||||
welcomeScreenModel.CloseDatabase();
|
||||
}
|
||||
|
||||
public void Dispose() {
|
||||
WelcomeScreenModel.Dispose();
|
||||
MainContentScreenModel?.Dispose();
|
||||
welcomeScreenModel.Dispose();
|
||||
mainContentScreenModel?.Dispose();
|
||||
db?.Dispose();
|
||||
db = null;
|
||||
}
|
||||
|
@ -16,7 +16,7 @@ using DHT.Utils.Models;
|
||||
using static DHT.Desktop.Program;
|
||||
|
||||
namespace DHT.Desktop.Main.Pages {
|
||||
sealed class ViewerPageModel : BaseModel {
|
||||
sealed class ViewerPageModel : BaseModel, IDisposable {
|
||||
public string ExportedMessageText { get; private set; } = "";
|
||||
|
||||
public bool DatabaseToolFilterModeKeep { get; set; } = true;
|
||||
@ -41,12 +41,17 @@ namespace DHT.Desktop.Main.Pages {
|
||||
this.window = window;
|
||||
this.db = db;
|
||||
|
||||
this.FilterModel = new FilterPanelModel(window, db);
|
||||
this.FilterModel.FilterPropertyChanged += OnFilterPropertyChanged;
|
||||
this.db.Statistics.PropertyChanged += OnDbStatisticsChanged;
|
||||
FilterModel = new FilterPanelModel(window, db);
|
||||
FilterModel.FilterPropertyChanged += OnFilterPropertyChanged;
|
||||
db.Statistics.PropertyChanged += OnDbStatisticsChanged;
|
||||
UpdateStatistics();
|
||||
}
|
||||
|
||||
public void Dispose() {
|
||||
db.Statistics.PropertyChanged -= OnDbStatisticsChanged;
|
||||
FilterModel.Dispose();
|
||||
}
|
||||
|
||||
private void OnFilterPropertyChanged(object? sender, PropertyChangedEventArgs e) {
|
||||
UpdateStatistics();
|
||||
HasFilters = FilterModel.HasAnyFilters;
|
||||
|
16
app/Resources/Tracker/bootstrap.js
vendored
16
app/Resources/Tracker/bootstrap.js
vendored
@ -42,6 +42,10 @@
|
||||
stopTrackingDelayed(() => isSending = false);
|
||||
};
|
||||
|
||||
const isNoAction = function(action) {
|
||||
return action === null || action === CONSTANTS.AUTOSCROLL_ACTION_NOTHING;
|
||||
};
|
||||
|
||||
const onTrackingContinued = function(anyNewMessages) {
|
||||
if (!STATE.isTracking()) {
|
||||
return;
|
||||
@ -59,14 +63,14 @@
|
||||
if (SETTINGS.autoscroll) {
|
||||
let action = null;
|
||||
|
||||
if (!anyNewMessages) {
|
||||
action = SETTINGS.afterSavedMsg;
|
||||
}
|
||||
else if (!DISCORD.hasMoreMessages()) {
|
||||
if (!DISCORD.hasMoreMessages()) {
|
||||
action = SETTINGS.afterFirstMsg;
|
||||
}
|
||||
if (isNoAction(action) && !anyNewMessages) {
|
||||
action = SETTINGS.afterSavedMsg;
|
||||
}
|
||||
|
||||
if (action === null || action === CONSTANTS.AUTOSCROLL_ACTION_NOTHING) {
|
||||
if (isNoAction(action)) {
|
||||
DISCORD.loadOlderMessages();
|
||||
}
|
||||
else if (action === CONSTANTS.AUTOSCROLL_ACTION_PAUSE || (action === CONSTANTS.AUTOSCROLL_ACTION_SWITCH && !DISCORD.selectNextTextChannel())) {
|
||||
@ -113,8 +117,8 @@
|
||||
|
||||
try {
|
||||
if (!messages.length) {
|
||||
DISCORD.loadOlderMessages();
|
||||
isSending = false;
|
||||
onTrackingContinued(false);
|
||||
}
|
||||
else {
|
||||
const anyNewMessages = await STATE.addDiscordMessages(info.id, messages);
|
||||
|
@ -67,14 +67,7 @@ class DISCORD {
|
||||
}
|
||||
|
||||
const messages = this.getMessages();
|
||||
let hasChanged = false;
|
||||
|
||||
for (const message of messages) {
|
||||
if (!previousMessages.has(message.id)) {
|
||||
hasChanged = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
const hasChanged = messages.some(message => !previousMessages.has(message.id)) || !this.hasMoreMessages();
|
||||
|
||||
if (!hasChanged) {
|
||||
return;
|
||||
@ -140,16 +133,27 @@ class DISCORD {
|
||||
try {
|
||||
let obj;
|
||||
|
||||
for (const ele of this.getMessageElements()) {
|
||||
const props = this.getMessageElementProps(ele);
|
||||
try {
|
||||
for (const child of DOM.getReactProps(DOM.queryReactClass("chatContent")).children) {
|
||||
if (child && child.props && child.props.channel) {
|
||||
obj = child.props.channel;
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("[DHT] Error retrieving selected channel from 'chatContent' element.", e);
|
||||
|
||||
if (props != null) {
|
||||
obj = props.channel;
|
||||
break;
|
||||
for (const ele of this.getMessageElements()) {
|
||||
const props = this.getMessageElementProps(ele);
|
||||
|
||||
if (props != null) {
|
||||
obj = props.channel;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!obj) {
|
||||
if (!obj || typeof obj.id !== "string") {
|
||||
return null;
|
||||
}
|
||||
|
||||
@ -215,7 +219,7 @@ class DISCORD {
|
||||
return null;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
console.error("[DHT] Error retrieving selected channel.", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@ -240,30 +244,21 @@ class DISCORD {
|
||||
}
|
||||
}
|
||||
else {
|
||||
const channelIcons = [
|
||||
/* normal */ "M5.88657 21C5.57547 21 5.3399 20.7189 5.39427 20.4126L6.00001 17H2.59511C2.28449 17 2.04905 16.7198 2.10259 16.4138L2.27759 15.4138C2.31946 15.1746 2.52722 15 2.77011 15H6.35001L7.41001 9H4.00511C3.69449 9 3.45905 8.71977 3.51259 8.41381L3.68759 7.41381C3.72946 7.17456 3.93722 7 4.18011 7H7.76001L8.39677 3.41262C8.43914 3.17391 8.64664 3 8.88907 3H9.87344C10.1845 3 10.4201 3.28107 10.3657 3.58738L9.76001 7H15.76L16.3968 3.41262C16.4391 3.17391 16.6466 3 16.8891 3H17.8734C18.1845 3 18.4201 3.28107 18.3657 3.58738L17.76 7H21.1649C21.4755 7 21.711 7.28023 21.6574 7.58619L21.4824 8.58619C21.4406 8.82544 21.2328 9 20.9899 9H17.41L16.35 15H19.7549C20.0655 15 20.301 15.2802 20.2474 15.5862L20.0724 16.5862C20.0306 16.8254 19.8228 17 19.5799 17H16L15.3632 20.5874C15.3209 20.8261 15.1134 21 14.8709 21H13.8866C13.5755 21 13.3399 20.7189 13.3943 20.4126L14 17H8.00001L7.36325 20.5874C7.32088 20.8261 7.11337 21 6.87094 21H5.88657ZM9.41045 9L8.35045 15H14.3504L15.4104 9H9.41045Z",
|
||||
/* normal + thread */ "M5.43309 21C5.35842 21 5.30189 20.9325 5.31494 20.859L5.99991 17H2.14274C2.06819 17 2.01168 16.9327 2.02453 16.8593L2.33253 15.0993C2.34258 15.0419 2.39244 15 2.45074 15H6.34991L7.40991 9H3.55274C3.47819 9 3.42168 8.93274 3.43453 8.85931L3.74253 7.09931C3.75258 7.04189 3.80244 7 3.86074 7H7.75991L8.45234 3.09903C8.46251 3.04174 8.51231 3 8.57049 3H10.3267C10.4014 3 10.4579 3.06746 10.4449 3.14097L9.75991 7H15.7599L16.4523 3.09903C16.4625 3.04174 16.5123 3 16.5705 3H18.3267C18.4014 3 18.4579 3.06746 18.4449 3.14097L17.7599 7H21.6171C21.6916 7 21.7481 7.06725 21.7353 7.14069L21.4273 8.90069C21.4172 8.95811 21.3674 9 21.3091 9H17.4099L17.0495 11.04H15.05L15.4104 9H9.41035L8.35035 15H10.5599V17H7.99991L7.30749 20.901C7.29732 20.9583 7.24752 21 7.18934 21H5.43309Z",
|
||||
/* nsfw or private */ "M14 8C14 7.44772 13.5523 7 13 7H9.76001L10.3657 3.58738C10.4201 3.28107 10.1845 3 9.87344 3H8.88907C8.64664 3 8.43914 3.17391 8.39677 3.41262L7.76001 7H4.18011C3.93722 7 3.72946 7.17456 3.68759 7.41381L3.51259 8.41381C3.45905 8.71977 3.69449 9 4.00511 9H7.41001L6.35001 15H2.77011C2.52722 15 2.31946 15.1746 2.27759 15.4138L2.10259 16.4138C2.04905 16.7198 2.28449 17 2.59511 17H6.00001L5.39427 20.4126C5.3399 20.7189 5.57547 21 5.88657 21H6.87094C7.11337 21 7.32088 20.8261 7.36325 20.5874L8.00001 17H14L13.3943 20.4126C13.3399 20.7189 13.5755 21 13.8866 21H14.8709C15.1134 21 15.3209 20.8261 15.3632 20.5874L16 17H19.5799C19.8228 17 20.0306 16.8254 20.0724 16.5862L20.2474 15.5862C20.301 15.2802 20.0655 15 19.7549 15H16.35L16.6758 13.1558C16.7823 12.5529 16.3186 12 15.7063 12C15.2286 12 14.8199 12.3429 14.7368 12.8133L14.3504 15H8.35045L9.41045 9H13C13.5523 9 14 8.55228 14 8Z",
|
||||
/* nsfw + thread */ "M14.4 7C14.5326 7 14.64 7.10745 14.64 7.24V8.76C14.64 8.89255 14.5326 9 14.4 9H9.41045L8.35045 15H10.56V17H8.00001L7.36325 20.5874C7.32088 20.8261 7.11337 21 6.87094 21H5.88657C5.57547 21 5.3399 20.7189 5.39427 20.4126L6.00001 17H2.59511C2.28449 17 2.04905 16.7198 2.10259 16.4138L2.27759 15.4138C2.31946 15.1746 2.52722 15 2.77011 15H6.35001L7.41001 9H4.00511C3.69449 9 3.45905 8.71977 3.51259 8.41381L3.68759 7.41381C3.72946 7.17456 3.93722 7 4.18011 7H7.76001L8.39677 3.41262C8.43914 3.17391 8.64664 3 8.88907 3H9.87344C10.1845 3 10.4201 3.28107 10.3657 3.58738L9.76001 7H14.4Z",
|
||||
/* private + thread */ "M15.44 6.99992C15.5725 6.99992 15.68 7.10737 15.68 7.23992V8.75992C15.68 8.89247 15.5725 8.99992 15.44 8.99992H9.41045L8.35045 14.9999H10.56V16.9999H8.00001L7.36325 20.5873C7.32088 20.826 7.11337 20.9999 6.87094 20.9999H5.88657C5.57547 20.9999 5.3399 20.7189 5.39427 20.4125L6.00001 16.9999H2.59511C2.28449 16.9999 2.04905 16.7197 2.10259 16.4137L2.27759 15.4137C2.31946 15.1745 2.52722 14.9999 2.77011 14.9999H6.35001L7.41001 8.99992H4.00511C3.69449 8.99992 3.45905 8.71969 3.51259 8.41373L3.68759 7.41373C3.72946 7.17448 3.93722 6.99992 4.18011 6.99992H7.76001L8.39677 3.41254C8.43914 3.17384 8.64664 2.99992 8.88907 2.99992H9.87344C10.1845 2.99992 10.4201 3.28099 10.3657 3.58731L9.76001 6.99992H15.44Z"
|
||||
];
|
||||
|
||||
const isValidChannelClass = cls => cls.includes("wrapper-") && !cls.includes("clickable-");
|
||||
const isValidChannelType = ele => channelIcons.some(icon => !!ele.querySelector("path[d=\"" + icon + "\"]"));
|
||||
const isValidChannel = ele => ele.childElementCount > 0 && isValidChannelClass(ele.children[0].className) && isValidChannelType(ele);
|
||||
|
||||
const channelListEle = document.querySelector("div[class*='sidebar'] > nav[class*='container'] > div[class*='scroller']");
|
||||
|
||||
const channelListEle = document.getElementById("channels");
|
||||
if (!channelListEle) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const allChannels = Array.prototype.filter.call(channelListEle.querySelectorAll("[class*='containerDefault']"), isValidChannel);
|
||||
function getLinkElement(channel) {
|
||||
return channel.querySelector("a[href^='/channels/'][role='link']");
|
||||
}
|
||||
|
||||
const allTextChannels = Array.prototype.filter.call(channelListEle.querySelectorAll("[class*='containerDefault']"), ele => getLinkElement(ele) !== null);
|
||||
let nextChannel = null;
|
||||
|
||||
for (let index = 0; index < allChannels.length - 1; index++) {
|
||||
if (allChannels[index].children[0].className.includes("modeSelected")) {
|
||||
nextChannel = allChannels[index + 1];
|
||||
for (let index = 0; index < allTextChannels.length - 1; index++) {
|
||||
if (allTextChannels[index].className.includes("selected-")) {
|
||||
nextChannel = allTextChannels[index + 1];
|
||||
break;
|
||||
}
|
||||
}
|
||||
@ -272,7 +267,7 @@ class DISCORD {
|
||||
return false;
|
||||
}
|
||||
|
||||
const nextChannelLink = nextChannel.querySelector("a[href^='/channels/']");
|
||||
const nextChannelLink = getLinkElement(nextChannel);
|
||||
if (!nextChannelLink) {
|
||||
return false;
|
||||
}
|
||||
|
@ -1,8 +1,7 @@
|
||||
using System.ComponentModel;
|
||||
using System.Runtime.CompilerServices;
|
||||
using DHT.Utils.Models;
|
||||
|
||||
namespace DHT.Server.Database {
|
||||
public sealed class DatabaseStatistics : INotifyPropertyChanged {
|
||||
public sealed class DatabaseStatistics : BaseModel {
|
||||
private long totalServers;
|
||||
private long totalChannels;
|
||||
private long totalUsers;
|
||||
@ -10,29 +9,22 @@ namespace DHT.Server.Database {
|
||||
|
||||
public long TotalServers {
|
||||
get => totalServers;
|
||||
internal set => Change(out totalServers, value);
|
||||
internal set => Change(ref totalServers, value);
|
||||
}
|
||||
|
||||
public long TotalChannels {
|
||||
get => totalChannels;
|
||||
internal set => Change(out totalChannels, value);
|
||||
internal set => Change(ref totalChannels, value);
|
||||
}
|
||||
|
||||
public long TotalUsers {
|
||||
get => totalUsers;
|
||||
internal set => Change(out totalUsers, value);
|
||||
internal set => Change(ref totalUsers, value);
|
||||
}
|
||||
|
||||
public long TotalMessages {
|
||||
get => totalMessages;
|
||||
internal set => Change(out totalMessages, value);
|
||||
}
|
||||
|
||||
public event PropertyChangedEventHandler? PropertyChanged;
|
||||
|
||||
private void Change<T>(out T field, T value, [CallerMemberName] string? propertyName = null) {
|
||||
field = value;
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
|
||||
internal set => Change(ref totalMessages, value);
|
||||
}
|
||||
|
||||
public DatabaseStatistics Clone() {
|
||||
|
@ -1,8 +1,8 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using DHT.Server.Database.Exceptions;
|
||||
using DHT.Server.Database.Sqlite.Utils;
|
||||
using DHT.Utils.Logging;
|
||||
using Microsoft.Data.Sqlite;
|
||||
|
||||
namespace DHT.Server.Database.Sqlite {
|
||||
sealed class Schema {
|
||||
@ -10,20 +10,14 @@ namespace DHT.Server.Database.Sqlite {
|
||||
|
||||
private static readonly Log Log = Log.ForType<Schema>();
|
||||
|
||||
private readonly SqliteConnection conn;
|
||||
private readonly ISqliteConnection conn;
|
||||
|
||||
public Schema(SqliteConnection conn) {
|
||||
public Schema(ISqliteConnection conn) {
|
||||
this.conn = conn;
|
||||
}
|
||||
|
||||
private SqliteCommand Sql(string sql) {
|
||||
var cmd = conn.CreateCommand();
|
||||
cmd.CommandText = sql;
|
||||
return cmd;
|
||||
}
|
||||
|
||||
private void Execute(string sql) {
|
||||
Sql(sql).ExecuteNonQuery();
|
||||
conn.Command(sql).ExecuteNonQuery();
|
||||
}
|
||||
|
||||
public async Task<bool> Setup(Func<Task<bool>> checkCanUpgradeSchemas) {
|
||||
|
@ -5,46 +5,57 @@ using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using DHT.Server.Data;
|
||||
using DHT.Server.Data.Filters;
|
||||
using DHT.Server.Database.Sqlite.Utils;
|
||||
using DHT.Utils.Collections;
|
||||
using DHT.Utils.Logging;
|
||||
using Microsoft.Data.Sqlite;
|
||||
|
||||
namespace DHT.Server.Database.Sqlite {
|
||||
public sealed class SqliteDatabaseFile : IDatabaseFile {
|
||||
private const int DefaultPoolSize = 5;
|
||||
|
||||
public static async Task<SqliteDatabaseFile?> OpenOrCreate(string path, Func<Task<bool>> checkCanUpgradeSchemas) {
|
||||
string connectionString = new SqliteConnectionStringBuilder {
|
||||
var connectionString = new SqliteConnectionStringBuilder {
|
||||
DataSource = path,
|
||||
Mode = SqliteOpenMode.ReadWriteCreate
|
||||
}.ToString();
|
||||
Mode = SqliteOpenMode.ReadWriteCreate,
|
||||
};
|
||||
|
||||
var conn = new SqliteConnection(connectionString);
|
||||
conn.Open();
|
||||
var pool = new SqliteConnectionPool(connectionString, DefaultPoolSize);
|
||||
|
||||
return await new Schema(conn).Setup(checkCanUpgradeSchemas) ? new SqliteDatabaseFile(path, conn) : null;
|
||||
using (var conn = pool.Take()) {
|
||||
if (!await new Schema(conn).Setup(checkCanUpgradeSchemas)) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return new SqliteDatabaseFile(path, pool);
|
||||
}
|
||||
|
||||
public string Path { get; }
|
||||
public DatabaseStatistics Statistics { get; }
|
||||
|
||||
private readonly Log log;
|
||||
private readonly SqliteConnection conn;
|
||||
private readonly SqliteConnectionPool pool;
|
||||
|
||||
private SqliteDatabaseFile(string path, SqliteConnection conn) {
|
||||
private SqliteDatabaseFile(string path, SqliteConnectionPool pool) {
|
||||
this.log = Log.ForType(typeof(SqliteDatabaseFile), System.IO.Path.GetFileName(path));
|
||||
this.conn = conn;
|
||||
this.pool = pool;
|
||||
this.Path = path;
|
||||
this.Statistics = new DatabaseStatistics();
|
||||
UpdateServerStatistics();
|
||||
UpdateChannelStatistics();
|
||||
UpdateUserStatistics();
|
||||
UpdateMessageStatistics();
|
||||
|
||||
using var conn = pool.Take();
|
||||
UpdateServerStatistics(conn);
|
||||
UpdateChannelStatistics(conn);
|
||||
UpdateUserStatistics(conn);
|
||||
UpdateMessageStatistics(conn);
|
||||
}
|
||||
|
||||
public void Dispose() {
|
||||
conn.Dispose();
|
||||
pool.Dispose();
|
||||
}
|
||||
|
||||
public void AddServer(Data.Server server) {
|
||||
using var conn = pool.Take();
|
||||
using var cmd = conn.Upsert("servers", new[] {
|
||||
("id", SqliteType.Integer),
|
||||
("name", SqliteType.Text),
|
||||
@ -55,13 +66,14 @@ namespace DHT.Server.Database.Sqlite {
|
||||
cmd.Set(":name", server.Name);
|
||||
cmd.Set(":type", ServerTypes.ToString(server.Type));
|
||||
cmd.ExecuteNonQuery();
|
||||
UpdateServerStatistics();
|
||||
UpdateServerStatistics(conn);
|
||||
}
|
||||
|
||||
public List<Data.Server> GetAllServers() {
|
||||
var perf = log.Start();
|
||||
var list = new List<Data.Server>();
|
||||
|
||||
using var conn = pool.Take();
|
||||
using var cmd = conn.Command("SELECT id, name, type FROM servers");
|
||||
using var reader = cmd.ExecuteReader();
|
||||
|
||||
@ -78,6 +90,7 @@ namespace DHT.Server.Database.Sqlite {
|
||||
}
|
||||
|
||||
public void AddChannel(Channel channel) {
|
||||
using var conn = pool.Take();
|
||||
using var cmd = conn.Upsert("channels", new[] {
|
||||
("id", SqliteType.Integer),
|
||||
("server", SqliteType.Integer),
|
||||
@ -96,12 +109,13 @@ namespace DHT.Server.Database.Sqlite {
|
||||
cmd.Set(":topic", channel.Topic);
|
||||
cmd.Set(":nsfw", channel.Nsfw);
|
||||
cmd.ExecuteNonQuery();
|
||||
UpdateChannelStatistics();
|
||||
UpdateChannelStatistics(conn);
|
||||
}
|
||||
|
||||
public List<Channel> GetAllChannels() {
|
||||
var list = new List<Channel>();
|
||||
|
||||
using var conn = pool.Take();
|
||||
using var cmd = conn.Command("SELECT id, server, name, parent_id, position, topic, nsfw FROM channels");
|
||||
using var reader = cmd.ExecuteReader();
|
||||
|
||||
@ -121,6 +135,7 @@ namespace DHT.Server.Database.Sqlite {
|
||||
}
|
||||
|
||||
public void AddUsers(User[] users) {
|
||||
using var conn = pool.Take();
|
||||
using var tx = conn.BeginTransaction();
|
||||
using var cmd = conn.Upsert("users", new[] {
|
||||
("id", SqliteType.Integer),
|
||||
@ -138,13 +153,14 @@ namespace DHT.Server.Database.Sqlite {
|
||||
}
|
||||
|
||||
tx.Commit();
|
||||
UpdateUserStatistics();
|
||||
UpdateUserStatistics(conn);
|
||||
}
|
||||
|
||||
public List<User> GetAllUsers() {
|
||||
var perf = log.Start();
|
||||
var list = new List<User>();
|
||||
|
||||
using var conn = pool.Take();
|
||||
using var cmd = conn.Command("SELECT id, name, avatar_url, discriminator FROM users");
|
||||
using var reader = cmd.ExecuteReader();
|
||||
|
||||
@ -162,7 +178,7 @@ namespace DHT.Server.Database.Sqlite {
|
||||
}
|
||||
|
||||
public void AddMessages(Message[] messages) {
|
||||
static SqliteCommand DeleteByMessageId(SqliteConnection conn, string tableName) {
|
||||
static SqliteCommand DeleteByMessageId(ISqliteConnection conn, string tableName) {
|
||||
return conn.Delete(tableName, ("message_id", SqliteType.Integer));
|
||||
}
|
||||
|
||||
@ -171,6 +187,7 @@ namespace DHT.Server.Database.Sqlite {
|
||||
cmd.ExecuteNonQuery();
|
||||
}
|
||||
|
||||
using var conn = pool.Take();
|
||||
using var tx = conn.BeginTransaction();
|
||||
|
||||
using var messageCmd = conn.Upsert("messages", new[] {
|
||||
@ -282,10 +299,11 @@ namespace DHT.Server.Database.Sqlite {
|
||||
}
|
||||
|
||||
tx.Commit();
|
||||
UpdateMessageStatistics();
|
||||
UpdateMessageStatistics(conn);
|
||||
}
|
||||
|
||||
public int CountMessages(MessageFilter? filter = null) {
|
||||
using var conn = pool.Take();
|
||||
using var cmd = conn.Command("SELECT COUNT(*) FROM messages" + filter.GenerateWhereClause());
|
||||
using var reader = cmd.ExecuteReader();
|
||||
|
||||
@ -300,6 +318,7 @@ namespace DHT.Server.Database.Sqlite {
|
||||
var embeds = GetAllEmbeds();
|
||||
var reactions = GetAllReactions();
|
||||
|
||||
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
|
||||
FROM messages m
|
||||
@ -342,16 +361,18 @@ LEFT JOIN replied_to rt ON m.message_id = rt.message_id" + filter.GenerateWhereC
|
||||
.Append("FROM messages")
|
||||
.Append(whereClause);
|
||||
|
||||
using var conn = pool.Take();
|
||||
using var cmd = conn.Command(build.ToString());
|
||||
cmd.ExecuteNonQuery();
|
||||
|
||||
UpdateMessageStatistics();
|
||||
UpdateMessageStatistics(conn);
|
||||
perf.End();
|
||||
}
|
||||
|
||||
private MultiDictionary<ulong, Attachment> GetAllAttachments() {
|
||||
var dict = new MultiDictionary<ulong, Attachment>();
|
||||
|
||||
using var conn = pool.Take();
|
||||
using var cmd = conn.Command("SELECT message_id, attachment_id, name, type, url, size FROM attachments");
|
||||
using var reader = cmd.ExecuteReader();
|
||||
|
||||
@ -373,6 +394,7 @@ LEFT JOIN replied_to rt ON m.message_id = rt.message_id" + filter.GenerateWhereC
|
||||
private MultiDictionary<ulong, Embed> GetAllEmbeds() {
|
||||
var dict = new MultiDictionary<ulong, Embed>();
|
||||
|
||||
using var conn = pool.Take();
|
||||
using var cmd = conn.Command("SELECT message_id, json FROM embeds");
|
||||
using var reader = cmd.ExecuteReader();
|
||||
|
||||
@ -390,6 +412,7 @@ LEFT JOIN replied_to rt ON m.message_id = rt.message_id" + filter.GenerateWhereC
|
||||
private MultiDictionary<ulong, Reaction> GetAllReactions() {
|
||||
var dict = new MultiDictionary<ulong, Reaction>();
|
||||
|
||||
using var conn = pool.Take();
|
||||
using var cmd = conn.Command("SELECT message_id, emoji_id, emoji_name, emoji_flags, count FROM reactions");
|
||||
using var reader = cmd.ExecuteReader();
|
||||
|
||||
@ -407,19 +430,19 @@ LEFT JOIN replied_to rt ON m.message_id = rt.message_id" + filter.GenerateWhereC
|
||||
return dict;
|
||||
}
|
||||
|
||||
private void UpdateServerStatistics() {
|
||||
private void UpdateServerStatistics(ISqliteConnection conn) {
|
||||
Statistics.TotalServers = conn.SelectScalar("SELECT COUNT(*) FROM servers") as long? ?? 0;
|
||||
}
|
||||
|
||||
private void UpdateChannelStatistics() {
|
||||
private void UpdateChannelStatistics(ISqliteConnection conn) {
|
||||
Statistics.TotalChannels = conn.SelectScalar("SELECT COUNT(*) FROM channels") as long? ?? 0;
|
||||
}
|
||||
|
||||
private void UpdateUserStatistics() {
|
||||
private void UpdateUserStatistics(ISqliteConnection conn) {
|
||||
Statistics.TotalUsers = conn.SelectScalar("SELECT COUNT(*) FROM users") as long? ?? 0;
|
||||
}
|
||||
|
||||
private void UpdateMessageStatistics() {
|
||||
private void UpdateMessageStatistics(ISqliteConnection conn) {
|
||||
Statistics.TotalMessages = conn.SelectScalar("SELECT COUNT(*) FROM messages") as long? ?? 0L;
|
||||
}
|
||||
}
|
||||
|
8
app/Server/Database/Sqlite/Utils/ISqliteConnection.cs
Normal file
8
app/Server/Database/Sqlite/Utils/ISqliteConnection.cs
Normal file
@ -0,0 +1,8 @@
|
||||
using System;
|
||||
using Microsoft.Data.Sqlite;
|
||||
|
||||
namespace DHT.Server.Database.Sqlite.Utils {
|
||||
interface ISqliteConnection : IDisposable {
|
||||
SqliteConnection InnerConnection { get; }
|
||||
}
|
||||
}
|
109
app/Server/Database/Sqlite/Utils/SqliteConnectionPool.cs
Normal file
109
app/Server/Database/Sqlite/Utils/SqliteConnectionPool.cs
Normal file
@ -0,0 +1,109 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using DHT.Utils.Logging;
|
||||
using Microsoft.Data.Sqlite;
|
||||
|
||||
namespace DHT.Server.Database.Sqlite.Utils {
|
||||
sealed class SqliteConnectionPool : IDisposable {
|
||||
private static string GetConnectionString(SqliteConnectionStringBuilder connectionStringBuilder) {
|
||||
connectionStringBuilder.Pooling = false;
|
||||
return connectionStringBuilder.ToString();
|
||||
}
|
||||
|
||||
private readonly object monitor = new ();
|
||||
private volatile bool isDisposed;
|
||||
|
||||
private readonly BlockingCollection<PooledConnection> free = new (new ConcurrentStack<PooledConnection>());
|
||||
private readonly List<PooledConnection> used;
|
||||
|
||||
public SqliteConnectionPool(SqliteConnectionStringBuilder connectionStringBuilder, int poolSize) {
|
||||
var connectionString = GetConnectionString(connectionStringBuilder);
|
||||
|
||||
for (int i = 0; i < poolSize; i++) {
|
||||
var conn = new SqliteConnection(connectionString);
|
||||
conn.Open();
|
||||
free.Add(new PooledConnection(this, conn));
|
||||
}
|
||||
|
||||
used = new List<PooledConnection>(poolSize);
|
||||
}
|
||||
|
||||
private void ThrowIfDisposed() {
|
||||
if (isDisposed) {
|
||||
throw new ObjectDisposedException(nameof(SqliteConnectionPool));
|
||||
}
|
||||
}
|
||||
|
||||
public ISqliteConnection Take() {
|
||||
PooledConnection? conn = null;
|
||||
|
||||
while (conn == null) {
|
||||
ThrowIfDisposed();
|
||||
lock (monitor) {
|
||||
if (free.TryTake(out conn, TimeSpan.FromMilliseconds(100))) {
|
||||
used.Add(conn);
|
||||
break;
|
||||
}
|
||||
else {
|
||||
Log.ForType<SqliteConnectionPool>().Warn("Thread " + Thread.CurrentThread.ManagedThreadId + " is starving for connections.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return conn;
|
||||
}
|
||||
|
||||
private void Return(PooledConnection conn) {
|
||||
ThrowIfDisposed();
|
||||
|
||||
lock (monitor) {
|
||||
if (used.Remove(conn)) {
|
||||
free.Add(conn);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose() {
|
||||
if (isDisposed) {
|
||||
return;
|
||||
}
|
||||
|
||||
isDisposed = true;
|
||||
|
||||
lock (monitor) {
|
||||
while (free.TryTake(out var conn)) {
|
||||
Close(conn.InnerConnection);
|
||||
}
|
||||
|
||||
foreach (var conn in used) {
|
||||
Close(conn.InnerConnection);
|
||||
}
|
||||
|
||||
free.Dispose();
|
||||
used.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
private static void Close(SqliteConnection conn) {
|
||||
conn.Close();
|
||||
conn.Dispose();
|
||||
}
|
||||
|
||||
private sealed class PooledConnection : ISqliteConnection {
|
||||
public SqliteConnection InnerConnection { get; }
|
||||
|
||||
private readonly SqliteConnectionPool pool;
|
||||
|
||||
public PooledConnection(SqliteConnectionPool pool, SqliteConnection conn) {
|
||||
this.pool = pool;
|
||||
this.InnerConnection = conn;
|
||||
}
|
||||
|
||||
void IDisposable.Dispose() {
|
||||
pool.Return(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -2,20 +2,24 @@ using System;
|
||||
using System.Linq;
|
||||
using Microsoft.Data.Sqlite;
|
||||
|
||||
namespace DHT.Server.Database.Sqlite {
|
||||
static class SqliteUtils {
|
||||
public static SqliteCommand Command(this SqliteConnection conn, string sql) {
|
||||
var cmd = conn.CreateCommand();
|
||||
namespace DHT.Server.Database.Sqlite.Utils {
|
||||
static class SqliteExtensions {
|
||||
public static SqliteCommand Command(this ISqliteConnection conn, string sql) {
|
||||
var cmd = conn.InnerConnection.CreateCommand();
|
||||
cmd.CommandText = sql;
|
||||
return cmd;
|
||||
}
|
||||
|
||||
public static object? SelectScalar(this SqliteConnection conn, string sql) {
|
||||
public static SqliteTransaction BeginTransaction(this ISqliteConnection conn) {
|
||||
return conn.InnerConnection.BeginTransaction();
|
||||
}
|
||||
|
||||
public static object? SelectScalar(this ISqliteConnection conn, string sql) {
|
||||
using var cmd = conn.Command(sql);
|
||||
return cmd.ExecuteScalar();
|
||||
}
|
||||
|
||||
public static SqliteCommand Insert(this SqliteConnection conn, string tableName, (string Name, SqliteType Type)[] columns) {
|
||||
public static SqliteCommand Insert(this ISqliteConnection conn, string tableName, (string Name, SqliteType Type)[] columns) {
|
||||
string columnNames = string.Join(',', columns.Select(static c => c.Name));
|
||||
string columnParams = string.Join(',', columns.Select(static c => ':' + c.Name));
|
||||
|
||||
@ -26,7 +30,7 @@ namespace DHT.Server.Database.Sqlite {
|
||||
return cmd;
|
||||
}
|
||||
|
||||
public static SqliteCommand Upsert(this SqliteConnection conn, string tableName, (string Name, SqliteType Type)[] columns) {
|
||||
public static SqliteCommand Upsert(this ISqliteConnection conn, string tableName, (string Name, SqliteType Type)[] columns) {
|
||||
string columnNames = string.Join(',', columns.Select(static c => c.Name));
|
||||
string columnParams = string.Join(',', columns.Select(static c => ':' + c.Name));
|
||||
string columnUpdates = string.Join(',', columns.Skip(1).Select(static c => c.Name + " = excluded." + c.Name));
|
||||
@ -40,7 +44,7 @@ namespace DHT.Server.Database.Sqlite {
|
||||
return cmd;
|
||||
}
|
||||
|
||||
public static SqliteCommand Delete(this SqliteConnection conn, string tableName, (string Name, SqliteType Type) column) {
|
||||
public static SqliteCommand Delete(this ISqliteConnection conn, string tableName, (string Name, SqliteType Type) column) {
|
||||
var cmd = conn.Command("DELETE FROM " + tableName + " WHERE " + column.Name + " = :" + column.Name);
|
||||
CreateParameters(cmd, new [] { column });
|
||||
return cmd;
|
@ -93,11 +93,12 @@ namespace DHT.Server.Service {
|
||||
if (Server != null) {
|
||||
Log.Info("Stopping server...");
|
||||
Server.StopAsync().Wait();
|
||||
Server.Dispose();
|
||||
Server = null;
|
||||
|
||||
Log.Info("Server stopped");
|
||||
IsRunning = false;
|
||||
ServerStatusChanged?.Invoke(null, EventArgs.Empty);
|
||||
Server = null;
|
||||
}
|
||||
}
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user