2021-08-01 12:51:54 -04:00
using System.Text.RegularExpressions ;
2020-02-12 15:16:19 +01:00
using PluralKit.Core ;
2021-11-26 21:10:56 -05:00
namespace PluralKit.Bot ;
public static class AvatarUtils
2021-08-27 11:03:47 -04:00
{
2021-11-26 21:10:56 -05:00
// Rewrite cdn.discordapp.com URLs to media.discordapp.net for jpg/png files
// This lets us add resizing parameters to "borrow" their media proxy server to downsize the image
// which in turn makes it more likely to be underneath the size limit!
private static readonly Regex DiscordCdnUrl =
2024-02-22 06:38:15 +01:00
new ( @"^https?://(?:cdn\.discordapp\.com|media\.discordapp\.net)/attachments/(\d{17,19})/(\d{17,19})/([^/\\&\?]+)\.(png|jpe?g|gif|webp)(?:\?(?<query>.*))?$" , RegexOptions . IgnoreCase ) ;
2021-08-08 17:44:30 -04:00
2021-11-26 21:10:56 -05:00
private static readonly string DiscordMediaUrlReplacement =
"https://media.discordapp.net/attachments/$1/$2/$3.$4?width=256&height=256" ;
2025-12-05 18:10:27 -05:00
// 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(?:/(?<divisor>[1-9][0-9]{0,6}))?(?:%(?<modulus>[1-9][0-9]{0,6}))?\}" , RegexOptions . IgnoreCase ) ;
2021-08-01 12:51:54 -04:00
2024-02-09 04:38:17 +01:00
public static string? TryRewriteCdnUrl ( string? url )
{
if ( url = = null )
return null ;
var match = DiscordCdnUrl . Match ( url ) ;
var newUrl = DiscordCdnUrl . Replace ( url , DiscordMediaUrlReplacement ) ;
if ( match . Groups [ "query" ] . Success )
newUrl + = "&" + match . Groups [ "query" ] . Value ;
2025-12-05 18:10:27 -05:00
newUrl = TimePlaceholder . Replace ( newUrl , ProcessTimePlaceholder ) ;
2024-02-09 04:38:17 +01:00
return newUrl ;
}
2025-12-05 18:10:27 -05:00
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 ( ) ;
}
2024-02-11 03:53:46 +01:00
public static bool IsDiscordCdnUrl ( string? url ) = > url ! = null & & DiscordCdnUrl . Match ( url ) . Success ;
2024-02-22 06:43:29 +01:00
}