Global usings and file scoped namespaces

This commit is contained in:
Kwoth
2021-12-19 05:14:11 +01:00
parent bc31dae965
commit ee33313519
548 changed files with 47528 additions and 49115 deletions

View File

@@ -1,118 +1,112 @@
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Discord;
using NadekoBot.Common.ModuleBehaviors;
using NadekoBot.Extensions;
using NadekoBot.Db.Models;
using System;
using Discord.WebSocket;
using Microsoft.EntityFrameworkCore;
using NadekoBot.Services;
using NadekoBot.Services.Database.Models;
using NadekoBot.Db;
using NadekoBot.Modules.Administration;
namespace NadekoBot.Modules.Utility.Services
namespace NadekoBot.Modules.Utility.Services;
public class CommandMapService : IInputTransformer, INService
{
public class CommandMapService : IInputTransformer, INService
private readonly IEmbedBuilderService _eb;
public ConcurrentDictionary<ulong, ConcurrentDictionary<string, string>> AliasMaps { get; } = new ConcurrentDictionary<ulong, ConcurrentDictionary<string, string>>();
private readonly DbService _db;
//commandmap
public CommandMapService(DiscordSocketClient client, DbService db, IEmbedBuilderService eb)
{
private readonly IEmbedBuilderService _eb;
_eb = eb;
public ConcurrentDictionary<ulong, ConcurrentDictionary<string, string>> AliasMaps { get; } = new ConcurrentDictionary<ulong, ConcurrentDictionary<string, string>>();
private readonly DbService _db;
//commandmap
public CommandMapService(DiscordSocketClient client, DbService db, IEmbedBuilderService eb)
using (var uow = db.GetDbContext())
{
_eb = eb;
using (var uow = db.GetDbContext())
{
var guildIds = client.Guilds.Select(x => x.Id).ToList();
var configs = uow.Set<GuildConfig>()
.Include(gc => gc.CommandAliases)
.Where(x => guildIds.Contains(x.GuildId))
.ToList();
var guildIds = client.Guilds.Select(x => x.Id).ToList();
var configs = uow.Set<GuildConfig>()
.Include(gc => gc.CommandAliases)
.Where(x => guildIds.Contains(x.GuildId))
.ToList();
AliasMaps = new ConcurrentDictionary<ulong, ConcurrentDictionary<string, string>>(configs
.ToDictionary(
x => x.GuildId,
x => new ConcurrentDictionary<string, string>(x.CommandAliases
.Distinct(new CommandAliasEqualityComparer())
.ToDictionary(ca => ca.Trigger, ca => ca.Mapping))));
AliasMaps = new ConcurrentDictionary<ulong, ConcurrentDictionary<string, string>>(configs
.ToDictionary(
x => x.GuildId,
x => new ConcurrentDictionary<string, string>(x.CommandAliases
.Distinct(new CommandAliasEqualityComparer())
.ToDictionary(ca => ca.Trigger, ca => ca.Mapping))));
_db = db;
}
_db = db;
}
}
public int ClearAliases(ulong guildId)
public int ClearAliases(ulong guildId)
{
AliasMaps.TryRemove(guildId, out _);
int count;
using (var uow = _db.GetDbContext())
{
AliasMaps.TryRemove(guildId, out _);
int count;
using (var uow = _db.GetDbContext())
{
var gc = uow.GuildConfigsForId(guildId, set => set.Include(x => x.CommandAliases));
count = gc.CommandAliases.Count;
gc.CommandAliases.Clear();
uow.SaveChanges();
}
return count;
var gc = uow.GuildConfigsForId(guildId, set => set.Include(x => x.CommandAliases));
count = gc.CommandAliases.Count;
gc.CommandAliases.Clear();
uow.SaveChanges();
}
return count;
}
public async Task<string> TransformInput(IGuild guild, IMessageChannel channel, IUser user, string input)
public async Task<string> TransformInput(IGuild guild, IMessageChannel channel, IUser user, string input)
{
await Task.Yield();
if (guild is null || string.IsNullOrWhiteSpace(input))
return input;
if (guild != null)
{
await Task.Yield();
if (guild is null || string.IsNullOrWhiteSpace(input))
return input;
if (guild != null)
if (AliasMaps.TryGetValue(guild.Id, out ConcurrentDictionary<string, string> maps))
{
if (AliasMaps.TryGetValue(guild.Id, out ConcurrentDictionary<string, string> maps))
var keys = maps.Keys
.OrderByDescending(x => x.Length);
foreach (var k in keys)
{
var keys = maps.Keys
.OrderByDescending(x => x.Length);
string newInput;
if (input.StartsWith(k + " ", StringComparison.InvariantCultureIgnoreCase))
newInput = maps[k] + input.Substring(k.Length, input.Length - k.Length);
else if (input.Equals(k, StringComparison.InvariantCultureIgnoreCase))
newInput = maps[k];
else
continue;
foreach (var k in keys)
try
{
string newInput;
if (input.StartsWith(k + " ", StringComparison.InvariantCultureIgnoreCase))
newInput = maps[k] + input.Substring(k.Length, input.Length - k.Length);
else if (input.Equals(k, StringComparison.InvariantCultureIgnoreCase))
newInput = maps[k];
else
continue;
try
var toDelete = await channel.SendConfirmAsync(_eb, $"{input} => {newInput}").ConfigureAwait(false);
var _ = Task.Run(async () =>
{
var toDelete = await channel.SendConfirmAsync(_eb, $"{input} => {newInput}").ConfigureAwait(false);
var _ = Task.Run(async () =>
await Task.Delay(1500).ConfigureAwait(false);
await toDelete.DeleteAsync(new RequestOptions()
{
await Task.Delay(1500).ConfigureAwait(false);
await toDelete.DeleteAsync(new RequestOptions()
{
RetryMode = RetryMode.AlwaysRetry
}).ConfigureAwait(false);
});
}
catch { }
return newInput;
RetryMode = RetryMode.AlwaysRetry
}).ConfigureAwait(false);
});
}
catch { }
return newInput;
}
}
return input;
}
}
public class CommandAliasEqualityComparer : IEqualityComparer<CommandAlias>
{
public bool Equals(CommandAlias x, CommandAlias y) => x.Trigger == y.Trigger;
public int GetHashCode(CommandAlias obj) => obj.Trigger.GetHashCode(StringComparison.InvariantCulture);
return input;
}
}
public class CommandAliasEqualityComparer : IEqualityComparer<CommandAlias>
{
public bool Equals(CommandAlias x, CommandAlias y) => x.Trigger == y.Trigger;
public int GetHashCode(CommandAlias obj) => obj.Trigger.GetHashCode(StringComparison.InvariantCulture);
}

View File

@@ -3,97 +3,93 @@ using NadekoBot.Services;
using NadekoBot.Extensions;
using NadekoBot.Modules.Utility.Common;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
namespace NadekoBot.Modules.Utility.Services
namespace NadekoBot.Modules.Utility.Services;
public class ConverterService : INService
{
public class ConverterService : INService
public ConvertUnit[] Units =>
_cache.Redis.GetDatabase()
.StringGet("converter_units")
.ToString()
.MapJson<ConvertUnit[]>();
private readonly Timer _currencyUpdater;
private readonly TimeSpan _updateInterval = new TimeSpan(12, 0, 0);
private readonly DbService _db;
private readonly IDataCache _cache;
private readonly IHttpClientFactory _httpFactory;
public ConverterService(DiscordSocketClient client, DbService db,
IDataCache cache, IHttpClientFactory factory)
{
public ConvertUnit[] Units =>
_cache.Redis.GetDatabase()
.StringGet("converter_units")
.ToString()
.MapJson<ConvertUnit[]>();
_db = db;
_cache = cache;
_httpFactory = factory;
private readonly Timer _currencyUpdater;
private readonly TimeSpan _updateInterval = new TimeSpan(12, 0, 0);
private readonly DbService _db;
private readonly IDataCache _cache;
private readonly IHttpClientFactory _httpFactory;
public ConverterService(DiscordSocketClient client, DbService db,
IDataCache cache, IHttpClientFactory factory)
if (client.ShardId == 0)
{
_db = db;
_cache = cache;
_httpFactory = factory;
if (client.ShardId == 0)
{
_currencyUpdater = new Timer(async (shouldLoad) => await UpdateCurrency((bool)shouldLoad).ConfigureAwait(false),
client.ShardId == 0,
TimeSpan.Zero,
_updateInterval);
}
}
private async Task<Rates> GetCurrencyRates()
{
using (var http = _httpFactory.CreateClient())
{
var res = await http.GetStringAsync("https://convertapi.nadeko.bot/latest").ConfigureAwait(false);
return JsonConvert.DeserializeObject<Rates>(res);
}
}
private async Task UpdateCurrency(bool shouldLoad)
{
try
{
var unitTypeString = "currency";
if (shouldLoad)
{
var currencyRates = await GetCurrencyRates().ConfigureAwait(false);
var baseType = new ConvertUnit()
{
Triggers = new[] { currencyRates.Base },
Modifier = decimal.One,
UnitType = unitTypeString
};
var range = currencyRates.ConversionRates.Select(u => new ConvertUnit()
{
Triggers = new[] { u.Key },
Modifier = u.Value,
UnitType = unitTypeString
}).ToArray();
var fileData = JsonConvert.DeserializeObject<ConvertUnit[]>(
File.ReadAllText("data/units.json"))
.Where(x => x.UnitType != "currency");
var data = JsonConvert.SerializeObject(range.Append(baseType).Concat(fileData).ToList());
_cache.Redis.GetDatabase()
.StringSet("converter_units", data);
}
}
catch
{
// ignored
}
_currencyUpdater = new Timer(async (shouldLoad) => await UpdateCurrency((bool)shouldLoad).ConfigureAwait(false),
client.ShardId == 0,
TimeSpan.Zero,
_updateInterval);
}
}
public class Rates
private async Task<Rates> GetCurrencyRates()
{
public string Base { get; set; }
public DateTime Date { get; set; }
[JsonProperty("rates")]
public Dictionary<string, decimal> ConversionRates { get; set; }
using (var http = _httpFactory.CreateClient())
{
var res = await http.GetStringAsync("https://convertapi.nadeko.bot/latest").ConfigureAwait(false);
return JsonConvert.DeserializeObject<Rates>(res);
}
}
private async Task UpdateCurrency(bool shouldLoad)
{
try
{
var unitTypeString = "currency";
if (shouldLoad)
{
var currencyRates = await GetCurrencyRates().ConfigureAwait(false);
var baseType = new ConvertUnit()
{
Triggers = new[] { currencyRates.Base },
Modifier = decimal.One,
UnitType = unitTypeString
};
var range = currencyRates.ConversionRates.Select(u => new ConvertUnit()
{
Triggers = new[] { u.Key },
Modifier = u.Value,
UnitType = unitTypeString
}).ToArray();
var fileData = JsonConvert.DeserializeObject<ConvertUnit[]>(
File.ReadAllText("data/units.json"))
.Where(x => x.UnitType != "currency");
var data = JsonConvert.SerializeObject(range.Append(baseType).Concat(fileData).ToList());
_cache.Redis.GetDatabase()
.StringSet("converter_units", data);
}
}
catch
{
// ignored
}
}
}
public class Rates
{
public string Base { get; set; }
public DateTime Date { get; set; }
[JsonProperty("rates")]
public Dictionary<string, decimal> ConversionRates { get; set; }
}

View File

@@ -2,32 +2,31 @@
using NadekoBot.Common;
using NadekoBot.Services;
namespace NadekoBot.Modules.Utility.Services
namespace NadekoBot.Modules.Utility.Services;
public class InviteService : INService
{
public class InviteService : INService
public class Options : INadekoCommandOptions
{
public class Options : INadekoCommandOptions
[Option('m', "max-uses", Required = false, Default = 0, HelpText = "Maximum number of times the invite can be used. Default 0 (never).")]
public int MaxUses { get; set; } = 0;
[Option('u', "unique", Required = false, Default = false, HelpText = "Not setting this flag will result in bot getting the existing invite with the same settings if it exists, instead of creating a new one.")]
public bool Unique { get; set; } = false;
[Option('t', "temporary", Required = false, Default = false, HelpText = "If this flag is set, the user will be kicked from the guild once they close their client.")]
public bool Temporary { get; set; } = false;
[Option('e', "expire", Required = false, Default = 0, HelpText = "Time in seconds to expire the invite. Default 0 (no expiry).")]
public int Expire { get; set; } = 0;
public void NormalizeOptions()
{
[Option('m', "max-uses", Required = false, Default = 0, HelpText = "Maximum number of times the invite can be used. Default 0 (never).")]
public int MaxUses { get; set; } = 0;
if (MaxUses < 0)
MaxUses = 0;
[Option('u', "unique", Required = false, Default = false, HelpText = "Not setting this flag will result in bot getting the existing invite with the same settings if it exists, instead of creating a new one.")]
public bool Unique { get; set; } = false;
[Option('t', "temporary", Required = false, Default = false, HelpText = "If this flag is set, the user will be kicked from the guild once they close their client.")]
public bool Temporary { get; set; } = false;
[Option('e', "expire", Required = false, Default = 0, HelpText = "Time in seconds to expire the invite. Default 0 (no expiry).")]
public int Expire { get; set; } = 0;
public void NormalizeOptions()
{
if (MaxUses < 0)
MaxUses = 0;
if (Expire < 0)
Expire = 0;
}
if (Expire < 0)
Expire = 0;
}
}
}
}

View File

@@ -2,196 +2,192 @@
using NadekoBot.Services;
using NadekoBot.Services.Database.Models;
using NadekoBot.Modules.Utility.Common.Patreon;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Json;
using System.Text.Json.Serialization;
using System.Threading;
using System.Threading.Tasks;
using System.Web;
using Discord;
using LinqToDB.EntityFrameworkCore;
using NadekoBot.Modules.Gambling.Services;
using NadekoBot.Extensions;
using Serilog;
using StackExchange.Redis;
using JsonSerializer = System.Text.Json.JsonSerializer;
namespace NadekoBot.Modules.Utility.Services
namespace NadekoBot.Modules.Utility.Services;
public class PatreonRewardsService : INService
{
public class PatreonRewardsService : INService
private readonly SemaphoreSlim getPledgesLocker = new SemaphoreSlim(1, 1);
private readonly Timer _updater;
private readonly SemaphoreSlim claimLockJustInCase = new SemaphoreSlim(1, 1);
public TimeSpan Interval { get; } = TimeSpan.FromMinutes(3);
private readonly DbService _db;
private readonly ICurrencyService _currency;
private readonly GamblingConfigService _gamblingConfigService;
private readonly ConnectionMultiplexer _redis;
private readonly IBotCredsProvider _credsProvider;
private readonly IHttpClientFactory _httpFactory;
private readonly IEmbedBuilderService _eb;
private readonly DiscordSocketClient _client;
public DateTime LastUpdate { get; private set; } = DateTime.UtcNow;
public PatreonRewardsService(
DbService db,
ICurrencyService currency,
IHttpClientFactory factory,
IEmbedBuilderService eb,
DiscordSocketClient client,
GamblingConfigService gamblingConfigService,
ConnectionMultiplexer redis,
IBotCredsProvider credsProvider)
{
private readonly SemaphoreSlim getPledgesLocker = new SemaphoreSlim(1, 1);
_db = db;
_currency = currency;
_gamblingConfigService = gamblingConfigService;
_redis = redis;
_credsProvider = credsProvider;
_httpFactory = factory;
_eb = eb;
_client = client;
private readonly Timer _updater;
private readonly SemaphoreSlim claimLockJustInCase = new SemaphoreSlim(1, 1);
if (client.ShardId == 0)
_updater = new Timer(async _ => await RefreshPledges(_credsProvider.GetCreds()).ConfigureAwait(false),
null, TimeSpan.Zero, Interval);
}
private DateTime LastAccessTokenUpdate(IBotCredentials creds)
{
var db = _redis.GetDatabase();
var val = db.StringGet($"{creds.RedisKey()}_patreon_update");
if (val == default)
return DateTime.MinValue;
var lastTime = DateTime.FromBinary((long)val);
return lastTime;
}
private sealed class PatreonRefreshData
{
[JsonPropertyName("access_token")]
public string AccessToken { get; set; }
[JsonPropertyName("refresh_token")]
public string RefreshToken { get; set; }
[JsonPropertyName("expires_in")]
public long ExpiresIn { get; set; }
[JsonPropertyName("scope")]
public string Scope { get; set; }
[JsonPropertyName("token_type")]
public string TokenType { get; set; }
}
public TimeSpan Interval { get; } = TimeSpan.FromMinutes(3);
private readonly DbService _db;
private readonly ICurrencyService _currency;
private readonly GamblingConfigService _gamblingConfigService;
private readonly ConnectionMultiplexer _redis;
private readonly IBotCredsProvider _credsProvider;
private readonly IHttpClientFactory _httpFactory;
private readonly IEmbedBuilderService _eb;
private readonly DiscordSocketClient _client;
public DateTime LastUpdate { get; private set; } = DateTime.UtcNow;
public PatreonRewardsService(
DbService db,
ICurrencyService currency,
IHttpClientFactory factory,
IEmbedBuilderService eb,
DiscordSocketClient client,
GamblingConfigService gamblingConfigService,
ConnectionMultiplexer redis,
IBotCredsProvider credsProvider)
private async Task<bool> UpdateAccessToken(IBotCredentials creds)
{
Log.Information("Updating patreon access token...");
try
{
_db = db;
_currency = currency;
_gamblingConfigService = gamblingConfigService;
_redis = redis;
_credsProvider = credsProvider;
_httpFactory = factory;
_eb = eb;
_client = client;
using var http = _httpFactory.CreateClient();
var res = await http.PostAsync($"https://www.patreon.com/api/oauth2/token" +
$"?grant_type=refresh_token" +
$"&refresh_token={creds.Patreon.RefreshToken}" +
$"&client_id={creds.Patreon.ClientId}" +
$"&client_secret={creds.Patreon.ClientSecret}",
new StringContent(string.Empty));
if (client.ShardId == 0)
_updater = new Timer(async _ => await RefreshPledges(_credsProvider.GetCreds()).ConfigureAwait(false),
null, TimeSpan.Zero, Interval);
}
res.EnsureSuccessStatusCode();
var data = await res.Content.ReadFromJsonAsync<PatreonRefreshData>();
if (data is null)
throw new("Invalid patreon response.");
_credsProvider.ModifyCredsFile(oldData =>
{
oldData.Patreon.AccessToken = data.AccessToken;
oldData.Patreon.RefreshToken = data.RefreshToken;
});
private DateTime LastAccessTokenUpdate(IBotCredentials creds)
{
var db = _redis.GetDatabase();
var val = db.StringGet($"{creds.RedisKey()}_patreon_update");
if (val == default)
return DateTime.MinValue;
var lastTime = DateTime.FromBinary((long)val);
return lastTime;
await db.StringSetAsync($"{creds.RedisKey()}_patreon_update", DateTime.UtcNow.ToBinary());
return true;
}
private sealed class PatreonRefreshData
catch (Exception ex)
{
[JsonPropertyName("access_token")]
public string AccessToken { get; set; }
[JsonPropertyName("refresh_token")]
public string RefreshToken { get; set; }
[JsonPropertyName("expires_in")]
public long ExpiresIn { get; set; }
[JsonPropertyName("scope")]
public string Scope { get; set; }
[JsonPropertyName("token_type")]
public string TokenType { get; set; }
Log.Error("Failed updating patreon access token: {ErrorMessage}", ex.ToString());
return false;
}
private async Task<bool> UpdateAccessToken(IBotCredentials creds)
}
private bool HasPatreonCreds(IBotCredentials creds)
{
var _1 = creds.Patreon.ClientId;
var _2 = creds.Patreon.ClientSecret;
var _4 = creds.Patreon.RefreshToken;
return !(string.IsNullOrWhiteSpace(_1)
|| string.IsNullOrWhiteSpace(_2)
|| string.IsNullOrWhiteSpace(_4));
}
public async Task RefreshPledges(IBotCredentials creds)
{
if (DateTime.UtcNow.Day < 5)
return;
// if the user has the necessary patreon creds
// and the access token expired or doesn't exist
// -> update access token
if (!HasPatreonCreds(creds))
return;
if (LastAccessTokenUpdate(creds).Month < DateTime.UtcNow.Month
|| string.IsNullOrWhiteSpace(creds.Patreon.AccessToken))
{
Log.Information("Updating patreon access token...");
try
{
using var http = _httpFactory.CreateClient();
var res = await http.PostAsync($"https://www.patreon.com/api/oauth2/token" +
$"?grant_type=refresh_token" +
$"&refresh_token={creds.Patreon.RefreshToken}" +
$"&client_id={creds.Patreon.ClientId}" +
$"&client_secret={creds.Patreon.ClientSecret}",
new StringContent(string.Empty));
res.EnsureSuccessStatusCode();
var data = await res.Content.ReadFromJsonAsync<PatreonRefreshData>();
if (data is null)
throw new("Invalid patreon response.");
_credsProvider.ModifyCredsFile(oldData =>
{
oldData.Patreon.AccessToken = data.AccessToken;
oldData.Patreon.RefreshToken = data.RefreshToken;
});
var db = _redis.GetDatabase();
await db.StringSetAsync($"{creds.RedisKey()}_patreon_update", DateTime.UtcNow.ToBinary());
return true;
}
catch (Exception ex)
{
Log.Error("Failed updating patreon access token: {ErrorMessage}", ex.ToString());
return false;
}
}
private bool HasPatreonCreds(IBotCredentials creds)
{
var _1 = creds.Patreon.ClientId;
var _2 = creds.Patreon.ClientSecret;
var _4 = creds.Patreon.RefreshToken;
return !(string.IsNullOrWhiteSpace(_1)
|| string.IsNullOrWhiteSpace(_2)
|| string.IsNullOrWhiteSpace(_4));
}
public async Task RefreshPledges(IBotCredentials creds)
{
if (DateTime.UtcNow.Day < 5)
var success = await UpdateAccessToken(creds);
if (!success)
return;
}
// if the user has the necessary patreon creds
// and the access token expired or doesn't exist
// -> update access token
if (!HasPatreonCreds(creds))
return;
if (LastAccessTokenUpdate(creds).Month < DateTime.UtcNow.Month
|| string.IsNullOrWhiteSpace(creds.Patreon.AccessToken))
{
var success = await UpdateAccessToken(creds);
if (!success)
return;
}
LastUpdate = DateTime.UtcNow;
await getPledgesLocker.WaitAsync().ConfigureAwait(false);
try
{
LastUpdate = DateTime.UtcNow;
await getPledgesLocker.WaitAsync().ConfigureAwait(false);
try
{
var members = new List<PatreonMember>();
var users = new List<PatreonUser>();
using (var http = _httpFactory.CreateClient())
var members = new List<PatreonMember>();
var users = new List<PatreonUser>();
using (var http = _httpFactory.CreateClient())
{
http.DefaultRequestHeaders.Clear();
http.DefaultRequestHeaders.TryAddWithoutValidation("Authorization",
$"Bearer {creds.Patreon.AccessToken}");
var page = $"https://www.patreon.com/api/oauth2/v2/campaigns/{creds.Patreon.CampaignId}/members" +
"?fields%5Bmember%5D=full_name,currently_entitled_amount_cents" +
"&fields%5Buser%5D=social_connections" +
"&include=user";
PatreonResponse data = null;
do
{
http.DefaultRequestHeaders.Clear();
http.DefaultRequestHeaders.TryAddWithoutValidation("Authorization",
$"Bearer {creds.Patreon.AccessToken}");
var res = await http.GetStringAsync(page).ConfigureAwait(false);
data = JsonSerializer.Deserialize<PatreonResponse>(res);
var page = $"https://www.patreon.com/api/oauth2/v2/campaigns/{creds.Patreon.CampaignId}/members" +
"?fields%5Bmember%5D=full_name,currently_entitled_amount_cents" +
"&fields%5Buser%5D=social_connections" +
"&include=user";
PatreonResponse data = null;
do
{
var res = await http.GetStringAsync(page).ConfigureAwait(false);
data = JsonSerializer.Deserialize<PatreonResponse>(res);
if (data is null)
break;
if (data is null)
break;
members.AddRange(data.Data);
users.AddRange(data.Included);
} while (!string.IsNullOrWhiteSpace(page = data?.Links?.Next));
}
members.AddRange(data.Data);
users.AddRange(data.Included);
} while (!string.IsNullOrWhiteSpace(page = data?.Links?.Next));
}
var userData = members.Join(users,
var userData = members.Join(users,
(m) => m.Relationships.User.Data.Id,
(u) => u.Id,
(m, u) => new
@@ -203,118 +199,117 @@ namespace NadekoBot.Modules.Utility.Services
: 0,
EntitledTo = m.Attributes.CurrentlyEntitledAmountCents,
})
.Where(x => x is
{
UserId: not 0,
EntitledTo: > 0
})
.ToList();
foreach (var pledge in userData)
.Where(x => x is
{
await ClaimReward(pledge.UserId, pledge.PatreonUserId, pledge.EntitledTo);
}
}
catch (Exception ex)
UserId: not 0,
EntitledTo: > 0
})
.ToList();
foreach (var pledge in userData)
{
Log.Warning(ex, "Error refreshing patreon pledges");
}
finally
{
getPledgesLocker.Release();
}
}
public async Task<int> ClaimReward(ulong userId, string patreonUserId, int cents)
{
await claimLockJustInCase.WaitAsync().ConfigureAwait(false);
var settings = _gamblingConfigService.Data;
var now = DateTime.UtcNow;
try
{
var eligibleFor = (int)(cents * settings.PatreonCurrencyPerCent);
using (var uow = _db.GetDbContext())
{
var users = uow.Set<RewardedUser>();
var usr = await users.FirstOrDefaultAsync(x => x.PatreonUserId == patreonUserId);
if (usr is null)
{
users.Add(new RewardedUser()
{
PatreonUserId = patreonUserId,
LastReward = now,
AmountRewardedThisMonth = eligibleFor,
});
await uow.SaveChangesAsync();
await _currency.AddAsync(userId, "Patreon reward - new", eligibleFor, gamble: true);
Log.Information($"Sending new currency reward to {userId}");
await SendMessageToUser(userId, $"Thank you for your pledge! " +
$"You've been awarded **{eligibleFor}**{settings.Currency.Sign} !");
return eligibleFor;
}
if (usr.LastReward.Month != now.Month)
{
usr.LastReward = now;
usr.AmountRewardedThisMonth = eligibleFor;
await uow.SaveChangesAsync();
await _currency.AddAsync(userId, "Patreon reward - recurring", eligibleFor, gamble: true);
Log.Information($"Sending recurring currency reward to {userId}");
await SendMessageToUser(userId, $"Thank you for your continued support! " +
$"You've been awarded **{eligibleFor}**{settings.Currency.Sign} for this month's support!");
return eligibleFor;
}
if (usr.AmountRewardedThisMonth < eligibleFor)
{
var toAward = eligibleFor - usr.AmountRewardedThisMonth;
usr.LastReward = now;
usr.AmountRewardedThisMonth = toAward;
await uow.SaveChangesAsync();
await _currency.AddAsync(userId, "Patreon reward - update", toAward, gamble: true);
Log.Information($"Sending updated currency reward to {userId}");
await SendMessageToUser(userId, $"Thank you for increasing your pledge! " +
$"You've been awarded an additional **{toAward}**{settings.Currency.Sign} !");
return toAward;
}
return 0;
}
}
finally
{
claimLockJustInCase.Release();
await ClaimReward(pledge.UserId, pledge.PatreonUserId, pledge.EntitledTo);
}
}
private async Task SendMessageToUser(ulong userId, string message)
catch (Exception ex)
{
try
Log.Warning(ex, "Error refreshing patreon pledges");
}
finally
{
getPledgesLocker.Release();
}
}
public async Task<int> ClaimReward(ulong userId, string patreonUserId, int cents)
{
await claimLockJustInCase.WaitAsync().ConfigureAwait(false);
var settings = _gamblingConfigService.Data;
var now = DateTime.UtcNow;
try
{
var eligibleFor = (int)(cents * settings.PatreonCurrencyPerCent);
using (var uow = _db.GetDbContext())
{
var user = (IUser)_client.GetUser(userId) ?? await _client.Rest.GetUserAsync(userId);
if (user is null)
return;
var channel = await user.GetOrCreateDMChannelAsync();
await channel.SendConfirmAsync(_eb, message);
}
catch
{
// ignored
var users = uow.Set<RewardedUser>();
var usr = await users.FirstOrDefaultAsyncEF(x => x.PatreonUserId == patreonUserId);
if (usr is null)
{
users.Add(new RewardedUser()
{
PatreonUserId = patreonUserId,
LastReward = now,
AmountRewardedThisMonth = eligibleFor,
});
await uow.SaveChangesAsync();
await _currency.AddAsync(userId, "Patreon reward - new", eligibleFor, gamble: true);
Log.Information($"Sending new currency reward to {userId}");
await SendMessageToUser(userId, $"Thank you for your pledge! " +
$"You've been awarded **{eligibleFor}**{settings.Currency.Sign} !");
return eligibleFor;
}
if (usr.LastReward.Month != now.Month)
{
usr.LastReward = now;
usr.AmountRewardedThisMonth = eligibleFor;
await uow.SaveChangesAsync();
await _currency.AddAsync(userId, "Patreon reward - recurring", eligibleFor, gamble: true);
Log.Information($"Sending recurring currency reward to {userId}");
await SendMessageToUser(userId, $"Thank you for your continued support! " +
$"You've been awarded **{eligibleFor}**{settings.Currency.Sign} for this month's support!");
return eligibleFor;
}
if (usr.AmountRewardedThisMonth < eligibleFor)
{
var toAward = eligibleFor - usr.AmountRewardedThisMonth;
usr.LastReward = now;
usr.AmountRewardedThisMonth = toAward;
await uow.SaveChangesAsync();
await _currency.AddAsync(userId, "Patreon reward - update", toAward, gamble: true);
Log.Information($"Sending updated currency reward to {userId}");
await SendMessageToUser(userId, $"Thank you for increasing your pledge! " +
$"You've been awarded an additional **{toAward}**{settings.Currency.Sign} !");
return toAward;
}
return 0;
}
}
finally
{
claimLockJustInCase.Release();
}
}
}
private async Task SendMessageToUser(ulong userId, string message)
{
try
{
var user = (IUser)_client.GetUser(userId) ?? await _client.Rest.GetUserAsync(userId);
if (user is null)
return;
var channel = await user.GetOrCreateDMChannelAsync();
await channel.SendConfirmAsync(_eb, message);
}
catch
{
// ignored
}
}
}

View File

@@ -1,182 +1,177 @@
using System;
using System.Linq;
using System.Text.RegularExpressions;
using System.Text.RegularExpressions;
using Discord;
using Discord.WebSocket;
using NadekoBot.Extensions;
using NadekoBot.Services;
using NadekoBot.Services.Database.Models;
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Serilog;
namespace NadekoBot.Modules.Utility.Services
namespace NadekoBot.Modules.Utility.Services;
public class RemindService : INService
{
public class RemindService : INService
private readonly Regex _regex = new Regex(@"^(?:in\s?)?\s*(?:(?<mo>\d+)(?:\s?(?:months?|mos?),?))?(?:(?:\sand\s|\s*)?(?<w>\d+)(?:\s?(?:weeks?|w),?))?(?:(?:\sand\s|\s*)?(?<d>\d+)(?:\s?(?:days?|d),?))?(?:(?:\sand\s|\s*)?(?<h>\d+)(?:\s?(?:hours?|h),?))?(?:(?:\sand\s|\s*)?(?<m>\d+)(?:\s?(?:minutes?|mins?|m),?))?\s+(?:to:?\s+)?(?<what>(?:\r\n|[\r\n]|.)+)",
RegexOptions.Compiled | RegexOptions.Multiline);
private readonly DiscordSocketClient _client;
private readonly DbService _db;
private readonly IBotCredentials _creds;
private readonly IEmbedBuilderService _eb;
public RemindService(DiscordSocketClient client, DbService db, IBotCredentials creds, IEmbedBuilderService eb)
{
private readonly Regex _regex = new Regex(@"^(?:in\s?)?\s*(?:(?<mo>\d+)(?:\s?(?:months?|mos?),?))?(?:(?:\sand\s|\s*)?(?<w>\d+)(?:\s?(?:weeks?|w),?))?(?:(?:\sand\s|\s*)?(?<d>\d+)(?:\s?(?:days?|d),?))?(?:(?:\sand\s|\s*)?(?<h>\d+)(?:\s?(?:hours?|h),?))?(?:(?:\sand\s|\s*)?(?<m>\d+)(?:\s?(?:minutes?|mins?|m),?))?\s+(?:to:?\s+)?(?<what>(?:\r\n|[\r\n]|.)+)",
RegexOptions.Compiled | RegexOptions.Multiline);
_client = client;
_db = db;
_creds = creds;
_eb = eb;
_ = StartReminderLoop();
}
private readonly DiscordSocketClient _client;
private readonly DbService _db;
private readonly IBotCredentials _creds;
private readonly IEmbedBuilderService _eb;
public RemindService(DiscordSocketClient client, DbService db, IBotCredentials creds, IEmbedBuilderService eb)
{
_client = client;
_db = db;
_creds = creds;
_eb = eb;
_ = StartReminderLoop();
}
private async Task StartReminderLoop()
{
while (true)
{
await Task.Delay(15000);
try
{
var now = DateTime.UtcNow;
var reminders = await GetRemindersBeforeAsync(now);
if (reminders.Count == 0)
continue;
Log.Information($"Executing {reminders.Count} reminders.");
// make groups of 5, with 1.5 second inbetween each one to ensure against ratelimits
foreach (var group in reminders.Chunk(5))
{
var executedReminders = group.ToList();
await Task.WhenAll(executedReminders.Select(ReminderTimerAction));
await RemoveReminders(executedReminders);
await Task.Delay(1500);
}
}
catch (Exception ex)
{
Log.Warning($"Error in reminder loop: {ex.Message}");
Log.Warning(ex.ToString());
}
}
}
private async Task RemoveReminders(List<Reminder> reminders)
{
using (var uow = _db.GetDbContext())
{
uow.Set<Reminder>()
.RemoveRange(reminders);
await uow.SaveChangesAsync();
}
}
private Task<List<Reminder>> GetRemindersBeforeAsync(DateTime now)
{
using (var uow = _db.GetDbContext())
{
return uow.Reminders
.FromSqlInterpolated($"select * from reminders where ((serverid >> 22) % {_creds.TotalShards}) == {_client.ShardId} and \"when\" < {now};")
.ToListAsync();
}
}
public struct RemindObject
{
public string What { get; set; }
public TimeSpan Time { get; set; }
}
public bool TryParseRemindMessage(string input, out RemindObject obj)
{
var m = _regex.Match(input);
obj = default;
if (m.Length == 0)
{
return false;
}
var values = new Dictionary<string, int>();
var what = m.Groups["what"].Value;
if (string.IsNullOrWhiteSpace(what))
{
Log.Warning("No message provided for the reminder.");
return false;
}
foreach (var groupName in _regex.GetGroupNames())
{
if (groupName == "0" || groupName== "what") continue;
if (string.IsNullOrWhiteSpace(m.Groups[groupName].Value))
{
values[groupName] = 0;
continue;
}
if (!int.TryParse(m.Groups[groupName].Value, out var value))
{
Log.Warning($"Reminder regex group {groupName} has invalid value.");
return false;
}
if (value < 1)
{
Log.Warning("Reminder time value has to be an integer greater than 0.");
return false;
}
values[groupName] = value;
}
var ts = new TimeSpan
(
30 * values["mo"] + 7 * values["w"] + values["d"],
values["h"],
values["m"],
0
);
obj = new RemindObject()
{
Time = ts,
What = what
};
return true;
}
private async Task ReminderTimerAction(Reminder r)
private async Task StartReminderLoop()
{
while (true)
{
await Task.Delay(15000);
try
{
IMessageChannel ch;
if (r.IsPrivate)
var now = DateTime.UtcNow;
var reminders = await GetRemindersBeforeAsync(now);
if (reminders.Count == 0)
continue;
Log.Information($"Executing {reminders.Count} reminders.");
// make groups of 5, with 1.5 second inbetween each one to ensure against ratelimits
foreach (var group in reminders.Chunk(5))
{
var user = _client.GetUser(r.ChannelId);
if (user is null)
return;
ch = await user.GetOrCreateDMChannelAsync().ConfigureAwait(false);
var executedReminders = group.ToList();
await Task.WhenAll(executedReminders.Select(ReminderTimerAction));
await RemoveReminders(executedReminders);
await Task.Delay(1500);
}
else
{
ch = _client.GetGuild(r.ServerId)?.GetTextChannel(r.ChannelId);
}
if (ch is null)
return;
}
catch (Exception ex)
{
Log.Warning($"Error in reminder loop: {ex.Message}");
Log.Warning(ex.ToString());
}
}
}
await ch.EmbedAsync(_eb.Create()
private async Task RemoveReminders(List<Reminder> reminders)
{
using (var uow = _db.GetDbContext())
{
uow.Set<Reminder>()
.RemoveRange(reminders);
await uow.SaveChangesAsync();
}
}
private Task<List<Reminder>> GetRemindersBeforeAsync(DateTime now)
{
using (var uow = _db.GetDbContext())
{
return uow.Reminders
.FromSqlInterpolated($"select * from reminders where ((serverid >> 22) % {_creds.TotalShards}) == {_client.ShardId} and \"when\" < {now};")
.ToListAsync();
}
}
public struct RemindObject
{
public string What { get; set; }
public TimeSpan Time { get; set; }
}
public bool TryParseRemindMessage(string input, out RemindObject obj)
{
var m = _regex.Match(input);
obj = default;
if (m.Length == 0)
{
return false;
}
var values = new Dictionary<string, int>();
var what = m.Groups["what"].Value;
if (string.IsNullOrWhiteSpace(what))
{
Log.Warning("No message provided for the reminder.");
return false;
}
foreach (var groupName in _regex.GetGroupNames())
{
if (groupName == "0" || groupName== "what") continue;
if (string.IsNullOrWhiteSpace(m.Groups[groupName].Value))
{
values[groupName] = 0;
continue;
}
if (!int.TryParse(m.Groups[groupName].Value, out var value))
{
Log.Warning($"Reminder regex group {groupName} has invalid value.");
return false;
}
if (value < 1)
{
Log.Warning("Reminder time value has to be an integer greater than 0.");
return false;
}
values[groupName] = value;
}
var ts = new TimeSpan
(
30 * values["mo"] + 7 * values["w"] + values["d"],
values["h"],
values["m"],
0
);
obj = new RemindObject()
{
Time = ts,
What = what
};
return true;
}
private async Task ReminderTimerAction(Reminder r)
{
try
{
IMessageChannel ch;
if (r.IsPrivate)
{
var user = _client.GetUser(r.ChannelId);
if (user is null)
return;
ch = await user.GetOrCreateDMChannelAsync().ConfigureAwait(false);
}
else
{
ch = _client.GetGuild(r.ServerId)?.GetTextChannel(r.ChannelId);
}
if (ch is null)
return;
await ch.EmbedAsync(_eb.Create()
.WithOkColor()
.WithTitle("Reminder")
.AddField("Created At", r.DateAdded.HasValue ? r.DateAdded.Value.ToLongDateString() : "?")
.AddField("By", (await ch.GetUserAsync(r.UserId).ConfigureAwait(false))?.ToString() ?? r.UserId.ToString()),
msg: r.Message).ConfigureAwait(false);
}
catch (Exception ex) { Log.Information(ex.Message + $"({r.Id})"); }
msg: r.Message).ConfigureAwait(false);
}
catch (Exception ex) { Log.Information(ex.Message + $"({r.Id})"); }
}
}

View File

@@ -1,437 +1,430 @@
#nullable enable
using Discord.WebSocket;
using Microsoft.EntityFrameworkCore;
using NadekoBot.Db.Models;
using NadekoBot.Extensions;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Discord;
using LinqToDB;
using LinqToDB.EntityFrameworkCore;
using NadekoBot.Common;
using NadekoBot.Common.Collections;
using NadekoBot.Common.ModuleBehaviors;
using NadekoBot.Common.Replacements;
using NadekoBot.Services;
using NadekoBot.Services.Database.Models;
using Serilog;
namespace NadekoBot.Modules.Utility.Services
namespace NadekoBot.Modules.Utility.Services;
public sealed class RepeaterService : IReadyExecutor, INService
{
public sealed class RepeaterService : IReadyExecutor, INService
public const int MAX_REPEATERS = 5;
private readonly DbService _db;
private readonly IBotCredentials _creds;
private readonly IEmbedBuilderService _eb;
private readonly DiscordSocketClient _client;
private LinkedList<RunningRepeater> _repeaterQueue;
private ConcurrentHashSet<int> _noRedundant;
private readonly object _queueLocker = new object();
public RepeaterService(DiscordSocketClient client, DbService db, IBotCredentials creds, IEmbedBuilderService eb)
{
public const int MAX_REPEATERS = 5;
private readonly DbService _db;
private readonly IBotCredentials _creds;
private readonly IEmbedBuilderService _eb;
private readonly DiscordSocketClient _client;
private LinkedList<RunningRepeater> _repeaterQueue;
private ConcurrentHashSet<int> _noRedundant;
private readonly object _queueLocker = new object();
public RepeaterService(DiscordSocketClient client, DbService db, IBotCredentials creds, IEmbedBuilderService eb)
{
_db = db;
_creds = creds;
_eb = eb;
_client = client;
_db = db;
_creds = creds;
_eb = eb;
_client = client;
var uow = _db.GetDbContext();
var shardRepeaters = uow
.Set<Repeater>()
.FromSqlInterpolated($@"select * from repeaters
var uow = _db.GetDbContext();
var shardRepeaters = uow
.Set<Repeater>()
.FromSqlInterpolated($@"select * from repeaters
where ((guildid >> 22) % {_creds.TotalShards}) == {_client.ShardId};")
.AsNoTracking()
.ToList();
.AsNoTracking()
.ToList();
_noRedundant = new ConcurrentHashSet<int>(shardRepeaters
.Where(x => x.NoRedundant)
.Select(x => x.Id));
_noRedundant = new ConcurrentHashSet<int>(shardRepeaters
.Where(x => x.NoRedundant)
.Select(x => x.Id));
_repeaterQueue = new LinkedList<RunningRepeater>(shardRepeaters
.Select(rep => new RunningRepeater(rep))
.OrderBy(x => x.NextTime));
}
_repeaterQueue = new LinkedList<RunningRepeater>(shardRepeaters
.Select(rep => new RunningRepeater(rep))
.OrderBy(x => x.NextTime));
}
public Task OnReadyAsync()
public Task OnReadyAsync()
{
_ = Task.Run(RunRepeatersLoop);
return Task.CompletedTask;
}
private async Task RunRepeatersLoop()
{
while (true)
{
_ = Task.Run(RunRepeatersLoop);
return Task.CompletedTask;
}
private async Task RunRepeatersLoop()
{
while (true)
{
try
{
// calculate timeout for the first item
var timeout = GetNextTimeout();
// wait it out, and recalculate afterwards
// because repeaters might've been modified meanwhile
if (timeout > TimeSpan.Zero)
{
await Task.Delay(timeout > TimeSpan.FromMinutes(1)
? TimeSpan.FromMinutes(1)
: timeout);
continue;
}
// collect (remove) all repeaters which need to run (3 seconds tolerance)
var now = DateTime.UtcNow + TimeSpan.FromSeconds(3);
var toExecute = new List<RunningRepeater>();
lock (_repeaterQueue)
{
var current = _repeaterQueue.First;
while (true)
{
if (current is null || current.Value.NextTime > now)
break;
toExecute.Add(current.Value);
current = current.Next;
}
}
// execute
foreach (var chunk in toExecute.Chunk(5))
{
await Task.WhenAll(chunk.Select(Trigger));
}
// reinsert
foreach (var rep in toExecute)
{
await HandlePostExecute(rep);
}
}
catch (Exception ex)
{
Log.Error(ex, "Critical error in repeater queue: {ErrorMessage}", ex.Message);
await Task.Delay(5000);
}
}
}
private async Task HandlePostExecute(RunningRepeater rep)
{
if (rep.ErrorCount >= 10)
{
RemoveFromQueue(rep.Repeater.Id);
await RemoveRepeaterInternal(rep.Repeater);
return;
}
UpdatePosition(rep);
}
private void UpdatePosition(RunningRepeater rep)
{
lock (_queueLocker)
{
rep.UpdateNextTime();
_repeaterQueue.Remove(rep);
AddToQueue(rep);
}
}
public async Task<bool> TriggerExternal(ulong guildId, int index)
{
using var uow = _db.GetDbContext();
var toTrigger = await uow.Repeaters
.AsNoTracking()
.Where(x => x.GuildId == guildId)
.Skip(index)
.FirstOrDefaultAsyncEF();
if (toTrigger is null)
return false;
LinkedListNode<RunningRepeater>? node;
lock (_queueLocker)
{
node = _repeaterQueue.FindNode(x => x.Repeater.Id == toTrigger.Id);
if (node is null)
return false;
_repeaterQueue.Remove(node);
}
await Trigger(node.Value);
await HandlePostExecute(node.Value);
return true;
}
private void AddToQueue(RunningRepeater rep)
{
lock (_queueLocker)
{
var current = _repeaterQueue.First;
if (current is null)
{
_repeaterQueue.AddFirst(rep);
return;
}
while (current is not null && current.Value.NextTime < rep.NextTime)
current = current.Next;
if (current is null)
_repeaterQueue.AddLast(rep);
else
_repeaterQueue.AddBefore(current, rep);
}
}
private TimeSpan GetNextTimeout()
{
lock (_queueLocker)
{
var first = _repeaterQueue.First;
// if there are no items in the queue, just wait out the minimum duration (1 minute) and try again
if (first is null)
return TimeSpan.FromMinutes(1);
return first.Value.NextTime - DateTime.UtcNow;
}
}
private async Task Trigger(RunningRepeater rr)
{
var repeater = rr.Repeater;
void ChannelMissingError()
{
rr.ErrorCount = Int32.MaxValue;
Log.Warning("[Repeater] Channel [{Channelid}] for not found or insufficient permissions. " +
"Repeater will be removed. ", repeater.ChannelId);
}
var channel = _client.GetChannel(repeater.ChannelId) as ITextChannel;
if (channel is null)
try { channel = await _client.Rest.GetChannelAsync(repeater.ChannelId) as ITextChannel; } catch { }
if (channel is null)
{
ChannelMissingError();
return;
}
var guild = _client.GetGuild(channel.GuildId);
if (guild is null)
{
ChannelMissingError();
return;
}
if (_noRedundant.Contains(repeater.Id))
{
try
{
var lastMsgInChannel = await channel.GetMessagesAsync(2).Flatten().FirstAsync();
if (lastMsgInChannel != null && lastMsgInChannel.Id == repeater.LastMessageId)
return;
}
catch (Exception ex)
{
Log.Warning(ex,
"[Repeater] Error while getting last channel message in {GuildId}/{ChannelId} " +
"Bot probably doesn't have the permission to read message history",
guild.Id,
channel.Id);
}
}
if (repeater.LastMessageId is ulong lastMessageId)
{
try
{
var oldMsg = await channel.GetMessageAsync(lastMessageId);
if (oldMsg != null)
{
await oldMsg.DeleteAsync().ConfigureAwait(false);
}
}
catch (Exception ex)
{
Log.Warning(ex, "[Repeater] Error while deleting previous message in {GuildId}/{ChannelId}", guild.Id, channel.Id);
}
}
var rep = new ReplacementBuilder()
.WithDefault(guild.CurrentUser, channel, guild, _client)
.Build();
try
{
var text = SmartText.CreateFrom(repeater.Message);
text = rep.Replace(text);
var newMsg = await channel.SendAsync(text);
_ = newMsg.AddReactionAsync(new Emoji("🔄"));
if (_noRedundant.Contains(repeater.Id))
// calculate timeout for the first item
var timeout = GetNextTimeout();
// wait it out, and recalculate afterwards
// because repeaters might've been modified meanwhile
if (timeout > TimeSpan.Zero)
{
await SetRepeaterLastMessageInternal(repeater.Id, newMsg.Id);
repeater.LastMessageId = newMsg.Id;
await Task.Delay(timeout > TimeSpan.FromMinutes(1)
? TimeSpan.FromMinutes(1)
: timeout);
continue;
}
rr.ErrorCount = 0;
// collect (remove) all repeaters which need to run (3 seconds tolerance)
var now = DateTime.UtcNow + TimeSpan.FromSeconds(3);
var toExecute = new List<RunningRepeater>();
lock (_repeaterQueue)
{
var current = _repeaterQueue.First;
while (true)
{
if (current is null || current.Value.NextTime > now)
break;
toExecute.Add(current.Value);
current = current.Next;
}
}
// execute
foreach (var chunk in toExecute.Chunk(5))
{
await Task.WhenAll(chunk.Select(Trigger));
}
// reinsert
foreach (var rep in toExecute)
{
await HandlePostExecute(rep);
}
}
catch (Exception ex)
{
Log.Error(ex, "[Repeater] Error sending repeat message ({ErrorCount})", rr.ErrorCount++);
Log.Error(ex, "Critical error in repeater queue: {ErrorMessage}", ex.Message);
await Task.Delay(5000);
}
}
private async Task RemoveRepeaterInternal(Repeater r)
{
_noRedundant.TryRemove(r.Id);
using var uow = _db.GetDbContext();
await uow
.Repeaters
.DeleteAsync(x => x.Id == r.Id);
await uow.SaveChangesAsync();
}
private RunningRepeater? RemoveFromQueue(int id)
{
lock (_queueLocker)
{
var node = _repeaterQueue.FindNode(x => x.Repeater.Id == id);
if (node is null)
return null;
_repeaterQueue.Remove(node);
return node.Value;
}
}
private async Task SetRepeaterLastMessageInternal(int repeaterId, ulong lastMsgId)
{
using var uow = _db.GetDbContext();
await uow.Repeaters
.AsQueryable()
.Where(x => x.Id == repeaterId)
.UpdateAsync(rep => new Repeater()
{
LastMessageId = lastMsgId
});
}
public async Task<RunningRepeater?> AddRepeaterAsync(
ulong channelId,
ulong guildId,
TimeSpan interval,
string message,
bool isNoRedundant,
TimeSpan? startTimeOfDay
)
{
var rep = new Repeater()
{
ChannelId = channelId,
GuildId = guildId,
Interval = interval,
Message = message,
NoRedundant = isNoRedundant,
LastMessageId = null,
StartTimeOfDay = startTimeOfDay,
DateAdded = DateTime.UtcNow
};
using var uow = _db.GetDbContext();
if (await uow.Repeaters.CountAsyncEF(x => x.GuildId == guildId) < MAX_REPEATERS)
uow.Repeaters.Add(rep);
else
return null;
await uow.SaveChangesAsync();
if (isNoRedundant)
_noRedundant.Add(rep.Id);
var runner = new RunningRepeater(rep);
AddToQueue(runner);
return runner;
}
public async Task<RunningRepeater?> RemoveByIndexAsync(ulong guildId, int index)
{
if (index > MAX_REPEATERS * 2)
throw new ArgumentOutOfRangeException(nameof(index));
using var uow = _db.GetDbContext();
var toRemove = await uow.Repeaters
.AsNoTracking()
.Where(x => x.GuildId == guildId)
.Skip(index)
.FirstOrDefaultAsyncEF();
if (toRemove is null)
return null;
// first try removing from queue because it can fail
// while triggering. Instruct user to try again
var removed = RemoveFromQueue(toRemove.Id);
if (removed is null)
return null;
_noRedundant.TryRemove(toRemove.Id);
uow.Repeaters.Remove(toRemove);
await uow.SaveChangesAsync();
return removed;
}
public IReadOnlyCollection<RunningRepeater> GetRepeaters(ulong guildId)
{
lock (_queueLocker)
{
return _repeaterQueue.Where(x => x.Repeater.GuildId == guildId).ToList();
}
}
public async Task<bool?> ToggleRedundantAsync(ulong guildId, int index)
{
using var uow = _db.GetDbContext();
var toToggle = await uow
.Repeaters
.AsQueryable()
.Where(x => x.GuildId == guildId)
.Skip(index)
.FirstOrDefaultAsyncEF();
if (toToggle is null)
return null;
var newValue = toToggle.NoRedundant = !toToggle.NoRedundant;
if (newValue)
{
_noRedundant.Add(toToggle.Id);
}
else
{
_noRedundant.TryRemove(toToggle.Id);
}
await uow.SaveChangesAsync();
return newValue;
}
public bool IsNoRedundant(int repeaterId)
{
return _noRedundant.Contains(repeaterId);
}
}
}
private async Task HandlePostExecute(RunningRepeater rep)
{
if (rep.ErrorCount >= 10)
{
RemoveFromQueue(rep.Repeater.Id);
await RemoveRepeaterInternal(rep.Repeater);
return;
}
UpdatePosition(rep);
}
private void UpdatePosition(RunningRepeater rep)
{
lock (_queueLocker)
{
rep.UpdateNextTime();
_repeaterQueue.Remove(rep);
AddToQueue(rep);
}
}
public async Task<bool> TriggerExternal(ulong guildId, int index)
{
using var uow = _db.GetDbContext();
var toTrigger = await uow.Repeaters
.AsNoTracking()
.Where(x => x.GuildId == guildId)
.Skip(index)
.FirstOrDefaultAsyncEF();
if (toTrigger is null)
return false;
LinkedListNode<RunningRepeater>? node;
lock (_queueLocker)
{
node = _repeaterQueue.FindNode(x => x.Repeater.Id == toTrigger.Id);
if (node is null)
return false;
_repeaterQueue.Remove(node);
}
await Trigger(node.Value);
await HandlePostExecute(node.Value);
return true;
}
private void AddToQueue(RunningRepeater rep)
{
lock (_queueLocker)
{
var current = _repeaterQueue.First;
if (current is null)
{
_repeaterQueue.AddFirst(rep);
return;
}
while (current is not null && current.Value.NextTime < rep.NextTime)
current = current.Next;
if (current is null)
_repeaterQueue.AddLast(rep);
else
_repeaterQueue.AddBefore(current, rep);
}
}
private TimeSpan GetNextTimeout()
{
lock (_queueLocker)
{
var first = _repeaterQueue.First;
// if there are no items in the queue, just wait out the minimum duration (1 minute) and try again
if (first is null)
return TimeSpan.FromMinutes(1);
return first.Value.NextTime - DateTime.UtcNow;
}
}
private async Task Trigger(RunningRepeater rr)
{
var repeater = rr.Repeater;
void ChannelMissingError()
{
rr.ErrorCount = Int32.MaxValue;
Log.Warning("[Repeater] Channel [{Channelid}] for not found or insufficient permissions. " +
"Repeater will be removed. ", repeater.ChannelId);
}
var channel = _client.GetChannel(repeater.ChannelId) as ITextChannel;
if (channel is null)
try { channel = await _client.Rest.GetChannelAsync(repeater.ChannelId) as ITextChannel; } catch { }
if (channel is null)
{
ChannelMissingError();
return;
}
var guild = _client.GetGuild(channel.GuildId);
if (guild is null)
{
ChannelMissingError();
return;
}
if (_noRedundant.Contains(repeater.Id))
{
try
{
var lastMsgInChannel = await channel.GetMessagesAsync(2).Flatten().FirstAsync();
if (lastMsgInChannel != null && lastMsgInChannel.Id == repeater.LastMessageId)
return;
}
catch (Exception ex)
{
Log.Warning(ex,
"[Repeater] Error while getting last channel message in {GuildId}/{ChannelId} " +
"Bot probably doesn't have the permission to read message history",
guild.Id,
channel.Id);
}
}
if (repeater.LastMessageId is ulong lastMessageId)
{
try
{
var oldMsg = await channel.GetMessageAsync(lastMessageId);
if (oldMsg != null)
{
await oldMsg.DeleteAsync().ConfigureAwait(false);
}
}
catch (Exception ex)
{
Log.Warning(ex, "[Repeater] Error while deleting previous message in {GuildId}/{ChannelId}", guild.Id, channel.Id);
}
}
var rep = new ReplacementBuilder()
.WithDefault(guild.CurrentUser, channel, guild, _client)
.Build();
try
{
var text = SmartText.CreateFrom(repeater.Message);
text = rep.Replace(text);
var newMsg = await channel.SendAsync(text);
_ = newMsg.AddReactionAsync(new Emoji("🔄"));
if (_noRedundant.Contains(repeater.Id))
{
await SetRepeaterLastMessageInternal(repeater.Id, newMsg.Id);
repeater.LastMessageId = newMsg.Id;
}
rr.ErrorCount = 0;
}
catch (Exception ex)
{
Log.Error(ex, "[Repeater] Error sending repeat message ({ErrorCount})", rr.ErrorCount++);
}
}
private async Task RemoveRepeaterInternal(Repeater r)
{
_noRedundant.TryRemove(r.Id);
using var uow = _db.GetDbContext();
await uow
.Repeaters
.DeleteAsync(x => x.Id == r.Id);
await uow.SaveChangesAsync();
}
private RunningRepeater? RemoveFromQueue(int id)
{
lock (_queueLocker)
{
var node = _repeaterQueue.FindNode(x => x.Repeater.Id == id);
if (node is null)
return null;
_repeaterQueue.Remove(node);
return node.Value;
}
}
private async Task SetRepeaterLastMessageInternal(int repeaterId, ulong lastMsgId)
{
using var uow = _db.GetDbContext();
await uow.Repeaters
.AsQueryable()
.Where(x => x.Id == repeaterId)
.UpdateAsync(rep => new Repeater()
{
LastMessageId = lastMsgId
});
}
public async Task<RunningRepeater?> AddRepeaterAsync(
ulong channelId,
ulong guildId,
TimeSpan interval,
string message,
bool isNoRedundant,
TimeSpan? startTimeOfDay
)
{
var rep = new Repeater()
{
ChannelId = channelId,
GuildId = guildId,
Interval = interval,
Message = message,
NoRedundant = isNoRedundant,
LastMessageId = null,
StartTimeOfDay = startTimeOfDay,
DateAdded = DateTime.UtcNow
};
using var uow = _db.GetDbContext();
if (await uow.Repeaters.CountAsyncEF(x => x.GuildId == guildId) < MAX_REPEATERS)
uow.Repeaters.Add(rep);
else
return null;
await uow.SaveChangesAsync();
if (isNoRedundant)
_noRedundant.Add(rep.Id);
var runner = new RunningRepeater(rep);
AddToQueue(runner);
return runner;
}
public async Task<RunningRepeater?> RemoveByIndexAsync(ulong guildId, int index)
{
if (index > MAX_REPEATERS * 2)
throw new ArgumentOutOfRangeException(nameof(index));
using var uow = _db.GetDbContext();
var toRemove = await uow.Repeaters
.AsNoTracking()
.Where(x => x.GuildId == guildId)
.Skip(index)
.FirstOrDefaultAsyncEF();
if (toRemove is null)
return null;
// first try removing from queue because it can fail
// while triggering. Instruct user to try again
var removed = RemoveFromQueue(toRemove.Id);
if (removed is null)
return null;
_noRedundant.TryRemove(toRemove.Id);
uow.Repeaters.Remove(toRemove);
await uow.SaveChangesAsync();
return removed;
}
public IReadOnlyCollection<RunningRepeater> GetRepeaters(ulong guildId)
{
lock (_queueLocker)
{
return _repeaterQueue.Where(x => x.Repeater.GuildId == guildId).ToList();
}
}
public async Task<bool?> ToggleRedundantAsync(ulong guildId, int index)
{
using var uow = _db.GetDbContext();
var toToggle = await uow
.Repeaters
.AsQueryable()
.Where(x => x.GuildId == guildId)
.Skip(index)
.FirstOrDefaultAsyncEF();
if (toToggle is null)
return null;
var newValue = toToggle.NoRedundant = !toToggle.NoRedundant;
if (newValue)
{
_noRedundant.Add(toToggle.Id);
}
else
{
_noRedundant.TryRemove(toToggle.Id);
}
await uow.SaveChangesAsync();
return newValue;
}
public bool IsNoRedundant(int repeaterId)
{
return _noRedundant.Contains(repeaterId);
}
}

View File

@@ -1,106 +1,104 @@
using System;
using NadekoBot.Services.Database.Models;
using NadekoBot.Services.Database.Models;
namespace NadekoBot.Modules.Utility.Services
namespace NadekoBot.Modules.Utility.Services;
public sealed class RunningRepeater
{
public sealed class RunningRepeater
public DateTime NextTime { get; private set; }
public Repeater Repeater { get; }
public int ErrorCount { get; set; }
public RunningRepeater(Repeater repeater)
{
public DateTime NextTime { get; private set; }
public Repeater Repeater { get; }
public int ErrorCount { get; set; }
public RunningRepeater(Repeater repeater)
{
this.Repeater = repeater;
NextTime = CalculateInitialExecution();
}
this.Repeater = repeater;
NextTime = CalculateInitialExecution();
}
public void UpdateNextTime()
{
NextTime = DateTime.UtcNow + Repeater.Interval;
}
public void UpdateNextTime()
{
NextTime = DateTime.UtcNow + Repeater.Interval;
}
private DateTime CalculateInitialExecution()
private DateTime CalculateInitialExecution()
{
if (Repeater.StartTimeOfDay != null)
{
if (Repeater.StartTimeOfDay != null)
// if there was a start time of day
// calculate whats the next time of day repeat should trigger at
// based on teh dateadded
// i know this is not null because of the check in the query
var added = Repeater.DateAdded;
// initial trigger was the time of day specified by the command.
var initialTriggerTimeOfDay = Repeater.StartTimeOfDay.Value;
DateTime initialDateTime;
// if added timeofday is less than specified timeofday for initial trigger
// that means the repeater first ran that same day at that exact specified time
if (added.TimeOfDay <= initialTriggerTimeOfDay)
{
// if there was a start time of day
// calculate whats the next time of day repeat should trigger at
// based on teh dateadded
// i know this is not null because of the check in the query
var added = Repeater.DateAdded;
// initial trigger was the time of day specified by the command.
var initialTriggerTimeOfDay = Repeater.StartTimeOfDay.Value;
DateTime initialDateTime;
// if added timeofday is less than specified timeofday for initial trigger
// that means the repeater first ran that same day at that exact specified time
if (added.TimeOfDay <= initialTriggerTimeOfDay)
{
// in that case, just add the difference to make sure the timeofday is the same
initialDateTime = added + (initialTriggerTimeOfDay - added.TimeOfDay);
}
else
{
// if not, then it ran at that time the following day
// in other words; Add one day, and subtract how much time passed since that time of day
initialDateTime = added + TimeSpan.FromDays(1) - (added.TimeOfDay - initialTriggerTimeOfDay);
}
return CalculateInitialInterval(initialDateTime);
// in that case, just add the difference to make sure the timeofday is the same
initialDateTime = added + (initialTriggerTimeOfDay - added.TimeOfDay);
}
else
{
// if not, then it ran at that time the following day
// in other words; Add one day, and subtract how much time passed since that time of day
initialDateTime = added + TimeSpan.FromDays(1) - (added.TimeOfDay - initialTriggerTimeOfDay);
}
// if repeater is not running daily, its initial time is the time it was Added at, plus the interval
return CalculateInitialInterval(Repeater.DateAdded + Repeater.Interval);
return CalculateInitialInterval(initialDateTime);
}
/// <summary>
/// Calculate when is the proper time to run the repeater again based on initial time repeater ran.
/// </summary>
/// <param name="repeaterter"></param>
/// <param name="initialDateTime">Initial time repeater ran at (or should run at).</param>
private DateTime CalculateInitialInterval(DateTime initialDateTime)
// if repeater is not running daily, its initial time is the time it was Added at, plus the interval
return CalculateInitialInterval(Repeater.DateAdded + Repeater.Interval);
}
/// <summary>
/// Calculate when is the proper time to run the repeater again based on initial time repeater ran.
/// </summary>
/// <param name="repeaterter"></param>
/// <param name="initialDateTime">Initial time repeater ran at (or should run at).</param>
private DateTime CalculateInitialInterval(DateTime initialDateTime)
{
// if the initial time is greater than now, that means the repeater didn't still execute a single time.
// just schedule it
if (initialDateTime > DateTime.UtcNow)
{
// if the initial time is greater than now, that means the repeater didn't still execute a single time.
// just schedule it
if (initialDateTime > DateTime.UtcNow)
{
return initialDateTime;
}
return initialDateTime;
}
// else calculate based on minutes difference
// else calculate based on minutes difference
// get the difference
var diff = DateTime.UtcNow - initialDateTime;
// get the difference
var diff = DateTime.UtcNow - initialDateTime;
// see how many times the repeater theoretically ran already
var triggerCount = diff / Repeater.Interval;
// see how many times the repeater theoretically ran already
var triggerCount = diff / Repeater.Interval;
// ok lets say repeater was scheduled to run 10h ago.
// we have an interval of 2.4h
// repeater should've ran 4 times- that's 9.6h
// next time should be in 2h from now exactly
// 10/2.4 is 4.166
// 4.166 - Math.Truncate(4.166) is 0.166
// initial interval multiplier is 1 - 0.166 = 0.834
// interval (2.4h) * 0.834 is 2.0016 and that is the initial interval
// ok lets say repeater was scheduled to run 10h ago.
// we have an interval of 2.4h
// repeater should've ran 4 times- that's 9.6h
// next time should be in 2h from now exactly
// 10/2.4 is 4.166
// 4.166 - Math.Truncate(4.166) is 0.166
// initial interval multiplier is 1 - 0.166 = 0.834
// interval (2.4h) * 0.834 is 2.0016 and that is the initial interval
var initialIntervalMultiplier = 1 - (triggerCount - Math.Truncate(triggerCount));
return DateTime.UtcNow + (Repeater.Interval * initialIntervalMultiplier);
}
var initialIntervalMultiplier = 1 - (triggerCount - Math.Truncate(triggerCount));
return DateTime.UtcNow + (Repeater.Interval * initialIntervalMultiplier);
}
public override bool Equals(object obj)
{
return obj is RunningRepeater rr && rr.Repeater.Id == this.Repeater.Id;
}
public override bool Equals(object obj)
{
return obj is RunningRepeater rr && rr.Repeater.Id == this.Repeater.Id;
}
public override int GetHashCode()
{
return this.Repeater.Id;
}
public override int GetHashCode()
{
return this.Repeater.Id;
}
}

View File

@@ -1,6 +1,4 @@
using System;
using System.Collections.Concurrent;
using System.Linq;
using System.Collections.Concurrent;
using System.Threading.Tasks;
using Discord;
using Discord.WebSocket;
@@ -12,311 +10,308 @@ using NadekoBot.Modules.Utility.Common;
using NadekoBot.Modules.Utility.Common.Exceptions;
using Discord.Net;
using NadekoBot.Db;
using NadekoBot.Modules.Administration;
using Serilog;
namespace NadekoBot.Modules.Utility.Services
namespace NadekoBot.Modules.Utility.Services;
public class StreamRoleService : INService
{
public class StreamRoleService : INService
{
private readonly DbService _db;
private readonly DiscordSocketClient _client;
private readonly ConcurrentDictionary<ulong, StreamRoleSettings> guildSettings;
private readonly DbService _db;
private readonly DiscordSocketClient _client;
private readonly ConcurrentDictionary<ulong, StreamRoleSettings> guildSettings;
public StreamRoleService(DiscordSocketClient client, DbService db, Bot bot)
public StreamRoleService(DiscordSocketClient client, DbService db, Bot bot)
{
_db = db;
_client = client;
guildSettings = bot.AllGuildConfigs
.ToDictionary(x => x.GuildId, x => x.StreamRole)
.Where(x => x.Value != null && x.Value.Enabled)
.ToConcurrent();
_client.GuildMemberUpdated += Client_GuildMemberUpdated;
var _ = Task.Run(async () =>
{
_db = db;
_client = client;
try
{
await Task.WhenAll(client.Guilds.Select(g => RescanUsers(g))).ConfigureAwait(false);
}
catch
{
// ignored
}
});
}
guildSettings = bot.AllGuildConfigs
.ToDictionary(x => x.GuildId, x => x.StreamRole)
.Where(x => x.Value != null && x.Value.Enabled)
.ToConcurrent();
private Task Client_GuildMemberUpdated(SocketGuildUser before, SocketGuildUser after)
{
var _ = Task.Run(async () =>
{
//if user wasn't streaming or didn't have a game status at all
if (guildSettings.TryGetValue(after.Guild.Id, out var setting))
{
await RescanUser(after, setting).ConfigureAwait(false);
}
});
_client.GuildMemberUpdated += Client_GuildMemberUpdated;
return Task.CompletedTask;
}
/// <summary>
/// Adds or removes a user from a blacklist or a whitelist in the specified guild.
/// </summary>
/// <param name="guild">Guild</param>
/// <param name="action">Add or rem action</param>
/// <param name="userId">User's Id</param>
/// <param name="userName">User's name#discrim</param>
/// <returns>Whether the operation was successful</returns>
public async Task<bool> ApplyListAction(StreamRoleListType listType, IGuild guild, AddRemove action, ulong userId, string userName)
{
userName.ThrowIfNull(nameof(userName));
var _ = Task.Run(async () =>
bool success = false;
using (var uow = _db.GetDbContext())
{
var streamRoleSettings = uow.GetStreamRoleSettings(guild.Id);
if (listType == StreamRoleListType.Whitelist)
{
var userObj = new StreamRoleWhitelistedUser()
{
UserId = userId,
Username = userName,
};
if (action == AddRemove.Rem)
{
var toDelete = streamRoleSettings.Whitelist.FirstOrDefault(x => x.Equals(userObj));
if (toDelete != null)
{
uow.Remove(toDelete);
success = true;
}
}
else
success = streamRoleSettings.Whitelist.Add(userObj);
}
else
{
var userObj = new StreamRoleBlacklistedUser()
{
UserId = userId,
Username = userName,
};
if (action == AddRemove.Rem)
{
var toRemove = streamRoleSettings.Blacklist.FirstOrDefault(x => x.Equals(userObj));
if (toRemove != null)
{
success = true;
success = streamRoleSettings.Blacklist.Remove(toRemove);
}
}
else
success = streamRoleSettings.Blacklist.Add(userObj);
}
await uow.SaveChangesAsync();
UpdateCache(guild.Id, streamRoleSettings);
}
if (success)
{
await RescanUsers(guild).ConfigureAwait(false);
}
return success;
}
/// <summary>
/// Sets keyword on a guild and updates the cache.
/// </summary>
/// <param name="guild">Guild Id</param>
/// <param name="keyword">Keyword to set</param>
/// <returns>The keyword set</returns>
public async Task<string> SetKeyword(IGuild guild, string keyword)
{
keyword = keyword?.Trim()?.ToLowerInvariant();
using (var uow = _db.GetDbContext())
{
var streamRoleSettings = uow.GetStreamRoleSettings(guild.Id);
streamRoleSettings.Keyword = keyword;
UpdateCache(guild.Id, streamRoleSettings);
uow.SaveChanges();
}
await RescanUsers(guild).ConfigureAwait(false);
return keyword;
}
/// <summary>
/// Gets the currently set keyword on a guild.
/// </summary>
/// <param name="guildId">Guild Id</param>
/// <returns>The keyword set</returns>
public string GetKeyword(ulong guildId)
{
if (guildSettings.TryGetValue(guildId, out var outSetting))
return outSetting.Keyword;
StreamRoleSettings setting;
using (var uow = _db.GetDbContext())
{
setting = uow.GetStreamRoleSettings(guildId);
}
UpdateCache(guildId, setting);
return setting.Keyword;
}
/// <summary>
/// Sets the role to monitor, and a role to which to add to
/// the user who starts streaming in the monitored role.
/// </summary>
/// <param name="fromRole">Role to monitor</param>
/// <param name="addRole">Role to add to the user</param>
public async Task SetStreamRole(IRole fromRole, IRole addRole)
{
fromRole.ThrowIfNull(nameof(fromRole));
addRole.ThrowIfNull(nameof(addRole));
StreamRoleSettings setting;
using (var uow = _db.GetDbContext())
{
var streamRoleSettings = uow.GetStreamRoleSettings(fromRole.Guild.Id);
streamRoleSettings.Enabled = true;
streamRoleSettings.AddRoleId = addRole.Id;
streamRoleSettings.FromRoleId = fromRole.Id;
setting = streamRoleSettings;
await uow.SaveChangesAsync();
}
UpdateCache(fromRole.Guild.Id, setting);
foreach (var usr in await fromRole.GetMembersAsync().ConfigureAwait(false))
{
if (usr is IGuildUser x)
await RescanUser(x, setting, addRole).ConfigureAwait(false);
}
}
/// <summary>
/// Stops the stream role feature on the specified guild.
/// </summary>
/// <param name="guildId">Guild's Id</param>
public async Task StopStreamRole(IGuild guild, bool cleanup = false)
{
using (var uow = _db.GetDbContext())
{
var streamRoleSettings = uow.GetStreamRoleSettings(guild.Id);
streamRoleSettings.Enabled = false;
streamRoleSettings.AddRoleId = 0;
streamRoleSettings.FromRoleId = 0;
await uow.SaveChangesAsync();
}
if (guildSettings.TryRemove(guild.Id, out var setting) && cleanup)
await RescanUsers(guild).ConfigureAwait(false);
}
private async Task RescanUser(IGuildUser user, StreamRoleSettings setting, IRole addRole = null)
{
if (user.IsBot)
return;
var g = (StreamingGame)user.Activities
.FirstOrDefault(a => a is StreamingGame &&
(string.IsNullOrWhiteSpace(setting.Keyword)
|| a.Name.ToUpperInvariant().Contains(setting.Keyword.ToUpperInvariant())
|| setting.Whitelist.Any(x => x.UserId == user.Id)));
if (g is not null
&& setting.Enabled
&& setting.Blacklist.All(x => x.UserId != user.Id)
&& user.RoleIds.Contains(setting.FromRoleId))
{
try
{
addRole ??= user.Guild.GetRole(setting.AddRoleId);
if (addRole is null)
{
await StopStreamRole(user.Guild).ConfigureAwait(false);
Log.Warning("Stream role in server {0} no longer exists. Stopping.", setting.AddRoleId);
return;
}
//check if he doesn't have addrole already, to avoid errors
if (!user.RoleIds.Contains(addRole.Id))
{
await user.AddRoleAsync(addRole).ConfigureAwait(false);
Log.Information("Added stream role to user {0} in {1} server", user.ToString(),
user.Guild.ToString());
}
}
catch (HttpException ex) when (ex.HttpCode == System.Net.HttpStatusCode.Forbidden)
{
await StopStreamRole(user.Guild).ConfigureAwait(false);
Log.Warning(ex, "Error adding stream role(s). Forcibly disabling stream role feature");
throw new StreamRolePermissionException();
}
catch (Exception ex)
{
Log.Warning(ex, "Failed adding stream role");
}
}
else
{
//check if user is in the addrole
if (user.RoleIds.Contains(setting.AddRoleId))
{
try
{
await Task.WhenAll(client.Guilds.Select(g => RescanUsers(g))).ConfigureAwait(false);
addRole = addRole ?? user.Guild.GetRole(setting.AddRoleId);
if (addRole is null)
throw new StreamRoleNotFoundException();
await user.RemoveRoleAsync(addRole).ConfigureAwait(false);
Log.Information("Removed stream role from the user {0} in {1} server", user.ToString(), user.Guild.ToString());
}
catch
catch (HttpException ex) when (ex.HttpCode == System.Net.HttpStatusCode.Forbidden)
{
// ignored
await StopStreamRole(user.Guild).ConfigureAwait(false);
Log.Warning(ex, "Error removing stream role(s). Forcibly disabling stream role feature");
throw new StreamRolePermissionException();
}
});
}
private Task Client_GuildMemberUpdated(SocketGuildUser before, SocketGuildUser after)
{
var _ = Task.Run(async () =>
{
//if user wasn't streaming or didn't have a game status at all
if (guildSettings.TryGetValue(after.Guild.Id, out var setting))
{
await RescanUser(after, setting).ConfigureAwait(false);
}
});
return Task.CompletedTask;
}
/// <summary>
/// Adds or removes a user from a blacklist or a whitelist in the specified guild.
/// </summary>
/// <param name="guild">Guild</param>
/// <param name="action">Add or rem action</param>
/// <param name="userId">User's Id</param>
/// <param name="userName">User's name#discrim</param>
/// <returns>Whether the operation was successful</returns>
public async Task<bool> ApplyListAction(StreamRoleListType listType, IGuild guild, AddRemove action, ulong userId, string userName)
{
userName.ThrowIfNull(nameof(userName));
bool success = false;
using (var uow = _db.GetDbContext())
{
var streamRoleSettings = uow.GetStreamRoleSettings(guild.Id);
if (listType == StreamRoleListType.Whitelist)
{
var userObj = new StreamRoleWhitelistedUser()
{
UserId = userId,
Username = userName,
};
if (action == AddRemove.Rem)
{
var toDelete = streamRoleSettings.Whitelist.FirstOrDefault(x => x.Equals(userObj));
if (toDelete != null)
{
uow.Remove(toDelete);
success = true;
}
}
else
success = streamRoleSettings.Whitelist.Add(userObj);
}
else
{
var userObj = new StreamRoleBlacklistedUser()
{
UserId = userId,
Username = userName,
};
if (action == AddRemove.Rem)
{
var toRemove = streamRoleSettings.Blacklist.FirstOrDefault(x => x.Equals(userObj));
if (toRemove != null)
{
success = true;
success = streamRoleSettings.Blacklist.Remove(toRemove);
}
}
else
success = streamRoleSettings.Blacklist.Add(userObj);
}
await uow.SaveChangesAsync();
UpdateCache(guild.Id, streamRoleSettings);
}
if (success)
{
await RescanUsers(guild).ConfigureAwait(false);
}
return success;
}
}
/// <summary>
/// Sets keyword on a guild and updates the cache.
/// </summary>
/// <param name="guild">Guild Id</param>
/// <param name="keyword">Keyword to set</param>
/// <returns>The keyword set</returns>
public async Task<string> SetKeyword(IGuild guild, string keyword)
private async Task RescanUsers(IGuild guild)
{
if (!guildSettings.TryGetValue(guild.Id, out var setting))
return;
var addRole = guild.GetRole(setting.AddRoleId);
if (addRole is null)
return;
if (setting.Enabled)
{
keyword = keyword?.Trim()?.ToLowerInvariant();
using (var uow = _db.GetDbContext())
{
var streamRoleSettings = uow.GetStreamRoleSettings(guild.Id);
streamRoleSettings.Keyword = keyword;
UpdateCache(guild.Id, streamRoleSettings);
uow.SaveChanges();
}
await RescanUsers(guild).ConfigureAwait(false);
return keyword;
}
/// <summary>
/// Gets the currently set keyword on a guild.
/// </summary>
/// <param name="guildId">Guild Id</param>
/// <returns>The keyword set</returns>
public string GetKeyword(ulong guildId)
{
if (guildSettings.TryGetValue(guildId, out var outSetting))
return outSetting.Keyword;
StreamRoleSettings setting;
using (var uow = _db.GetDbContext())
{
setting = uow.GetStreamRoleSettings(guildId);
}
UpdateCache(guildId, setting);
return setting.Keyword;
}
/// <summary>
/// Sets the role to monitor, and a role to which to add to
/// the user who starts streaming in the monitored role.
/// </summary>
/// <param name="fromRole">Role to monitor</param>
/// <param name="addRole">Role to add to the user</param>
public async Task SetStreamRole(IRole fromRole, IRole addRole)
{
fromRole.ThrowIfNull(nameof(fromRole));
addRole.ThrowIfNull(nameof(addRole));
StreamRoleSettings setting;
using (var uow = _db.GetDbContext())
{
var streamRoleSettings = uow.GetStreamRoleSettings(fromRole.Guild.Id);
streamRoleSettings.Enabled = true;
streamRoleSettings.AddRoleId = addRole.Id;
streamRoleSettings.FromRoleId = fromRole.Id;
setting = streamRoleSettings;
await uow.SaveChangesAsync();
}
UpdateCache(fromRole.Guild.Id, setting);
foreach (var usr in await fromRole.GetMembersAsync().ConfigureAwait(false))
var users = await guild.GetUsersAsync(CacheMode.CacheOnly).ConfigureAwait(false);
foreach (var usr in users.Where(x => x.RoleIds.Contains(setting.FromRoleId) || x.RoleIds.Contains(addRole.Id)))
{
if (usr is IGuildUser x)
await RescanUser(x, setting, addRole).ConfigureAwait(false);
}
}
/// <summary>
/// Stops the stream role feature on the specified guild.
/// </summary>
/// <param name="guildId">Guild's Id</param>
public async Task StopStreamRole(IGuild guild, bool cleanup = false)
{
using (var uow = _db.GetDbContext())
{
var streamRoleSettings = uow.GetStreamRoleSettings(guild.Id);
streamRoleSettings.Enabled = false;
streamRoleSettings.AddRoleId = 0;
streamRoleSettings.FromRoleId = 0;
await uow.SaveChangesAsync();
}
if (guildSettings.TryRemove(guild.Id, out var setting) && cleanup)
await RescanUsers(guild).ConfigureAwait(false);
}
private async Task RescanUser(IGuildUser user, StreamRoleSettings setting, IRole addRole = null)
{
if (user.IsBot)
return;
var g = (StreamingGame)user.Activities
.FirstOrDefault(a => a is StreamingGame &&
(string.IsNullOrWhiteSpace(setting.Keyword)
|| a.Name.ToUpperInvariant().Contains(setting.Keyword.ToUpperInvariant())
|| setting.Whitelist.Any(x => x.UserId == user.Id)));
if (g is not null
&& setting.Enabled
&& setting.Blacklist.All(x => x.UserId != user.Id)
&& user.RoleIds.Contains(setting.FromRoleId))
{
try
{
addRole ??= user.Guild.GetRole(setting.AddRoleId);
if (addRole is null)
{
await StopStreamRole(user.Guild).ConfigureAwait(false);
Log.Warning("Stream role in server {0} no longer exists. Stopping.", setting.AddRoleId);
return;
}
//check if he doesn't have addrole already, to avoid errors
if (!user.RoleIds.Contains(addRole.Id))
{
await user.AddRoleAsync(addRole).ConfigureAwait(false);
Log.Information("Added stream role to user {0} in {1} server", user.ToString(),
user.Guild.ToString());
}
}
catch (HttpException ex) when (ex.HttpCode == System.Net.HttpStatusCode.Forbidden)
{
await StopStreamRole(user.Guild).ConfigureAwait(false);
Log.Warning(ex, "Error adding stream role(s). Forcibly disabling stream role feature");
throw new StreamRolePermissionException();
}
catch (Exception ex)
{
Log.Warning(ex, "Failed adding stream role");
}
}
else
{
//check if user is in the addrole
if (user.RoleIds.Contains(setting.AddRoleId))
{
try
{
addRole = addRole ?? user.Guild.GetRole(setting.AddRoleId);
if (addRole is null)
throw new StreamRoleNotFoundException();
await user.RemoveRoleAsync(addRole).ConfigureAwait(false);
Log.Information("Removed stream role from the user {0} in {1} server", user.ToString(), user.Guild.ToString());
}
catch (HttpException ex) when (ex.HttpCode == System.Net.HttpStatusCode.Forbidden)
{
await StopStreamRole(user.Guild).ConfigureAwait(false);
Log.Warning(ex, "Error removing stream role(s). Forcibly disabling stream role feature");
throw new StreamRolePermissionException();
}
}
}
}
private async Task RescanUsers(IGuild guild)
{
if (!guildSettings.TryGetValue(guild.Id, out var setting))
return;
var addRole = guild.GetRole(setting.AddRoleId);
if (addRole is null)
return;
if (setting.Enabled)
{
var users = await guild.GetUsersAsync(CacheMode.CacheOnly).ConfigureAwait(false);
foreach (var usr in users.Where(x => x.RoleIds.Contains(setting.FromRoleId) || x.RoleIds.Contains(addRole.Id)))
{
if (usr is IGuildUser x)
await RescanUser(x, setting, addRole).ConfigureAwait(false);
}
}
}
private void UpdateCache(ulong guildId, StreamRoleSettings setting)
{
guildSettings.AddOrUpdate(guildId, (key) => setting, (key, old) => setting);
}
}
}
private void UpdateCache(ulong guildId, StreamRoleSettings setting)
{
guildSettings.AddOrUpdate(guildId, (key) => setting, (key, old) => setting);
}
}

View File

@@ -1,5 +1,4 @@
using System.Linq;
using System.Threading.Tasks;
using System.Threading.Tasks;
using Discord;
using Discord.Commands;
using NadekoBot.Common.Collections;
@@ -7,69 +6,67 @@ using NadekoBot.Extensions;
using NadekoBot.Modules.Help.Services;
using NadekoBot.Services;
using NadekoBot.Db;
using NadekoBot.Modules.Administration;
namespace NadekoBot.Modules.Utility.Services
namespace NadekoBot.Modules.Utility.Services;
public class VerboseErrorsService : INService
{
public class VerboseErrorsService : INService
private readonly ConcurrentHashSet<ulong> guildsEnabled;
private readonly DbService _db;
private readonly CommandHandler _ch;
private readonly HelpService _hs;
public VerboseErrorsService(Bot bot, DbService db, CommandHandler ch, HelpService hs)
{
private readonly ConcurrentHashSet<ulong> guildsEnabled;
private readonly DbService _db;
private readonly CommandHandler _ch;
private readonly HelpService _hs;
_db = db;
_ch = ch;
_hs = hs;
public VerboseErrorsService(Bot bot, DbService db, CommandHandler ch, HelpService hs)
_ch.CommandErrored += LogVerboseError;
guildsEnabled = new ConcurrentHashSet<ulong>(bot
.AllGuildConfigs
.Where(x => x.VerboseErrors)
.Select(x => x.GuildId));
}
private async Task LogVerboseError(CommandInfo cmd, ITextChannel channel, string reason)
{
if (channel is null || !guildsEnabled.Contains(channel.GuildId))
return;
try
{
_db = db;
_ch = ch;
_hs = hs;
var embed = _hs.GetCommandHelp(cmd, channel.Guild)
.WithTitle("Command Error")
.WithDescription(reason)
.WithErrorColor();
_ch.CommandErrored += LogVerboseError;
guildsEnabled = new ConcurrentHashSet<ulong>(bot
.AllGuildConfigs
.Where(x => x.VerboseErrors)
.Select(x => x.GuildId));
await channel.EmbedAsync(embed).ConfigureAwait(false);
}
private async Task LogVerboseError(CommandInfo cmd, ITextChannel channel, string reason)
catch
{
if (channel is null || !guildsEnabled.Contains(channel.GuildId))
return;
try
{
var embed = _hs.GetCommandHelp(cmd, channel.Guild)
.WithTitle("Command Error")
.WithDescription(reason)
.WithErrorColor();
await channel.EmbedAsync(embed).ConfigureAwait(false);
}
catch
{
//ignore
}
}
public bool ToggleVerboseErrors(ulong guildId, bool? enabled=null)
{
using (var uow = _db.GetDbContext())
{
var gc = uow.GuildConfigsForId(guildId, set => set);
if (enabled==null) enabled = gc.VerboseErrors = !gc.VerboseErrors; // Old behaviour, now behind a condition
else gc.VerboseErrors = (bool)enabled; // New behaviour, just set it.
uow.SaveChanges();
}
if ((bool)enabled) // This doesn't need to be duplicated inside the using block
guildsEnabled.Add(guildId);
else
guildsEnabled.TryRemove(guildId);
return (bool)enabled;
//ignore
}
}
}
public bool ToggleVerboseErrors(ulong guildId, bool? enabled=null)
{
using (var uow = _db.GetDbContext())
{
var gc = uow.GuildConfigsForId(guildId, set => set);
if (enabled==null) enabled = gc.VerboseErrors = !gc.VerboseErrors; // Old behaviour, now behind a condition
else gc.VerboseErrors = (bool)enabled; // New behaviour, just set it.
uow.SaveChanges();
}
if ((bool)enabled) // This doesn't need to be duplicated inside the using block
guildsEnabled.Add(guildId);
else
guildsEnabled.TryRemove(guildId);
return (bool)enabled;
}
}