using LinqToDB; using LinqToDB.EntityFrameworkCore; using EllieBot.Common.ModuleBehaviors; using EllieBot.Db.Models; namespace EllieBot.Modules.Administration; public sealed class NotifyService : IReadyExecutor, INotifySubscriber, IEService { private readonly DbService _db; private readonly IMessageSenderService _mss; private readonly DiscordSocketClient _client; private readonly IBotCreds _creds; private readonly IReplacementService _repSvc; private readonly IPubSub _pubSub; private ConcurrentDictionary<NotifyType, ConcurrentDictionary<ulong, Notify>> _events = new(); public NotifyService( DbService db, IMessageSenderService mss, DiscordSocketClient client, IBotCreds creds, IReplacementService repSvc, IPubSub pubSub) { _db = db; _mss = mss; _client = client; _creds = creds; _repSvc = repSvc; _pubSub = pubSub; } public async Task OnReadyAsync() { await using var uow = _db.GetDbContext(); _events = (await uow.GetTable<Notify>() .Where(x => Linq2DbExpressions.GuildOnShard(x.GuildId, _creds.TotalShards, _client.ShardId)) .ToListAsyncLinqToDB()) .GroupBy(x => x.Type) .ToDictionary(x => x.Key, x => x.ToDictionary(x => x.GuildId).ToConcurrent()) .ToConcurrent(); await SubscribeToEvent<LevelUpNotifyModel>(); } private async Task SubscribeToEvent<T>() where T : struct, INotifyModel { await _pubSub.Sub(new TypedKey<T>(T.KeyName), async (model) => await OnEvent(model)); } public async Task NotifyAsync<T>(T data, bool isShardLocal = false) where T : struct, INotifyModel { try { if (isShardLocal) { await OnEvent(data); return; } await _pubSub.Pub(data.GetTypedKey(), data); } catch (Exception ex) { Log.Warning(ex, "Unknown error occurred while trying to triger {NotifyEvent} for {NotifyModel}", T.KeyName, data); } } private async Task OnEvent<T>(T model) where T : struct, INotifyModel { if (_events.TryGetValue(T.NotifyType, out var subs)) { if (model.TryGetGuildId(out var gid)) { if (!subs.TryGetValue(gid, out var conf)) return; await HandleNotifyEvent(conf, model); return; } foreach (var key in subs.Keys.ToArray()) { if (subs.TryGetValue(key, out var notif)) { try { await HandleNotifyEvent(notif, model); } catch (Exception ex) { Log.Error(ex, "Error occured while sending notification {NotifyEvent} to guild {GuildId}: {ErrorMessage}", T.NotifyType, key, ex.Message); } await Task.Delay(500); } } } } private async Task HandleNotifyEvent(Notify conf, INotifyModel model) { var guild = _client.GetGuild(conf.GuildId); var channel = guild?.GetTextChannel(conf.ChannelId); if (guild is null || channel is null) return; IUser? user = null; if (model.TryGetUserId(out var userId)) { user = guild.GetUser(userId) ?? _client.GetUser(userId); } var rctx = new ReplacementContext(guild: guild, channel: channel, user: user); var st = SmartText.CreateFrom(conf.Message); foreach (var modelRep in model.GetReplacements()) { rctx.WithOverride(modelRep.Key, () => modelRep.Value(guild)); } st = await _repSvc.ReplaceAsync(st, rctx); if (st is SmartPlainText spt) { await _mss.Response(channel) .Confirm(spt.Text) .SendAsync(); return; } await _mss.Response(channel) .Text(st) .SendAsync(); } public async Task EnableAsync( ulong guildId, ulong channelId, NotifyType nType, string message) { await using var uow = _db.GetDbContext(); await uow.GetTable<Notify>() .InsertOrUpdateAsync(() => new() { GuildId = guildId, ChannelId = channelId, Type = nType, Message = message, }, (_) => new() { Message = message, ChannelId = channelId }, () => new() { GuildId = guildId, Type = nType }); var eventDict = _events.GetOrAdd(nType, _ => new()); eventDict[guildId] = new() { GuildId = guildId, ChannelId = channelId, Type = nType, Message = message }; } public async Task DisableAsync(ulong guildId, NotifyType nType) { await using var uow = _db.GetDbContext(); var deleted = await uow.GetTable<Notify>() .Where(x => x.GuildId == guildId && x.Type == nType) .DeleteAsync(); if (deleted == 0) return; if (!_events.TryGetValue(nType, out var guildsDict)) return; guildsDict.TryRemove(guildId, out _); } }