This repository has been archived on 2024-12-22. You can view files and clone it, but you cannot make any changes to it's state, such as pushing and creating new issues, pull requests or comments.
elliebot/src/EllieBot/Modules/Administration/GreetBye/GreetService.cs

527 lines
No EOL
18 KiB
C#

using LinqToDB;
using LinqToDB.EntityFrameworkCore;
using LinqToDB.Tools;
using EllieBot.Common.ModuleBehaviors;
using System.Threading.Channels;
namespace EllieBot.Services;
public class GreetService : IEService, IReadyExecutor
{
public bool GroupGreets
=> _bss.Data.GroupGreets;
private readonly DbService _db;
private ConcurrentHashSet<ulong> _greetDmEnabledGuilds = new();
private ConcurrentHashSet<ulong> _boostEnabledGuilds = new();
private ConcurrentHashSet<ulong> _greetEnabledGuilds = new();
private ConcurrentHashSet<ulong> _byeEnabledGuilds = new();
private readonly DiscordSocketClient _client;
private readonly GreetGrouper<IGuildUser> _greets = new();
private readonly GreetGrouper<IUser> _byes = new();
private readonly BotConfigService _bss;
private readonly IReplacementService _repSvc;
private readonly IMessageSenderService _sender;
public GreetService(
DiscordSocketClient client,
DbService db,
BotConfigService bss,
IMessageSenderService sender,
IReplacementService repSvc
)
{
_db = db;
_client = client;
_bss = bss;
_repSvc = repSvc;
_sender = sender;
}
public async Task OnReadyAsync()
{
// cache all enabled guilds
await using (var uow = _db.GetDbContext())
{
var guilds = _client.Guilds.Select(x => x.Id).ToList();
var enabled = await uow.GetTable<GreetSettings>()
.Where(x => x.GuildId.In(guilds))
.Where(x => x.SendChannelGreetMessage
|| x.SendBoostMessage
|| x.SendChannelByeMessage
|| x.SendDmGreetMessage)
.ToListAsync();
_boostEnabledGuilds = new(enabled.Where(x => x.SendBoostMessage).Select(x => x.GuildId));
_byeEnabledGuilds = new(enabled.Where(x => x.SendChannelByeMessage).Select(x => x.GuildId));
_greetDmEnabledGuilds = new(enabled.Where(x => x.SendDmGreetMessage).Select(x => x.GuildId));
_greetEnabledGuilds = new(enabled.Where(x => x.SendChannelGreetMessage).Select(x => x.GuildId));
}
_client.UserJoined += OnUserJoined;
_client.UserLeft += OnUserLeft;
_client.LeftGuild += OnClientLeftGuild;
_client.GuildMemberUpdated += ClientOnGuildMemberUpdated;
var timer = new PeriodicTimer(TimeSpan.FromSeconds(2));
while (await timer.WaitForNextTickAsync())
{
var (conf, user, compl) = await _greetDmQueue.Reader.ReadAsync();
var res = await GreetDmUserInternal(conf, user);
compl.TrySetResult(res);
}
}
private Task ClientOnGuildMemberUpdated(Cacheable<SocketGuildUser, ulong> optOldUser, SocketGuildUser newUser)
{
// if user is a new booster
// or boosted again the same server
if ((optOldUser.Value is { PremiumSince: null } && newUser is { PremiumSince: not null })
|| (optOldUser.Value?.PremiumSince is { } oldDate
&& newUser.PremiumSince is { } newDate
&& newDate > oldDate))
{
_ = Task.Run(async () =>
{
var conf = await GetGreetSettingsAsync(newUser.Guild.Id);
if (conf is null || !conf.SendBoostMessage)
return;
await TriggerBoostMessage(conf, newUser);
});
}
return Task.CompletedTask;
}
private async Task TriggerBoostMessage(GreetSettings conf, SocketGuildUser user)
{
var channel = user.Guild.GetTextChannel(conf.BoostMessageChannelId);
if (channel is null)
return;
await SendBoostMessage(conf, user, channel);
}
private async Task<bool> SendBoostMessage(GreetSettings conf, IGuildUser user, ITextChannel channel)
{
if (string.IsNullOrWhiteSpace(conf.BoostMessage))
return false;
var toSend = SmartText.CreateFrom(conf.BoostMessage);
try
{
var newContent = await _repSvc.ReplaceAsync(toSend,
new(client: _client, guild: user.Guild, channel: channel, users: user));
var toDelete = await _sender.Response(channel).Text(newContent).Sanitize(false).SendAsync();
if (conf.BoostMessageDeleteAfter > 0)
toDelete.DeleteAfter(conf.BoostMessageDeleteAfter);
return true;
}
catch (Exception ex)
{
Log.Error(ex, "Error sending boost message");
}
return false;
}
private async Task OnClientLeftGuild(SocketGuild arg)
{
_boostEnabledGuilds.TryRemove(arg.Id);
_byeEnabledGuilds.TryRemove(arg.Id);
_greetDmEnabledGuilds.TryRemove(arg.Id);
_greetEnabledGuilds.TryRemove(arg.Id);
await using var uow = _db.GetDbContext();
await uow.GetTable<GreetSettings>()
.Where(x => x.GuildId == arg.Id)
.DeleteAsync();
}
private Task OnUserLeft(SocketGuild guild, SocketUser user)
{
_ = Task.Run(async () =>
{
try
{
var conf = await GetGreetSettingsAsync(guild.Id);
if (conf is null)
return;
var channel = guild.TextChannels.FirstOrDefault(c => c.Id == conf.ByeMessageChannelId);
if (channel is null) //maybe warn the server owner that the channel is missing
return;
if (GroupGreets)
{
// if group is newly created, greet that user right away,
// but any user which joins in the next 5 seconds will
// be greeted in a group greet
if (_byes.CreateOrAdd(guild.Id, user))
{
// greet single user
await ByeUsers(conf, channel, new[] { user });
var groupClear = false;
while (!groupClear)
{
await Task.Delay(5000);
groupClear = _byes.ClearGroup(guild.Id, 5, out var toBye);
await ByeUsers(conf, channel, toBye);
}
}
}
else
{
await ByeUsers(conf, channel, new[] { user });
}
}
catch
{
// ignored
}
});
return Task.CompletedTask;
}
public async Task<GreetSettings?> GetGreetSettingsAsync(ulong gid, GreetType type)
{
await using var uow = _db.GetDbContext();
var res = await uow.GetTable<GreetSettings>()
.Where(x => x.GuildId == gid && x.GreetType == type)
.FirstOrDefaultAsync();
return res;
}
private Task ByeUsers(GreetSettings conf, ITextChannel channel, IUser user)
=> ByeUsers(conf, channel, new[] { user });
private async Task ByeUsers(GreetSettings conf, ITextChannel channel, IReadOnlyCollection<IUser> users)
{
if (!users.Any())
return;
var repCtx = new ReplacementContext(client: _client,
guild: channel.Guild,
channel: channel,
users: users.ToArray());
var text = SmartText.CreateFrom(conf.ChannelByeMessageText);
text = await _repSvc.ReplaceAsync(text, repCtx);
try
{
var toDelete = await _sender.Response(channel).Text(text).Sanitize(false).SendAsync();
if (conf.AutoDeleteByeMessagesTimer > 0)
toDelete.DeleteAfter(conf.AutoDeleteByeMessagesTimer);
}
catch (HttpException ex) when (ex.DiscordCode == DiscordErrorCode.InsufficientPermissions
|| ex.DiscordCode == DiscordErrorCode.MissingPermissions
|| ex.DiscordCode == DiscordErrorCode.UnknownChannel)
{
Log.Warning(ex,
"Missing permissions to send a bye message, the bye message will be disabled on server: {GuildId}",
channel.GuildId);
await SetGreet(channel.GuildId, channel.Id, GreetType.Bye, false);
}
catch (Exception ex)
{
Log.Warning(ex, "Error embeding bye message");
}
}
private Task GreetUsers(GreetSettings conf, ITextChannel channel, IGuildUser user)
=> GreetUsers(conf, channel, new[] { user });
private async Task GreetUsers(GreetSettings conf, ITextChannel channel, IReadOnlyCollection<IGuildUser> users)
{
if (users.Count == 0)
return;
var repCtx = new ReplacementContext(client: _client,
guild: channel.Guild,
channel: channel,
users: users.ToArray());
var text = SmartText.CreateFrom(conf.ChannelGreetMessageText);
text = await _repSvc.ReplaceAsync(text, repCtx);
try
{
var toDelete = await _sender.Response(channel).Text(text).Sanitize(false).SendAsync();
if (conf.AutoDeleteGreetMessagesTimer > 0)
toDelete.DeleteAfter(conf.AutoDeleteGreetMessagesTimer);
}
catch (HttpException ex) when (ex.DiscordCode is DiscordErrorCode.InsufficientPermissions
or DiscordErrorCode.MissingPermissions
or DiscordErrorCode.UnknownChannel)
{
Log.Warning(ex,
"Missing permissions to send a bye message, the greet message will be disabled on server: {GuildId}",
channel.GuildId);
await SetGreet(channel.GuildId, channel.Id, GreetType.Greet, false);
}
catch (Exception ex)
{
Log.Warning(ex, "Error embeding greet message");
}
}
private readonly Channel<(GreetSettings, IGuildUser, TaskCompletionSource<bool>)> _greetDmQueue =
Channel.CreateBounded<(GreetSettings, IGuildUser, TaskCompletionSource<bool>)>(new BoundedChannelOptions(60)
{
// The limit of 60 users should be only hit when there's a raid. In that case
// probably the best thing to do is to drop newest (raiding) users
FullMode = BoundedChannelFullMode.DropNewest
});
private async Task<bool> GreetDmUser(GreetSettings conf, IGuildUser user)
{
var completionSource = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
await _greetDmQueue.Writer.WriteAsync((conf, user, completionSource));
return await completionSource.Task;
}
private async Task<bool> GreetDmUserInternal(GreetSettings conf, IGuildUser user)
{
try
{
var repCtx = new ReplacementContext(client: _client, guild: user.Guild, users: user);
var smartText = SmartText.CreateFrom(conf.DmGreetMessageText);
smartText = await _repSvc.ReplaceAsync(smartText, repCtx);
if (smartText is SmartPlainText pt)
{
smartText = new SmartEmbedText()
{
Description = pt.Text
};
}
if (smartText is SmartEmbedText set)
{
smartText = set with
{
Footer = CreateFooterSource(user)
};
}
else if (smartText is SmartEmbedTextArray seta)
{
// if the greet dm message is a text array
var ebElem = seta.Embeds.LastOrDefault();
if (ebElem is null)
{
// if there are no embeds, add an embed with the footer
smartText = seta with
{
Embeds =
[
new SmartEmbedArrayElementText()
{
Footer = CreateFooterSource(user)
}
]
};
}
else
{
// if the maximum amount of embeds is reached, edit the last embed
if (seta.Embeds.Length >= 10)
{
seta.Embeds[^1] = seta.Embeds[^1] with
{
Footer = CreateFooterSource(user)
};
}
else
{
// if there is less than 10 embeds, add an embed with footer only
seta.Embeds = seta.Embeds.Append(new SmartEmbedArrayElementText()
{
Footer = CreateFooterSource(user)
})
.ToArray();
}
}
}
await _sender.Response(user).Text(smartText).Sanitize(false).SendAsync();
}
catch
{
return false;
}
return true;
}
private static SmartTextEmbedFooter CreateFooterSource(IGuildUser user)
=> new()
{
Text = $"This message was sent from {user.Guild} server.",
IconUrl = user.Guild.IconUrl
};
private Task OnUserJoined(IGuildUser user)
{
_ = Task.Run(async () =>
{
try
{
var conf = await GetGreetSettingsAsync(user.GuildId);
if (conf is null)
return;
if (conf.SendChannelGreetMessage)
{
var channel = await user.Guild.GetTextChannelAsync(conf.GreetMessageChannelId);
if (channel is not null)
{
if (GroupGreets)
{
// if group is newly created, greet that user right away,
// but any user which joins in the next 5 seconds will
// be greeted in a group greet
if (_greets.CreateOrAdd(user.GuildId, user))
{
// greet single user
await GreetUsers(conf, channel, new[] { user });
var groupClear = false;
while (!groupClear)
{
await Task.Delay(5000);
groupClear = _greets.ClearGroup(user.GuildId, 5, out var toGreet);
await GreetUsers(conf, channel, toGreet);
}
}
}
else
await GreetUsers(conf, channel, new[] { user });
}
}
if (conf.SendDmGreetMessage)
await GreetDmUser(conf, user);
}
catch
{
// ignored
}
});
return Task.CompletedTask;
}
// public GreetSettings GetOrAddSettingsForGuild(ulong guildId)
// {
// if (_greetDmEnabledGuilds.TryGetValue(guildId, out var settings))
// return settings;
//
// using (var uow = _db.GetDbContext())
// {
// var gc = uow.GuildConfigsForId(guildId, set => set);
// settings = GreetSettings.Create(gc);
// }
//
// _greetDmEnabledGuilds.TryAdd(guildId, settings);
// return settings;
// }
public async Task<bool> SetGreet(
ulong guildId,
ulong? channelId,
GreetType greetType,
bool? value = null)
{
await using var uow = _db.GetDbContext();
var q = uow.GetTable<GreetSettings>();
if (value is { } v)
{
await q
.InsertOrUpdateAsync(() => new()
{
IsEnabled = v,
ChannelId = channelId,
},
(old) => new()
{
IsEnabled = v,
ChannelId = channelId,
},
() => new()
{
GuildId = guildId,
GreetType = greetType,
});
}
else
{
await q
.Where(x => x.GuildId == guildId && x.GreetType == greetType)
.UpdateAsync((old) => new()
{
IsEnabled = !old.IsEnabled
});
}
return true;
}
public async Task<bool> SetGreetTypeMessage(ulong guildId, GreetType greetType, string message)
{
message = message.SanitizeMentions();
if (string.IsNullOrWhiteSpace(message))
throw new ArgumentNullException(nameof(message));
await using (var uow = _db.GetDbContext())
{
await uow.GetTable<GreetSettings>()
.InsertOrUpdateAsync(() => new()
{
MessageText = message
},
x => new()
{
MessageText = message
},
() => new()
{
GuildId = guildId,
GreetType = greetType
});
}
var conf = await GetGreetSettingsAsync(guildId, type);
return conf?.IsEnabled ?? false;
}
public async Task<bool> Test(
ulong guildId,
GreetType type,
IMessageChannel channel,
IGuildUser user)
{
var conf = await GetGreetSettingsAsync(guildId, type);
return SendMessage(conf, user, channel);
}
public async Task<bool> SendMessage(GreetSettings conf, IMessageChannel channel, IGuildUser user)
{
if (conf.GreetType == GreetType.GreetDm)
{
await GreetDmUser(conf, user);
}
}
}