using Serilog; using System.Net; using System.Text; using System.Text.Json; using NodaTime; using Myriad.Serialization; using Myriad.Types; namespace Myriad.Cache; public class HttpDiscordCache: IDiscordCache { private readonly ILogger _logger; private readonly HttpClient _client; private readonly Uri _cacheEndpoint; private readonly string? _eventTarget; private readonly int _shardCount; private readonly ulong _ownUserId; private readonly MemoryDiscordCache _innerCache; private readonly JsonSerializerOptions _jsonSerializerOptions; public EventHandler<(bool?, string)> OnDebug; public HttpDiscordCache(ILogger logger, HttpClient client, string cacheEndpoint, string? eventTarget, int shardCount, ulong ownUserId, bool useInnerCache) { _logger = logger; _client = client; _cacheEndpoint = new Uri(cacheEndpoint); _eventTarget = eventTarget; _shardCount = shardCount; _ownUserId = ownUserId; _jsonSerializerOptions = new JsonSerializerOptions().ConfigureForMyriad(); if (useInnerCache) _innerCache = new MemoryDiscordCache(ownUserId); } public ValueTask SaveGuild(Guild guild) => _innerCache?.SaveGuild(guild) ?? default; public ValueTask SaveChannel(Channel channel) => _innerCache?.SaveChannel(channel) ?? default; public ValueTask SaveUser(User user) => default; public ValueTask SaveSelfMember(ulong guildId, GuildMemberPartial member) => _innerCache?.SaveSelfMember(guildId, member) ?? default; public ValueTask SaveRole(ulong guildId, Myriad.Types.Role role) => _innerCache?.SaveRole(guildId, role) ?? default; public ValueTask SaveDmChannelStub(ulong channelId) => _innerCache?.SaveDmChannelStub(channelId) ?? default; public ValueTask RemoveGuild(ulong guildId) => _innerCache?.RemoveGuild(guildId) ?? default; public ValueTask RemoveChannel(ulong channelId) => _innerCache?.RemoveChannel(channelId) ?? default; public ValueTask RemoveUser(ulong userId) => _innerCache?.RemoveUser(userId) ?? default; public ValueTask RemoveRole(ulong guildId, ulong roleId) => _innerCache?.RemoveRole(guildId, roleId) ?? default; public ulong GetOwnUser() => _ownUserId; private async Task QueryCache(string endpoint, ulong guildId) { var cluster = _cacheEndpoint.Authority; // todo: there should not be infra-specific code here if (cluster.Contains(".service.consul") || cluster.Contains("process.pluralkit-gateway.internal")) // int(((guild_id >> 22) % shard_count) / 16) cluster = $"cluster{(int)(((guildId >> 22) % (ulong)_shardCount) / 16)}.{cluster}"; var response = await _client.GetAsync($"{_cacheEndpoint.Scheme}://{cluster}{endpoint}"); if (response.StatusCode == HttpStatusCode.NotFound) return default; if (response.StatusCode != HttpStatusCode.Found) throw new Exception($"failed to query http cache: {response.StatusCode}"); var plaintext = await response.Content.ReadAsStringAsync(); return JsonSerializer.Deserialize(plaintext, _jsonSerializerOptions); } public Task GetLastMessage(ulong guildId, ulong channelId) => QueryCache($"/guilds/{guildId}/channels/{channelId}/last_message", guildId); private Task AwaitEvent(ulong guildId, object data) => AwaitEventShard((int)((guildId >> 22) % (ulong)_shardCount), data); private async Task AwaitEventShard(int shardId, object data) { if (_eventTarget == null) throw new Exception("missing event target for remote await event"); var cluster = _cacheEndpoint.Authority; // todo: there should not be infra-specific code here if (cluster.Contains(".service.consul") || cluster.Contains("process.pluralkit-gateway.internal")) // int(((guild_id >> 22) % shard_count) / 16) cluster = $"cluster{shardId / 16}.{cluster}"; var response = await _client.PostAsync( $"{_cacheEndpoint.Scheme}://{cluster}/await_event", new StringContent(JsonSerializer.Serialize(data), Encoding.UTF8) ); if (response.StatusCode != HttpStatusCode.NoContent) throw new Exception($"failed to await event from gateway: {response.StatusCode}"); } public async Task AwaitReaction(ulong guildId, ulong messageId, ulong userId, Duration? timeout) { var obj = new { message_id = messageId, user_id = userId, target = _eventTarget!, timeout = timeout?.TotalSeconds, }; await AwaitEvent(guildId, obj); } public async Task AwaitMessage(ulong guildId, ulong channelId, ulong authorId, Duration? timeout, string[] options = null) { var obj = new { channel_id = channelId, author_id = authorId, target = _eventTarget!, timeout = timeout?.TotalSeconds, options = options, }; await AwaitEvent(guildId, obj); } public async Task AwaitInteraction(int shardId, string id, Duration? timeout) { var obj = new { id = id, target = _eventTarget!, timeout = timeout?.TotalSeconds, }; await AwaitEventShard(shardId, obj); } public async Task TryGetGuild(ulong guildId) { var hres = await QueryCache($"/guilds/{guildId}", guildId); if (_innerCache == null) return hres; var lres = await _innerCache.TryGetGuild(guildId); if (lres == null && hres == null) return null; if (lres == null) { _logger.Warning($"TryGetGuild({guildId}) was only successful on remote cache"); OnDebug(null, (true, "guild")); return hres; } if (hres == null) { _logger.Warning($"TryGetGuild({guildId}) was only successful on local cache"); OnDebug(null, (false, "guild")); return lres; } return hres; } public async Task TryGetChannel(ulong guildId, ulong channelId) { var hres = await QueryCache($"/guilds/{guildId}/channels/{channelId}", guildId); if (_innerCache == null) return hres; var lres = await _innerCache.TryGetChannel(guildId, channelId); if (lres == null && hres == null) return null; if (lres == null) { _logger.Warning($"TryGetChannel({guildId}, {channelId}) was only successful on remote cache"); OnDebug(null, (true, "channel")); return hres; } if (hres == null) { _logger.Warning($"TryGetChannel({guildId}, {channelId}) was only successful on local cache"); OnDebug(null, (false, "channel")); return lres; } return hres; } // this should be a GetUserCached method on nirn-proxy (it's always called as GetOrFetchUser) // so just return nothing public Task TryGetUser(ulong userId) => Task.FromResult(null); public async Task TryGetSelfMember(ulong guildId) { var hres = await QueryCache($"/guilds/{guildId}/members/@me", guildId); if (_innerCache == null) return hres; var lres = await _innerCache.TryGetSelfMember(guildId); if (lres == null && hres == null) return null; if (lres == null) { _logger.Warning($"TryGetSelfMember({guildId}) was only successful on remote cache"); OnDebug(null, (true, "self_member")); return hres; } if (hres == null) { _logger.Warning($"TryGetSelfMember({guildId}) was only successful on local cache"); OnDebug(null, (false, "self_member")); return lres; } return hres; } // public async Task BotChannelPermissions(ulong guildId, ulong channelId) // { // // todo: local cache throws rather than returning null // // we need to throw too, and try/catch local cache here // var lres = await _innerCache.BotPermissionsIn(guildId, channelId); // var hres = await QueryCache($"/guilds/{guildId}/channels/{channelId}/permissions/@me", guildId); // if (lres == null && hres == null) return null; // if (lres == null) // { // _logger.Warning($"TryGetChannel({guildId}, {channelId}) was only successful on remote cache"); // OnDebug(null, (true, "botchannelperms")); // return hres; // } // if (hres == null) // { // _logger.Warning($"TryGetChannel({guildId}, {channelId}) was only successful on local cache"); // OnDebug(null, (false, "botchannelperms")); // return lres; // } // // // this one is easy to check, so let's check it // if ((int)lres != (int)hres) // { // // trust local // _logger.Warning($"got different permissions for {channelId} (local {(int)lres}, remote {(int)hres})"); // OnDebug(null, (null, "botchannelperms")); // return lres; // } // return hres; // } // it's fine #pragma warning disable CS8603 public async Task> GetGuildChannels(ulong guildId) { var hres = await QueryCache>($"/guilds/{guildId}/channels", guildId); if (_innerCache == null) return hres; var lres = await _innerCache.GetGuildChannels(guildId); if (lres == null && hres == null) return null; if (lres == null) { _logger.Warning($"GetGuildChannels({guildId}) was only successful on remote cache"); OnDebug(null, (true, "guild_channels")); return hres; } if (hres == null) { _logger.Warning($"GetGuildChannels({guildId}) was only successful on local cache"); OnDebug(null, (false, "guild_channels")); return lres; } return hres; } }