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