Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 8 additions & 6 deletions LiveBot.Core/Repository/Static/Queues.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,15 @@ namespace LiveBot.Core.Repository.Static
{
public class Queues
{
public static string QueueURL
{
get => $"rabbitmq://{Environment.GetEnvironmentVariable("RabbitMQ_URL")}";
}
private static readonly string _queueHost = Environment.GetEnvironmentVariable("RabbitMQ_URL")
?? throw new InvalidOperationException("Required environment variable 'RabbitMQ_URL' is not set.");

public static readonly string QueueUsername = Environment.GetEnvironmentVariable("RabbitMQ_Username");
public static readonly string QueuePassword = Environment.GetEnvironmentVariable("RabbitMQ_Password");
public static string QueueURL => $"rabbitmq://{_queueHost}";

public static readonly string QueueUsername = Environment.GetEnvironmentVariable("RabbitMQ_Username")
?? throw new InvalidOperationException("Required environment variable 'RabbitMQ_Username' is not set.");
public static readonly string QueuePassword = Environment.GetEnvironmentVariable("RabbitMQ_Password")
?? throw new InvalidOperationException("Required environment variable 'RabbitMQ_Password' is not set.");

public static readonly ushort PrefetchCount = 32;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,18 @@ public async Task Consume(ConsumeContext<IDiscordChannelDelete> context)
var subscriptions = await _work.SubscriptionRepository.FindAsync(i => i.DiscordChannel.DiscordId == message.ChannelId && i.DiscordGuild.DiscordId == message.GuildId);
foreach (var subscription in subscriptions)
{
_logger.LogInformation("Removing Stream Subscription for {Username} on {ServiceType} because channel was delete {GuildId} {ChannelId} {ChannelName} - {SubscriptionId}", subscription.User.Username, subscription.User.ServiceType, channel.DiscordGuild.DiscordId, channel.DiscordId, channel.Name, subscription.Id);
var rolesToMention = await _work.RoleToMentionRepository.FindAsync(i => i.StreamSubscription == subscription);
foreach (var roleToMention in rolesToMention)
await _work.RoleToMentionRepository.RemoveAsync(roleToMention.Id);
await _work.SubscriptionRepository.RemoveAsync(subscription.Id);
try
{
_logger.LogInformation("Removing Stream Subscription for {Username} on {ServiceType} because channel was delete {GuildId} {ChannelId} {ChannelName} - {SubscriptionId}", subscription.User.Username, subscription.User.ServiceType, channel.DiscordGuild.DiscordId, channel.DiscordId, channel.Name, subscription.Id);
var rolesToMention = await _work.RoleToMentionRepository.FindAsync(i => i.StreamSubscription == subscription);
foreach (var roleToMention in rolesToMention)
await _work.RoleToMentionRepository.RemoveAsync(roleToMention.Id);
await _work.SubscriptionRepository.RemoveAsync(subscription.Id);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to remove Stream Subscription {SubscriptionId} for channel {ChannelId}", subscription.Id, channel.DiscordId);
}
}

var guildConfig = await _work.GuildConfigRepository.SingleOrDefaultAsync(i => i.DiscordGuild.DiscordId == message.GuildId);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,16 +33,32 @@ public async Task Consume(ConsumeContext<IDiscordGuildDelete> context)
// Remove Stream Subscriptions for this Guild
foreach (var streamSubscription in streamSubscriptions)
{
_logger.LogInformation("Removing Stream Subscription for {Username} on {ServiceType} because have left the Guild {GuildId} - {SubscriptionId}", streamSubscription.User.Username, streamSubscription.User.ServiceType, discordGuild.DiscordId, streamSubscription.Id);
var rolesToMention = await _work.RoleToMentionRepository.FindAsync(i => i.StreamSubscription == streamSubscription);
foreach (var roleToMention in rolesToMention)
await _work.RoleToMentionRepository.RemoveAsync(roleToMention.Id);
await _work.SubscriptionRepository.RemoveAsync(streamSubscription.Id);
try
{
_logger.LogInformation("Removing Stream Subscription for {Username} on {ServiceType} because have left the Guild {GuildId} - {SubscriptionId}", streamSubscription.User.Username, streamSubscription.User.ServiceType, discordGuild.DiscordId, streamSubscription.Id);
var rolesToMention = await _work.RoleToMentionRepository.FindAsync(i => i.StreamSubscription == streamSubscription);
foreach (var roleToMention in rolesToMention)
await _work.RoleToMentionRepository.RemoveAsync(roleToMention.Id);
await _work.SubscriptionRepository.RemoveAsync(streamSubscription.Id);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to remove Stream Subscription {SubscriptionId} for guild {GuildId}", streamSubscription.Id, discordGuild.DiscordId);
}
}

// Remove Discord Channels for this Guild
foreach (var discordChannel in discordChannels)
await _work.ChannelRepository.RemoveAsync(discordChannel.Id);
{
try
{
await _work.ChannelRepository.RemoveAsync(discordChannel.Id);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to remove Discord Channel {ChannelId} for guild {GuildId}", discordChannel.DiscordId, discordGuild.DiscordId);
}
}

// Remove Discord Guild
await _work.GuildRepository.RemoveAsync(discordGuild.Id);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,19 +57,22 @@ public async Task Consume(ConsumeContext<IDiscordMemberLive> context)
if (userGame == null)
return;

var seenGuildIds = new HashSet<ulong>();
var mutualGuilds = new List<SocketGuild>();
foreach (DiscordSocketClient shard in _client.Shards)
{
foreach (SocketGuild guild in shard.Guilds)
{
if (guild.GetUser(user.Id) != null)
if (!mutualGuilds.Any(i => i.Id == guild.Id))
mutualGuilds.Add(guild);
if (guild.GetUser(user.Id) != null && seenGuildIds.Add(guild.Id))
mutualGuilds.Add(guild);
}
}

foreach (var guild in mutualGuilds)
{
try
{

if (guild?.Id == null) continue;

var discordGuild = await _work.GuildRepository.SingleOrDefaultAsync(i => i.DiscordId == guild.Id);
Expand Down Expand Up @@ -241,6 +244,12 @@ public async Task Consume(ConsumeContext<IDiscordMemberLive> context)
);
}
}

} // end per-guild try
catch (Exception ex)
{
_logger.LogError(ex, "Unhandled error processing member live notification for guild {GuildId}", guild?.Id);
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ public async Task Consume(ConsumeContext<IStreamOffline> context)
var streamSubscriptions = await GetStreamSubscriptionsAsync(streamUser);
if (!streamSubscriptions.Any())
{
_streamOfflineLogger.LogInformation("No subscriptions found for {Username} ({UserId})",
_streamOfflineLogger.LogDebug("No subscriptions found for {Username} ({UserId})",
user.Username, user.Id);
return;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -289,10 +289,22 @@ private StreamNotification CreateStreamNotification(ILiveBotStream stream, Strea

private async Task<bool> ObtainLock(string recordId, Guid lockGuid, TimeSpan lockTimeout)
{
var deadline = DateTime.UtcNow.Add(lockTimeout);
int delayMs = 50;
bool obtainedLock;
do
{
obtainedLock = await _cache.ObtainLockAsync(recordId: recordId, identifier: lockGuid, expiryTime: lockTimeout);
if (!obtainedLock)
{
if (DateTime.UtcNow >= deadline)
{
_streamOnlineLogger.LogWarning("Timed out waiting for lock on {RecordId} after {Timeout}s", recordId, lockTimeout.TotalSeconds);
return false;
}
await Task.Delay(delayMs);
delayMs = Math.Min(delayMs * 2, 1000);
}
}
while (!obtainedLock);

Expand Down Expand Up @@ -542,14 +554,13 @@ private double CalculateNotificationDelay(StreamNotification streamNotification,

private async Task<IUserMessage> SendDiscordMessage(SocketTextChannel channel, string notificationMessage, Embed embed, TimeSpan lockTimeout)
{
CancellationTokenSource cancellationToken = new();
cancellationToken.CancelAfter((int)lockTimeout.TotalMilliseconds);
using var cts = new CancellationTokenSource((int)lockTimeout.TotalMilliseconds);

var messageRequestOptions = new RequestOptions()
{
RetryMode = RetryMode.AlwaysFail,
Timeout = (int)lockTimeout.TotalMilliseconds,
CancelToken = cancellationToken.Token
CancelToken = cts.Token
};

return await channel.SendMessageAsync(text: notificationMessage, embed: embed, options: messageRequestOptions);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,17 +33,35 @@ public async Task Consume(ConsumeContext<IStreamUpdate> context)
if (monitor == null)
return;

ILiveBotUser user = stream.User ?? await monitor.GetUserById(stream.UserId);
ILiveBotUser? user = stream.User ?? await monitor.GetUserById(stream.UserId);
if (user == null)
{
_logger.LogWarning("Could not resolve user for stream {StreamId} on {ServiceType} during stream update; skipping",
stream.Id, stream.ServiceType);
return;
}

ILiveBotGame game = stream.Game ?? await monitor.GetGame(stream.GameId);

Expression<Func<StreamGame, bool>> templateGamePredicate = (i => i.ServiceType == stream.ServiceType && i.SourceId == "0");
var templateGame = await _work.GameRepository.SingleOrDefaultAsync(templateGamePredicate);
var streamUser = await _work.UserRepository.SingleOrDefaultAsync(i => i.ServiceType == stream.ServiceType && i.SourceID == user.Id);

if (streamUser == null)
{
_logger.LogWarning("StreamUser not found for {UserId} on {ServiceType} during stream update; skipping",
user.Id, stream.ServiceType);
return;
}

var streamSubscriptions = await _work.SubscriptionRepository.FindAsync(i => i.User == streamUser);

StreamGame streamGame;
if (!streamSubscriptions.Any())
return;

// Ensure the game record exists in the database
Expression<Func<StreamGame, bool>> templateGamePredicate = (i => i.ServiceType == stream.ServiceType && i.SourceId == "0");
if (game.Id == "0" || string.IsNullOrEmpty(game.Id))
{
var templateGame = await _work.GameRepository.SingleOrDefaultAsync(templateGamePredicate);
if (templateGame == null)
{
StreamGame newStreamGame = new StreamGame
Expand All @@ -54,9 +72,7 @@ public async Task Consume(ConsumeContext<IStreamUpdate> context)
ThumbnailURL = ""
};
await _work.GameRepository.AddOrUpdateAsync(newStreamGame, templateGamePredicate);
templateGame = await _work.GameRepository.SingleOrDefaultAsync(templateGamePredicate);
}
streamGame = templateGame;
}
else
{
Expand All @@ -68,12 +84,8 @@ public async Task Consume(ConsumeContext<IStreamUpdate> context)
ThumbnailURL = game.ThumbnailURL
};
await _work.GameRepository.AddOrUpdateAsync(newStreamGame, i => i.ServiceType == stream.ServiceType && i.SourceId == stream.GameId);
streamGame = await _work.GameRepository.SingleOrDefaultAsync(i => i.ServiceType == stream.ServiceType && i.SourceId == stream.GameId);
}

if (!streamSubscriptions.Any())
return;

bool hasValidSubscriptions = false;

foreach (StreamSubscription streamSubscription in streamSubscriptions)
Expand Down
8 changes: 6 additions & 2 deletions LiveBot.Discord.SlashCommands/Helpers/MonitorUtils.cs
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,10 @@ internal static Embed GetSubscriptionEmbed(SocketGuild guild, StreamSubscription
{
var roles = new List<SocketRole>();
if (subscription.RolesToMention.Any())
roles = subscription.RolesToMention.Select(i => guild.GetRole(i.DiscordRoleId)).ToList();
roles = subscription.RolesToMention
.Select(i => guild.GetRole(i.DiscordRoleId))
.Where(r => r != null)
.ToList();

var roleStrings = new List<string>();
foreach (var role in roles.OrderBy(i => i.Name))
Expand Down Expand Up @@ -86,7 +89,8 @@ internal static async Task<bool> ConsolidateRoleMentions(IUnitOfWork work, Strea
if (currentRolesToMention.Any())
{
var rolesToDelete = currentRolesToMention.Where(i => !roleIds.Contains(i.DiscordRoleId)).ToList();
rolesToDelete.ForEach(async i => await work.RoleToMentionRepository.RemoveAsync(i.Id));
foreach (var roleToDelete in rolesToDelete)
await work.RoleToMentionRepository.RemoveAsync(roleToDelete.Id);
RolesUpdated = true;
}
foreach (var roleId in roleIds)
Expand Down
8 changes: 5 additions & 3 deletions LiveBot.Discord.SlashCommands/Helpers/NotificationHelpers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,10 @@ namespace LiveBot.Discord.SlashCommands.Helpers
{
public static class NotificationHelpers
{
public static string EscapeSpecialDiscordCharacters(string input)
public static string EscapeSpecialDiscordCharacters(string? input)
{
if (string.IsNullOrEmpty(input))
return string.Empty;
return Format.Sanitize(input);
}

Expand Down Expand Up @@ -121,10 +123,10 @@ public static Embed GetStreamEmbed(ILiveBotStream stream, ILiveBotUser user, ILi
.WithThumbnailUrl(user.AvatarURL);

// Add Game field
builder.AddField(name: "Game", value: game.Name, inline: true);
builder.AddField(name: "Game", value: string.IsNullOrWhiteSpace(game.Name) ? "[Not Set]" : game.Name, inline: true);

// Add Stream URL field
builder.AddField(name: "Stream", value: stream.StreamURL, inline: true);
builder.AddField(name: "Stream", value: string.IsNullOrWhiteSpace(stream.StreamURL) ? user.ProfileURL : stream.StreamURL, inline: true);

// Add Status Field
//builder.AddField(name: "Status", value: "", inline: false);
Expand Down
13 changes: 10 additions & 3 deletions LiveBot.Discord.SlashCommands/InteractionHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,10 @@ internal async Task ReadyAsync(DiscordSocketClient client)
_logger.LogInformation(message: "Finished registering AdminModule with shard {ShardId}", client.ShardId);
RegisteredCommands = true;
}
catch { }
catch (Exception ex)
{
_logger.LogWarning(ex, "Failed to register AdminModule on shard {ShardId}", client.ShardId);
}
}
}

Expand All @@ -65,8 +68,9 @@ internal async Task HandleInteraction(SocketInteraction interaction)

await Task.CompletedTask;
}
catch
catch (Exception ex)
{
_logger.LogError(ex, "Unhandled exception executing interaction {InteractionType}", interaction.Type);
// If Slash Command execution fails it is most likely that the original interaction acknowledgement will persist. It is a good idea to delete the original
// response, or at least let the user know that something went wrong during the command execution.
if (interaction.Type is InteractionType.ApplicationCommand)
Expand All @@ -89,7 +93,10 @@ internal async Task InteractionExecuted(ICommandInfo commandInfo, IInteractionCo

await context.Interaction.FollowupAsync(ephemeral: true, embed: embed);
}
catch { }
catch (Exception ex)
{
_logger.LogWarning(ex, "Failed to send error followup for {CommandName}", commandInfo?.Name ?? "Unknown");
}
}

var logLevel = LogLevel.Information;
Expand Down
4 changes: 2 additions & 2 deletions LiveBot.Discord.SlashCommands/LiveBotDiscordEventHandlers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -73,8 +73,8 @@ public async Task GuildJoined(SocketGuild guild)
public async Task GuildUpdated(SocketGuild beforeGuild, SocketGuild afterGuild)
{
if (
beforeGuild.Name.Equals(afterGuild.Name, StringComparison.InvariantCultureIgnoreCase) &&
beforeGuild.IconUrl.Equals(afterGuild.IconUrl, StringComparison.InvariantCultureIgnoreCase)
string.Equals(beforeGuild.Name, afterGuild.Name, StringComparison.InvariantCultureIgnoreCase) &&
string.Equals(beforeGuild.IconUrl, afterGuild.IconUrl, StringComparison.InvariantCultureIgnoreCase)
)
return;
var context = new DiscordGuildUpdate { GuildId = afterGuild.Id, GuildName = afterGuild.Name, IconUrl = afterGuild.IconUrl };
Expand Down
3 changes: 3 additions & 0 deletions LiveBot.Discord.SlashCommands/Modules/MonitorModule.cs
Original file line number Diff line number Diff line change
Expand Up @@ -612,6 +612,9 @@ private ILiveBotMonitor GetMonitor(Uri uri)
private async Task<StreamUser> GetStreamUserAsync(ILiveBotMonitor monitor, Uri uri)
{
var monitorUser = await monitor.GetUser(profileURL: uri.AbsoluteUri);
if (monitorUser == null)
throw new ArgumentException($"Could not find a user for the provided URL: {Format.EscapeUrl(uri.AbsoluteUri)}");

StreamUser streamUser = new()
{
ServiceType = monitorUser.ServiceType,
Expand Down
17 changes: 13 additions & 4 deletions LiveBot.Repository/ModelRepository.cs
Original file line number Diff line number Diff line change
Expand Up @@ -234,11 +234,18 @@ public virtual async Task AddOrUpdateAsync(TEntity entity, Expression<Func<TEnti
}
else // Update the db entry from the entity and save.
{
entity.Id = exists.Id;
Context.Entry(exists).CurrentValues.SetValues(entity);
await Context.SaveChangesAsync().ConfigureAwait(false);
try
{
entity.Id = exists.Id;
Context.Entry(exists).CurrentValues.SetValues(entity);
await Context.SaveChangesAsync().ConfigureAwait(false);
}
finally
{
syncLock.Release();
}
return;
}
syncLock.Release();
}

/// <inheritdoc/>
Expand All @@ -254,6 +261,8 @@ public async Task RemoveAsync(long Id)
{
await syncLock.WaitAsync().ConfigureAwait(false);
TEntity entity = await DbSet.FindAsync(Id).ConfigureAwait(false);
if (entity == null)
return;
DbSet.Remove(entity);
await Context.SaveChangesAsync().ConfigureAwait(false);
}
Expand Down
Loading