292 lines
No EOL
8.4 KiB
C#
292 lines
No EOL
8.4 KiB
C#
using LinqToDB;
|
|
using LinqToDB.EntityFrameworkCore;
|
|
using EllieBot.Common.ModuleBehaviors;
|
|
using EllieBot.Db.Models;
|
|
using EllieBot.Generators;
|
|
using EllieBot.Modules.Administration.Services;
|
|
using EllieBot.Modules.Gambling;
|
|
using EllieBot.Modules.Xp.Services;
|
|
|
|
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;
|
|
}
|
|
|
|
private void RegisterModels()
|
|
{
|
|
RegisterModel<LevelUpNotifyModel>();
|
|
RegisterModel<ProtectionNotifyModel>();
|
|
RegisterModel<AddRoleRewardNotifyModel>();
|
|
RegisterModel<RemoveRoleRewardNotifyModel>();
|
|
RegisterModel<NiceCatchNotifyModel>();
|
|
}
|
|
|
|
public async Task OnReadyAsync()
|
|
{
|
|
await using var uow = _db.GetDbContext();
|
|
_events = (await uow.GetTable<Notify>()
|
|
.Where(x => Queries.GuildOnShard(x.GuildId,
|
|
_creds.TotalShards,
|
|
_client.ShardId))
|
|
.ToListAsyncLinqToDB())
|
|
.GroupBy(x => x.Type)
|
|
.ToDictionary(x => x.Key, x => x.ToDictionary(x => x.GuildId).ToConcurrent())
|
|
.ToConcurrent();
|
|
|
|
RegisterModels();
|
|
}
|
|
|
|
private async Task SubscribeToEvent<T>()
|
|
where T : struct, INotifyModel<T>
|
|
{
|
|
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<T>
|
|
{
|
|
try
|
|
{
|
|
if (isShardLocal)
|
|
{
|
|
_ = Task.Run(async () => await OnEvent(data));
|
|
return;
|
|
}
|
|
|
|
await _pubSub.Pub(data.GetTypedKey(), data);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Log.Warning(ex,
|
|
"Unknown error occurred while trying to trigger {NotifyEvent} for {NotifyModel}",
|
|
T.KeyName,
|
|
data);
|
|
}
|
|
}
|
|
|
|
private async Task OnEvent<T>(T model)
|
|
where T : struct, INotifyModel<T>
|
|
{
|
|
if (!_events.TryGetValue(T.NotifyType, out var subs))
|
|
return;
|
|
|
|
// make sure the event is consumed
|
|
// only in the guild it was meant for
|
|
if (model.TryGetGuildId(out var gid))
|
|
{
|
|
if (!subs.TryGetValue(gid, out var conf))
|
|
return;
|
|
|
|
await HandleNotifyEvent(conf, model);
|
|
return;
|
|
}
|
|
|
|
// todo optimize this
|
|
foreach (var key in subs.Keys)
|
|
{
|
|
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<T>(Notify conf, T model)
|
|
where T : struct, INotifyModel<T>
|
|
{
|
|
var guild = _client.GetGuild(conf.GuildId);
|
|
|
|
// bot probably left the guild, cleanup?
|
|
if (guild is null)
|
|
return;
|
|
|
|
IMessageChannel? channel;
|
|
// if notify channel is specified for this event, send the event to that channel
|
|
if (conf.ChannelId is ulong confCid)
|
|
{
|
|
channel = guild.GetTextChannel(confCid);
|
|
}
|
|
else
|
|
{
|
|
// otherwise get the origin channel of the event
|
|
if (!model.TryGetChannelId(out var cid))
|
|
return;
|
|
|
|
channel = guild.GetChannel(cid) as IMessageChannel;
|
|
}
|
|
|
|
if (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 T.GetReplacements())
|
|
{
|
|
rctx.WithOverride(GetPhToken(modelRep.Name), () => modelRep.Func(model, 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)
|
|
.Sanitize(false)
|
|
.SendAsync();
|
|
}
|
|
|
|
private static string GetPhToken(string name)
|
|
=> $"%event.{name}%";
|
|
|
|
public async Task<bool> EnableAsync(
|
|
ulong guildId,
|
|
ulong? channelId,
|
|
NotifyType nType,
|
|
string message)
|
|
{
|
|
// check if the notify type model supports null channel
|
|
if (channelId is null)
|
|
{
|
|
var model = GetRegisteredModel(nType);
|
|
if (!model.SupportsOriginTarget)
|
|
return false;
|
|
}
|
|
|
|
|
|
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
|
|
};
|
|
|
|
return true;
|
|
}
|
|
|
|
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 _);
|
|
}
|
|
|
|
public async Task<IReadOnlyCollection<Notify>> GetForGuildAsync(ulong guildId, int page = 0)
|
|
{
|
|
ArgumentOutOfRangeException.ThrowIfNegative(page);
|
|
|
|
await using var ctx = _db.GetDbContext();
|
|
var list = await ctx.GetTable<Notify>()
|
|
.Where(x => x.GuildId == guildId)
|
|
.OrderBy(x => x.Type)
|
|
.Skip(page * 10)
|
|
.Take(10)
|
|
.ToListAsyncLinqToDB();
|
|
|
|
return list;
|
|
}
|
|
|
|
public async Task<Notify?> GetNotifyAsync(ulong guildId, NotifyType nType)
|
|
{
|
|
await using var ctx = _db.GetDbContext();
|
|
return await ctx.GetTable<Notify>()
|
|
.Where(x => x.GuildId == guildId && x.Type == nType)
|
|
.FirstOrDefaultAsyncLinqToDB();
|
|
}
|
|
|
|
// messed up big time, it was supposed to be fully extensible, but it's stored as an enum in the database already...
|
|
private readonly ConcurrentDictionary<NotifyType, NotifyModelData> _models = new();
|
|
|
|
public void RegisterModel<T>() where T : struct, INotifyModel<T>
|
|
{
|
|
var data = new NotifyModelData(T.NotifyType,
|
|
T.SupportsOriginTarget,
|
|
T.GetReplacements().Map(x => x.Name));
|
|
|
|
_models[T.NotifyType] = data;
|
|
|
|
_pubSub.Sub<T>(new(T.KeyName), async (data) => await OnEvent(data));
|
|
}
|
|
|
|
public NotifyModelData GetRegisteredModel(NotifyType nType)
|
|
=> _models[nType];
|
|
} |