Skip to content

Commit

Permalink
Merge pull request #9 from PaulRitter/runtime_cache_avatars
Browse files Browse the repository at this point in the history
runtime cache avatars
  • Loading branch information
PaulRitter authored Nov 6, 2023
2 parents 6640596 + 24bba31 commit a0fd2e8
Show file tree
Hide file tree
Showing 5 changed files with 44 additions and 48 deletions.
37 changes: 10 additions & 27 deletions Turbulence.Desktop/Converters/UserAvatarConverter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,17 @@
using Avalonia.Data.Converters;
using Avalonia.Platform;
using System.Globalization;
using CommunityToolkit.Mvvm.DependencyInjection;
using Turbulence.Discord;
using Turbulence.Discord.Models.DiscordUser;
using Bitmap = Avalonia.Media.Imaging.Bitmap;

namespace Turbulence.Desktop.Converters;

public class UserAvatarConverter : IValueConverter
{
private readonly IPlatformClient _client = Ioc.Default.GetService<IPlatformClient>()!;

public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
{
if (value is not User user)
Expand All @@ -21,39 +25,18 @@ public class UserAvatarConverter : IValueConverter
return new Bitmap(AssetLoader.Open(new Uri("resm:Avalonia.Skia.Assets.NoiseAsset_256X256_PNG.png?assembly=Avalonia.Skia")));
}

// TODO: use client avatar get
if (user.Avatar is { } avatar)
{
return Task.Run(async () =>
await LoadFromWeb(new Uri($"https://cdn.discordapp.com/avatars/{user.Id}/{avatar}.png?size=80"))).Result;
}
else
var data = Task.Run(async () => await _client.GetAvatarAsync(user, 80)).Result;
var bmp = new Bitmap(new MemoryStream(data));
if (bmp.PixelSize.Height > 80)
{
return Task.Run(async () =>
await LoadFromWeb(new Uri($"https://cdn.discordapp.com/embed/avatars/{(user.Id >> 22) % 6}.png"))).Result!.CreateScaledBitmap(new PixelSize(80, 80));
bmp = bmp.CreateScaledBitmap(new PixelSize(80, 80));
}
// Image.Source = new Bitmap(new MemoryStream(await _client.GetAvatar(message.Author)));

return bmp;
}

public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
{
throw new NotImplementedException();
}

private static async Task<Bitmap?> LoadFromWeb(Uri url)
{
using var httpClient = new HttpClient();
try
{
var response = await httpClient.GetAsync(url);
response.EnsureSuccessStatusCode();
var data = await response.Content.ReadAsByteArrayAsync();
return new Bitmap(new MemoryStream(data));
}
catch (HttpRequestException ex)
{
Console.WriteLine($"An error occurred while downloading image '{url}': {ex.Message}");
return null;
}
}
}
18 changes: 11 additions & 7 deletions Turbulence.Discord/Api.cs
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ private static async Task<T> Delete<T>(HttpClient client, string endpoint)

private static async Task<byte[]> CdnGet(HttpClient client, string endpoint)
{
var req = new HttpRequestMessage(HttpMethod.Get, $"{ApiRoot}{endpoint}");
var req = new HttpRequestMessage(HttpMethod.Get, $"{CdnRoot}{endpoint}");
var response = await client.SendAsync(req);
if (!response.IsSuccessStatusCode)
{
Expand Down Expand Up @@ -192,16 +192,20 @@ public static async Task DeleteMessage(HttpClient client, Message message)
await Delete(client, $"/channels/{message.ChannelId}/messages/{message.Id}");
}

public static async Task<Image> GetAvatar(HttpClient client, Snowflake user, string avatar, int size = 32)
//https://discord.com/developers/docs/reference#image-formatting
public static Task<byte[]> GetAvatarAsync(HttpClient client, User user, int size = 32)
{
var data = await CdnGet(client, $"avatars/{user}/{avatar}.png?size={size}");
return new Image(data, size); // TODO: Size could easily be a lie, as the API will just send the largest available instead of given size
return CdnGet(client, $"avatars/{user.Id}/{user.Avatar}.png?size={size}");
// TODO: Size could easily be a lie, as the API will just send the largest available instead of given size
}

public static async Task<Image> GetDefaultAvatar(HttpClient client, int index)
//https://discord.com/developers/docs/reference#image-formatting
public static Task<byte[]> GetDefaultAvatarAsync(HttpClient client, User user)
{
var data = await CdnGet(client, $"embed/avatars/{index}.png"); // TODO: Doesn't work
return new Image(data, 256);
// index depends on whether the user switched to new username system
// users with a username have a Discriminator of "0"
var index = user.Discriminator == "0" ? (int)(user.Id >> 22) % 6 : int.Parse(user.Discriminator) % 5;
return CdnGet(client, $"embed/avatars/{index}.png");
}

// https://discord.com/developers/docs/resources/channel#get-pinned-messages
Expand Down
18 changes: 10 additions & 8 deletions Turbulence.Discord/Client.cs
Original file line number Diff line number Diff line change
Expand Up @@ -427,20 +427,22 @@ public async Task<Guild> GetGuild(Snowflake guildId)
return Guilds.TryGetValue(guildId, out var ret) ? ret : await Api.GetGuild(HttpClient, guildId);
}

public async Task<Image> GetAvatar(User user, int size = 128)
public async Task<byte[]> GetAvatarAsync(User user, int size = 128)
{
if (_cache.GetAvatar(user.Id) is { } avatar)
if (_cache.GetAvatar(user.Id, size) is { } avatar)
return avatar;

// https://discord.com/developers/docs/reference#image-formatting
if (user.Avatar == null)
{
// index depends on whether the user switched to new username system
// users with a username have a Discriminator of "0"
var index = user.Discriminator == "0" ? (int)(user.Id >> 22) % 6 : int.Parse(user.Discriminator) % 5;
return await Api.GetDefaultAvatar(CdnClient, index);
avatar = await Api.GetDefaultAvatarAsync(CdnClient, user);
}
return await Api.GetAvatar(CdnClient, user.Id, user.Avatar!, size);
else
{
avatar = await Api.GetAvatarAsync(CdnClient, user, size);
}

_cache.SetAvatar(user.Id, size, avatar);
return avatar;
}

public async Task<Channel> GetChannel(Snowflake channelId)
Expand Down
2 changes: 1 addition & 1 deletion Turbulence.Discord/IPlatformClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ public interface IPlatformClient
public event EventHandler<Event<TypingStartEvent>>? TypingStart;

public User? CurrentUser { get; set; }
public Task<Image> GetAvatar(User user, int size = 128);
public Task<byte[]> GetAvatarAsync(User user, int size = 128);

#region Discord specific shit that should not be here

Expand Down
17 changes: 12 additions & 5 deletions Turbulence.Discord/Services/Cache.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,22 @@ namespace Turbulence.Discord.Services;

public interface ICache
{
public Image? GetAvatar(Snowflake userId);
public byte[]? GetAvatar(Snowflake userId, int size);

public void SetAvatar(Snowflake userId, int size, byte[] avatar);
}

public class Cache : ICache
{
public Image? GetAvatar(Snowflake userId)
private readonly Dictionary<(Snowflake, int), byte[]> _avatars = new();

public byte[]? GetAvatar(Snowflake userId, int size)
{
return null;
return _avatars.TryGetValue((userId, size), out var avatar) ? avatar : null;
}
}

public record Image(byte[] Data, int Size);
public void SetAvatar(Snowflake userId, int size, byte[] avatar)
{
_avatars[(userId, size)] = avatar;
}
}

0 comments on commit a0fd2e8

Please sign in to comment.