From 3a3f42d755cc308e80a10761ea8eb9cfbfd7156a Mon Sep 17 00:00:00 2001 From: Una Kearney Date: Fri, 5 Dec 2025 18:10:27 -0500 Subject: [PATCH 1/5] Add support for a time placeholder in URLs --- PluralKit.Bot/Utils/AvatarUtils.cs | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/PluralKit.Bot/Utils/AvatarUtils.cs b/PluralKit.Bot/Utils/AvatarUtils.cs index 65a208ed..c56f22f0 100644 --- a/PluralKit.Bot/Utils/AvatarUtils.cs +++ b/PluralKit.Bot/Utils/AvatarUtils.cs @@ -14,6 +14,10 @@ public static class AvatarUtils private static readonly string DiscordMediaUrlReplacement = "https://media.discordapp.net/attachments/$1/$2/$3.$4?width=256&height=256"; + + // Rewrite time "cachebuster" parameters for randomly generated/chosen avatars with custom URLs. + // Number match uses `[1-9][0-9]{0,6}` rather than `[0-9]+` to avoid needing to deal with special cases for zero and limit to reasonable numbers. + private static readonly Regex TimePlaceholder = new(@"\{time(?:/(?[1-9][0-9]{0,6}))?(?:%(?[1-9][0-9]{0,6}))?\}", RegexOptions.IgnoreCase); public static string? TryRewriteCdnUrl(string? url) { @@ -25,8 +29,24 @@ public static class AvatarUtils if (match.Groups["query"].Success) newUrl += "&" + match.Groups["query"].Value; + newUrl = TimePlaceholder.Replace(newUrl, ProcessTimePlaceholder); + return newUrl; } + + private static string? ProcessTimePlaceholder(Match m) { + // Minutes are the maximum accuracy to avoid too much cache thrashing + // AND with the maximum positive value so it's always positive (as if this code will exist long enough for the 64-bit signed unix time to go negative...) + var time = (DateTimeOffset.UtcNow.ToUnixTimeSeconds()/60)&Int64.MaxValue; + + if (m.Groups["divisor"].Success) + time /= Int32.Parse(m.Groups["divisor"].Value); // as above - guaranteed to not throw and be > 0 + + if (m.Groups["modulus"].Success) + time %= Int32.Parse(m.Groups["modulus"].Value); + + return time.ToString(); + } public static bool IsDiscordCdnUrl(string? url) => url != null && DiscordCdnUrl.Match(url).Success; } \ No newline at end of file From 776b562ce701badf6f7a88edb0d9bd2b2b1a7570 Mon Sep 17 00:00:00 2001 From: Una Kearney Date: Fri, 5 Dec 2025 19:35:33 -0500 Subject: [PATCH 2/5] Move time placeholder impl to TryGetCleanCdnUrl, drop divmod in favor of fixed options --- PluralKit.Bot/Utils/AvatarUtils.cs | 20 -------------------- PluralKit.Core/Utils/MiscUtils.cs | 26 +++++++++++++++++++++++++- 2 files changed, 25 insertions(+), 21 deletions(-) diff --git a/PluralKit.Bot/Utils/AvatarUtils.cs b/PluralKit.Bot/Utils/AvatarUtils.cs index c56f22f0..65a208ed 100644 --- a/PluralKit.Bot/Utils/AvatarUtils.cs +++ b/PluralKit.Bot/Utils/AvatarUtils.cs @@ -14,10 +14,6 @@ public static class AvatarUtils private static readonly string DiscordMediaUrlReplacement = "https://media.discordapp.net/attachments/$1/$2/$3.$4?width=256&height=256"; - - // Rewrite time "cachebuster" parameters for randomly generated/chosen avatars with custom URLs. - // Number match uses `[1-9][0-9]{0,6}` rather than `[0-9]+` to avoid needing to deal with special cases for zero and limit to reasonable numbers. - private static readonly Regex TimePlaceholder = new(@"\{time(?:/(?[1-9][0-9]{0,6}))?(?:%(?[1-9][0-9]{0,6}))?\}", RegexOptions.IgnoreCase); public static string? TryRewriteCdnUrl(string? url) { @@ -29,24 +25,8 @@ public static class AvatarUtils if (match.Groups["query"].Success) newUrl += "&" + match.Groups["query"].Value; - newUrl = TimePlaceholder.Replace(newUrl, ProcessTimePlaceholder); - return newUrl; } - - private static string? ProcessTimePlaceholder(Match m) { - // Minutes are the maximum accuracy to avoid too much cache thrashing - // AND with the maximum positive value so it's always positive (as if this code will exist long enough for the 64-bit signed unix time to go negative...) - var time = (DateTimeOffset.UtcNow.ToUnixTimeSeconds()/60)&Int64.MaxValue; - - if (m.Groups["divisor"].Success) - time /= Int32.Parse(m.Groups["divisor"].Value); // as above - guaranteed to not throw and be > 0 - - if (m.Groups["modulus"].Success) - time %= Int32.Parse(m.Groups["modulus"].Value); - - return time.ToString(); - } public static bool IsDiscordCdnUrl(string? url) => url != null && DiscordCdnUrl.Match(url).Success; } \ No newline at end of file diff --git a/PluralKit.Core/Utils/MiscUtils.cs b/PluralKit.Core/Utils/MiscUtils.cs index 366deb6b..bb995809 100644 --- a/PluralKit.Core/Utils/MiscUtils.cs +++ b/PluralKit.Core/Utils/MiscUtils.cs @@ -4,6 +4,7 @@ namespace PluralKit.Core; public static class MiscUtils { + // discord mediaproxy URLs used to be stored directly in the database, so now we cleanup image urls before using them outside of proxying private static readonly Regex MediaProxyUrl = new( @@ -11,6 +12,10 @@ public static class MiscUtils private static readonly string DiscordCdnReplacement = "https://cdn.discordapp.com/attachments/$1/$2/$3.$4"; + // Rewrite time "cachebuster" parameters for randomly generated/chosen avatars with custom URLs. + private static readonly Regex TimePlaceholder = new(@"\{(time(?:stamp|_(?:1m|5m|30m|1h|6h|1d)))\}"); + private const Int64 TimeAccuracy = 60; + public static bool TryMatchUri(string input, out Uri uri) { if (input.StartsWith('<') && input.EndsWith('>')) @@ -32,5 +37,24 @@ public static class MiscUtils } public static string? TryGetCleanCdnUrl(this string? url) => - url == null ? null : MediaProxyUrl.Replace(url, DiscordCdnReplacement); + url == null ? null : TimePlaceholder.Replace(MediaProxyUrl.Replace(url, DiscordCdnReplacement), ProcessTimePlaceholder); + + private static string? ProcessTimePlaceholder(Match m) { + // Limit maximum accuracy to avoid too much cache thrashing, multiply for standard-ish Unix time + // AND with the maximum positive value so it's always positive (as if this code will exist long enough for the 64-bit signed unix time to go negative...) + var time = ((DateTimeOffset.UtcNow.ToUnixTimeSeconds()/TimeAccuracy)*TimeAccuracy)&Int64.MaxValue; + + switch (m.Groups[1].Value) { + case "timestamp": break; + case "time_1m": time /= 60; break; + case "time_5m": time /= 60*5; break; + case "time_30m": time /= 60*30; break; + case "time_1h": time /= 60*60; break; + case "time_6h": time /= 6*60*60; break; + case "time_1d": time /= 24*60*60; break; + } + + return time.ToString(); + } + } \ No newline at end of file From 6cb39bb399b6d4cb5bb046164f8d21e62c11f13f Mon Sep 17 00:00:00 2001 From: Una Kearney Date: Fri, 5 Dec 2025 19:53:04 -0500 Subject: [PATCH 3/5] Indentation fixes --- PluralKit.Core/Utils/MiscUtils.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/PluralKit.Core/Utils/MiscUtils.cs b/PluralKit.Core/Utils/MiscUtils.cs index bb995809..b6a25964 100644 --- a/PluralKit.Core/Utils/MiscUtils.cs +++ b/PluralKit.Core/Utils/MiscUtils.cs @@ -4,7 +4,7 @@ namespace PluralKit.Core; public static class MiscUtils { - + // discord mediaproxy URLs used to be stored directly in the database, so now we cleanup image urls before using them outside of proxying private static readonly Regex MediaProxyUrl = new( @@ -52,7 +52,7 @@ public static class MiscUtils case "time_1h": time /= 60*60; break; case "time_6h": time /= 6*60*60; break; case "time_1d": time /= 24*60*60; break; - } + } return time.ToString(); } From 7465e73e9d17277136696ccf270c585345f11b1a Mon Sep 17 00:00:00 2001 From: Una Kearney Date: Fri, 5 Dec 2025 19:58:48 -0500 Subject: [PATCH 4/5] rm junk newline --- PluralKit.Core/Utils/MiscUtils.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/PluralKit.Core/Utils/MiscUtils.cs b/PluralKit.Core/Utils/MiscUtils.cs index b6a25964..6ea2c3a6 100644 --- a/PluralKit.Core/Utils/MiscUtils.cs +++ b/PluralKit.Core/Utils/MiscUtils.cs @@ -4,7 +4,6 @@ namespace PluralKit.Core; public static class MiscUtils { - // discord mediaproxy URLs used to be stored directly in the database, so now we cleanup image urls before using them outside of proxying private static readonly Regex MediaProxyUrl = new( From d23241feb90c5b4252e0735efe51dcdd96336750 Mon Sep 17 00:00:00 2001 From: Una Kearney Date: Fri, 5 Dec 2025 20:03:29 -0500 Subject: [PATCH 5/5] Also handle percent-escaped brackets --- PluralKit.Core/Utils/MiscUtils.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PluralKit.Core/Utils/MiscUtils.cs b/PluralKit.Core/Utils/MiscUtils.cs index 6ea2c3a6..2525a35a 100644 --- a/PluralKit.Core/Utils/MiscUtils.cs +++ b/PluralKit.Core/Utils/MiscUtils.cs @@ -12,7 +12,7 @@ public static class MiscUtils private static readonly string DiscordCdnReplacement = "https://cdn.discordapp.com/attachments/$1/$2/$3.$4"; // Rewrite time "cachebuster" parameters for randomly generated/chosen avatars with custom URLs. - private static readonly Regex TimePlaceholder = new(@"\{(time(?:stamp|_(?:1m|5m|30m|1h|6h|1d)))\}"); + private static readonly Regex TimePlaceholder = new(@"(?:\{|%7B)(time(?:stamp|_(?:1m|5m|30m|1h|6h|1d)))(?:\}|%7D)"); private const Int64 TimeAccuracy = 60; public static bool TryMatchUri(string input, out Uri uri)