feat: clickable URLs in chat log
Adds a parsing step when constructing `Message` objects that scans the message content for anything that looks URL-like, and inserts new `TextChunk`s into the message content with a URIPayload set. Hovering over a URL shows an on-hover effect. Clicking a URL opens it in the default browser. Right clicking shows the hostname, with an option to open and an option to copy the URL to the clipboard.
This commit is contained in:
@@ -0,0 +1,6 @@
|
|||||||
|
[*]
|
||||||
|
indent_style = space
|
||||||
|
tab_width = 4
|
||||||
|
indent_size = 4
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
insert_final_newline = true
|
||||||
@@ -59,6 +59,19 @@ internal class TextChunk : Chunk {
|
|||||||
public TextChunk() : base(ChunkSource.None, null) {
|
public TextChunk() : base(ChunkSource.None, null) {
|
||||||
}
|
}
|
||||||
#pragma warning restore CS8618
|
#pragma warning restore CS8618
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a new TextChunk with identical styling to this one.
|
||||||
|
/// </summary>
|
||||||
|
public TextChunk NewWithStyle(ChunkSource source, Payload? link, string content)
|
||||||
|
{
|
||||||
|
return new TextChunk(source, link, content) {
|
||||||
|
FallbackColour = this.FallbackColour,
|
||||||
|
Foreground = this.Foreground,
|
||||||
|
Glow = this.Glow,
|
||||||
|
Italic = this.Italic,
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal class IconChunk : Chunk {
|
internal class IconChunk : Chunk {
|
||||||
|
|||||||
+88
-1
@@ -1,7 +1,9 @@
|
|||||||
using ChatTwo.Code;
|
using ChatTwo.Code;
|
||||||
|
using ChatTwo.Util;
|
||||||
using Dalamud.Game.Text.SeStringHandling;
|
using Dalamud.Game.Text.SeStringHandling;
|
||||||
using Dalamud.Game.Text.SeStringHandling.Payloads;
|
using Dalamud.Game.Text.SeStringHandling.Payloads;
|
||||||
using LiteDB;
|
using LiteDB;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
|
||||||
namespace ChatTwo;
|
namespace ChatTwo;
|
||||||
|
|
||||||
@@ -70,7 +72,7 @@ internal class Message {
|
|||||||
this.Date = DateTime.UtcNow;
|
this.Date = DateTime.UtcNow;
|
||||||
this.Code = code;
|
this.Code = code;
|
||||||
this.Sender = sender;
|
this.Sender = sender;
|
||||||
this.Content = content;
|
this.Content = ReplaceContentURLs(content);
|
||||||
this.SenderSource = senderSource;
|
this.SenderSource = senderSource;
|
||||||
this.ContentSource = contentSource;
|
this.ContentSource = contentSource;
|
||||||
this.SortCode = new SortCode(this.Code.Type, this.Code.Source);
|
this.SortCode = new SortCode(this.Code.Type, this.Code.Source);
|
||||||
@@ -89,6 +91,8 @@ internal class Message {
|
|||||||
this.Date = date;
|
this.Date = date;
|
||||||
this.Code = BsonMapper.Global.ToObject<ChatCode>(code);
|
this.Code = BsonMapper.Global.ToObject<ChatCode>(code);
|
||||||
this.Sender = BsonMapper.Global.Deserialize<List<Chunk>>(sender);
|
this.Sender = BsonMapper.Global.Deserialize<List<Chunk>>(sender);
|
||||||
|
// Don't call ReplaceContentURLs here since we're loading the message
|
||||||
|
// from the database and it should already have parsed URL data.
|
||||||
this.Content = BsonMapper.Global.Deserialize<List<Chunk>>(content);
|
this.Content = BsonMapper.Global.Deserialize<List<Chunk>>(content);
|
||||||
this.SenderSource = BsonMapper.Global.Deserialize<SeString>(senderSource);
|
this.SenderSource = BsonMapper.Global.Deserialize<SeString>(senderSource);
|
||||||
this.ContentSource = BsonMapper.Global.Deserialize<SeString>(contentSource);
|
this.ContentSource = BsonMapper.Global.Deserialize<SeString>(contentSource);
|
||||||
@@ -108,6 +112,8 @@ internal class Message {
|
|||||||
this.Date = date;
|
this.Date = date;
|
||||||
this.Code = BsonMapper.Global.ToObject<ChatCode>(code);
|
this.Code = BsonMapper.Global.ToObject<ChatCode>(code);
|
||||||
this.Sender = BsonMapper.Global.Deserialize<List<Chunk>>(sender);
|
this.Sender = BsonMapper.Global.Deserialize<List<Chunk>>(sender);
|
||||||
|
// Don't call ReplaceContentURLs here since we're loading the message
|
||||||
|
// from the database and it should already have parsed URL data.
|
||||||
this.Content = BsonMapper.Global.Deserialize<List<Chunk>>(content);
|
this.Content = BsonMapper.Global.Deserialize<List<Chunk>>(content);
|
||||||
this.SenderSource = BsonMapper.Global.Deserialize<SeString>(senderSource);
|
this.SenderSource = BsonMapper.Global.Deserialize<SeString>(senderSource);
|
||||||
this.ContentSource = BsonMapper.Global.Deserialize<SeString>(contentSource);
|
this.ContentSource = BsonMapper.Global.Deserialize<SeString>(contentSource);
|
||||||
@@ -138,4 +144,85 @@ internal class Message {
|
|||||||
|
|
||||||
return Guid.Empty;
|
return Guid.Empty;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// URLRegex returns a regex object that matches URLs like:
|
||||||
|
/// - https://example.com
|
||||||
|
/// - http://example.com
|
||||||
|
/// - www.example.com
|
||||||
|
/// - https://sub.example.com
|
||||||
|
/// - example.com
|
||||||
|
/// - sub.example.com
|
||||||
|
///
|
||||||
|
/// It matches URLs with www. or https:// prefix, and also matches URLs
|
||||||
|
/// without a prefix on specific TLDs.
|
||||||
|
/// </summary>
|
||||||
|
private static Regex URLRegex = new Regex(
|
||||||
|
@"((https?:\/\/|www\.)[a-z0-9-]+(\.[a-z0-9-]+)*|([a-z0-9-]+(\.[a-z0-9-]+)*\.(com|net|org|co|io|app)))(:[\d]{1,5})?(\/[^\s]+)?",
|
||||||
|
RegexOptions.Compiled | RegexOptions.IgnoreCase
|
||||||
|
);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Finds all URL strings in all TextChunks, splits the parent TextChunk
|
||||||
|
/// apart and inserts a new TextChunk with a URIPayload.
|
||||||
|
/// </summary>
|
||||||
|
private List<Chunk> ReplaceContentURLs(List<Chunk> content)
|
||||||
|
{
|
||||||
|
var newChunks = new List<Chunk>();
|
||||||
|
void AddChunkWithMessage(Chunk chunk) {
|
||||||
|
chunk.Message = this;
|
||||||
|
newChunks.Add(chunk);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var chunk in content)
|
||||||
|
{
|
||||||
|
// Use as is if it's not a text chunk or it already has a payload.
|
||||||
|
if (chunk is not TextChunk text || chunk.Link != null)
|
||||||
|
{
|
||||||
|
// No need to call AddChunkWithMessage here since the chunk
|
||||||
|
// already has the Message field set.
|
||||||
|
newChunks.Add(chunk);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find all URLs with the regex and insert a new TextChunk with a
|
||||||
|
// URIPayload.
|
||||||
|
var matches = URLRegex.Matches(text.Content);
|
||||||
|
var remainderIndex = 0;
|
||||||
|
foreach (Match match in matches.Cast<Match>())
|
||||||
|
{
|
||||||
|
// Add the text before the URL.
|
||||||
|
if (match.Index > remainderIndex)
|
||||||
|
{
|
||||||
|
AddChunkWithMessage(text.NewWithStyle(chunk.Source, chunk.Link, text.Content[remainderIndex..match.Index]));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the remainder index.
|
||||||
|
remainderIndex = match.Index + match.Length;
|
||||||
|
|
||||||
|
// Create a new TextChunk with a URIPayload for the URL text.
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var link = URIPayload.ResolveURI(match.Value);
|
||||||
|
AddChunkWithMessage(text.NewWithStyle(chunk.Source, link, match.Value));
|
||||||
|
}
|
||||||
|
catch (UriFormatException)
|
||||||
|
{
|
||||||
|
Plugin.Log.Debug($"Invalid URL accepted by Regex but failed URI parsing: '{match.Value}'");
|
||||||
|
// If the URL is invalid, set the remainder index to the
|
||||||
|
// beginning of the match so it'll get included in the next
|
||||||
|
// regular text chunk.
|
||||||
|
remainderIndex = match.Index;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add the text after the last URL.
|
||||||
|
if (remainderIndex < text.Content.Length)
|
||||||
|
{
|
||||||
|
AddChunkWithMessage(text.NewWithStyle(chunk.Source, null, text.Content[remainderIndex..]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return newChunks;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ using Lumina.Excel.GeneratedSheets;
|
|||||||
using Action = System.Action;
|
using Action = System.Action;
|
||||||
using DalamudPartyFinderPayload = Dalamud.Game.Text.SeStringHandling.Payloads.PartyFinderPayload;
|
using DalamudPartyFinderPayload = Dalamud.Game.Text.SeStringHandling.Payloads.PartyFinderPayload;
|
||||||
using ChatTwoPartyFinderPayload = ChatTwo.Util.PartyFinderPayload;
|
using ChatTwoPartyFinderPayload = ChatTwo.Util.PartyFinderPayload;
|
||||||
|
using System.Diagnostics;
|
||||||
|
|
||||||
namespace ChatTwo;
|
namespace ChatTwo;
|
||||||
|
|
||||||
@@ -86,6 +87,11 @@ public sealed class PayloadHandler {
|
|||||||
drawn = true;
|
drawn = true;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case URIPayload uri: {
|
||||||
|
DrawUriPopup(uri);
|
||||||
|
drawn = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ContextFooter(drawn, chunk);
|
ContextFooter(drawn, chunk);
|
||||||
@@ -215,6 +221,11 @@ public sealed class PayloadHandler {
|
|||||||
DoHover(() => HoverItem(item), hoverSize);
|
DoHover(() => HoverItem(item), hoverSize);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case URIPayload uri:
|
||||||
|
{
|
||||||
|
DoHover(() => HoverURI(uri), hoverSize);
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -334,6 +345,11 @@ public sealed class PayloadHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void HoverURI(URIPayload uri) {
|
||||||
|
ImGui.TextUnformatted(string.Format(Language.Context_URLDomain, uri.Uri.Authority));
|
||||||
|
ImGuiUtil.WarningText(Language.Context_URLWarning);
|
||||||
|
}
|
||||||
|
|
||||||
private void LeftClickPayload(Chunk chunk, Payload? payload) {
|
private void LeftClickPayload(Chunk chunk, Payload? payload) {
|
||||||
switch (payload) {
|
switch (payload) {
|
||||||
case MapLinkPayload map: {
|
case MapLinkPayload map: {
|
||||||
@@ -372,6 +388,10 @@ public sealed class PayloadHandler {
|
|||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case URIPayload uri: {
|
||||||
|
TryOpenURI(uri.Uri);
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -625,4 +645,38 @@ public sealed class PayloadHandler {
|
|||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void DrawUriPopup(URIPayload uri)
|
||||||
|
{
|
||||||
|
ImGui.TextUnformatted(string.Format(Language.Context_URLDomain, uri.Uri.Authority));
|
||||||
|
ImGuiUtil.WarningText(Language.Context_URLWarning, false);
|
||||||
|
ImGui.Separator();
|
||||||
|
|
||||||
|
if (ImGui.Selectable(Language.Context_OpenInBrowser))
|
||||||
|
{
|
||||||
|
TryOpenURI(uri.Uri);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ImGui.Selectable(Language.Context_CopyLink))
|
||||||
|
{
|
||||||
|
ImGui.SetClipboardText(uri.Uri.ToString());
|
||||||
|
WrapperUtil.AddNotification(Language.Context_CopyLinkNotification, NotificationType.Info);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void TryOpenURI(Uri uri)
|
||||||
|
{
|
||||||
|
new Thread(() => {
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Plugin.Log.Info($"Opening URI {uri} in default browser");
|
||||||
|
Process.Start(new ProcessStartInfo(uri.ToString()) { UseShellExecute = true });
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Plugin.Log.Error($"Error opening URI: {ex}");
|
||||||
|
WrapperUtil.AddNotification(Language.Context_OpenInBrowserError, NotificationType.Error);
|
||||||
|
}
|
||||||
|
}).Start();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Generated
+54
@@ -1040,6 +1040,24 @@ namespace ChatTwo.Resources {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Looks up a localized string similar to Copy link to clipboard.
|
||||||
|
/// </summary>
|
||||||
|
internal static string Context_CopyLink {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("Context_CopyLink", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Looks up a localized string similar to Copied link to clipboard.
|
||||||
|
/// </summary>
|
||||||
|
internal static string Context_CopyLinkNotification {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("Context_CopyLinkNotification", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Looks up a localized string similar to Hide chat.
|
/// Looks up a localized string similar to Hide chat.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -1121,6 +1139,24 @@ namespace ChatTwo.Resources {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Looks up a localized string similar to Open link in browser.
|
||||||
|
/// </summary>
|
||||||
|
internal static string Context_OpenInBrowser {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("Context_OpenInBrowser", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Looks up a localized string similar to Failed to open the link in the browser, please report this issue.
|
||||||
|
/// </summary>
|
||||||
|
internal static string Context_OpenInBrowserError {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("Context_OpenInBrowserError", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Looks up a localized string similar to Promote.
|
/// Looks up a localized string similar to Promote.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -1202,6 +1238,24 @@ namespace ChatTwo.Resources {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Looks up a localized string similar to URL at {0}.
|
||||||
|
/// </summary>
|
||||||
|
internal static string Context_URLDomain {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("Context_URLDomain", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Looks up a localized string similar to Only open URLs from websites you trust.
|
||||||
|
/// </summary>
|
||||||
|
internal static string Context_URLWarning {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("Context_URLWarning", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Looks up a localized string similar to Chinese (full).
|
/// Looks up a localized string similar to Chinese (full).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -871,4 +871,22 @@
|
|||||||
<data name="Options_TooltipOffset_Desc" xml:space="preserve">
|
<data name="Options_TooltipOffset_Desc" xml:space="preserve">
|
||||||
<value>Use this option if you experience cut-off tooltips.</value>
|
<value>Use this option if you experience cut-off tooltips.</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="Context_CopyLink" xml:space="preserve">
|
||||||
|
<value>Copy link to clipboard</value>
|
||||||
|
</data>
|
||||||
|
<data name="Context_CopyLinkNotification" xml:space="preserve">
|
||||||
|
<value>Copied link to clipboard</value>
|
||||||
|
</data>
|
||||||
|
<data name="Context_OpenInBrowser" xml:space="preserve">
|
||||||
|
<value>Open link in browser</value>
|
||||||
|
</data>
|
||||||
|
<data name="Context_OpenInBrowserError" xml:space="preserve">
|
||||||
|
<value>Failed to open the link in the browser, please report this issue</value>
|
||||||
|
</data>
|
||||||
|
<data name="Context_URLDomain" xml:space="preserve">
|
||||||
|
<value>URL at {0}</value>
|
||||||
|
</data>
|
||||||
|
<data name="Context_URLWarning" xml:space="preserve">
|
||||||
|
<value>Only open URLs from websites you trust</value>
|
||||||
|
</data>
|
||||||
</root>
|
</root>
|
||||||
|
|||||||
@@ -86,6 +86,11 @@ internal class Store : IDisposable {
|
|||||||
["Type"] = new("PartyFinder"),
|
["Type"] = new("PartyFinder"),
|
||||||
["Id"] = new(partyFinder.Id),
|
["Id"] = new(partyFinder.Id),
|
||||||
});
|
});
|
||||||
|
case URIPayload uri:
|
||||||
|
return new BsonDocument(new Dictionary<string, BsonValue> {
|
||||||
|
["Type"] = new("URI"),
|
||||||
|
["Uri"] = new(uri.Uri.ToString()),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return payload?.Encode();
|
return payload?.Encode();
|
||||||
@@ -99,6 +104,7 @@ internal class Store : IDisposable {
|
|||||||
return bson["Type"].AsString switch {
|
return bson["Type"].AsString switch {
|
||||||
"Achievement" => new AchievementPayload((uint) bson["Id"].AsInt64),
|
"Achievement" => new AchievementPayload((uint) bson["Id"].AsInt64),
|
||||||
"PartyFinder" => new PartyFinderPayload((uint) bson["Id"].AsInt64),
|
"PartyFinder" => new PartyFinderPayload((uint) bson["Id"].AsInt64),
|
||||||
|
"URI" => new URIPayload(new Uri(bson["Uri"].AsString)),
|
||||||
_ => null,
|
_ => null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
using ChatTwo.Code;
|
using ChatTwo.Code;
|
||||||
using Dalamud.Game.Text.SeStringHandling;
|
using Dalamud.Game.Text.SeStringHandling;
|
||||||
using Dalamud.Game.Text.SeStringHandling.Payloads;
|
using Dalamud.Game.Text.SeStringHandling.Payloads;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
namespace ChatTwo.Util;
|
namespace ChatTwo.Util;
|
||||||
|
|
||||||
@@ -100,6 +101,10 @@ internal static class ChunkUtil {
|
|||||||
var reader = new BinaryReader(new MemoryStream(rawPayload.Data[4..]));
|
var reader = new BinaryReader(new MemoryStream(rawPayload.Data[4..]));
|
||||||
var id = GetInteger(reader);
|
var id = GetInteger(reader);
|
||||||
link = new AchievementPayload(id);
|
link = new AchievementPayload(id);
|
||||||
|
} else if (rawPayload.Data.Length > 5 && rawPayload.Data[1] == 0x27 && rawPayload.Data[3] == 0x07) {
|
||||||
|
// uri payload
|
||||||
|
var uri = new Uri(Encoding.UTF8.GetString(rawPayload.Data[4..]));
|
||||||
|
link = new URIPayload(uri);
|
||||||
} else if (Equals(rawPayload, RawPayload.LinkTerminator)) {
|
} else if (Equals(rawPayload, RawPayload.LinkTerminator)) {
|
||||||
link = null;
|
link = null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -197,16 +197,16 @@ internal static class ImGuiUtil {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal static void WarningText(string text) {
|
internal static void WarningText(string text, bool wrap = true) {
|
||||||
var style = StyleModel.GetConfiguredStyle() ?? StyleModel.GetFromCurrent();
|
var style = StyleModel.GetConfiguredStyle() ?? StyleModel.GetFromCurrent();
|
||||||
var dalamudOrange = style.BuiltInColors?.DalamudOrange;
|
var dalamudOrange = style.BuiltInColors?.DalamudOrange;
|
||||||
if (dalamudOrange != null) {
|
if (dalamudOrange != null) {
|
||||||
ImGui.PushStyleColor(ImGuiCol.Text, dalamudOrange.Value);
|
ImGui.PushStyleColor(ImGuiCol.Text, dalamudOrange.Value);
|
||||||
}
|
}
|
||||||
|
|
||||||
ImGui.PushTextWrapPos();
|
if (wrap) ImGui.PushTextWrapPos();
|
||||||
ImGui.TextUnformatted(text);
|
ImGui.TextUnformatted(text);
|
||||||
ImGui.PopTextWrapPos();
|
if (wrap) ImGui.PopTextWrapPos();
|
||||||
|
|
||||||
if (dalamudOrange != null) {
|
if (dalamudOrange != null) {
|
||||||
ImGui.PopStyleColor();
|
ImGui.PopStyleColor();
|
||||||
|
|||||||
@@ -37,3 +37,51 @@ internal class AchievementPayload : Payload {
|
|||||||
throw new NotImplementedException();
|
throw new NotImplementedException();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
internal class URIPayload(Uri uri) : Payload
|
||||||
|
{
|
||||||
|
public override PayloadType Type => (PayloadType) 0x52;
|
||||||
|
|
||||||
|
public Uri Uri { get; init; } = uri;
|
||||||
|
|
||||||
|
private static readonly string[] ExpectedSchemes = ["http", "https"];
|
||||||
|
private static readonly string DefaultScheme = "https";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Create a URIPayload from a raw URI string. If the URI does not have a
|
||||||
|
/// scheme, it will default to https://.
|
||||||
|
/// </summary>
|
||||||
|
/// <exception cref="UriFormatException">
|
||||||
|
/// If the URI is invalid, or if the scheme is not supported.
|
||||||
|
/// </exception>
|
||||||
|
public static URIPayload ResolveURI(string rawURI)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(rawURI);
|
||||||
|
|
||||||
|
// Check for expected scheme ://, if not add https://
|
||||||
|
foreach (var scheme in ExpectedSchemes)
|
||||||
|
{
|
||||||
|
if (rawURI.StartsWith($"{scheme}://"))
|
||||||
|
{
|
||||||
|
return new URIPayload(new Uri(rawURI));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (rawURI.Contains("://"))
|
||||||
|
{
|
||||||
|
throw new UriFormatException($"Unsupported scheme in URL: {rawURI}");
|
||||||
|
}
|
||||||
|
|
||||||
|
return new URIPayload(new Uri($"{DefaultScheme}://{rawURI}"));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void DecodeImpl(BinaryReader reader, long endOfStream)
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override byte[] EncodeImpl()
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user