mirror of
				https://github.com/chylex/Discord-History-Tracker.git
				synced 2025-10-31 11:17:15 +01:00 
			
		
		
		
	Compare commits
	
		
			8 Commits
		
	
	
		
			v42.0
			...
			1700f99bf7
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 1700f99bf7 | |||
| 84acf5f5d5 | |||
| ae56433836 | |||
| c3d4fa5532 | |||
| a6225b9721 | |||
| 943163473a | |||
| fa00df10d8 | |||
| f54465e5fe | 
| @@ -249,11 +249,8 @@ sealed partial class MessageFilterPanelModel : ObservableObject, IDisposable { | ||||
| 			var checkBoxItems = new List<CheckBoxItem<ulong>>(); | ||||
|  | ||||
| 			await foreach (var user in state.Db.Users.Get()) { | ||||
| 				var name = user.Name; | ||||
| 				var discriminator = user.Discriminator; | ||||
|  | ||||
| 				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) | ||||
| 				}); | ||||
| 			} | ||||
|   | ||||
| @@ -73,6 +73,7 @@ sealed class DebugPageModel { | ||||
| 		var users = Enumerable.Range(0, userCount).Select(_ => new User { | ||||
| 			Id = RandomId(rand), | ||||
| 			Name = RandomName("u"), | ||||
| 			DisplayName = RandomName("u"), | ||||
| 			AvatarUrl = null, | ||||
| 			Discriminator = rand.Next(0, 9999).ToString(), | ||||
| 		}).ToArray(); | ||||
|   | ||||
| @@ -9,6 +9,8 @@ items: | ||||
|           pattern: "^[0-9]+$" | ||||
|         name: | ||||
|           type: string | ||||
|         displayName: | ||||
|           type: string | ||||
|         avatar: | ||||
|           type: string | ||||
|         discriminator: | ||||
|   | ||||
| @@ -69,6 +69,7 @@ const STATE = (function() { | ||||
| 	 * @property {String} id | ||||
| 	 * @property {String} username | ||||
| 	 * @property {String} discriminator | ||||
| 	 * @property {String} [globalName] | ||||
| 	 * @property {String} [avatar] | ||||
| 	 * @property {Boolean} [bot] | ||||
| 	 */ | ||||
| @@ -200,6 +201,10 @@ const STATE = (function() { | ||||
| 						name: user.username | ||||
| 					}; | ||||
| 					 | ||||
| 					if (user.globalName) { | ||||
| 						obj.displayName = user.globalName; | ||||
| 					} | ||||
| 					 | ||||
| 					if (user.avatar) { | ||||
| 						obj.avatar = user.avatar; | ||||
| 					} | ||||
|   | ||||
| @@ -25,6 +25,8 @@ | ||||
|     <div id="menu"> | ||||
|       <button id="btn-settings">Settings</button> | ||||
|        | ||||
|       <div class="splitter"></div> | ||||
|        | ||||
|       <div> <!-- needed to stop the select from messing up --> | ||||
|         <select id="opt-messages-per-page"> | ||||
|           <option value="50">50 messages per page </option> | ||||
|   | ||||
| @@ -1,6 +1,7 @@ | ||||
| import discord from "./discord.mjs"; | ||||
| import gui from "./gui.mjs"; | ||||
| import state from "./state.mjs"; | ||||
| import "./polyfills.mjs"; | ||||
|  | ||||
| window.DISCORD = discord; | ||||
|  | ||||
|   | ||||
| @@ -80,7 +80,7 @@ export default (function() { | ||||
| 		processed = processed | ||||
| 			.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.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.customEmojiAnimated, (full, m1, m2) => getEmoji(m1, m2, animatedEmojiExtension)); | ||||
| 		 | ||||
| @@ -129,7 +129,7 @@ export default (function() { | ||||
| 			templateMessageNoAvatar = new template([ | ||||
| 				"<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>", | ||||
| 				"{reactions}", | ||||
| 				"</div>" | ||||
| @@ -141,7 +141,7 @@ export default (function() { | ||||
| 				"<div class='avatar-wrapper'>", | ||||
| 				"<div class='avatar'>{avatar}</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>", | ||||
| 				"{reactions}", | ||||
| 				"</div>", | ||||
| @@ -227,8 +227,8 @@ export default (function() { | ||||
| 				if (property === "avatar") { | ||||
| 					return value ? templateUserAvatar.apply(getAvatarUrlObject(value)) : ""; | ||||
| 				} | ||||
| 				else if (property === "user.tag") { | ||||
| 					return value ? value : "????"; | ||||
| 				else if (property === "user.displayName") { | ||||
| 					return value ? value : message.user.name; | ||||
| 				} | ||||
| 				else if (property === "timestamp") { | ||||
| 					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>" : ""; | ||||
| 					} | ||||
| 					 | ||||
| 					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 contents = value.contents ? "<span class='reply-contents'>" + processMessageContents(value.contents) + "</span>" : ""; | ||||
| 					 | ||||
|   | ||||
| @@ -243,10 +243,11 @@ export default (function() { | ||||
| 			 | ||||
| 			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"); | ||||
| 				option.value = key; | ||||
| 				option.text = users[key].name; | ||||
| 				option.value = id; | ||||
| 				option.text = user.displayName ? `${user.displayName} (${user.name})` : user.name; | ||||
| 				options.push(option); | ||||
| 			} | ||||
| 			 | ||||
|   | ||||
							
								
								
									
										35
									
								
								app/Resources/Viewer/scripts/polyfills.mjs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								app/Resources/Viewer/scripts/polyfills.mjs
									
									
									
									
									
										Normal 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; | ||||
| @@ -316,16 +316,16 @@ export default (function() { | ||||
| 			return (channelObj && channelObj.name) || channel; | ||||
| 		}, | ||||
| 		 | ||||
| 		getUserTag(user) { | ||||
| 			const userObj = loadedFileMeta.users[user]; | ||||
| 			return (userObj && userObj.tag) || "????"; | ||||
| 		}, | ||||
| 		 | ||||
| 		getUserName(user) { | ||||
| 			const userObj = loadedFileMeta.users[user]; | ||||
| 			return (userObj && userObj.name) || user; | ||||
| 		}, | ||||
| 		 | ||||
| 		getUserDisplayName(user) { | ||||
| 			const userObj = loadedFileMeta.users[user]; | ||||
| 			return (userObj && (userObj.displayName || userObj.name)) || user; | ||||
| 		}, | ||||
| 		 | ||||
| 		selectChannel(channel) { | ||||
| 			currentPage = 1; | ||||
| 			selectedChannel = channel; | ||||
|   | ||||
| @@ -1,15 +1,16 @@ | ||||
| #menu { | ||||
|   width: 100%; | ||||
|   height: 48px; | ||||
|   display: flex; | ||||
|   flex-direction: row; | ||||
|   align-items: stretch; | ||||
|   gap: 8px; | ||||
|   padding: 8px; | ||||
|   background-color: #17181c; | ||||
|   border-bottom: 1px dotted #5d626b; | ||||
| } | ||||
|  | ||||
| #menu .splitter { | ||||
|   width: 1px; | ||||
|   margin: 9px 4px; | ||||
|   flex: 0 0 1px; | ||||
|   margin: 9px 1px; | ||||
|   background-color: #5d626b; | ||||
| } | ||||
|  | ||||
| @@ -23,7 +24,8 @@ | ||||
| } | ||||
|  | ||||
| #menu button, #menu select, #menu input[type="text"] { | ||||
|   margin: 8px; | ||||
|   height: 31px; | ||||
|   padding: 0 10px; | ||||
|   background-color: #7289da; | ||||
|   color: #fff; | ||||
|   text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.75); | ||||
| @@ -31,28 +33,25 @@ | ||||
|  | ||||
| #menu button { | ||||
|   font-size: 17px; | ||||
|   padding: 0 12px; | ||||
|   border: 0; | ||||
|   cursor: pointer; | ||||
|   white-space: nowrap; | ||||
| } | ||||
|  | ||||
| #menu select { | ||||
|   font-size: 14px; | ||||
|   padding: 6px; | ||||
|   border: 0; | ||||
|   cursor: pointer; | ||||
| } | ||||
|  | ||||
| #menu input[type="text"] { | ||||
|   font-size: 14px; | ||||
|   padding: 7px 12px; | ||||
|   border: 0; | ||||
| } | ||||
|  | ||||
| #menu .nav { | ||||
|   display: flex; | ||||
|   flex-direction: row; | ||||
|   margin: 0 8px; | ||||
| } | ||||
|  | ||||
| #menu .nav > button { | ||||
| @@ -66,7 +65,7 @@ | ||||
| } | ||||
|  | ||||
| #menu .nav > button, #menu .nav > p { | ||||
|   margin: 8px 1px; | ||||
|   margin: 0 1px; | ||||
| } | ||||
|  | ||||
| #opt-filter-list > select, #opt-filter-list > input { | ||||
| @@ -76,3 +75,7 @@ | ||||
| #opt-filter-list > .active { | ||||
|   display: block; | ||||
| } | ||||
|  | ||||
| #btn-about { | ||||
|   margin-left: auto; | ||||
| } | ||||
|   | ||||
| @@ -3,6 +3,7 @@ namespace DHT.Server.Data; | ||||
| public readonly struct User { | ||||
| 	public ulong Id { get; init; } | ||||
| 	public string Name { get; init; } | ||||
| 	public string? DisplayName { get; init; } | ||||
| 	public string? AvatarUrl { get; init; } | ||||
| 	public string? Discriminator { get; init; } | ||||
| } | ||||
|   | ||||
| @@ -14,10 +14,10 @@ static class ViewerJson { | ||||
| 		public required string Name { get; init; } | ||||
| 		 | ||||
| 		[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] | ||||
| 		public string? Avatar { get; init; } | ||||
| 		public string? DisplayName { get; init; } | ||||
| 		 | ||||
| 		[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] | ||||
| 		public string? Tag { get; init; } | ||||
| 		public string? Avatar { get; init; } | ||||
| 	} | ||||
|  | ||||
| 	public sealed class JsonServer { | ||||
|   | ||||
| @@ -68,8 +68,8 @@ static class ViewerJsonExport { | ||||
| 		await foreach (var user in db.Users.Get(cancellationToken)) { | ||||
| 			users[user.Id] = new ViewerJson.JsonUser { | ||||
| 				Name = user.Name, | ||||
| 				DisplayName = user.DisplayName, | ||||
| 				Avatar = user.AvatarUrl, | ||||
| 				Tag = user.Discriminator | ||||
| 			}; | ||||
| 		} | ||||
|  | ||||
|   | ||||
| @@ -20,7 +20,7 @@ sealed class SqliteDownloadRepository(SqliteConnectionPool pool) : BaseSqliteRep | ||||
|  | ||||
| 	internal sealed class NewDownloadCollector : IAsyncDisposable { | ||||
| 		private readonly SqliteDownloadRepository repository; | ||||
| 		private bool hasAdded = false; | ||||
| 		private bool hasChanged = false; | ||||
|  | ||||
| 		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) | ||||
| 				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); | ||||
| @@ -39,6 +48,7 @@ sealed class SqliteDownloadRepository(SqliteConnectionPool pool) : BaseSqliteRep | ||||
| 			metadataCmd.Add(":status", SqliteType.Integer); | ||||
| 			metadataCmd.Add(":type", SqliteType.Text); | ||||
| 			metadataCmd.Add(":size", SqliteType.Integer); | ||||
| 			metadataCmd.AddAndSet(":success", SqliteType.Integer, (int) DownloadStatus.Success); | ||||
| 		} | ||||
|  | ||||
| 		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(":type", download.Type); | ||||
| 			metadataCmd.Set(":size", download.Size); | ||||
| 			hasAdded |= await metadataCmd.ExecuteNonQueryAsync() > 0; | ||||
| 			hasChanged |= await metadataCmd.ExecuteNonQueryAsync() > 0; | ||||
| 		} | ||||
|  | ||||
| 		public void OnCommitted() { | ||||
| 			if (hasAdded) { | ||||
| 			if (hasChanged) { | ||||
| 				repository.UpdateTotalCount(); | ||||
| 			} | ||||
| 		} | ||||
| @@ -90,7 +100,8 @@ sealed class SqliteDownloadRepository(SqliteConnectionPool pool) : BaseSqliteRep | ||||
| 					""" | ||||
| 					INSERT INTO download_blobs (normalized_url, blob) | ||||
| 					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 | ||||
| 					""" | ||||
| 				); | ||||
|   | ||||
| @@ -14,16 +14,8 @@ using Microsoft.Data.Sqlite; | ||||
|  | ||||
| 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 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) { | ||||
| 		if (messages.Count == 0) { | ||||
| @@ -50,25 +42,7 @@ sealed class SqliteMessageRepository : BaseSqliteRepository, IMessageRepository | ||||
| 				("timestamp", SqliteType.Integer) | ||||
| 			]); | ||||
|  | ||||
| 			await using var deleteEditTimestampCmd = DeleteByMessageId(conn, "edit_timestamps"); | ||||
| 			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), | ||||
| 			await using var attachmentCmd = conn.Upsert("attachments", [ | ||||
| 				("attachment_id", SqliteType.Integer), | ||||
| 				("name", SqliteType.Text), | ||||
| 				("type", SqliteType.Text), | ||||
| @@ -79,19 +53,41 @@ sealed class SqliteMessageRepository : BaseSqliteRepository, IMessageRepository | ||||
| 				("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), | ||||
| 				("json", SqliteType.Text) | ||||
| 			]); | ||||
|  | ||||
| 			await using var reactionCmd = conn.Insert("reactions", [ | ||||
| 			await using var messageReactionCmd = conn.Insert("message_reactions", [ | ||||
| 				("message_id", SqliteType.Integer), | ||||
| 				("emoji_id", SqliteType.Integer), | ||||
| 				("emoji_name", SqliteType.Text), | ||||
| 				("emoji_flags", SqliteType.Integer), | ||||
| 				("count", SqliteType.Integer) | ||||
| 			]); | ||||
| 			 | ||||
|  | ||||
| 			await using var downloadCollector = new SqliteDownloadRepository.NewDownloadCollector(downloads, conn); | ||||
|  | ||||
| 			foreach (var message in messages) { | ||||
| @@ -104,29 +100,30 @@ sealed class SqliteMessageRepository : BaseSqliteRepository, IMessageRepository | ||||
| 				messageCmd.Set(":timestamp", message.Timestamp); | ||||
| 				await messageCmd.ExecuteNonQueryAsync(); | ||||
|  | ||||
| 				await ExecuteDeleteByMessageId(deleteEditTimestampCmd, messageId); | ||||
| 				await ExecuteDeleteByMessageId(deleteRepliedToCmd, messageId); | ||||
| 				await ExecuteDeleteByMessageId(deleteMessageEditTimestampCmd, messageId); | ||||
| 				await ExecuteDeleteByMessageId(deleteMessageRepliedToCmd, messageId); | ||||
|  | ||||
| 				await ExecuteDeleteByMessageId(deleteAttachmentsCmd, messageId); | ||||
| 				await ExecuteDeleteByMessageId(deleteEmbedsCmd, messageId); | ||||
| 				await ExecuteDeleteByMessageId(deleteReactionsCmd, messageId); | ||||
| 				await ExecuteDeleteByMessageId(deleteMessageAttachmentsCmd, messageId); | ||||
| 				await ExecuteDeleteByMessageId(deleteMessageEmbedsCmd, messageId); | ||||
| 				await ExecuteDeleteByMessageId(deleteMessageReactionsCmd, messageId); | ||||
|  | ||||
| 				if (message.EditTimestamp is {} timestamp) { | ||||
| 					editTimestampCmd.Set(":message_id", messageId); | ||||
| 					editTimestampCmd.Set(":edit_timestamp", timestamp); | ||||
| 					await editTimestampCmd.ExecuteNonQueryAsync(); | ||||
| 					messageEditTimestampCmd.Set(":message_id", messageId); | ||||
| 					messageEditTimestampCmd.Set(":edit_timestamp", timestamp); | ||||
| 					await messageEditTimestampCmd.ExecuteNonQueryAsync(); | ||||
| 				} | ||||
|  | ||||
| 				if (message.RepliedToId is {} repliedToId) { | ||||
| 					repliedToCmd.Set(":message_id", messageId); | ||||
| 					repliedToCmd.Set(":replied_to_id", repliedToId); | ||||
| 					await repliedToCmd.ExecuteNonQueryAsync(); | ||||
| 					messageRepliedToCmd.Set(":message_id", messageId); | ||||
| 					messageRepliedToCmd.Set(":replied_to_id", repliedToId); | ||||
| 					await messageRepliedToCmd.ExecuteNonQueryAsync(); | ||||
| 				} | ||||
|  | ||||
| 				if (!message.Attachments.IsEmpty) { | ||||
| 					foreach (var attachment in message.Attachments) { | ||||
| 						attachmentCmd.Set(":message_id", messageId); | ||||
| 						attachmentCmd.Set(":attachment_id", attachment.Id); | ||||
| 						object attachmentId = attachment.Id; | ||||
|  | ||||
| 						attachmentCmd.Set(":attachment_id", attachmentId); | ||||
| 						attachmentCmd.Set(":name", attachment.Name); | ||||
| 						attachmentCmd.Set(":type", attachment.Type); | ||||
| 						attachmentCmd.Set(":normalized_url", attachment.NormalizedUrl); | ||||
| @@ -135,16 +132,20 @@ sealed class SqliteMessageRepository : BaseSqliteRepository, IMessageRepository | ||||
| 						attachmentCmd.Set(":width", attachment.Width); | ||||
| 						attachmentCmd.Set(":height", attachment.Height); | ||||
| 						await attachmentCmd.ExecuteNonQueryAsync(); | ||||
| 						 | ||||
|  | ||||
| 						messageAttachmentCmd.Set(":message_id", messageId); | ||||
| 						messageAttachmentCmd.Set(":attachment_id", attachmentId); | ||||
| 						await messageAttachmentCmd.ExecuteNonQueryAsync(); | ||||
|  | ||||
| 						await downloadCollector.Add(DownloadLinkExtractor.FromAttachment(attachment)); | ||||
| 					} | ||||
| 				} | ||||
|  | ||||
| 				if (!message.Embeds.IsEmpty) { | ||||
| 					foreach (var embed in message.Embeds) { | ||||
| 						embedCmd.Set(":message_id", messageId); | ||||
| 						embedCmd.Set(":json", embed.Json); | ||||
| 						await embedCmd.ExecuteNonQueryAsync(); | ||||
| 						messageEmbedCmd.Set(":message_id", messageId); | ||||
| 						messageEmbedCmd.Set(":json", embed.Json); | ||||
| 						await messageEmbedCmd.ExecuteNonQueryAsync(); | ||||
|  | ||||
| 						if (DownloadLinkExtractor.TryFromEmbedJson(embed.Json) is {} download) { | ||||
| 							await downloadCollector.Add(download); | ||||
| @@ -154,12 +155,12 @@ sealed class SqliteMessageRepository : BaseSqliteRepository, IMessageRepository | ||||
|  | ||||
| 				if (!message.Reactions.IsEmpty) { | ||||
| 					foreach (var reaction in message.Reactions) { | ||||
| 						reactionCmd.Set(":message_id", messageId); | ||||
| 						reactionCmd.Set(":emoji_id", reaction.EmojiId); | ||||
| 						reactionCmd.Set(":emoji_name", reaction.EmojiName); | ||||
| 						reactionCmd.Set(":emoji_flags", (int) reaction.EmojiFlags); | ||||
| 						reactionCmd.Set(":count", reaction.Count); | ||||
| 						await reactionCmd.ExecuteNonQueryAsync(); | ||||
| 						messageReactionCmd.Set(":message_id", messageId); | ||||
| 						messageReactionCmd.Set(":emoji_id", reaction.EmojiId); | ||||
| 						messageReactionCmd.Set(":emoji_name", reaction.EmojiName); | ||||
| 						messageReactionCmd.Set(":emoji_flags", (int) reaction.EmojiFlags); | ||||
| 						messageReactionCmd.Set(":count", reaction.Count); | ||||
| 						await messageReactionCmd.ExecuteNonQueryAsync(); | ||||
|  | ||||
| 						if (reaction.EmojiId is {} emojiId) { | ||||
| 							await downloadCollector.Add(DownloadLinkExtractor.FromEmoji(emojiId, reaction.EmojiFlags)); | ||||
| @@ -178,7 +179,7 @@ sealed class SqliteMessageRepository : BaseSqliteRepository, IMessageRepository | ||||
| 	public override Task<long> Count(CancellationToken cancellationToken) { | ||||
| 		return Count(filter: null, cancellationToken); | ||||
| 	} | ||||
| 	 | ||||
|  | ||||
| 	public async Task<long> Count(MessageFilter? filter, CancellationToken cancellationToken) { | ||||
| 		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); | ||||
| @@ -221,7 +222,8 @@ sealed class SqliteMessageRepository : BaseSqliteRepository, IMessageRepository | ||||
| 			""" | ||||
| 			SELECT attachment_id, name, type, normalized_url, download_url, size, width, height | ||||
| 			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 { | ||||
| @@ -238,7 +240,7 @@ sealed class SqliteMessageRepository : BaseSqliteRepository, IMessageRepository | ||||
| 		const string EmbedSql = | ||||
| 			""" | ||||
| 			SELECT json | ||||
| 			FROM embeds | ||||
| 			FROM message_embeds | ||||
| 			WHERE message_id = :message_id | ||||
| 			"""; | ||||
|  | ||||
| @@ -249,7 +251,7 @@ sealed class SqliteMessageRepository : BaseSqliteRepository, IMessageRepository | ||||
| 		const string ReactionSql = | ||||
| 			""" | ||||
| 			SELECT emoji_id, emoji_name, emoji_flags, count | ||||
| 			FROM reactions | ||||
| 			FROM message_reactions | ||||
| 			WHERE message_id = :message_id | ||||
| 			"""; | ||||
|  | ||||
| @@ -262,10 +264,10 @@ sealed class SqliteMessageRepository : BaseSqliteRepository, IMessageRepository | ||||
|  | ||||
| 		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 | ||||
| 			 LEFT JOIN edit_timestamps et ON m.message_id = et.message_id | ||||
| 			 LEFT JOIN replied_to rt ON m.message_id = rt.message_id | ||||
| 			 LEFT JOIN message_edit_timestamps met ON m.message_id = met.message_id | ||||
| 			 LEFT JOIN message_replied_to mrt ON m.message_id = mrt.message_id | ||||
| 			 {filter.GenerateConditions("m").BuildWhereClause()} | ||||
| 			 """ | ||||
| 		); | ||||
| @@ -292,7 +294,7 @@ sealed class SqliteMessageRepository : BaseSqliteRepository, IMessageRepository | ||||
|  | ||||
| 	public async IAsyncEnumerable<ulong> GetIds(MessageFilter? filter) { | ||||
| 		await using var conn = await pool.Take(); | ||||
| 		 | ||||
|  | ||||
| 		await using var cmd = conn.Command("SELECT message_id FROM messages" + filter.GenerateConditions().BuildWhereClause()); | ||||
| 		await using var reader = await cmd.ExecuteReaderAsync(); | ||||
|  | ||||
|   | ||||
| @@ -29,6 +29,7 @@ sealed class SqliteUserRepository : BaseSqliteRepository, IUserRepository { | ||||
| 			await using var cmd = conn.Upsert("users", [ | ||||
| 				("id", SqliteType.Integer), | ||||
| 				("name", SqliteType.Text), | ||||
| 				("display_name", SqliteType.Text), | ||||
| 				("avatar_url", SqliteType.Text), | ||||
| 				("discriminator", SqliteType.Text) | ||||
| 			]); | ||||
| @@ -38,6 +39,7 @@ sealed class SqliteUserRepository : BaseSqliteRepository, IUserRepository { | ||||
| 			foreach (var user in users) { | ||||
| 				cmd.Set(":id", user.Id); | ||||
| 				cmd.Set(":name", user.Name); | ||||
| 				cmd.Set(":display_name", user.DisplayName); | ||||
| 				cmd.Set(":avatar_url", user.AvatarUrl); | ||||
| 				cmd.Set(":discriminator", user.Discriminator); | ||||
| 				await cmd.ExecuteNonQueryAsync(); | ||||
| @@ -62,15 +64,16 @@ sealed class SqliteUserRepository : BaseSqliteRepository, IUserRepository { | ||||
| 	public async IAsyncEnumerable<User> Get([EnumeratorCancellation] CancellationToken cancellationToken) { | ||||
| 		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); | ||||
|  | ||||
| 		while (await reader.ReadAsync(cancellationToken)) { | ||||
| 			yield return new User { | ||||
| 				Id = reader.GetUint64(0), | ||||
| 				Name = reader.GetString(1), | ||||
| 				AvatarUrl = reader.IsDBNull(2) ? null : reader.GetString(2), | ||||
| 				Discriminator = reader.IsDBNull(3) ? null : reader.GetString(3), | ||||
| 				DisplayName = reader.IsDBNull(2) ? null : reader.GetString(2), | ||||
| 				AvatarUrl = reader.IsDBNull(3) ? null : reader.GetString(3), | ||||
| 				Discriminator = reader.IsDBNull(4) ? null : reader.GetString(4), | ||||
| 			}; | ||||
| 		} | ||||
| 	} | ||||
|   | ||||
| @@ -8,7 +8,10 @@ sealed class SqliteSchemaUpgradeTo3 : ISchemaUpgrade { | ||||
| 		await reporter.MainWork("Applying schema changes...", 0, 1); | ||||
|  | ||||
| 		await SqliteSchema.CreateMessageEditTimestampTable(conn); | ||||
| 		await conn.ExecuteAsync("ALTER TABLE message_edit_timestamps RENAME TO edit_timestamps"); | ||||
| 		 | ||||
| 		await SqliteSchema.CreateMessageRepliedToTable(conn); | ||||
| 		await conn.ExecuteAsync("ALTER TABLE message_replied_to RENAME TO replied_to"); | ||||
|  | ||||
| 		await conn.ExecuteAsync(""" | ||||
| 		                        INSERT INTO edit_timestamps (message_id, edit_timestamp) | ||||
|   | ||||
							
								
								
									
										11
									
								
								app/Server/Database/Sqlite/Schema/SqliteSchemaUpgradeTo8.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								app/Server/Database/Sqlite/Schema/SqliteSchemaUpgradeTo8.cs
									
									
									
									
									
										Normal 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"); | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										23
									
								
								app/Server/Database/Sqlite/Schema/SqliteSchemaUpgradeTo9.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								app/Server/Database/Sqlite/Schema/SqliteSchemaUpgradeTo9.cs
									
									
									
									
									
										Normal 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"); | ||||
| 	} | ||||
| } | ||||
| @@ -8,7 +8,7 @@ using DHT.Utils.Logging; | ||||
| namespace DHT.Server.Database.Sqlite; | ||||
|  | ||||
| sealed class SqliteSchema { | ||||
| 	internal const int Version = 7; | ||||
| 	internal const int Version = 9; | ||||
|  | ||||
| 	private static readonly Log Log = Log.ForType<SqliteSchema>(); | ||||
|  | ||||
| @@ -48,6 +48,7 @@ sealed class SqliteSchema { | ||||
| 		                        CREATE TABLE users ( | ||||
| 		                        	id            INTEGER PRIMARY KEY NOT NULL, | ||||
| 		                        	name          TEXT NOT NULL, | ||||
| 		                        	display_name  TEXT, | ||||
| 		                        	avatar_url    TEXT, | ||||
| 		                        	discriminator TEXT | ||||
| 		                        ) | ||||
| @@ -85,7 +86,6 @@ sealed class SqliteSchema { | ||||
|  | ||||
| 		await conn.ExecuteAsync(""" | ||||
| 		                        CREATE TABLE attachments ( | ||||
| 		                        	message_id     INTEGER NOT NULL, | ||||
| 		                        	attachment_id  INTEGER NOT NULL PRIMARY KEY NOT NULL, | ||||
| 		                        	name           TEXT NOT NULL, | ||||
| 		                        	type           TEXT, | ||||
| @@ -98,14 +98,14 @@ sealed class SqliteSchema { | ||||
| 		                        """); | ||||
|  | ||||
| 		await conn.ExecuteAsync(""" | ||||
| 		                        CREATE TABLE embeds ( | ||||
| 		                        CREATE TABLE message_embeds ( | ||||
| 		                        	message_id INTEGER NOT NULL, | ||||
| 		                        	json       TEXT NOT NULL | ||||
| 		                        ) | ||||
| 		                        """); | ||||
|  | ||||
| 		await conn.ExecuteAsync(""" | ||||
| 		                        CREATE TABLE reactions ( | ||||
| 		                        CREATE TABLE message_reactions ( | ||||
| 		                        	message_id  INTEGER NOT NULL, | ||||
| 		                        	emoji_id    INTEGER, | ||||
| 		                        	emoji_name  TEXT, | ||||
| @@ -117,17 +117,17 @@ sealed class SqliteSchema { | ||||
| 		await CreateMessageEditTimestampTable(conn); | ||||
| 		await CreateMessageRepliedToTable(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 embeds(message_id)"); | ||||
| 		await conn.ExecuteAsync("CREATE INDEX reactions_message_ix ON reactions(message_id)"); | ||||
| 		await conn.ExecuteAsync("CREATE INDEX embeds_message_ix ON message_embeds(message_id)"); | ||||
| 		await conn.ExecuteAsync("CREATE INDEX reactions_message_ix ON message_reactions(message_id)"); | ||||
|  | ||||
| 		await conn.ExecuteAsync("INSERT INTO metadata (key, value) VALUES ('version', " + Version + ")"); | ||||
| 	} | ||||
|  | ||||
| 	internal static async Task CreateMessageEditTimestampTable(ISqliteConnection conn) { | ||||
| 		await conn.ExecuteAsync(""" | ||||
| 		                        CREATE TABLE edit_timestamps ( | ||||
| 		                        CREATE TABLE message_edit_timestamps ( | ||||
| 		                        	message_id     INTEGER PRIMARY KEY NOT NULL, | ||||
| 		                        	edit_timestamp INTEGER NOT NULL | ||||
| 		                        ) | ||||
| @@ -136,7 +136,7 @@ sealed class SqliteSchema { | ||||
|  | ||||
| 	internal static async Task CreateMessageRepliedToTable(ISqliteConnection conn) { | ||||
| 		await conn.ExecuteAsync(""" | ||||
| 		                        CREATE TABLE replied_to ( | ||||
| 		                        CREATE TABLE message_replied_to ( | ||||
| 		                        	message_id    INTEGER PRIMARY KEY 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) { | ||||
| 		var upgrades = new Dictionary<int, ISchemaUpgrade> { | ||||
| @@ -171,13 +183,15 @@ sealed class SqliteSchema { | ||||
| 			{ 4, new SqliteSchemaUpgradeTo5() }, | ||||
| 			{ 5, new SqliteSchemaUpgradeTo6() }, | ||||
| 			{ 6, new SqliteSchemaUpgradeTo7() }, | ||||
| 			{ 7, new SqliteSchemaUpgradeTo8() }, | ||||
| 			{ 8, new SqliteSchemaUpgradeTo9() }, | ||||
| 		}; | ||||
|  | ||||
| 		var perf = Log.Start("from version " + dbVersion); | ||||
|  | ||||
| 		for (int fromVersion = dbVersion; fromVersion < Version; fromVersion++) { | ||||
| 			var toVersion = fromVersion + 1; | ||||
| 			 | ||||
|  | ||||
| 			if (upgrades.TryGetValue(fromVersion, out var upgrade)) { | ||||
| 				await upgrade.Run(conn, reporter); | ||||
| 			} | ||||
|   | ||||
| @@ -30,6 +30,7 @@ sealed class TrackUsersEndpoint(IDatabaseFile db) : BaseEndpoint(db) { | ||||
| 	private static User ReadUser(JsonElement json, string path) => new () { | ||||
| 		Id = json.RequireSnowflake("id", path), | ||||
| 		Name = json.RequireString("name", path), | ||||
| 		DisplayName = json.HasKey("displayName") ? json.RequireString("displayName", path) : null, | ||||
| 		AvatarUrl = json.HasKey("avatar") ? json.RequireString("avatar", path) : null, | ||||
| 		Discriminator = json.HasKey("discriminator") ? json.RequireString("discriminator", path) : null | ||||
| 	}; | ||||
|   | ||||
| @@ -8,5 +8,5 @@ using DHT.Utils; | ||||
| namespace DHT.Utils; | ||||
|  | ||||
| static class Version { | ||||
| 	public const string Tag = "42.0.0.0"; | ||||
| 	public const string Tag = "42.1.0.0"; | ||||
| } | ||||
|   | ||||
							
								
								
									
										
											BIN
										
									
								
								app/empty.dht
									
									
									
									
									
								
							
							
						
						
									
										
											BIN
										
									
								
								app/empty.dht
									
									
									
									
									
								
							
										
											Binary file not shown.
										
									
								
							
		Reference in New Issue
	
	Block a user