1
0
mirror of https://github.com/chylex/Discord-History-Tracker.git synced 2025-09-15 10:32:09 +02:00

8 Commits

24 changed files with 228 additions and 112 deletions

View File

@@ -249,11 +249,8 @@ sealed partial class MessageFilterPanelModel : ObservableObject, IDisposable {
var checkBoxItems = new List<CheckBoxItem<ulong>>(); var checkBoxItems = new List<CheckBoxItem<ulong>>();
await foreach (var user in state.Db.Users.Get()) { await foreach (var user in state.Db.Users.Get()) {
var name = user.Name;
var discriminator = user.Discriminator;
checkBoxItems.Add(new CheckBoxItem<ulong>(user.Id) { checkBoxItems.Add(new CheckBoxItem<ulong>(user.Id) {
Title = discriminator == null ? name : name + " #" + discriminator, Title = user.DisplayName == null ? user.Name : $"{user.DisplayName} ({user.Name})",
IsChecked = IncludedUsers == null || IncludedUsers.Contains(user.Id) IsChecked = IncludedUsers == null || IncludedUsers.Contains(user.Id)
}); });
} }

View File

@@ -73,6 +73,7 @@ sealed class DebugPageModel {
var users = Enumerable.Range(0, userCount).Select(_ => new User { var users = Enumerable.Range(0, userCount).Select(_ => new User {
Id = RandomId(rand), Id = RandomId(rand),
Name = RandomName("u"), Name = RandomName("u"),
DisplayName = RandomName("u"),
AvatarUrl = null, AvatarUrl = null,
Discriminator = rand.Next(0, 9999).ToString(), Discriminator = rand.Next(0, 9999).ToString(),
}).ToArray(); }).ToArray();

View File

@@ -9,6 +9,8 @@ items:
pattern: "^[0-9]+$" pattern: "^[0-9]+$"
name: name:
type: string type: string
displayName:
type: string
avatar: avatar:
type: string type: string
discriminator: discriminator:

View File

@@ -69,6 +69,7 @@ const STATE = (function() {
* @property {String} id * @property {String} id
* @property {String} username * @property {String} username
* @property {String} discriminator * @property {String} discriminator
* @property {String} [globalName]
* @property {String} [avatar] * @property {String} [avatar]
* @property {Boolean} [bot] * @property {Boolean} [bot]
*/ */
@@ -200,6 +201,10 @@ const STATE = (function() {
name: user.username name: user.username
}; };
if (user.globalName) {
obj.displayName = user.globalName;
}
if (user.avatar) { if (user.avatar) {
obj.avatar = user.avatar; obj.avatar = user.avatar;
} }

View File

@@ -25,6 +25,8 @@
<div id="menu"> <div id="menu">
<button id="btn-settings">Settings</button> <button id="btn-settings">Settings</button>
<div class="splitter"></div>
<div> <!-- needed to stop the select from messing up --> <div> <!-- needed to stop the select from messing up -->
<select id="opt-messages-per-page"> <select id="opt-messages-per-page">
<option value="50">50 messages per page&nbsp;</option> <option value="50">50 messages per page&nbsp;</option>

View File

@@ -1,6 +1,7 @@
import discord from "./discord.mjs"; import discord from "./discord.mjs";
import gui from "./gui.mjs"; import gui from "./gui.mjs";
import state from "./state.mjs"; import state from "./state.mjs";
import "./polyfills.mjs";
window.DISCORD = discord; window.DISCORD = discord;

View File

@@ -80,7 +80,7 @@ export default (function() {
processed = processed processed = processed
.replace(regex.formatUrl, "<a href='$1' target='_blank' rel='noreferrer'>$1</a>") .replace(regex.formatUrl, "<a href='$1' target='_blank' rel='noreferrer'>$1</a>")
.replace(regex.mentionChannel, (full, match) => "<span class='link mention-chat'>#" + state.getChannelName(match) + "</span>") .replace(regex.mentionChannel, (full, match) => "<span class='link mention-chat'>#" + state.getChannelName(match) + "</span>")
.replace(regex.mentionUser, (full, match) => "<span class='link mention-user' title='#" + (state.getUserTag(match) || "????") + "'>@" + state.getUserName(match) + "</span>") .replace(regex.mentionUser, (full, match) => "<span class='link mention-user' title='" + state.getUserName(match) + "'>@" + state.getUserDisplayName(match) + "</span>")
.replace(regex.customEmojiStatic, (full, m1, m2) => getEmoji(m1, m2, "webp")) .replace(regex.customEmojiStatic, (full, m1, m2) => getEmoji(m1, m2, "webp"))
.replace(regex.customEmojiAnimated, (full, m1, m2) => getEmoji(m1, m2, animatedEmojiExtension)); .replace(regex.customEmojiAnimated, (full, m1, m2) => getEmoji(m1, m2, animatedEmojiExtension));
@@ -129,7 +129,7 @@ export default (function() {
templateMessageNoAvatar = new template([ templateMessageNoAvatar = new template([
"<div>", "<div>",
"<div class='reply-message'>{reply}</div>", "<div class='reply-message'>{reply}</div>",
"<h2><strong class='username' title='#{user.tag}'>{user.name}</strong><span class='info time'>{timestamp}</span>{edit}{jump}</h2>", "<h2><strong class='username' title='{user.name}'>{user.displayName}</strong><span class='info time'>{timestamp}</span>{edit}{jump}</h2>",
"<div class='message'>{contents}{embeds}{attachments}</div>", "<div class='message'>{contents}{embeds}{attachments}</div>",
"{reactions}", "{reactions}",
"</div>" "</div>"
@@ -141,7 +141,7 @@ export default (function() {
"<div class='avatar-wrapper'>", "<div class='avatar-wrapper'>",
"<div class='avatar'>{avatar}</div>", "<div class='avatar'>{avatar}</div>",
"<div>", "<div>",
"<h2><strong class='username' title='#{user.tag}'>{user.name}</strong><span class='info time'>{timestamp}</span>{edit}{jump}</h2>", "<h2><strong class='username' title='{user.name}'>{user.displayName}</strong><span class='info time'>{timestamp}</span>{edit}{jump}</h2>",
"<div class='message'>{contents}{embeds}{attachments}</div>", "<div class='message'>{contents}{embeds}{attachments}</div>",
"{reactions}", "{reactions}",
"</div>", "</div>",
@@ -227,8 +227,8 @@ export default (function() {
if (property === "avatar") { if (property === "avatar") {
return value ? templateUserAvatar.apply(getAvatarUrlObject(value)) : ""; return value ? templateUserAvatar.apply(getAvatarUrlObject(value)) : "";
} }
else if (property === "user.tag") { else if (property === "user.displayName") {
return value ? value : "????"; return value ? value : message.user.name;
} }
else if (property === "timestamp") { else if (property === "timestamp") {
return dom.getHumanReadableTime(value); return dom.getHumanReadableTime(value);
@@ -292,7 +292,7 @@ export default (function() {
return value === null ? "<span class='reply-contents reply-missing'>(replies to an unknown message)</span>" : ""; return value === null ? "<span class='reply-contents reply-missing'>(replies to an unknown message)</span>" : "";
} }
const user = "<span class='reply-username' title='#" + (value.user.tag ? value.user.tag : "????") + "'>" + value.user.name + "</span>"; const user = "<span class='reply-username' title='" + value.user.name + "'>" + (value.user.displayName ?? value.user.name) + "</span>";
const avatar = settings.enableUserAvatars && value.avatar ? "<span class='reply-avatar'>" + templateUserAvatar.apply(getAvatarUrlObject(value.avatar)) + "</span>" : ""; const avatar = settings.enableUserAvatars && value.avatar ? "<span class='reply-avatar'>" + templateUserAvatar.apply(getAvatarUrlObject(value.avatar)) + "</span>" : "";
const contents = value.contents ? "<span class='reply-contents'>" + processMessageContents(value.contents) + "</span>" : ""; const contents = value.contents ? "<span class='reply-contents'>" + processMessageContents(value.contents) + "</span>" : "";

View File

@@ -243,10 +243,11 @@ export default (function() {
const options = []; const options = [];
for (const key of Object.keys(users)) { for (const id of Object.keys(users)) {
const user = users[id];
const option = document.createElement("option"); const option = document.createElement("option");
option.value = key; option.value = id;
option.text = users[key].name; option.text = user.displayName ? `${user.displayName} (${user.name})` : user.name;
options.push(option); options.push(option);
} }

View File

@@ -0,0 +1,35 @@
// https://gist.github.com/MattiasBuelens/496fc1d37adb50a733edd43853f2f60e/088f061ab79b296f29225467ae9ba86ff990195d
ReadableStream.prototype.values ??= function({ preventCancel = false } = {}) {
const reader = this.getReader();
return {
async next() {
try {
const result = await reader.read();
if (result.done) {
reader.releaseLock();
}
return result;
} catch (e) {
reader.releaseLock();
throw e;
}
},
async return(value) {
if (!preventCancel) {
const cancelPromise = reader.cancel(value);
reader.releaseLock();
await cancelPromise;
}
else {
reader.releaseLock();
}
return { done: true, value };
},
[Symbol.asyncIterator]() {
return this;
}
};
};
ReadableStream.prototype[Symbol.asyncIterator] ??= ReadableStream.prototype.values;

View File

@@ -316,16 +316,16 @@ export default (function() {
return (channelObj && channelObj.name) || channel; return (channelObj && channelObj.name) || channel;
}, },
getUserTag(user) {
const userObj = loadedFileMeta.users[user];
return (userObj && userObj.tag) || "????";
},
getUserName(user) { getUserName(user) {
const userObj = loadedFileMeta.users[user]; const userObj = loadedFileMeta.users[user];
return (userObj && userObj.name) || user; return (userObj && userObj.name) || user;
}, },
getUserDisplayName(user) {
const userObj = loadedFileMeta.users[user];
return (userObj && (userObj.displayName || userObj.name)) || user;
},
selectChannel(channel) { selectChannel(channel) {
currentPage = 1; currentPage = 1;
selectedChannel = channel; selectedChannel = channel;

View File

@@ -1,15 +1,16 @@
#menu { #menu {
width: 100%;
height: 48px;
display: flex; display: flex;
flex-direction: row; flex-direction: row;
align-items: stretch;
gap: 8px;
padding: 8px;
background-color: #17181c; background-color: #17181c;
border-bottom: 1px dotted #5d626b; border-bottom: 1px dotted #5d626b;
} }
#menu .splitter { #menu .splitter {
width: 1px; flex: 0 0 1px;
margin: 9px 4px; margin: 9px 1px;
background-color: #5d626b; background-color: #5d626b;
} }
@@ -23,7 +24,8 @@
} }
#menu button, #menu select, #menu input[type="text"] { #menu button, #menu select, #menu input[type="text"] {
margin: 8px; height: 31px;
padding: 0 10px;
background-color: #7289da; background-color: #7289da;
color: #fff; color: #fff;
text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.75); text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.75);
@@ -31,28 +33,25 @@
#menu button { #menu button {
font-size: 17px; font-size: 17px;
padding: 0 12px;
border: 0; border: 0;
cursor: pointer; cursor: pointer;
white-space: nowrap;
} }
#menu select { #menu select {
font-size: 14px; font-size: 14px;
padding: 6px;
border: 0; border: 0;
cursor: pointer; cursor: pointer;
} }
#menu input[type="text"] { #menu input[type="text"] {
font-size: 14px; font-size: 14px;
padding: 7px 12px;
border: 0; border: 0;
} }
#menu .nav { #menu .nav {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
margin: 0 8px;
} }
#menu .nav > button { #menu .nav > button {
@@ -66,7 +65,7 @@
} }
#menu .nav > button, #menu .nav > p { #menu .nav > button, #menu .nav > p {
margin: 8px 1px; margin: 0 1px;
} }
#opt-filter-list > select, #opt-filter-list > input { #opt-filter-list > select, #opt-filter-list > input {
@@ -76,3 +75,7 @@
#opt-filter-list > .active { #opt-filter-list > .active {
display: block; display: block;
} }
#btn-about {
margin-left: auto;
}

View File

@@ -3,6 +3,7 @@ namespace DHT.Server.Data;
public readonly struct User { public readonly struct User {
public ulong Id { get; init; } public ulong Id { get; init; }
public string Name { get; init; } public string Name { get; init; }
public string? DisplayName { get; init; }
public string? AvatarUrl { get; init; } public string? AvatarUrl { get; init; }
public string? Discriminator { get; init; } public string? Discriminator { get; init; }
} }

View File

@@ -14,10 +14,10 @@ static class ViewerJson {
public required string Name { get; init; } public required string Name { get; init; }
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string? Avatar { get; init; } public string? DisplayName { get; init; }
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string? Tag { get; init; } public string? Avatar { get; init; }
} }
public sealed class JsonServer { public sealed class JsonServer {

View File

@@ -68,8 +68,8 @@ static class ViewerJsonExport {
await foreach (var user in db.Users.Get(cancellationToken)) { await foreach (var user in db.Users.Get(cancellationToken)) {
users[user.Id] = new ViewerJson.JsonUser { users[user.Id] = new ViewerJson.JsonUser {
Name = user.Name, Name = user.Name,
DisplayName = user.DisplayName,
Avatar = user.AvatarUrl, Avatar = user.AvatarUrl,
Tag = user.Discriminator
}; };
} }

View File

@@ -20,7 +20,7 @@ sealed class SqliteDownloadRepository(SqliteConnectionPool pool) : BaseSqliteRep
internal sealed class NewDownloadCollector : IAsyncDisposable { internal sealed class NewDownloadCollector : IAsyncDisposable {
private readonly SqliteDownloadRepository repository; private readonly SqliteDownloadRepository repository;
private bool hasAdded = false; private bool hasChanged = false;
private readonly SqliteCommand metadataCmd; private readonly SqliteCommand metadataCmd;
@@ -31,7 +31,16 @@ sealed class SqliteDownloadRepository(SqliteConnectionPool pool) : BaseSqliteRep
""" """
INSERT INTO download_metadata (normalized_url, download_url, status, type, size) INSERT INTO download_metadata (normalized_url, download_url, status, type, size)
VALUES (:normalized_url, :download_url, :status, :type, :size) VALUES (:normalized_url, :download_url, :status, :type, :size)
ON CONFLICT DO NOTHING ON CONFLICT (normalized_url)
DO UPDATE SET
download_url = excluded.download_url,
type = IFNULL(excluded.type, type),
size = IFNULL(excluded.size, size)
WHERE status != :success
AND (download_url != excluded.download_url
OR (excluded.type IS NOT NULL AND type IS NOT excluded.type)
OR (excluded.size IS NOT NULL AND size IS NOT excluded.size)
)
""" """
); );
metadataCmd.Add(":normalized_url", SqliteType.Text); metadataCmd.Add(":normalized_url", SqliteType.Text);
@@ -39,6 +48,7 @@ sealed class SqliteDownloadRepository(SqliteConnectionPool pool) : BaseSqliteRep
metadataCmd.Add(":status", SqliteType.Integer); metadataCmd.Add(":status", SqliteType.Integer);
metadataCmd.Add(":type", SqliteType.Text); metadataCmd.Add(":type", SqliteType.Text);
metadataCmd.Add(":size", SqliteType.Integer); metadataCmd.Add(":size", SqliteType.Integer);
metadataCmd.AddAndSet(":success", SqliteType.Integer, (int) DownloadStatus.Success);
} }
public async Task Add(Data.Download download) { public async Task Add(Data.Download download) {
@@ -47,11 +57,11 @@ sealed class SqliteDownloadRepository(SqliteConnectionPool pool) : BaseSqliteRep
metadataCmd.Set(":status", (int) download.Status); metadataCmd.Set(":status", (int) download.Status);
metadataCmd.Set(":type", download.Type); metadataCmd.Set(":type", download.Type);
metadataCmd.Set(":size", download.Size); metadataCmd.Set(":size", download.Size);
hasAdded |= await metadataCmd.ExecuteNonQueryAsync() > 0; hasChanged |= await metadataCmd.ExecuteNonQueryAsync() > 0;
} }
public void OnCommitted() { public void OnCommitted() {
if (hasAdded) { if (hasChanged) {
repository.UpdateTotalCount(); repository.UpdateTotalCount();
} }
} }
@@ -90,7 +100,8 @@ sealed class SqliteDownloadRepository(SqliteConnectionPool pool) : BaseSqliteRep
""" """
INSERT INTO download_blobs (normalized_url, blob) INSERT INTO download_blobs (normalized_url, blob)
VALUES (:normalized_url, ZEROBLOB(:blob_length)) VALUES (:normalized_url, ZEROBLOB(:blob_length))
ON CONFLICT (normalized_url) DO UPDATE SET blob = excluded.blob ON CONFLICT (normalized_url)
DO UPDATE SET blob = excluded.blob
RETURNING rowid RETURNING rowid
""" """
); );

View File

@@ -14,16 +14,8 @@ using Microsoft.Data.Sqlite;
namespace DHT.Server.Database.Sqlite.Repositories; namespace DHT.Server.Database.Sqlite.Repositories;
sealed class SqliteMessageRepository : BaseSqliteRepository, IMessageRepository { sealed class SqliteMessageRepository(SqliteConnectionPool pool, SqliteDownloadRepository downloads) : BaseSqliteRepository(Log), IMessageRepository {
private static readonly Log Log = Log.ForType<SqliteMessageRepository>(); private static readonly Log Log = Log.ForType<SqliteMessageRepository>();
private readonly SqliteConnectionPool pool;
private readonly SqliteDownloadRepository downloads;
public SqliteMessageRepository(SqliteConnectionPool pool, SqliteDownloadRepository downloads) : base(Log) {
this.pool = pool;
this.downloads = downloads;
}
public async Task Add(IReadOnlyList<Message> messages) { public async Task Add(IReadOnlyList<Message> messages) {
if (messages.Count == 0) { if (messages.Count == 0) {
@@ -50,25 +42,7 @@ sealed class SqliteMessageRepository : BaseSqliteRepository, IMessageRepository
("timestamp", SqliteType.Integer) ("timestamp", SqliteType.Integer)
]); ]);
await using var deleteEditTimestampCmd = DeleteByMessageId(conn, "edit_timestamps"); await using var attachmentCmd = conn.Upsert("attachments", [
await using var deleteRepliedToCmd = DeleteByMessageId(conn, "replied_to");
await using var deleteAttachmentsCmd = DeleteByMessageId(conn, "attachments");
await using var deleteEmbedsCmd = DeleteByMessageId(conn, "embeds");
await using var deleteReactionsCmd = DeleteByMessageId(conn, "reactions");
await using var editTimestampCmd = conn.Insert("edit_timestamps", [
("message_id", SqliteType.Integer),
("edit_timestamp", SqliteType.Integer)
]);
await using var repliedToCmd = conn.Insert("replied_to", [
("message_id", SqliteType.Integer),
("replied_to_id", SqliteType.Integer)
]);
await using var attachmentCmd = conn.Insert("attachments", [
("message_id", SqliteType.Integer),
("attachment_id", SqliteType.Integer), ("attachment_id", SqliteType.Integer),
("name", SqliteType.Text), ("name", SqliteType.Text),
("type", SqliteType.Text), ("type", SqliteType.Text),
@@ -79,19 +53,41 @@ sealed class SqliteMessageRepository : BaseSqliteRepository, IMessageRepository
("height", SqliteType.Integer) ("height", SqliteType.Integer)
]); ]);
await using var embedCmd = conn.Insert("embeds", [ await using var deleteMessageEditTimestampCmd = DeleteByMessageId(conn, "message_edit_timestamps");
await using var deleteMessageRepliedToCmd = DeleteByMessageId(conn, "message_replied_to");
await using var deleteMessageAttachmentsCmd = DeleteByMessageId(conn, "message_attachments");
await using var deleteMessageEmbedsCmd = DeleteByMessageId(conn, "message_embeds");
await using var deleteMessageReactionsCmd = DeleteByMessageId(conn, "message_reactions");
await using var messageEditTimestampCmd = conn.Insert("message_edit_timestamps", [
("message_id", SqliteType.Integer),
("edit_timestamp", SqliteType.Integer)
]);
await using var messageRepliedToCmd = conn.Insert("message_replied_to", [
("message_id", SqliteType.Integer),
("replied_to_id", SqliteType.Integer)
]);
await using var messageAttachmentCmd = conn.Insert("message_attachments", [
("message_id", SqliteType.Integer),
("attachment_id", SqliteType.Integer)
]);
await using var messageEmbedCmd = conn.Insert("message_embeds", [
("message_id", SqliteType.Integer), ("message_id", SqliteType.Integer),
("json", SqliteType.Text) ("json", SqliteType.Text)
]); ]);
await using var reactionCmd = conn.Insert("reactions", [ await using var messageReactionCmd = conn.Insert("message_reactions", [
("message_id", SqliteType.Integer), ("message_id", SqliteType.Integer),
("emoji_id", SqliteType.Integer), ("emoji_id", SqliteType.Integer),
("emoji_name", SqliteType.Text), ("emoji_name", SqliteType.Text),
("emoji_flags", SqliteType.Integer), ("emoji_flags", SqliteType.Integer),
("count", SqliteType.Integer) ("count", SqliteType.Integer)
]); ]);
await using var downloadCollector = new SqliteDownloadRepository.NewDownloadCollector(downloads, conn); await using var downloadCollector = new SqliteDownloadRepository.NewDownloadCollector(downloads, conn);
foreach (var message in messages) { foreach (var message in messages) {
@@ -104,29 +100,30 @@ sealed class SqliteMessageRepository : BaseSqliteRepository, IMessageRepository
messageCmd.Set(":timestamp", message.Timestamp); messageCmd.Set(":timestamp", message.Timestamp);
await messageCmd.ExecuteNonQueryAsync(); await messageCmd.ExecuteNonQueryAsync();
await ExecuteDeleteByMessageId(deleteEditTimestampCmd, messageId); await ExecuteDeleteByMessageId(deleteMessageEditTimestampCmd, messageId);
await ExecuteDeleteByMessageId(deleteRepliedToCmd, messageId); await ExecuteDeleteByMessageId(deleteMessageRepliedToCmd, messageId);
await ExecuteDeleteByMessageId(deleteAttachmentsCmd, messageId); await ExecuteDeleteByMessageId(deleteMessageAttachmentsCmd, messageId);
await ExecuteDeleteByMessageId(deleteEmbedsCmd, messageId); await ExecuteDeleteByMessageId(deleteMessageEmbedsCmd, messageId);
await ExecuteDeleteByMessageId(deleteReactionsCmd, messageId); await ExecuteDeleteByMessageId(deleteMessageReactionsCmd, messageId);
if (message.EditTimestamp is {} timestamp) { if (message.EditTimestamp is {} timestamp) {
editTimestampCmd.Set(":message_id", messageId); messageEditTimestampCmd.Set(":message_id", messageId);
editTimestampCmd.Set(":edit_timestamp", timestamp); messageEditTimestampCmd.Set(":edit_timestamp", timestamp);
await editTimestampCmd.ExecuteNonQueryAsync(); await messageEditTimestampCmd.ExecuteNonQueryAsync();
} }
if (message.RepliedToId is {} repliedToId) { if (message.RepliedToId is {} repliedToId) {
repliedToCmd.Set(":message_id", messageId); messageRepliedToCmd.Set(":message_id", messageId);
repliedToCmd.Set(":replied_to_id", repliedToId); messageRepliedToCmd.Set(":replied_to_id", repliedToId);
await repliedToCmd.ExecuteNonQueryAsync(); await messageRepliedToCmd.ExecuteNonQueryAsync();
} }
if (!message.Attachments.IsEmpty) { if (!message.Attachments.IsEmpty) {
foreach (var attachment in message.Attachments) { foreach (var attachment in message.Attachments) {
attachmentCmd.Set(":message_id", messageId); object attachmentId = attachment.Id;
attachmentCmd.Set(":attachment_id", attachment.Id);
attachmentCmd.Set(":attachment_id", attachmentId);
attachmentCmd.Set(":name", attachment.Name); attachmentCmd.Set(":name", attachment.Name);
attachmentCmd.Set(":type", attachment.Type); attachmentCmd.Set(":type", attachment.Type);
attachmentCmd.Set(":normalized_url", attachment.NormalizedUrl); attachmentCmd.Set(":normalized_url", attachment.NormalizedUrl);
@@ -135,16 +132,20 @@ sealed class SqliteMessageRepository : BaseSqliteRepository, IMessageRepository
attachmentCmd.Set(":width", attachment.Width); attachmentCmd.Set(":width", attachment.Width);
attachmentCmd.Set(":height", attachment.Height); attachmentCmd.Set(":height", attachment.Height);
await attachmentCmd.ExecuteNonQueryAsync(); await attachmentCmd.ExecuteNonQueryAsync();
messageAttachmentCmd.Set(":message_id", messageId);
messageAttachmentCmd.Set(":attachment_id", attachmentId);
await messageAttachmentCmd.ExecuteNonQueryAsync();
await downloadCollector.Add(DownloadLinkExtractor.FromAttachment(attachment)); await downloadCollector.Add(DownloadLinkExtractor.FromAttachment(attachment));
} }
} }
if (!message.Embeds.IsEmpty) { if (!message.Embeds.IsEmpty) {
foreach (var embed in message.Embeds) { foreach (var embed in message.Embeds) {
embedCmd.Set(":message_id", messageId); messageEmbedCmd.Set(":message_id", messageId);
embedCmd.Set(":json", embed.Json); messageEmbedCmd.Set(":json", embed.Json);
await embedCmd.ExecuteNonQueryAsync(); await messageEmbedCmd.ExecuteNonQueryAsync();
if (DownloadLinkExtractor.TryFromEmbedJson(embed.Json) is {} download) { if (DownloadLinkExtractor.TryFromEmbedJson(embed.Json) is {} download) {
await downloadCollector.Add(download); await downloadCollector.Add(download);
@@ -154,12 +155,12 @@ sealed class SqliteMessageRepository : BaseSqliteRepository, IMessageRepository
if (!message.Reactions.IsEmpty) { if (!message.Reactions.IsEmpty) {
foreach (var reaction in message.Reactions) { foreach (var reaction in message.Reactions) {
reactionCmd.Set(":message_id", messageId); messageReactionCmd.Set(":message_id", messageId);
reactionCmd.Set(":emoji_id", reaction.EmojiId); messageReactionCmd.Set(":emoji_id", reaction.EmojiId);
reactionCmd.Set(":emoji_name", reaction.EmojiName); messageReactionCmd.Set(":emoji_name", reaction.EmojiName);
reactionCmd.Set(":emoji_flags", (int) reaction.EmojiFlags); messageReactionCmd.Set(":emoji_flags", (int) reaction.EmojiFlags);
reactionCmd.Set(":count", reaction.Count); messageReactionCmd.Set(":count", reaction.Count);
await reactionCmd.ExecuteNonQueryAsync(); await messageReactionCmd.ExecuteNonQueryAsync();
if (reaction.EmojiId is {} emojiId) { if (reaction.EmojiId is {} emojiId) {
await downloadCollector.Add(DownloadLinkExtractor.FromEmoji(emojiId, reaction.EmojiFlags)); await downloadCollector.Add(DownloadLinkExtractor.FromEmoji(emojiId, reaction.EmojiFlags));
@@ -178,7 +179,7 @@ sealed class SqliteMessageRepository : BaseSqliteRepository, IMessageRepository
public override Task<long> Count(CancellationToken cancellationToken) { public override Task<long> Count(CancellationToken cancellationToken) {
return Count(filter: null, cancellationToken); return Count(filter: null, cancellationToken);
} }
public async Task<long> Count(MessageFilter? filter, CancellationToken cancellationToken) { public async Task<long> Count(MessageFilter? filter, CancellationToken cancellationToken) {
await using var conn = await pool.Take(); await using var conn = await pool.Take();
return await conn.ExecuteReaderAsync("SELECT COUNT(*) FROM messages" + filter.GenerateConditions().BuildWhereClause(), static reader => reader?.GetInt64(0) ?? 0L, cancellationToken); return await conn.ExecuteReaderAsync("SELECT COUNT(*) FROM messages" + filter.GenerateConditions().BuildWhereClause(), static reader => reader?.GetInt64(0) ?? 0L, cancellationToken);
@@ -221,7 +222,8 @@ sealed class SqliteMessageRepository : BaseSqliteRepository, IMessageRepository
""" """
SELECT attachment_id, name, type, normalized_url, download_url, size, width, height SELECT attachment_id, name, type, normalized_url, download_url, size, width, height
FROM attachments FROM attachments
WHERE message_id = :message_id JOIN message_attachments USING (attachment_id)
WHERE message_attachments.message_id = :message_id
"""; """;
await using var attachmentCmd = new MessageToManyCommand<Attachment>(conn, AttachmentSql, static reader => new Attachment { await using var attachmentCmd = new MessageToManyCommand<Attachment>(conn, AttachmentSql, static reader => new Attachment {
@@ -238,7 +240,7 @@ sealed class SqliteMessageRepository : BaseSqliteRepository, IMessageRepository
const string EmbedSql = const string EmbedSql =
""" """
SELECT json SELECT json
FROM embeds FROM message_embeds
WHERE message_id = :message_id WHERE message_id = :message_id
"""; """;
@@ -249,7 +251,7 @@ sealed class SqliteMessageRepository : BaseSqliteRepository, IMessageRepository
const string ReactionSql = const string ReactionSql =
""" """
SELECT emoji_id, emoji_name, emoji_flags, count SELECT emoji_id, emoji_name, emoji_flags, count
FROM reactions FROM message_reactions
WHERE message_id = :message_id WHERE message_id = :message_id
"""; """;
@@ -262,10 +264,10 @@ sealed class SqliteMessageRepository : BaseSqliteRepository, IMessageRepository
await using var messageCmd = conn.Command( await using var messageCmd = 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, m.text, m.timestamp, met.edit_timestamp, mrt.replied_to_id
FROM messages m FROM messages m
LEFT JOIN edit_timestamps et ON m.message_id = et.message_id LEFT JOIN message_edit_timestamps met ON m.message_id = met.message_id
LEFT JOIN replied_to rt ON m.message_id = rt.message_id LEFT JOIN message_replied_to mrt ON m.message_id = mrt.message_id
{filter.GenerateConditions("m").BuildWhereClause()} {filter.GenerateConditions("m").BuildWhereClause()}
""" """
); );
@@ -292,7 +294,7 @@ sealed class SqliteMessageRepository : BaseSqliteRepository, IMessageRepository
public async IAsyncEnumerable<ulong> GetIds(MessageFilter? filter) { public async IAsyncEnumerable<ulong> GetIds(MessageFilter? filter) {
await using var conn = await pool.Take(); await using var conn = await pool.Take();
await using var cmd = conn.Command("SELECT message_id FROM messages" + filter.GenerateConditions().BuildWhereClause()); await using var cmd = conn.Command("SELECT message_id FROM messages" + filter.GenerateConditions().BuildWhereClause());
await using var reader = await cmd.ExecuteReaderAsync(); await using var reader = await cmd.ExecuteReaderAsync();

View File

@@ -29,6 +29,7 @@ sealed class SqliteUserRepository : BaseSqliteRepository, IUserRepository {
await using var cmd = conn.Upsert("users", [ await using var cmd = conn.Upsert("users", [
("id", SqliteType.Integer), ("id", SqliteType.Integer),
("name", SqliteType.Text), ("name", SqliteType.Text),
("display_name", SqliteType.Text),
("avatar_url", SqliteType.Text), ("avatar_url", SqliteType.Text),
("discriminator", SqliteType.Text) ("discriminator", SqliteType.Text)
]); ]);
@@ -38,6 +39,7 @@ sealed class SqliteUserRepository : BaseSqliteRepository, IUserRepository {
foreach (var user in users) { foreach (var user in users) {
cmd.Set(":id", user.Id); cmd.Set(":id", user.Id);
cmd.Set(":name", user.Name); cmd.Set(":name", user.Name);
cmd.Set(":display_name", user.DisplayName);
cmd.Set(":avatar_url", user.AvatarUrl); cmd.Set(":avatar_url", user.AvatarUrl);
cmd.Set(":discriminator", user.Discriminator); cmd.Set(":discriminator", user.Discriminator);
await cmd.ExecuteNonQueryAsync(); await cmd.ExecuteNonQueryAsync();
@@ -62,15 +64,16 @@ sealed class SqliteUserRepository : BaseSqliteRepository, IUserRepository {
public async IAsyncEnumerable<User> Get([EnumeratorCancellation] CancellationToken cancellationToken) { 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, display_name, avatar_url, discriminator FROM users");
await using var reader = await cmd.ExecuteReaderAsync(cancellationToken); await using var reader = await cmd.ExecuteReaderAsync(cancellationToken);
while (await reader.ReadAsync(cancellationToken)) { 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),
AvatarUrl = reader.IsDBNull(2) ? null : reader.GetString(2), DisplayName = reader.IsDBNull(2) ? null : reader.GetString(2),
Discriminator = reader.IsDBNull(3) ? null : reader.GetString(3), AvatarUrl = reader.IsDBNull(3) ? null : reader.GetString(3),
Discriminator = reader.IsDBNull(4) ? null : reader.GetString(4),
}; };
} }
} }

View File

@@ -8,7 +8,10 @@ sealed class SqliteSchemaUpgradeTo3 : ISchemaUpgrade {
await reporter.MainWork("Applying schema changes...", 0, 1); await reporter.MainWork("Applying schema changes...", 0, 1);
await SqliteSchema.CreateMessageEditTimestampTable(conn); await SqliteSchema.CreateMessageEditTimestampTable(conn);
await conn.ExecuteAsync("ALTER TABLE message_edit_timestamps RENAME TO edit_timestamps");
await SqliteSchema.CreateMessageRepliedToTable(conn); await SqliteSchema.CreateMessageRepliedToTable(conn);
await conn.ExecuteAsync("ALTER TABLE message_replied_to RENAME TO replied_to");
await conn.ExecuteAsync(""" await conn.ExecuteAsync("""
INSERT INTO edit_timestamps (message_id, edit_timestamp) INSERT INTO edit_timestamps (message_id, edit_timestamp)

View File

@@ -0,0 +1,11 @@
using System.Threading.Tasks;
using DHT.Server.Database.Sqlite.Utils;
namespace DHT.Server.Database.Sqlite.Schema;
sealed class SqliteSchemaUpgradeTo8 : ISchemaUpgrade {
async Task ISchemaUpgrade.Run(ISqliteConnection conn, ISchemaUpgradeCallbacks.IProgressReporter reporter) {
await reporter.MainWork("Applying schema changes...", 0, 1);
await conn.ExecuteAsync("ALTER TABLE users ADD display_name TEXT");
}
}

View File

@@ -0,0 +1,23 @@
using System.Threading.Tasks;
using DHT.Server.Database.Sqlite.Utils;
namespace DHT.Server.Database.Sqlite.Schema;
sealed class SqliteSchemaUpgradeTo9 : ISchemaUpgrade {
async Task ISchemaUpgrade.Run(ISqliteConnection conn, ISchemaUpgradeCallbacks.IProgressReporter reporter) {
await reporter.MainWork("Applying schema changes...", 0, 3);
await SqliteSchema.CreateMessageAttachmentsTable(conn);
await reporter.MainWork("Migrating message attachments...", 1, 3);
await conn.ExecuteAsync("INSERT INTO message_attachments (message_id, attachment_id) SELECT message_id, attachment_id FROM attachments");
await reporter.MainWork("Applying schema changes...", 2, 3);
await conn.ExecuteAsync("DROP INDEX attachments_message_ix");
await conn.ExecuteAsync("ALTER TABLE attachments DROP COLUMN message_id");
await conn.ExecuteAsync("ALTER TABLE embeds RENAME TO message_embeds");
await conn.ExecuteAsync("ALTER TABLE edit_timestamps RENAME TO message_edit_timestamps");
await conn.ExecuteAsync("ALTER TABLE replied_to RENAME TO message_replied_to");
await conn.ExecuteAsync("ALTER TABLE reactions RENAME TO message_reactions");
}
}

View File

@@ -8,7 +8,7 @@ using DHT.Utils.Logging;
namespace DHT.Server.Database.Sqlite; namespace DHT.Server.Database.Sqlite;
sealed class SqliteSchema { sealed class SqliteSchema {
internal const int Version = 7; internal const int Version = 9;
private static readonly Log Log = Log.ForType<SqliteSchema>(); private static readonly Log Log = Log.ForType<SqliteSchema>();
@@ -48,6 +48,7 @@ sealed class SqliteSchema {
CREATE TABLE users ( CREATE TABLE users (
id INTEGER PRIMARY KEY NOT NULL, id INTEGER PRIMARY KEY NOT NULL,
name TEXT NOT NULL, name TEXT NOT NULL,
display_name TEXT,
avatar_url TEXT, avatar_url TEXT,
discriminator TEXT discriminator TEXT
) )
@@ -85,7 +86,6 @@ sealed class SqliteSchema {
await conn.ExecuteAsync(""" await conn.ExecuteAsync("""
CREATE TABLE attachments ( CREATE TABLE attachments (
message_id INTEGER NOT NULL,
attachment_id INTEGER NOT NULL PRIMARY KEY NOT NULL, attachment_id INTEGER NOT NULL PRIMARY KEY NOT NULL,
name TEXT NOT NULL, name TEXT NOT NULL,
type TEXT, type TEXT,
@@ -98,14 +98,14 @@ sealed class SqliteSchema {
"""); """);
await conn.ExecuteAsync(""" await conn.ExecuteAsync("""
CREATE TABLE embeds ( CREATE TABLE message_embeds (
message_id INTEGER NOT NULL, message_id INTEGER NOT NULL,
json TEXT NOT NULL json TEXT NOT NULL
) )
"""); """);
await conn.ExecuteAsync(""" await conn.ExecuteAsync("""
CREATE TABLE reactions ( CREATE TABLE message_reactions (
message_id INTEGER NOT NULL, message_id INTEGER NOT NULL,
emoji_id INTEGER, emoji_id INTEGER,
emoji_name TEXT, emoji_name TEXT,
@@ -117,17 +117,17 @@ sealed class SqliteSchema {
await CreateMessageEditTimestampTable(conn); await CreateMessageEditTimestampTable(conn);
await CreateMessageRepliedToTable(conn); await CreateMessageRepliedToTable(conn);
await CreateDownloadTables(conn); await CreateDownloadTables(conn);
await CreateMessageAttachmentsTable(conn);
await conn.ExecuteAsync("CREATE INDEX attachments_message_ix ON attachments(message_id)"); await conn.ExecuteAsync("CREATE INDEX embeds_message_ix ON message_embeds(message_id)");
await conn.ExecuteAsync("CREATE INDEX embeds_message_ix ON embeds(message_id)"); await conn.ExecuteAsync("CREATE INDEX reactions_message_ix ON message_reactions(message_id)");
await conn.ExecuteAsync("CREATE INDEX reactions_message_ix ON reactions(message_id)");
await conn.ExecuteAsync("INSERT INTO metadata (key, value) VALUES ('version', " + Version + ")"); await conn.ExecuteAsync("INSERT INTO metadata (key, value) VALUES ('version', " + Version + ")");
} }
internal static async Task CreateMessageEditTimestampTable(ISqliteConnection conn) { internal static async Task CreateMessageEditTimestampTable(ISqliteConnection conn) {
await conn.ExecuteAsync(""" await conn.ExecuteAsync("""
CREATE TABLE edit_timestamps ( CREATE TABLE message_edit_timestamps (
message_id INTEGER PRIMARY KEY NOT NULL, message_id INTEGER PRIMARY KEY NOT NULL,
edit_timestamp INTEGER NOT NULL edit_timestamp INTEGER NOT NULL
) )
@@ -136,7 +136,7 @@ sealed class SqliteSchema {
internal static async Task CreateMessageRepliedToTable(ISqliteConnection conn) { internal static async Task CreateMessageRepliedToTable(ISqliteConnection conn) {
await conn.ExecuteAsync(""" await conn.ExecuteAsync("""
CREATE TABLE replied_to ( CREATE TABLE message_replied_to (
message_id INTEGER PRIMARY KEY NOT NULL, message_id INTEGER PRIMARY KEY NOT NULL,
replied_to_id INTEGER NOT NULL replied_to_id INTEGER NOT NULL
) )
@@ -162,6 +162,18 @@ sealed class SqliteSchema {
) )
"""); """);
} }
internal static async Task CreateMessageAttachmentsTable(ISqliteConnection conn) {
await conn.ExecuteAsync("""
CREATE TABLE message_attachments (
message_id INTEGER NOT NULL,
attachment_id INTEGER NOT NULL,
PRIMARY KEY (message_id, attachment_id),
FOREIGN KEY (message_id) REFERENCES messages (message_id) ON UPDATE CASCADE ON DELETE CASCADE,
FOREIGN KEY (attachment_id) REFERENCES attachments (attachment_id) ON UPDATE CASCADE ON DELETE CASCADE
)
""");
}
private async Task UpgradeSchemas(int dbVersion, ISchemaUpgradeCallbacks.IProgressReporter reporter) { private async Task UpgradeSchemas(int dbVersion, ISchemaUpgradeCallbacks.IProgressReporter reporter) {
var upgrades = new Dictionary<int, ISchemaUpgrade> { var upgrades = new Dictionary<int, ISchemaUpgrade> {
@@ -171,13 +183,15 @@ sealed class SqliteSchema {
{ 4, new SqliteSchemaUpgradeTo5() }, { 4, new SqliteSchemaUpgradeTo5() },
{ 5, new SqliteSchemaUpgradeTo6() }, { 5, new SqliteSchemaUpgradeTo6() },
{ 6, new SqliteSchemaUpgradeTo7() }, { 6, new SqliteSchemaUpgradeTo7() },
{ 7, new SqliteSchemaUpgradeTo8() },
{ 8, new SqliteSchemaUpgradeTo9() },
}; };
var perf = Log.Start("from version " + dbVersion); var perf = Log.Start("from version " + dbVersion);
for (int fromVersion = dbVersion; fromVersion < Version; fromVersion++) { for (int fromVersion = dbVersion; fromVersion < Version; fromVersion++) {
var toVersion = fromVersion + 1; var toVersion = fromVersion + 1;
if (upgrades.TryGetValue(fromVersion, out var upgrade)) { if (upgrades.TryGetValue(fromVersion, out var upgrade)) {
await upgrade.Run(conn, reporter); await upgrade.Run(conn, reporter);
} }

View File

@@ -30,6 +30,7 @@ sealed class TrackUsersEndpoint(IDatabaseFile db) : BaseEndpoint(db) {
private static User ReadUser(JsonElement json, string path) => new () { private static User ReadUser(JsonElement json, string path) => new () {
Id = json.RequireSnowflake("id", path), Id = json.RequireSnowflake("id", path),
Name = json.RequireString("name", path), Name = json.RequireString("name", path),
DisplayName = json.HasKey("displayName") ? json.RequireString("displayName", path) : null,
AvatarUrl = json.HasKey("avatar") ? json.RequireString("avatar", path) : null, AvatarUrl = json.HasKey("avatar") ? json.RequireString("avatar", path) : null,
Discriminator = json.HasKey("discriminator") ? json.RequireString("discriminator", path) : null Discriminator = json.HasKey("discriminator") ? json.RequireString("discriminator", path) : null
}; };

View File

@@ -8,5 +8,5 @@ using DHT.Utils;
namespace DHT.Utils; namespace DHT.Utils;
static class Version { static class Version {
public const string Tag = "42.0.0.0"; public const string Tag = "42.1.0.0";
} }

Binary file not shown.