mirror of
https://gitlab.com/Kwoth/nadekobot.git
synced 2025-09-12 02:08:27 -04:00
Global usings and file scoped namespaces
This commit is contained in:
@@ -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);
|
||||
}
|
@@ -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; }
|
||||
}
|
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
@@ -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})"); }
|
||||
}
|
||||
}
|
@@ -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);
|
||||
}
|
||||
}
|
@@ -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;
|
||||
}
|
||||
}
|
@@ -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);
|
||||
}
|
||||
}
|
@@ -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;
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user