2025-03-19 14:15:05 +13:00
|
|
|
|
using System.Net;
|
|
|
|
|
using LinqToDB;
|
|
|
|
|
using LinqToDB.EntityFrameworkCore;
|
|
|
|
|
using Microsoft.EntityFrameworkCore;
|
|
|
|
|
using EllieBot.Common.ModuleBehaviors;
|
|
|
|
|
using EllieBot.Db.Models;
|
|
|
|
|
using Newtonsoft.Json.Linq;
|
|
|
|
|
|
|
|
|
|
namespace EllieBot.Modules.Utility.LiveChannel;
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Service for managing live channels.
|
|
|
|
|
/// </summary>
|
|
|
|
|
public class LiveChannelService(
|
|
|
|
|
DbService db,
|
|
|
|
|
DiscordSocketClient client,
|
|
|
|
|
IReplacementService repSvc,
|
|
|
|
|
ShardData shardData) : IReadyExecutor, IEService
|
|
|
|
|
{
|
|
|
|
|
public const int MAX_LIVECHANNELS = 5;
|
|
|
|
|
|
|
|
|
|
private readonly ConcurrentDictionary<ulong, ConcurrentDictionary<ulong, LiveChannelConfig>> _liveChannels = new();
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Initializes data when bot is ready
|
|
|
|
|
/// </summary>
|
|
|
|
|
public async Task OnReadyAsync()
|
|
|
|
|
{
|
|
|
|
|
// Load all existing live channels into memory
|
|
|
|
|
await using var uow = db.GetDbContext();
|
|
|
|
|
var configs = await uow.GetTable<LiveChannelConfig>()
|
|
|
|
|
.AsNoTracking()
|
|
|
|
|
.Where(x => Queries.GuildOnShard(x.GuildId, shardData.TotalShards, shardData.ShardId))
|
|
|
|
|
.ToListAsyncLinqToDB();
|
|
|
|
|
|
|
|
|
|
foreach (var config in configs)
|
|
|
|
|
{
|
|
|
|
|
var guildDict = _liveChannels.GetOrAdd(
|
|
|
|
|
config.GuildId,
|
|
|
|
|
_ => new());
|
|
|
|
|
|
|
|
|
|
guildDict[config.ChannelId] = config;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
using var timer = new PeriodicTimer(TimeSpan.FromMinutes(10));
|
|
|
|
|
while (await timer.WaitForNextTickAsync())
|
|
|
|
|
{
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
// get all live channels from cache
|
|
|
|
|
var channels = new List<LiveChannelConfig>(_liveChannels.Count * 2);
|
|
|
|
|
|
|
|
|
|
foreach (var (_, vals) in _liveChannels)
|
|
|
|
|
{
|
|
|
|
|
foreach (var (_, config) in vals)
|
|
|
|
|
{
|
|
|
|
|
channels.Add(config);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
foreach (var config in channels)
|
|
|
|
|
{
|
|
|
|
|
var guild = client.GetGuild(config.GuildId);
|
|
|
|
|
var channel = guild?.GetChannel(config.ChannelId);
|
|
|
|
|
|
|
|
|
|
if (channel is null)
|
|
|
|
|
{
|
|
|
|
|
await RemoveLiveChannelAsync(config.GuildId, config.ChannelId);
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var repCtx = new ReplacementContext(
|
|
|
|
|
user: null,
|
|
|
|
|
guild: guild,
|
|
|
|
|
client: client
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
var text = await repSvc.ReplaceAsync(config.Template, repCtx);
|
|
|
|
|
|
|
|
|
|
// only update if needed
|
|
|
|
|
if (channel.Name != text)
|
|
|
|
|
await channel.ModifyAsync(x => x.Name = text);
|
|
|
|
|
}
|
2025-03-19 14:38:14 +13:00
|
|
|
|
catch (HttpException ex) when (ex.HttpCode == HttpStatusCode.Forbidden
|
|
|
|
|
|| ex.DiscordCode == DiscordErrorCode.MissingPermissions
|
|
|
|
|
|| ex.HttpCode == HttpStatusCode.NotFound)
|
2025-03-19 14:15:05 +13:00
|
|
|
|
{
|
|
|
|
|
await RemoveLiveChannelAsync(config.GuildId, config.ChannelId);
|
|
|
|
|
Log.Warning(
|
|
|
|
|
"Channel {ChannelId} in guild {GuildId} is not accessible. Live channel will be removed",
|
|
|
|
|
config.ChannelId,
|
|
|
|
|
config.GuildId);
|
|
|
|
|
}
|
|
|
|
|
catch (HttpException ex) when (ex.HttpCode == HttpStatusCode.TooManyRequests ||
|
|
|
|
|
ex.DiscordCode == DiscordErrorCode.ChannelWriteRatelimit)
|
|
|
|
|
{
|
|
|
|
|
Log.Warning(ex, "LiveChannel hit a ratelimit. Sleeping for 2 minutes: {Message}", ex.Message);
|
|
|
|
|
await Task.Delay(2.Minutes());
|
|
|
|
|
}
|
|
|
|
|
catch (Exception e)
|
|
|
|
|
{
|
|
|
|
|
Log.Error(e, "Error in live channel service: {ErrorMessage}", e.Message);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// wait for half a second to reduce the chance of global ratelimits
|
|
|
|
|
await Task.Delay(500);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
{
|
|
|
|
|
Log.Error(ex, "Error in live channel service: {ErrorMessage}", ex.Message);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Adds a new live channel configuration to the specified guild.
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <param name="guildId">ID of the guild</param>
|
|
|
|
|
/// <param name="channel">Channel to set as live</param>
|
|
|
|
|
/// <param name="template">Template text to use for the channel</param>
|
|
|
|
|
/// <returns>True if successfully added, false otherwise</returns>
|
|
|
|
|
public async Task<bool> AddLiveChannelAsync(ulong guildId, IChannel channel, string template)
|
|
|
|
|
{
|
|
|
|
|
var guildDict = _liveChannels.GetOrAdd(
|
|
|
|
|
guildId,
|
|
|
|
|
_ => new());
|
|
|
|
|
|
|
|
|
|
if (guildDict.Count >= MAX_LIVECHANNELS)
|
|
|
|
|
return false;
|
|
|
|
|
|
|
|
|
|
await using var uow = db.GetDbContext();
|
|
|
|
|
await uow.GetTable<LiveChannelConfig>()
|
|
|
|
|
.InsertOrUpdateAsync(() => new()
|
|
|
|
|
{
|
|
|
|
|
GuildId = guildId,
|
|
|
|
|
ChannelId = channel.Id,
|
|
|
|
|
Template = template
|
|
|
|
|
},
|
|
|
|
|
(_) => new()
|
|
|
|
|
{
|
|
|
|
|
Template = template
|
|
|
|
|
},
|
|
|
|
|
() => new()
|
|
|
|
|
{
|
|
|
|
|
GuildId = guildId,
|
|
|
|
|
ChannelId = channel.Id
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Add to in-memory cache
|
|
|
|
|
var newConfig = new LiveChannelConfig
|
|
|
|
|
{
|
|
|
|
|
GuildId = guildId,
|
|
|
|
|
ChannelId = channel.Id,
|
|
|
|
|
Template = template
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
guildDict[channel.Id] = newConfig;
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Removes a live channel configuration from the specified guild.
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <param name="guildId">ID of the guild</param>
|
|
|
|
|
/// <param name="channelId">ID of the channel to remove as live</param>
|
|
|
|
|
/// <returns>True if successfully removed, false otherwise</returns>
|
|
|
|
|
public async Task<bool> RemoveLiveChannelAsync(ulong guildId, ulong channelId)
|
|
|
|
|
{
|
|
|
|
|
if (!_liveChannels.TryGetValue(guildId, out var guildDict) ||
|
|
|
|
|
!guildDict.TryRemove(channelId, out _))
|
|
|
|
|
return false;
|
|
|
|
|
|
|
|
|
|
await using var uow = db.GetDbContext();
|
|
|
|
|
await uow.GetTable<LiveChannelConfig>()
|
|
|
|
|
.Where(x => x.GuildId == guildId && x.ChannelId == channelId)
|
|
|
|
|
.DeleteAsync();
|
|
|
|
|
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Gets all live channels for a guild.
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <param name="guildId">ID of the guild</param>
|
|
|
|
|
/// <returns>List of live channel configurations</returns>
|
|
|
|
|
public async Task<List<LiveChannelConfig>> GetLiveChannelsAsync(ulong guildId)
|
|
|
|
|
{
|
|
|
|
|
await using var uow = db.GetDbContext();
|
|
|
|
|
return await uow.GetTable<LiveChannelConfig>()
|
|
|
|
|
.AsNoTracking()
|
|
|
|
|
.Where(x => x.GuildId == guildId)
|
|
|
|
|
.ToListAsyncLinqToDB();
|
|
|
|
|
}
|
|
|
|
|
}
|