mirror of
				https://github.com/chylex/Discord-History-Tracker.git
				synced 2025-10-31 11:17:15 +01:00 
			
		
		
		
	Compare commits
	
		
			17 Commits
		
	
	
		
			v36.2.beta
			...
			4db8c302d8
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 4db8c302d8 | |||
| 7e7d140957 | |||
| 8c68438fbb | |||
| f625a39b4d | |||
| 7fd644449c | |||
| e4a09515b0 | |||
| 9ac9f2246f | |||
| bbc734ba9b | |||
| 6837b05b0d | |||
| c94808a15f | |||
| 739e87c5ab | |||
| d463b407f4 | |||
| cd418f4871 | |||
| 176a81e055 | |||
|   | 1cf3e76644 | ||
| 33f5ab7cce | |||
| b9a5664740 | 
| @@ -33,9 +33,6 @@ | |||||||
|         <Style Selector="TextBox:focus /template/ Border#PART_BorderElement"> |         <Style Selector="TextBox:focus /template/ Border#PART_BorderElement"> | ||||||
|             <Setter Property="BorderThickness" Value="1" /> |             <Setter Property="BorderThickness" Value="1" /> | ||||||
|         </Style> |         </Style> | ||||||
|         <Style Selector="TextBox:disabled"><!-- TODO bug in Avalonia (https://github.com/AvaloniaUI/Avalonia/pull/7792) --> |  | ||||||
|             <Setter Property="Foreground" Value="{DynamicResource TextControlForegroundDisabled}" /> |  | ||||||
|         </Style> |  | ||||||
|         <Style Selector="TextBox:error DataValidationErrors"> |         <Style Selector="TextBox:error DataValidationErrors"> | ||||||
|             <Style.Resources> |             <Style.Resources> | ||||||
|                 <ControlTemplate x:Key="InlineDataValidationContentTemplate" TargetType="DataValidationErrors"> |                 <ControlTemplate x:Key="InlineDataValidationContentTemplate" TargetType="DataValidationErrors"> | ||||||
|   | |||||||
| @@ -4,12 +4,26 @@ using Avalonia.Data.Converters; | |||||||
|  |  | ||||||
| namespace DHT.Desktop.Common { | namespace DHT.Desktop.Common { | ||||||
| 	sealed class BytesValueConverter : IValueConverter { | 	sealed class BytesValueConverter : IValueConverter { | ||||||
| 		private static readonly string[] Units = { | 		private sealed class Unit { | ||||||
| 			"B", | 			private readonly string label; | ||||||
| 			"kB", | 			private readonly string numberFormat; | ||||||
| 			"MB", | 			 | ||||||
| 			"GB", | 			public Unit(string label, int decimalPlaces) { | ||||||
| 			"TB" | 				this.label = label; | ||||||
|  | 				this.numberFormat = "{0:n" + decimalPlaces + "}"; | ||||||
|  | 			} | ||||||
|  | 			 | ||||||
|  | 			public string Format(double size) { | ||||||
|  | 				return string.Format(Program.Culture, numberFormat, size) + " " + label; | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 		 | ||||||
|  | 		private static readonly Unit[] Units = { | ||||||
|  | 			new ("B", decimalPlaces: 0), | ||||||
|  | 			new ("kB", decimalPlaces: 0), | ||||||
|  | 			new ("MB", decimalPlaces: 1), | ||||||
|  | 			new ("GB", decimalPlaces: 1), | ||||||
|  | 			new ("TB", decimalPlaces: 1) | ||||||
| 		}; | 		}; | ||||||
|  |  | ||||||
| 		private const int Scale = 1000; | 		private const int Scale = 1000; | ||||||
| @@ -17,13 +31,7 @@ namespace DHT.Desktop.Common { | |||||||
| 		private static string Convert(ulong size) { | 		private static string Convert(ulong size) { | ||||||
| 			int power = size == 0L ? 0 : (int) Math.Log(size, Scale); | 			int power = size == 0L ? 0 : (int) Math.Log(size, Scale); | ||||||
| 			int unit = power >= Units.Length ? Units.Length - 1 : power; | 			int unit = power >= Units.Length ? Units.Length - 1 : power; | ||||||
| 			if (unit == 0) { | 			return Units[unit].Format(unit == 0 ? size : size / Math.Pow(Scale, unit)); | ||||||
| 				return string.Format(Program.Culture, "{0:n0}", size) + " " + Units[unit]; |  | ||||||
| 			} |  | ||||||
| 			else { |  | ||||||
| 				double humanReadableSize = size / Math.Pow(Scale, unit); |  | ||||||
| 				return string.Format(Program.Culture, "{0:n0}", humanReadableSize) + " " + Units[unit]; |  | ||||||
| 			} |  | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		public object Convert(object? value, Type targetType, object? parameter, CultureInfo culture) { | 		public object Convert(object? value, Type targetType, object? parameter, CultureInfo culture) { | ||||||
|   | |||||||
| @@ -21,10 +21,10 @@ | |||||||
|     <DebugType>none</DebugType> |     <DebugType>none</DebugType> | ||||||
|   </PropertyGroup> |   </PropertyGroup> | ||||||
|   <ItemGroup> |   <ItemGroup> | ||||||
|     <PackageReference Include="Avalonia" Version="0.10.14" /> |     <PackageReference Include="Avalonia" Version="0.10.16" /> | ||||||
|     <PackageReference Include="Avalonia.Controls.DataGrid" Version="0.10.14" /> |     <PackageReference Include="Avalonia.Controls.DataGrid" Version="0.10.16" /> | ||||||
|     <PackageReference Include="Avalonia.Desktop" Version="0.10.14" /> |     <PackageReference Include="Avalonia.Desktop" Version="0.10.16" /> | ||||||
|     <PackageReference Include="Avalonia.Diagnostics" Version="0.10.14" Condition=" '$(Configuration)' == 'Debug' " /> |     <PackageReference Include="Avalonia.Diagnostics" Version="0.10.16" Condition=" '$(Configuration)' == 'Debug' " /> | ||||||
|   </ItemGroup> |   </ItemGroup> | ||||||
|   <ItemGroup> |   <ItemGroup> | ||||||
|     <ProjectReference Include="..\Server\Server.csproj" /> |     <ProjectReference Include="..\Server\Server.csproj" /> | ||||||
|   | |||||||
| @@ -11,9 +11,11 @@ using Avalonia.Controls; | |||||||
| using DHT.Desktop.Common; | using DHT.Desktop.Common; | ||||||
| using DHT.Desktop.Dialogs.Message; | using DHT.Desktop.Dialogs.Message; | ||||||
| using DHT.Desktop.Main.Controls; | using DHT.Desktop.Main.Controls; | ||||||
|  | using DHT.Desktop.Server; | ||||||
| using DHT.Server.Data.Filters; | using DHT.Server.Data.Filters; | ||||||
| using DHT.Server.Database; | using DHT.Server.Database; | ||||||
| using DHT.Server.Database.Export; | using DHT.Server.Database.Export; | ||||||
|  | using DHT.Server.Database.Export.Strategy; | ||||||
| using DHT.Utils.Models; | using DHT.Utils.Models; | ||||||
| using static DHT.Desktop.Program; | using static DHT.Desktop.Program; | ||||||
|  |  | ||||||
| @@ -55,7 +57,7 @@ namespace DHT.Desktop.Main.Pages { | |||||||
| 			HasFilters = FilterModel.HasAnyFilters; | 			HasFilters = FilterModel.HasAnyFilters; | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		private async Task WriteViewerFile(string path) { | 		private async Task WriteViewerFile(string path, IViewerExportStrategy strategy) { | ||||||
| 			const string ArchiveTag = "/*[ARCHIVE]*/"; | 			const string ArchiveTag = "/*[ARCHIVE]*/"; | ||||||
|  |  | ||||||
| 			string indexFile = await Resources.ReadTextAsync("Viewer/index.html"); | 			string indexFile = await Resources.ReadTextAsync("Viewer/index.html"); | ||||||
| @@ -68,7 +70,7 @@ namespace DHT.Desktop.Main.Pages { | |||||||
| 			string jsonTempFile = path + ".tmp"; | 			string jsonTempFile = path + ".tmp"; | ||||||
|  |  | ||||||
| 			await using (var jsonStream = new FileStream(jsonTempFile, FileMode.Create, FileAccess.ReadWrite, FileShare.Read)) { | 			await using (var jsonStream = new FileStream(jsonTempFile, FileMode.Create, FileAccess.ReadWrite, FileShare.Read)) { | ||||||
| 				await ViewerJsonExport.Generate(jsonStream, db, FilterModel.CreateFilter()); | 				await ViewerJsonExport.Generate(jsonStream, strategy, db, FilterModel.CreateFilter()); | ||||||
|  |  | ||||||
| 				char[] jsonBuffer = new char[Math.Min(32768, jsonStream.Position)]; | 				char[] jsonBuffer = new char[Math.Min(32768, jsonStream.Position)]; | ||||||
| 				jsonStream.Position = 0; | 				jsonStream.Position = 0; | ||||||
| @@ -106,7 +108,7 @@ namespace DHT.Desktop.Main.Pages { | |||||||
| 			TemporaryFiles.Add(fullPath); | 			TemporaryFiles.Add(fullPath); | ||||||
| 			 | 			 | ||||||
| 			Directory.CreateDirectory(rootPath); | 			Directory.CreateDirectory(rootPath); | ||||||
| 			await WriteViewerFile(fullPath); | 			await WriteViewerFile(fullPath, new LiveViewerExportStrategy(ServerManager.Port, ServerManager.Token)); | ||||||
|  |  | ||||||
| 			Process.Start(new ProcessStartInfo(fullPath) { UseShellExecute = true }); | 			Process.Start(new ProcessStartInfo(fullPath) { UseShellExecute = true }); | ||||||
| 		} | 		} | ||||||
| @@ -126,7 +128,7 @@ namespace DHT.Desktop.Main.Pages { | |||||||
|  |  | ||||||
| 			string? path = await dialog; | 			string? path = await dialog; | ||||||
| 			if (!string.IsNullOrEmpty(path)) { | 			if (!string.IsNullOrEmpty(path)) { | ||||||
| 				await WriteViewerFile(path); | 				await WriteViewerFile(path, StandaloneViewerExportStrategy.Instance); | ||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
|   | |||||||
| @@ -91,11 +91,13 @@ class DISCORD { | |||||||
| 	static getMessageElementProps(ele) { | 	static getMessageElementProps(ele) { | ||||||
| 		const props = DOM.getReactProps(ele); | 		const props = DOM.getReactProps(ele); | ||||||
| 		 | 		 | ||||||
| 		if (props.children && props.children.length >= 4) { | 		if (props.children && props.children.length) { | ||||||
| 			const childProps = props.children[3].props; | 			for (let i = 3; i < props.children.length; i++) { | ||||||
|  | 				const childProps = props.children[i].props; | ||||||
| 				 | 				 | ||||||
| 			if ("message" in childProps && "channel" in childProps) { | 				if (childProps && "message" in childProps && "channel" in childProps) { | ||||||
| 				return childProps; | 					return childProps; | ||||||
|  | 				} | ||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
| 		 | 		 | ||||||
| @@ -195,9 +197,22 @@ class DISCORD { | |||||||
| 				return { server, channel }; | 				return { server, channel }; | ||||||
| 			} | 			} | ||||||
| 			else if (obj.guild_id) { | 			else if (obj.guild_id) { | ||||||
|  | 				let guild; | ||||||
|  | 				 | ||||||
|  | 				for (const child of DOM.getReactProps(document.querySelector("nav header [class*='headerContent-']")).children) { | ||||||
|  | 					if (child && child.props && child.props.guild) { | ||||||
|  | 						guild = child.props.guild; | ||||||
|  | 						break; | ||||||
|  | 					} | ||||||
|  | 				} | ||||||
|  | 				 | ||||||
|  | 				if (!guild || typeof guild.name !== "string" || obj.guild_id !== guild.id) { | ||||||
|  | 					return null; | ||||||
|  | 				} | ||||||
|  | 				 | ||||||
| 				const server = { | 				const server = { | ||||||
| 					"id": obj.guild_id, | 					"id": guild.id, | ||||||
| 					"name": document.querySelector("nav header h1[class*='name-']").innerText, | 					"name": guild.name, | ||||||
| 					"type": "SERVER" | 					"type": "SERVER" | ||||||
| 				}; | 				}; | ||||||
| 				 | 				 | ||||||
|   | |||||||
| @@ -251,6 +251,11 @@ const STATE = (function() { | |||||||
| 							mapped.type = attachment.content_type; | 							mapped.type = attachment.content_type; | ||||||
| 						} | 						} | ||||||
| 						 | 						 | ||||||
|  | 						if (attachment.width && attachment.height) { | ||||||
|  | 							mapped.width = attachment.width; | ||||||
|  | 							mapped.height = attachment.height; | ||||||
|  | 						} | ||||||
|  | 						 | ||||||
| 						return mapped; | 						return mapped; | ||||||
| 					}); | 					}); | ||||||
| 				} | 				} | ||||||
|   | |||||||
| @@ -1,9 +1,5 @@ | |||||||
| #app-mount div[class*="app-"] { | #app-mount { | ||||||
|   margin-bottom: 48px !important; |   height: calc(100% - 48px) !important; | ||||||
| } |  | ||||||
|  |  | ||||||
| #app-mount div[class*="app-"] > div[class*="app-"] { |  | ||||||
|   margin-bottom: 0 !important; |  | ||||||
| } | } | ||||||
|  |  | ||||||
| #dht-ctrl { | #dht-ctrl { | ||||||
|   | |||||||
| @@ -78,6 +78,12 @@ const DISCORD = (function() { | |||||||
| 		} | 		} | ||||||
| 	}; | 	}; | ||||||
| 	 | 	 | ||||||
|  | 	const isImageUrl = function(url) { | ||||||
|  | 		const dot = url.pathname.lastIndexOf("."); | ||||||
|  | 		const ext = dot === -1 ? "" : url.pathname.substring(dot).toLowerCase(); | ||||||
|  | 		return ext === ".png" || ext === ".gif" || ext === ".jpg" || ext === ".jpeg"; | ||||||
|  | 	}; | ||||||
|  | 	 | ||||||
| 	return { | 	return { | ||||||
| 		setup() { | 		setup() { | ||||||
| 			templateChannelServer = new TEMPLATE([ | 			templateChannelServer = new TEMPLATE([ | ||||||
| @@ -123,7 +129,7 @@ const DISCORD = (function() { | |||||||
| 			 | 			 | ||||||
| 			// noinspection HtmlUnknownTarget | 			// noinspection HtmlUnknownTarget | ||||||
| 			templateAttachmentDownload = new TEMPLATE([ | 			templateAttachmentDownload = new TEMPLATE([ | ||||||
| 				"<a href='{url}' class='embed download'>Download {filename}</a>" | 				"<a href='{url}' class='embed download'>Download {name}</a>" | ||||||
| 			].join("")); | 			].join("")); | ||||||
| 			 | 			 | ||||||
| 			// noinspection HtmlUnknownTarget | 			// noinspection HtmlUnknownTarget | ||||||
| @@ -176,9 +182,8 @@ const DISCORD = (function() { | |||||||
| 		}, | 		}, | ||||||
| 		 | 		 | ||||||
| 		isImageAttachment(attachment) { | 		isImageAttachment(attachment) { | ||||||
| 			const dot = attachment.url.lastIndexOf("."); | 			const url = DOM.tryParseUrl(attachment.url); | ||||||
| 			const ext = dot === -1 ? "" : attachment.url.substring(dot).toLowerCase(); | 			return url != null && isImageUrl(url); | ||||||
| 			return ext === ".png" || ext === ".gif" || ext === ".jpg" || ext === ".jpeg"; |  | ||||||
| 		}, | 		}, | ||||||
| 		 | 		 | ||||||
| 		getChannelHTML(channel) { // noinspection FunctionWithInconsistentReturnsJS | 		getChannelHTML(channel) { // noinspection FunctionWithInconsistentReturnsJS | ||||||
| @@ -235,16 +240,14 @@ const DISCORD = (function() { | |||||||
| 					} | 					} | ||||||
| 					 | 					 | ||||||
| 					return value.map(attachment => { | 					return value.map(attachment => { | ||||||
| 						if (this.isImageAttachment(attachment) && SETTINGS.enableImagePreviews) { | 						if (!DISCORD.isImageAttachment(attachment) || !SETTINGS.enableImagePreviews) { | ||||||
| 							return templateEmbedImage.apply({ url: attachment.url, src: attachment.url }); | 							return templateAttachmentDownload.apply(attachment); | ||||||
|  | 						} | ||||||
|  | 						else if ("width" in attachment && "height" in attachment) { | ||||||
|  | 							return templateEmbedImageWithSize.apply({ url: attachment.url, src: attachment.url, width: attachment.width, height: attachment.height }); | ||||||
| 						} | 						} | ||||||
| 						else { | 						else { | ||||||
| 							const sliced = attachment.url.split("/"); | 							return templateEmbedImage.apply({ url: attachment.url, src: attachment.url }); | ||||||
| 							 |  | ||||||
| 							return templateAttachmentDownload.apply({ |  | ||||||
| 								"url": attachment.url, |  | ||||||
| 								"filename": sliced[sliced.length - 1] |  | ||||||
| 							}); |  | ||||||
| 						} | 						} | ||||||
| 					}).join(""); | 					}).join(""); | ||||||
| 				} | 				} | ||||||
|   | |||||||
| @@ -51,4 +51,15 @@ class DOM { | |||||||
| 		const date = new Date(timestamp); | 		const date = new Date(timestamp); | ||||||
| 		return date.toLocaleDateString() + ", " + date.toLocaleTimeString(); | 		return date.toLocaleDateString() + ", " + date.toLocaleTimeString(); | ||||||
| 	}; | 	}; | ||||||
|  | 	 | ||||||
|  | 	/** | ||||||
|  | 	 * Parses a url string into a URL object and returns it. If the parsing fails, returns null. | ||||||
|  | 	 */ | ||||||
|  | 	static tryParseUrl(url) { | ||||||
|  | 		try { | ||||||
|  | 			return new URL(url); | ||||||
|  | 		} catch (ignore) { | ||||||
|  | 			return null; | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
| } | } | ||||||
|   | |||||||
| @@ -5,5 +5,7 @@ namespace DHT.Server.Data { | |||||||
| 		public string? Type { get; internal init; } | 		public string? Type { get; internal init; } | ||||||
| 		public string Url { get; internal init; } | 		public string Url { get; internal init; } | ||||||
| 		public ulong Size { get; internal init; } | 		public ulong Size { get; internal init; } | ||||||
|  | 		public int? Width { get; internal init; } | ||||||
|  | 		public int? Height { get; internal init; } | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|   | |||||||
							
								
								
									
										6
									
								
								app/Server/Data/DownloadedAttachment.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								app/Server/Data/DownloadedAttachment.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,6 @@ | |||||||
|  | namespace DHT.Server.Data { | ||||||
|  | 	public readonly struct DownloadedAttachment { | ||||||
|  | 		public string? Type { get; internal init; } | ||||||
|  | 		public byte[] Data { get; internal init; } | ||||||
|  | 	} | ||||||
|  | } | ||||||
| @@ -63,6 +63,10 @@ namespace DHT.Server.Database { | |||||||
| 			return download; | 			return download; | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
|  | 		public DownloadedAttachment? GetDownloadedAttachment(string url) { | ||||||
|  | 			return null; | ||||||
|  | 		} | ||||||
|  |  | ||||||
| 		public void AddDownload(Data.Download download) {} | 		public void AddDownload(Data.Download download) {} | ||||||
|  |  | ||||||
| 		public void EnqueueDownloadItems(AttachmentFilter? filter = null) {} | 		public void EnqueueDownloadItems(AttachmentFilter? filter = null) {} | ||||||
|   | |||||||
| @@ -0,0 +1,7 @@ | |||||||
|  | using DHT.Server.Data; | ||||||
|  |  | ||||||
|  | namespace DHT.Server.Database.Export.Strategy { | ||||||
|  | 	public interface IViewerExportStrategy { | ||||||
|  | 		string GetAttachmentUrl(Attachment attachment); | ||||||
|  | 	} | ||||||
|  | } | ||||||
| @@ -0,0 +1,18 @@ | |||||||
|  | using System.Net; | ||||||
|  | using DHT.Server.Data; | ||||||
|  |  | ||||||
|  | namespace DHT.Server.Database.Export.Strategy { | ||||||
|  | 	public sealed class LiveViewerExportStrategy : IViewerExportStrategy { | ||||||
|  | 		private readonly string safePort; | ||||||
|  | 		private readonly string safeToken; | ||||||
|  | 		 | ||||||
|  | 		public LiveViewerExportStrategy(ushort port, string token) { | ||||||
|  | 			this.safePort = port.ToString(); | ||||||
|  | 			this.safeToken = WebUtility.UrlEncode(token); | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		public string GetAttachmentUrl(Attachment attachment) { | ||||||
|  | 			return "http://127.0.0.1:" + safePort + "/get-attachment/" + WebUtility.UrlEncode(attachment.Url) + "?token=" + safeToken; | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | } | ||||||
| @@ -0,0 +1,13 @@ | |||||||
|  | using DHT.Server.Data; | ||||||
|  |  | ||||||
|  | namespace DHT.Server.Database.Export.Strategy { | ||||||
|  | 	public sealed class StandaloneViewerExportStrategy : IViewerExportStrategy { | ||||||
|  | 		public static StandaloneViewerExportStrategy Instance { get; } = new (); | ||||||
|  |  | ||||||
|  | 		private StandaloneViewerExportStrategy() {} | ||||||
|  |  | ||||||
|  | 		public string GetAttachmentUrl(Attachment attachment) { | ||||||
|  | 			return attachment.Url; | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | } | ||||||
| @@ -1,3 +1,4 @@ | |||||||
|  | using System; | ||||||
| using System.Collections.Generic; | using System.Collections.Generic; | ||||||
| using System.IO; | using System.IO; | ||||||
| using System.Linq; | using System.Linq; | ||||||
| @@ -5,13 +6,14 @@ using System.Text.Json; | |||||||
| using System.Threading.Tasks; | using System.Threading.Tasks; | ||||||
| using DHT.Server.Data; | using DHT.Server.Data; | ||||||
| using DHT.Server.Data.Filters; | using DHT.Server.Data.Filters; | ||||||
|  | using DHT.Server.Database.Export.Strategy; | ||||||
| using DHT.Utils.Logging; | using DHT.Utils.Logging; | ||||||
|  |  | ||||||
| namespace DHT.Server.Database.Export { | namespace DHT.Server.Database.Export { | ||||||
| 	public static class ViewerJsonExport { | 	public static class ViewerJsonExport { | ||||||
| 		private static readonly Log Log = Log.ForType(typeof(ViewerJsonExport)); | 		private static readonly Log Log = Log.ForType(typeof(ViewerJsonExport)); | ||||||
|  |  | ||||||
| 		public static async Task Generate(Stream stream, IDatabaseFile db, MessageFilter? filter = null) { | 		public static async Task Generate(Stream stream, IViewerExportStrategy strategy, IDatabaseFile db, MessageFilter? filter = null) { | ||||||
| 			var perf = Log.Start(); | 			var perf = Log.Start(); | ||||||
|  |  | ||||||
| 			var includedUserIds = new HashSet<ulong>(); | 			var includedUserIds = new HashSet<ulong>(); | ||||||
| @@ -41,7 +43,7 @@ namespace DHT.Server.Database.Export { | |||||||
|  |  | ||||||
| 			var value = new { | 			var value = new { | ||||||
| 				meta = new { users, userindex, servers, channels }, | 				meta = new { users, userindex, servers, channels }, | ||||||
| 				data = GenerateMessageList(includedMessages, userIndices) | 				data = GenerateMessageList(includedMessages, userIndices, strategy) | ||||||
| 			}; | 			}; | ||||||
| 			 | 			 | ||||||
| 			perf.Step("Generate value object"); | 			perf.Step("Generate value object"); | ||||||
| @@ -138,7 +140,7 @@ namespace DHT.Server.Database.Export { | |||||||
| 			return channels; | 			return channels; | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		private static object GenerateMessageList(List<Message> includedMessages, Dictionary<ulong, object> userIndices) { | 		private static object GenerateMessageList( List<Message> includedMessages, Dictionary<ulong, object> userIndices, IViewerExportStrategy strategy) { | ||||||
| 			var data = new Dictionary<string, Dictionary<string, object>>(); | 			var data = new Dictionary<string, Dictionary<string, object>>(); | ||||||
|  |  | ||||||
| 			foreach (var grouping in includedMessages.GroupBy(static message => message.Channel)) { | 			foreach (var grouping in includedMessages.GroupBy(static message => message.Channel)) { | ||||||
| @@ -164,8 +166,18 @@ namespace DHT.Server.Database.Export { | |||||||
| 					} | 					} | ||||||
|  |  | ||||||
| 					if (!message.Attachments.IsEmpty) { | 					if (!message.Attachments.IsEmpty) { | ||||||
| 						obj["a"] = message.Attachments.Select(static attachment => new Dictionary<string, object> { | 						obj["a"] = message.Attachments.Select(attachment => { | ||||||
| 							{ "url", attachment.Url } | 							var a = new Dictionary<string, object> { | ||||||
|  | 								{ "url", strategy.GetAttachmentUrl(attachment) }, | ||||||
|  | 								{ "name", Uri.TryCreate(attachment.Url, UriKind.Absolute, out var uri) ? Path.GetFileName(uri.LocalPath) : attachment.Url } | ||||||
|  | 							}; | ||||||
|  |  | ||||||
|  | 							if (attachment.Width != null && attachment.Height != null) { | ||||||
|  | 								a["width"] = attachment.Width; | ||||||
|  | 								a["height"] = attachment.Height; | ||||||
|  | 							} | ||||||
|  | 							 | ||||||
|  | 							return a; | ||||||
| 						}).ToArray(); | 						}).ToArray(); | ||||||
| 					} | 					} | ||||||
|  |  | ||||||
| @@ -188,7 +200,7 @@ namespace DHT.Server.Database.Export { | |||||||
| 							r["a"] = reaction.EmojiFlags.HasFlag(EmojiFlags.Animated); | 							r["a"] = reaction.EmojiFlags.HasFlag(EmojiFlags.Animated); | ||||||
| 							r["c"] = reaction.Count; | 							r["c"] = reaction.Count; | ||||||
| 							return r; | 							return r; | ||||||
| 						}); | 						}).ToArray(); | ||||||
| 					} | 					} | ||||||
|  |  | ||||||
| 					channelData[message.Id.ToString()] = obj; | 					channelData[message.Id.ToString()] = obj; | ||||||
|   | |||||||
| @@ -31,6 +31,7 @@ namespace DHT.Server.Database { | |||||||
| 		void AddDownload(Data.Download download); | 		void AddDownload(Data.Download download); | ||||||
| 		List<Data.Download> GetDownloadsWithoutData(); | 		List<Data.Download> GetDownloadsWithoutData(); | ||||||
| 		Data.Download GetDownloadWithData(Data.Download download); | 		Data.Download GetDownloadWithData(Data.Download download); | ||||||
|  | 		DownloadedAttachment? GetDownloadedAttachment(string url); | ||||||
|  |  | ||||||
| 		void EnqueueDownloadItems(AttachmentFilter? filter = null); | 		void EnqueueDownloadItems(AttachmentFilter? filter = null); | ||||||
| 		List<DownloadItem> GetEnqueuedDownloadItems(int count); | 		List<DownloadItem> GetEnqueuedDownloadItems(int count); | ||||||
|   | |||||||
| @@ -6,7 +6,7 @@ using DHT.Utils.Logging; | |||||||
|  |  | ||||||
| namespace DHT.Server.Database.Sqlite { | namespace DHT.Server.Database.Sqlite { | ||||||
| 	sealed class Schema { | 	sealed class Schema { | ||||||
| 		internal const int Version = 4; | 		internal const int Version = 5; | ||||||
|  |  | ||||||
| 		private static readonly Log Log = Log.ForType<Schema>(); | 		private static readonly Log Log = Log.ForType<Schema>(); | ||||||
|  |  | ||||||
| @@ -79,7 +79,9 @@ namespace DHT.Server.Database.Sqlite { | |||||||
| 			        name TEXT NOT NULL, | 			        name TEXT NOT NULL, | ||||||
| 			        type TEXT, | 			        type TEXT, | ||||||
| 			        url TEXT NOT NULL, | 			        url TEXT NOT NULL, | ||||||
| 			        size INTEGER NOT NULL)"); | 			        size INTEGER NOT NULL, | ||||||
|  | 			        width INTEGER, | ||||||
|  | 			        height INTEGER)"); | ||||||
|  |  | ||||||
| 			Execute(@"CREATE TABLE embeds ( | 			Execute(@"CREATE TABLE embeds ( | ||||||
| 			        message_id INTEGER NOT NULL, | 			        message_id INTEGER NOT NULL, | ||||||
| @@ -159,6 +161,12 @@ namespace DHT.Server.Database.Sqlite { | |||||||
| 				perf.Step("Upgrade to version 4"); | 				perf.Step("Upgrade to version 4"); | ||||||
| 			} | 			} | ||||||
|  |  | ||||||
|  | 			if (dbVersion <= 4) { | ||||||
|  | 				Execute("ALTER TABLE attachments ADD width INTEGER"); | ||||||
|  | 				Execute("ALTER TABLE attachments ADD height INTEGER"); | ||||||
|  | 				perf.Step("Upgrade to version 5"); | ||||||
|  | 			} | ||||||
|  |  | ||||||
| 			perf.End(); | 			perf.End(); | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
|   | |||||||
| @@ -252,7 +252,9 @@ namespace DHT.Server.Database.Sqlite { | |||||||
| 					("name", SqliteType.Text), | 					("name", SqliteType.Text), | ||||||
| 					("type", SqliteType.Text), | 					("type", SqliteType.Text), | ||||||
| 					("url", SqliteType.Text), | 					("url", SqliteType.Text), | ||||||
| 					("size", SqliteType.Integer) | 					("size", SqliteType.Integer), | ||||||
|  | 					("width", SqliteType.Integer), | ||||||
|  | 					("height", SqliteType.Integer) | ||||||
| 				}); | 				}); | ||||||
|  |  | ||||||
| 				using var embedCmd = conn.Insert("embeds", new[] { | 				using var embedCmd = conn.Insert("embeds", new[] { | ||||||
| @@ -307,6 +309,8 @@ namespace DHT.Server.Database.Sqlite { | |||||||
| 							attachmentCmd.Set(":type", attachment.Type); | 							attachmentCmd.Set(":type", attachment.Type); | ||||||
| 							attachmentCmd.Set(":url", attachment.Url); | 							attachmentCmd.Set(":url", attachment.Url); | ||||||
| 							attachmentCmd.Set(":size", attachment.Size); | 							attachmentCmd.Set(":size", attachment.Size); | ||||||
|  | 							attachmentCmd.Set(":width", attachment.Width); | ||||||
|  | 							attachmentCmd.Set(":height", attachment.Height); | ||||||
| 							attachmentCmd.ExecuteNonQuery(); | 							attachmentCmd.ExecuteNonQuery(); | ||||||
| 						} | 						} | ||||||
| 					} | 					} | ||||||
| @@ -470,6 +474,28 @@ LEFT JOIN replied_to rt ON m.message_id = rt.message_id" + filter.GenerateWhereC | |||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
|  | 		public DownloadedAttachment? GetDownloadedAttachment(string url) { | ||||||
|  | 			using var conn = pool.Take(); | ||||||
|  | 			using var cmd = conn.Command(@" | ||||||
|  | SELECT a.type, d.blob FROM downloads d | ||||||
|  | LEFT JOIN attachments a ON d.url = a.url | ||||||
|  | WHERE d.url = :url AND d.status = :success AND d.blob IS NOT NULL"); | ||||||
|  | 			 | ||||||
|  | 			cmd.AddAndSet(":url", SqliteType.Text, url); | ||||||
|  | 			cmd.AddAndSet(":success", SqliteType.Integer, (int) DownloadStatus.Success); | ||||||
|  | 			 | ||||||
|  | 			using var reader = cmd.ExecuteReader(); | ||||||
|  |  | ||||||
|  | 			if (!reader.Read()) { | ||||||
|  | 				return null; | ||||||
|  | 			} | ||||||
|  | 			 | ||||||
|  | 			return new DownloadedAttachment { | ||||||
|  | 				Type = reader.IsDBNull(0) ? null : reader.GetString(0), | ||||||
|  | 				Data = (byte[]) reader["blob"] | ||||||
|  | 			}; | ||||||
|  | 		} | ||||||
|  |  | ||||||
| 		public void EnqueueDownloadItems(AttachmentFilter? filter = null) { | 		public void EnqueueDownloadItems(AttachmentFilter? filter = null) { | ||||||
| 			using var conn = pool.Take(); | 			using var conn = pool.Take(); | ||||||
| 			using var cmd = conn.Command("INSERT INTO downloads (url, status, size) SELECT a.url, :enqueued, MAX(a.size) FROM attachments a" + filter.GenerateWhereClause("a") + " GROUP BY a.url"); | 			using var cmd = conn.Command("INSERT INTO downloads (url, status, size) SELECT a.url, :enqueued, MAX(a.size) FROM attachments a" + filter.GenerateWhereClause("a") + " GROUP BY a.url"); | ||||||
| @@ -549,7 +575,7 @@ FROM downloads"); | |||||||
| 			var dict = new MultiDictionary<ulong, Attachment>(); | 			var dict = new MultiDictionary<ulong, Attachment>(); | ||||||
|  |  | ||||||
| 			using var conn = pool.Take(); | 			using var conn = pool.Take(); | ||||||
| 			using var cmd = conn.Command("SELECT message_id, attachment_id, name, type, url, size FROM attachments"); | 			using var cmd = conn.Command("SELECT message_id, attachment_id, name, type, url, size, width, height FROM attachments"); | ||||||
| 			using var reader = cmd.ExecuteReader(); | 			using var reader = cmd.ExecuteReader(); | ||||||
|  |  | ||||||
| 			while (reader.Read()) { | 			while (reader.Read()) { | ||||||
| @@ -560,7 +586,9 @@ FROM downloads"); | |||||||
| 					Name = reader.GetString(2), | 					Name = reader.GetString(2), | ||||||
| 					Type = reader.IsDBNull(3) ? null : reader.GetString(3), | 					Type = reader.IsDBNull(3) ? null : reader.GetString(3), | ||||||
| 					Url = reader.GetString(4), | 					Url = reader.GetString(4), | ||||||
| 					Size = reader.GetUint64(5) | 					Size = reader.GetUint64(5), | ||||||
|  | 					Width = reader.IsDBNull(6) ? null : reader.GetInt32(6), | ||||||
|  | 					Height = reader.IsDBNull(7) ? null : reader.GetInt32(7) | ||||||
| 				}); | 				}); | ||||||
| 			} | 			} | ||||||
|  |  | ||||||
|   | |||||||
| @@ -45,22 +45,21 @@ namespace DHT.Server.Database.Sqlite.Utils { | |||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		public ISqliteConnection Take() { | 		public ISqliteConnection Take() { | ||||||
| 			PooledConnection? conn = null; | 			while (true) { | ||||||
| 			 |  | ||||||
| 			while (conn == null) { |  | ||||||
| 				ThrowIfDisposed(); | 				ThrowIfDisposed(); | ||||||
|  | 				 | ||||||
| 				lock (monitor) { | 				lock (monitor) { | ||||||
| 					if (free.TryTake(out conn, TimeSpan.FromMilliseconds(rand.Next(100, 200)))) { | 					if (free.TryTake(out var conn)) { | ||||||
| 						used.Add(conn); | 						used.Add(conn); | ||||||
| 						break; | 						return conn; | ||||||
| 					} | 					} | ||||||
| 					else { | 					else { | ||||||
| 						Log.ForType<SqliteConnectionPool>().Warn("Thread " + Thread.CurrentThread.ManagedThreadId + " is starving for connections."); | 						Log.ForType<SqliteConnectionPool>().Warn("Thread " + Thread.CurrentThread.ManagedThreadId + " is starving for connections."); | ||||||
| 					} | 					} | ||||||
| 				} | 				} | ||||||
| 			} |  | ||||||
| 				 | 				 | ||||||
| 			return conn; | 				Thread.Sleep(TimeSpan.FromMilliseconds(rand.Next(100, 200))); | ||||||
|  | 			} | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		private void Return(PooledConnection conn) { | 		private void Return(PooledConnection conn) { | ||||||
|   | |||||||
| @@ -8,6 +8,7 @@ using DHT.Utils.Http; | |||||||
| using DHT.Utils.Logging; | using DHT.Utils.Logging; | ||||||
| using Microsoft.AspNetCore.Http; | using Microsoft.AspNetCore.Http; | ||||||
| using Microsoft.AspNetCore.Http.Extensions; | using Microsoft.AspNetCore.Http.Extensions; | ||||||
|  | using Microsoft.Extensions.Primitives; | ||||||
|  |  | ||||||
| namespace DHT.Server.Endpoints { | namespace DHT.Server.Endpoints { | ||||||
| 	abstract class BaseEndpoint { | 	abstract class BaseEndpoint { | ||||||
| @@ -21,26 +22,22 @@ namespace DHT.Server.Endpoints { | |||||||
| 			this.parameters = parameters; | 			this.parameters = parameters; | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		public async Task Handle(HttpContext ctx) { | 		private async Task Handle(HttpContext ctx, StringValues token) { | ||||||
| 			var request = ctx.Request; | 			var request = ctx.Request; | ||||||
| 			var response = ctx.Response; | 			var response = ctx.Response; | ||||||
|  |  | ||||||
| 			Log.Info("Request: " + request.GetDisplayUrl() + " (" + request.ContentLength + " B)"); | 			Log.Info("Request: " + request.GetDisplayUrl() + " (" + request.ContentLength + " B)"); | ||||||
| 			 | 			 | ||||||
| 			var requestToken = request.Headers["X-DHT-Token"]; | 			if (token.Count != 1 || token[0] != parameters.Token) { | ||||||
| 			if (requestToken.Count != 1 || requestToken[0] != parameters.Token) { | 				Log.Error("Token: " + (token.Count == 1 ? token[0] : "<missing>")); | ||||||
| 				Log.Error("Token: " + (requestToken.Count == 1 ? requestToken[0] : "<missing>")); |  | ||||||
| 				response.StatusCode = (int) HttpStatusCode.Forbidden; | 				response.StatusCode = (int) HttpStatusCode.Forbidden; | ||||||
| 				return; | 				return; | ||||||
| 			} | 			} | ||||||
|  |  | ||||||
| 			try { | 			try { | ||||||
| 				var (statusCode, output) = await Respond(ctx); | 				response.StatusCode = (int) HttpStatusCode.OK; | ||||||
| 				response.StatusCode = (int) statusCode; | 				var output = await Respond(ctx); | ||||||
|  | 				await output.WriteTo(response); | ||||||
| 				if (output != null) { |  | ||||||
| 					await response.WriteAsJsonAsync(output); |  | ||||||
| 				} |  | ||||||
| 			} catch (HttpException e) { | 			} catch (HttpException e) { | ||||||
| 				Log.Error(e); | 				Log.Error(e); | ||||||
| 				response.StatusCode = (int) e.StatusCode; | 				response.StatusCode = (int) e.StatusCode; | ||||||
| @@ -51,7 +48,15 @@ namespace DHT.Server.Endpoints { | |||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		protected abstract Task<(HttpStatusCode, object?)> Respond(HttpContext ctx); | 		public async Task HandleGet(HttpContext ctx) { | ||||||
|  | 			await Handle(ctx, ctx.Request.Query["token"]); | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		public async Task HandlePost(HttpContext ctx) { | ||||||
|  | 			await Handle(ctx, ctx.Request.Headers["X-DHT-Token"]); | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		protected abstract Task<IHttpOutput> Respond(HttpContext ctx); | ||||||
|  |  | ||||||
| 		protected static async Task<JsonElement> ReadJson(HttpContext ctx) { | 		protected static async Task<JsonElement> ReadJson(HttpContext ctx) { | ||||||
| 			return await ctx.Request.ReadFromJsonAsync<JsonElement?>() ?? throw new HttpException(HttpStatusCode.UnsupportedMediaType, "This endpoint only accepts JSON."); | 			return await ctx.Request.ReadFromJsonAsync<JsonElement?>() ?? throw new HttpException(HttpStatusCode.UnsupportedMediaType, "This endpoint only accepts JSON."); | ||||||
|   | |||||||
							
								
								
									
										25
									
								
								app/Server/Endpoints/GetAttachmentEndpoint.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								app/Server/Endpoints/GetAttachmentEndpoint.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,25 @@ | |||||||
|  | using System.Net; | ||||||
|  | using System.Threading.Tasks; | ||||||
|  | using DHT.Server.Data; | ||||||
|  | using DHT.Server.Database; | ||||||
|  | using DHT.Server.Service; | ||||||
|  | using DHT.Utils.Http; | ||||||
|  | using Microsoft.AspNetCore.Http; | ||||||
|  |  | ||||||
|  | namespace DHT.Server.Endpoints { | ||||||
|  | 	sealed class GetAttachmentEndpoint : BaseEndpoint { | ||||||
|  | 		public GetAttachmentEndpoint(IDatabaseFile db, ServerParameters parameters) : base(db, parameters) {} | ||||||
|  | 		 | ||||||
|  | 		protected override Task<IHttpOutput> Respond(HttpContext ctx) { | ||||||
|  | 			string attachmentUrl = WebUtility.UrlDecode((string) ctx.Request.RouteValues["url"]!); | ||||||
|  | 			DownloadedAttachment? maybeDownloadedAttachment = Db.GetDownloadedAttachment(attachmentUrl); | ||||||
|  |  | ||||||
|  | 			if (maybeDownloadedAttachment is {} downloadedAttachment) { | ||||||
|  | 				return Task.FromResult<IHttpOutput>(new HttpOutput.File(downloadedAttachment.Type, downloadedAttachment.Data)); | ||||||
|  | 			} | ||||||
|  | 			else { | ||||||
|  | 				return Task.FromResult<IHttpOutput>(new HttpOutput.Redirect(attachmentUrl, permanent: false)); | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | } | ||||||
| @@ -11,7 +11,7 @@ namespace DHT.Server.Endpoints { | |||||||
| 	sealed class TrackChannelEndpoint : BaseEndpoint { | 	sealed class TrackChannelEndpoint : BaseEndpoint { | ||||||
| 		public TrackChannelEndpoint(IDatabaseFile db, ServerParameters parameters) : base(db, parameters) {} | 		public TrackChannelEndpoint(IDatabaseFile db, ServerParameters parameters) : base(db, parameters) {} | ||||||
|  |  | ||||||
| 		protected override async Task<(HttpStatusCode, object?)> Respond(HttpContext ctx) { | 		protected override async Task<IHttpOutput> Respond(HttpContext ctx) { | ||||||
| 			var root = await ReadJson(ctx); | 			var root = await ReadJson(ctx); | ||||||
| 			var server = ReadServer(root.RequireObject("server"), "server"); | 			var server = ReadServer(root.RequireObject("server"), "server"); | ||||||
| 			var channel = ReadChannel(root.RequireObject("channel"), "channel", server.Id); | 			var channel = ReadChannel(root.RequireObject("channel"), "channel", server.Id); | ||||||
| @@ -19,7 +19,7 @@ namespace DHT.Server.Endpoints { | |||||||
| 			Db.AddServer(server); | 			Db.AddServer(server); | ||||||
| 			Db.AddChannel(channel); | 			Db.AddChannel(channel); | ||||||
|  |  | ||||||
| 			return (HttpStatusCode.OK, null); | 			return HttpOutput.None; | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		private static Data.Server ReadServer(JsonElement json, string path) => new() { | 		private static Data.Server ReadServer(JsonElement json, string path) => new() { | ||||||
|   | |||||||
| @@ -17,7 +17,7 @@ namespace DHT.Server.Endpoints { | |||||||
| 	sealed class TrackMessagesEndpoint : BaseEndpoint { | 	sealed class TrackMessagesEndpoint : BaseEndpoint { | ||||||
| 		public TrackMessagesEndpoint(IDatabaseFile db, ServerParameters parameters) : base(db, parameters) {} | 		public TrackMessagesEndpoint(IDatabaseFile db, ServerParameters parameters) : base(db, parameters) {} | ||||||
|  |  | ||||||
| 		protected override async Task<(HttpStatusCode, object?)> Respond(HttpContext ctx) { | 		protected override async Task<IHttpOutput> Respond(HttpContext ctx) { | ||||||
| 			var root = await ReadJson(ctx); | 			var root = await ReadJson(ctx); | ||||||
|  |  | ||||||
| 			if (root.ValueKind != JsonValueKind.Array) { | 			if (root.ValueKind != JsonValueKind.Array) { | ||||||
| @@ -39,7 +39,7 @@ namespace DHT.Server.Endpoints { | |||||||
|  |  | ||||||
| 			Db.AddMessages(messages); | 			Db.AddMessages(messages); | ||||||
|  |  | ||||||
| 			return (HttpStatusCode.OK, anyNewMessages ? 1 : 0); | 			return new HttpOutput.Json(anyNewMessages ? 1 : 0); | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		private static Message ReadMessage(JsonElement json, string path) => new() { | 		private static Message ReadMessage(JsonElement json, string path) => new() { | ||||||
| @@ -61,7 +61,9 @@ namespace DHT.Server.Endpoints { | |||||||
| 			Name = ele.RequireString("name", path), | 			Name = ele.RequireString("name", path), | ||||||
| 			Type = ele.HasKey("type") ? ele.RequireString("type", path) : null, | 			Type = ele.HasKey("type") ? ele.RequireString("type", path) : null, | ||||||
| 			Url = ele.RequireString("url", path), | 			Url = ele.RequireString("url", path), | ||||||
| 			Size = (ulong) ele.RequireLong("size", path) | 			Size = (ulong) ele.RequireLong("size", path), | ||||||
|  | 			Width = ele.HasKey("width") ? ele.RequireInt("width", path) : null, | ||||||
|  | 			Height = ele.HasKey("height") ? ele.RequireInt("height", path) : null | ||||||
| 		}).DistinctByKeyStable(static attachment => { | 		}).DistinctByKeyStable(static attachment => { | ||||||
| 			// Some Discord messages have duplicate attachments with the same id for unknown reasons. | 			// Some Discord messages have duplicate attachments with the same id for unknown reasons. | ||||||
| 			return attachment.Id; | 			return attachment.Id; | ||||||
|   | |||||||
| @@ -11,7 +11,7 @@ namespace DHT.Server.Endpoints { | |||||||
| 	sealed class TrackUsersEndpoint : BaseEndpoint { | 	sealed class TrackUsersEndpoint : BaseEndpoint { | ||||||
| 		public TrackUsersEndpoint(IDatabaseFile db, ServerParameters parameters) : base(db, parameters) {} | 		public TrackUsersEndpoint(IDatabaseFile db, ServerParameters parameters) : base(db, parameters) {} | ||||||
|  |  | ||||||
| 		protected override async Task<(HttpStatusCode, object?)> Respond(HttpContext ctx) { | 		protected override async Task<IHttpOutput> Respond(HttpContext ctx) { | ||||||
| 			var root = await ReadJson(ctx); | 			var root = await ReadJson(ctx); | ||||||
|  |  | ||||||
| 			if (root.ValueKind != JsonValueKind.Array) { | 			if (root.ValueKind != JsonValueKind.Array) { | ||||||
| @@ -27,7 +27,7 @@ namespace DHT.Server.Endpoints { | |||||||
|  |  | ||||||
| 			Db.AddUsers(users); | 			Db.AddUsers(users); | ||||||
|  |  | ||||||
| 			return (HttpStatusCode.OK, null); | 			return HttpOutput.None; | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		private static User ReadUser(JsonElement json, string path) => new() { | 		private static User ReadUser(JsonElement json, string path) => new() { | ||||||
|   | |||||||
| @@ -20,7 +20,7 @@ | |||||||
|     <FrameworkReference Include="Microsoft.AspNetCore.App" /> |     <FrameworkReference Include="Microsoft.AspNetCore.App" /> | ||||||
|   </ItemGroup> |   </ItemGroup> | ||||||
|   <ItemGroup> |   <ItemGroup> | ||||||
|     <PackageReference Include="Microsoft.Data.Sqlite" Version="6.0.5" /> |     <PackageReference Include="Microsoft.Data.Sqlite" Version="6.0.7" /> | ||||||
|   </ItemGroup> |   </ItemGroup> | ||||||
|   <ItemGroup> |   <ItemGroup> | ||||||
|     <ProjectReference Include="..\Utils\Utils.csproj" /> |     <ProjectReference Include="..\Utils\Utils.csproj" /> | ||||||
|   | |||||||
| @@ -9,6 +9,13 @@ using Microsoft.Extensions.Hosting; | |||||||
|  |  | ||||||
| namespace DHT.Server.Service { | namespace DHT.Server.Service { | ||||||
| 	sealed class Startup { | 	sealed class Startup { | ||||||
|  | 		private static readonly string[] AllowedOrigins = { | ||||||
|  | 			"https://discord.com", | ||||||
|  | 			"https://ptb.discord.com", | ||||||
|  | 			"https://canary.discord.com", | ||||||
|  | 			"https://discordapp.com" | ||||||
|  | 		}; | ||||||
|  |  | ||||||
| 		public void ConfigureServices(IServiceCollection services) { | 		public void ConfigureServices(IServiceCollection services) { | ||||||
| 			services.Configure<JsonOptions>(static options => { | 			services.Configure<JsonOptions>(static options => { | ||||||
| 				options.SerializerOptions.NumberHandling = JsonNumberHandling.Strict; | 				options.SerializerOptions.NumberHandling = JsonNumberHandling.Strict; | ||||||
| @@ -16,7 +23,7 @@ namespace DHT.Server.Service { | |||||||
|  |  | ||||||
| 			services.AddCors(static cors => { | 			services.AddCors(static cors => { | ||||||
| 				cors.AddDefaultPolicy(static builder => { | 				cors.AddDefaultPolicy(static builder => { | ||||||
| 					builder.WithOrigins("https://discord.com", "https://discordapp.com").AllowCredentials().AllowAnyMethod().AllowAnyHeader(); | 					builder.WithOrigins(AllowedOrigins).AllowCredentials().AllowAnyMethod().AllowAnyHeader(); | ||||||
| 				}); | 				}); | ||||||
| 			}); | 			}); | ||||||
| 		} | 		} | ||||||
| @@ -27,13 +34,16 @@ namespace DHT.Server.Service { | |||||||
| 			app.UseCors(); | 			app.UseCors(); | ||||||
| 			app.UseEndpoints(endpoints => { | 			app.UseEndpoints(endpoints => { | ||||||
| 				TrackChannelEndpoint trackChannel = new(db, parameters); | 				TrackChannelEndpoint trackChannel = new(db, parameters); | ||||||
| 				endpoints.MapPost("/track-channel", async context => await trackChannel.Handle(context)); | 				endpoints.MapPost("/track-channel", async context => await trackChannel.HandlePost(context)); | ||||||
|  |  | ||||||
| 				TrackUsersEndpoint trackUsers = new(db, parameters); | 				TrackUsersEndpoint trackUsers = new(db, parameters); | ||||||
| 				endpoints.MapPost("/track-users", async context => await trackUsers.Handle(context)); | 				endpoints.MapPost("/track-users", async context => await trackUsers.HandlePost(context)); | ||||||
|  |  | ||||||
| 				TrackMessagesEndpoint trackMessages = new(db, parameters); | 				TrackMessagesEndpoint trackMessages = new(db, parameters); | ||||||
| 				endpoints.MapPost("/track-messages", async context => await trackMessages.Handle(context)); | 				endpoints.MapPost("/track-messages", async context => await trackMessages.HandlePost(context)); | ||||||
|  |  | ||||||
|  | 				GetAttachmentEndpoint getAttachment = new(db, parameters); | ||||||
|  | 				endpoints.MapGet("/get-attachment/{url}", async context => await getAttachment.HandleGet(context)); | ||||||
| 			}); | 			}); | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
|   | |||||||
							
								
								
									
										56
									
								
								app/Utils/Http/HttpOutput.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										56
									
								
								app/Utils/Http/HttpOutput.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,56 @@ | |||||||
|  | using System.Threading.Tasks; | ||||||
|  | using Microsoft.AspNetCore.Http; | ||||||
|  |  | ||||||
|  | namespace DHT.Utils.Http { | ||||||
|  | 	public static class HttpOutput { | ||||||
|  | 		public static IHttpOutput None { get; } = new NoneImpl(); | ||||||
|  | 		 | ||||||
|  | 		private sealed class NoneImpl : IHttpOutput { | ||||||
|  | 			public Task WriteTo(HttpResponse response) { | ||||||
|  | 				return Task.CompletedTask; | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		public sealed class Json : IHttpOutput { | ||||||
|  | 			private readonly object? obj; | ||||||
|  | 			 | ||||||
|  | 			public Json(object? obj) { | ||||||
|  | 				this.obj = obj; | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 			public Task WriteTo(HttpResponse response) { | ||||||
|  | 				return response.WriteAsJsonAsync(obj); | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		public sealed class File : IHttpOutput { | ||||||
|  | 			private readonly string? contentType; | ||||||
|  | 			private readonly byte[] bytes; | ||||||
|  | 			 | ||||||
|  | 			public File(string? contentType, byte[] bytes) { | ||||||
|  | 				this.contentType = contentType; | ||||||
|  | 				this.bytes = bytes; | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 			public async Task WriteTo(HttpResponse response) { | ||||||
|  | 				response.ContentType = contentType ?? string.Empty; | ||||||
|  | 				await response.Body.WriteAsync(bytes); | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		public sealed class Redirect : IHttpOutput { | ||||||
|  | 			private readonly string url; | ||||||
|  | 			private readonly bool permanent; | ||||||
|  |  | ||||||
|  | 			public Redirect(string url, bool permanent) { | ||||||
|  | 				this.url = url; | ||||||
|  | 				this.permanent = permanent; | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 			public Task WriteTo(HttpResponse response) { | ||||||
|  | 				response.Redirect(url, permanent); | ||||||
|  | 				return Task.CompletedTask; | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | } | ||||||
							
								
								
									
										8
									
								
								app/Utils/Http/IHttpOutput.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								app/Utils/Http/IHttpOutput.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,8 @@ | |||||||
|  | using System.Threading.Tasks; | ||||||
|  | using Microsoft.AspNetCore.Http; | ||||||
|  |  | ||||||
|  | namespace DHT.Utils.Http { | ||||||
|  | 	public interface IHttpOutput { | ||||||
|  | 		Task WriteTo(HttpResponse response); | ||||||
|  | 	} | ||||||
|  | } | ||||||
| @@ -17,7 +17,10 @@ | |||||||
|     <DebugType>none</DebugType> |     <DebugType>none</DebugType> | ||||||
|   </PropertyGroup> |   </PropertyGroup> | ||||||
|   <ItemGroup> |   <ItemGroup> | ||||||
|     <PackageReference Include="JetBrains.Annotations" Version="10.3.0" /> |     <FrameworkReference Include="Microsoft.AspNetCore.App" /> | ||||||
|  |   </ItemGroup> | ||||||
|  |   <ItemGroup> | ||||||
|  |     <PackageReference Include="JetBrains.Annotations" Version="2022.1.0" /> | ||||||
|   </ItemGroup> |   </ItemGroup> | ||||||
|   <ItemGroup> |   <ItemGroup> | ||||||
|     <Compile Include="..\Version.cs" Link="Version.cs" /> |     <Compile Include="..\Version.cs" Link="Version.cs" /> | ||||||
|   | |||||||
| @@ -7,6 +7,6 @@ using DHT.Utils; | |||||||
|  |  | ||||||
| namespace DHT.Utils { | namespace DHT.Utils { | ||||||
| 	static class Version { | 	static class Version { | ||||||
| 		public const string Tag = "36.2.0.0"; | 		public const string Tag = "37.2.0.0"; | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|   | |||||||
							
								
								
									
										
											BIN
										
									
								
								app/empty.dht
									
									
									
									
									
								
							
							
						
						
									
										
											BIN
										
									
								
								app/empty.dht
									
									
									
									
									
								
							
										
											Binary file not shown.
										
									
								
							
		Reference in New Issue
	
	Block a user