Global usings and file scoped namespaces

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

View File

@@ -1,69 +1,64 @@
using Discord.Commands;
using NadekoBot.Extensions;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;
using NadekoBot.Common.Attributes;
namespace NadekoBot.Modules.Utility
namespace NadekoBot.Modules.Utility;
public partial class Utility
{
public partial class Utility
[Group]
public class CalcCommands : NadekoSubmodule
{
[Group]
public class CalcCommands : NadekoSubmodule
[NadekoCommand, Aliases]
public async Task Calculate([Leftover] string expression)
{
[NadekoCommand, Aliases]
public async Task Calculate([Leftover] string expression)
{
var expr = new NCalc.Expression(expression, NCalc.EvaluateOptions.IgnoreCase | NCalc.EvaluateOptions.NoCache);
expr.EvaluateParameter += Expr_EvaluateParameter;
var result = expr.Evaluate();
if (!expr.HasErrors())
await SendConfirmAsync("⚙ " + GetText(strs.result), result.ToString()).ConfigureAwait(false);
else
await SendErrorAsync("⚙ " + GetText(strs.error), expr.Error).ConfigureAwait(false);
}
var expr = new NCalc.Expression(expression, NCalc.EvaluateOptions.IgnoreCase | NCalc.EvaluateOptions.NoCache);
expr.EvaluateParameter += Expr_EvaluateParameter;
var result = expr.Evaluate();
if (!expr.HasErrors())
await SendConfirmAsync("⚙ " + GetText(strs.result), result.ToString()).ConfigureAwait(false);
else
await SendErrorAsync("⚙ " + GetText(strs.error), expr.Error).ConfigureAwait(false);
}
private static void Expr_EvaluateParameter(string name, NCalc.ParameterArgs args)
private static void Expr_EvaluateParameter(string name, NCalc.ParameterArgs args)
{
switch (name.ToLowerInvariant())
{
switch (name.ToLowerInvariant())
{
case "pi":
args.Result = Math.PI;
break;
case "e":
args.Result = Math.E;
break;
default:
break;
}
}
[NadekoCommand, Aliases]
public async Task CalcOps()
{
var selection = typeof(Math).GetTypeInfo()
.GetMethods()
.Distinct(new MethodInfoEqualityComparer())
.Select(x => x.Name)
.Except(new[]
{
"ToString",
"Equals",
"GetHashCode",
"GetType"
});
await SendConfirmAsync(GetText(strs.calcops(Prefix)), string.Join(", ", selection));
case "pi":
args.Result = Math.PI;
break;
case "e":
args.Result = Math.E;
break;
default:
break;
}
}
private class MethodInfoEqualityComparer : IEqualityComparer<MethodInfo>
[NadekoCommand, Aliases]
public async Task CalcOps()
{
public bool Equals(MethodInfo x, MethodInfo y) => x.Name == y.Name;
public int GetHashCode(MethodInfo obj) => obj.Name.GetHashCode(StringComparison.InvariantCulture);
var selection = typeof(Math).GetTypeInfo()
.GetMethods()
.Distinct(new MethodInfoEqualityComparer())
.Select(x => x.Name)
.Except(new[]
{
"ToString",
"Equals",
"GetHashCode",
"GetType"
});
await SendConfirmAsync(GetText(strs.calcops(Prefix)), string.Join(", ", selection));
}
}
private class MethodInfoEqualityComparer : IEqualityComparer<MethodInfo>
{
public bool Equals(MethodInfo x, MethodInfo y) => x.Name == y.Name;
public int GetHashCode(MethodInfo obj) => obj.Name.GetHashCode(StringComparison.InvariantCulture);
}
}

View File

@@ -3,147 +3,142 @@ using Discord.Commands;
using Discord.WebSocket;
using Microsoft.EntityFrameworkCore;
using NadekoBot.Extensions;
using NadekoBot.Db.Models;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using NadekoBot.Common.Attributes;
using NadekoBot.Services;
using NadekoBot.Services.Database.Models;
using NadekoBot.Db;
using NadekoBot.Modules.Administration;
using NadekoBot.Modules.Utility.Services;
namespace NadekoBot.Modules.Utility
namespace NadekoBot.Modules.Utility;
public partial class Utility
{
public partial class Utility
[Group]
public class CommandMapCommands : NadekoSubmodule<CommandMapService>
{
[Group]
public class CommandMapCommands : NadekoSubmodule<CommandMapService>
private readonly DbService _db;
private readonly DiscordSocketClient _client;
public CommandMapCommands(DbService db, DiscordSocketClient client)
{
private readonly DbService _db;
private readonly DiscordSocketClient _client;
_db = db;
_client = client;
}
public CommandMapCommands(DbService db, DiscordSocketClient client)
[NadekoCommand, Aliases]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.Administrator)]
public async Task AliasesClear()
{
var count = _service.ClearAliases(ctx.Guild.Id);
await ReplyConfirmLocalizedAsync(strs.aliases_cleared(count));
}
[NadekoCommand, Aliases]
[UserPerm(GuildPerm.Administrator)]
[RequireContext(ContextType.Guild)]
public async Task Alias(string trigger, [Leftover] string mapping = null)
{
var channel = (ITextChannel)ctx.Channel;
if (string.IsNullOrWhiteSpace(trigger))
return;
trigger = trigger.Trim().ToLowerInvariant();
if (string.IsNullOrWhiteSpace(mapping))
{
_db = db;
_client = client;
}
[NadekoCommand, Aliases]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.Administrator)]
public async Task AliasesClear()
{
var count = _service.ClearAliases(ctx.Guild.Id);
await ReplyConfirmLocalizedAsync(strs.aliases_cleared(count));
}
[NadekoCommand, Aliases]
[UserPerm(GuildPerm.Administrator)]
[RequireContext(ContextType.Guild)]
public async Task Alias(string trigger, [Leftover] string mapping = null)
{
var channel = (ITextChannel)ctx.Channel;
if (string.IsNullOrWhiteSpace(trigger))
return;
trigger = trigger.Trim().ToLowerInvariant();
if (string.IsNullOrWhiteSpace(mapping))
if (!_service.AliasMaps.TryGetValue(ctx.Guild.Id, out var maps) ||
!maps.TryRemove(trigger, out _))
{
if (!_service.AliasMaps.TryGetValue(ctx.Guild.Id, out var maps) ||
!maps.TryRemove(trigger, out _))
{
await ReplyErrorLocalizedAsync(strs.alias_remove_fail(Format.Code(trigger))).ConfigureAwait(false);
return;
}
using (var uow = _db.GetDbContext())
{
var config = uow.GuildConfigsForId(ctx.Guild.Id, set => set.Include(x => x.CommandAliases));
var toAdd = new CommandAlias()
{
Mapping = mapping,
Trigger = trigger
};
var tr = config.CommandAliases.FirstOrDefault(x => x.Trigger == trigger);
if (tr != null)
uow.Set<CommandAlias>().Remove(tr);
uow.SaveChanges();
}
await ReplyConfirmLocalizedAsync(strs.alias_removed(Format.Code(trigger))).ConfigureAwait(false);
await ReplyErrorLocalizedAsync(strs.alias_remove_fail(Format.Code(trigger))).ConfigureAwait(false);
return;
}
_service.AliasMaps.AddOrUpdate(ctx.Guild.Id, (_) =>
using (var uow = _db.GetDbContext())
{
using (var uow = _db.GetDbContext())
var config = uow.GuildConfigsForId(ctx.Guild.Id, set => set.Include(x => x.CommandAliases));
var toAdd = new CommandAlias()
{
var config = uow.GuildConfigsForId(ctx.Guild.Id, set => set.Include(x => x.CommandAliases));
config.CommandAliases.Add(new CommandAlias()
{
Mapping = mapping,
Trigger = trigger
});
uow.SaveChanges();
}
return new ConcurrentDictionary<string, string>(new Dictionary<string, string>() {
{trigger.Trim().ToLowerInvariant(), mapping.ToLowerInvariant() },
Mapping = mapping,
Trigger = trigger
};
var tr = config.CommandAliases.FirstOrDefault(x => x.Trigger == trigger);
if (tr != null)
uow.Set<CommandAlias>().Remove(tr);
uow.SaveChanges();
}
await ReplyConfirmLocalizedAsync(strs.alias_removed(Format.Code(trigger))).ConfigureAwait(false);
return;
}
_service.AliasMaps.AddOrUpdate(ctx.Guild.Id, (_) =>
{
using (var uow = _db.GetDbContext())
{
var config = uow.GuildConfigsForId(ctx.Guild.Id, set => set.Include(x => x.CommandAliases));
config.CommandAliases.Add(new CommandAlias()
{
Mapping = mapping,
Trigger = trigger
});
}, (_, map) =>
{
using (var uow = _db.GetDbContext())
{
var config = uow.GuildConfigsForId(ctx.Guild.Id, set => set.Include(x => x.CommandAliases));
var toAdd = new CommandAlias()
{
Mapping = mapping,
Trigger = trigger
};
var toRemove = config.CommandAliases.Where(x => x.Trigger == trigger);
if (toRemove.Any())
uow.RemoveRange(toRemove.ToArray());
config.CommandAliases.Add(toAdd);
uow.SaveChanges();
}
map.AddOrUpdate(trigger, mapping, (key, old) => mapping);
return map;
uow.SaveChanges();
}
return new ConcurrentDictionary<string, string>(new Dictionary<string, string>() {
{trigger.Trim().ToLowerInvariant(), mapping.ToLowerInvariant() },
});
}, (_, map) =>
{
using (var uow = _db.GetDbContext())
{
var config = uow.GuildConfigsForId(ctx.Guild.Id, set => set.Include(x => x.CommandAliases));
var toAdd = new CommandAlias()
{
Mapping = mapping,
Trigger = trigger
};
var toRemove = config.CommandAliases.Where(x => x.Trigger == trigger);
if (toRemove.Any())
uow.RemoveRange(toRemove.ToArray());
config.CommandAliases.Add(toAdd);
uow.SaveChanges();
}
map.AddOrUpdate(trigger, mapping, (key, old) => mapping);
return map;
});
await ReplyConfirmLocalizedAsync(strs.alias_added(Format.Code(trigger), Format.Code(mapping))).ConfigureAwait(false);
await ReplyConfirmLocalizedAsync(strs.alias_added(Format.Code(trigger), Format.Code(mapping))).ConfigureAwait(false);
}
[NadekoCommand, Aliases]
[RequireContext(ContextType.Guild)]
public async Task AliasList(int page = 1)
{
var channel = (ITextChannel)ctx.Channel;
page -= 1;
if (page < 0)
return;
if (!_service.AliasMaps.TryGetValue(ctx.Guild.Id, out var maps) || !maps.Any())
{
await ReplyErrorLocalizedAsync(strs.aliases_none).ConfigureAwait(false);
return;
}
var arr = maps.ToArray();
[NadekoCommand, Aliases]
[RequireContext(ContextType.Guild)]
public async Task AliasList(int page = 1)
await ctx.SendPaginatedConfirmAsync(page, (curPage) =>
{
var channel = (ITextChannel)ctx.Channel;
page -= 1;
if (page < 0)
return;
if (!_service.AliasMaps.TryGetValue(ctx.Guild.Id, out var maps) || !maps.Any())
{
await ReplyErrorLocalizedAsync(strs.aliases_none).ConfigureAwait(false);
return;
}
var arr = maps.ToArray();
await ctx.SendPaginatedConfirmAsync(page, (curPage) =>
{
return _eb.Create().WithOkColor()
return _eb.Create().WithOkColor()
.WithTitle(GetText(strs.alias_list))
.WithDescription(string.Join("\n",
arr.Skip(curPage * 10).Take(10).Select(x => $"`{x.Key}` => `{x.Value}`")));
}, arr.Length, 10).ConfigureAwait(false);
}
}, arr.Length, 10).ConfigureAwait(false);
}
}
}

View File

@@ -1,12 +1,11 @@
using System.Diagnostics;
namespace NadekoBot.Modules.Utility.Common
namespace NadekoBot.Modules.Utility.Common;
[DebuggerDisplay("Type: {UnitType} Trigger: {Triggers[0]} Mod: {Modifier}")]
public class ConvertUnit
{
[DebuggerDisplay("Type: {UnitType} Trigger: {Triggers[0]} Mod: {Modifier}")]
public class ConvertUnit
{
public string[] Triggers { get; set; }
public string UnitType { get; set; }
public decimal Modifier { get; set; }
}
}
public string[] Triggers { get; set; }
public string UnitType { get; set; }
public decimal Modifier { get; set; }
}

View File

@@ -1,19 +1,16 @@
using System;
namespace NadekoBot.Modules.Utility.Common.Exceptions;
namespace NadekoBot.Modules.Utility.Common.Exceptions
public class StreamRoleNotFoundException : Exception
{
public class StreamRoleNotFoundException : Exception
public StreamRoleNotFoundException() : base("Stream role wasn't found.")
{
public StreamRoleNotFoundException() : base("Stream role wasn't found.")
{
}
public StreamRoleNotFoundException(string message) : base(message)
{
}
public StreamRoleNotFoundException(string message, Exception innerException) : base(message, innerException)
{
}
}
}
public StreamRoleNotFoundException(string message) : base(message)
{
}
public StreamRoleNotFoundException(string message, Exception innerException) : base(message, innerException)
{
}
}

View File

@@ -1,19 +1,16 @@
using System;
namespace NadekoBot.Modules.Utility.Common.Exceptions;
namespace NadekoBot.Modules.Utility.Common.Exceptions
public class StreamRolePermissionException : Exception
{
public class StreamRolePermissionException : Exception
public StreamRolePermissionException() : base("Stream role was unable to be applied.")
{
public StreamRolePermissionException() : base("Stream role was unable to be applied.")
{
}
public StreamRolePermissionException(string message) : base(message)
{
}
public StreamRolePermissionException(string message, Exception innerException) : base(message, innerException)
{
}
}
}
public StreamRolePermissionException(string message) : base(message)
{
}
public StreamRolePermissionException(string message, Exception innerException) : base(message, innerException)
{
}
}

View File

@@ -1,134 +1,131 @@
using System;
using System.Collections.Generic;
using System.Text.Json.Serialization;
using System.Text.Json.Serialization;
namespace NadekoBot.Modules.Utility.Common.Patreon
namespace NadekoBot.Modules.Utility.Common.Patreon;
public sealed class Attributes
{
public sealed class Attributes
{
[JsonPropertyName("full_name")]
public string FullName { get; set; }
[JsonPropertyName("full_name")]
public string FullName { get; set; }
[JsonPropertyName("is_follower")]
public bool IsFollower { get; set; }
[JsonPropertyName("is_follower")]
public bool IsFollower { get; set; }
[JsonPropertyName("last_charge_date")]
public DateTime LastChargeDate { get; set; }
[JsonPropertyName("last_charge_date")]
public DateTime LastChargeDate { get; set; }
[JsonPropertyName("last_charge_status")]
public string LastChargeStatus { get; set; }
[JsonPropertyName("last_charge_status")]
public string LastChargeStatus { get; set; }
[JsonPropertyName("lifetime_support_cents")]
public int LifetimeSupportCents { get; set; }
[JsonPropertyName("lifetime_support_cents")]
public int LifetimeSupportCents { get; set; }
[JsonPropertyName("currently_entitled_amount_cents")]
public int CurrentlyEntitledAmountCents { get; set; }
[JsonPropertyName("currently_entitled_amount_cents")]
public int CurrentlyEntitledAmountCents { get; set; }
[JsonPropertyName("patron_status")]
public string PatronStatus { get; set; }
}
public sealed class Data
{
[JsonPropertyName("id")]
public string Id { get; set; }
[JsonPropertyName("type")]
public string Type { get; set; }
}
public sealed class Address
{
[JsonPropertyName("data")]
public Data Data { get; set; }
}
// public sealed class CurrentlyEntitledTiers
// {
// [JsonPropertyName("data")]
// public List<Datum> Data { get; set; }
// }
// public sealed class Relationships
// {
// [JsonPropertyName("address")]
// public Address Address { get; set; }
//
// // [JsonPropertyName("currently_entitled_tiers")]
// // public CurrentlyEntitledTiers CurrentlyEntitledTiers { get; set; }
// }
public sealed class PatreonResponse
{
[JsonPropertyName("data")]
public List<PatreonMember> Data { get; set; }
[JsonPropertyName("included")]
public List<PatreonUser> Included { get; set; }
[JsonPropertyName("links")]
public PatreonLinks Links { get; set; }
}
public sealed class PatreonLinks
{
[JsonPropertyName("next")]
public string Next { get; set; }
}
public sealed class PatreonUser
{
[JsonPropertyName("attributes")]
public PatreonUserAttributes Attributes { get; set; }
[JsonPropertyName("id")]
public string Id { get; set; }
// public string Type { get; set; }
}
public sealed class PatreonUserAttributes
{
[JsonPropertyName("social_connections")]
public PatreonSocials SocialConnections { get; set; }
}
public sealed class PatreonSocials
{
[JsonPropertyName("discord")]
public DiscordSocial Discord { get; set; }
}
public sealed class DiscordSocial
{
[JsonPropertyName("user_id")]
public string UserId { get; set; }
}
public sealed class PatreonMember
{
[JsonPropertyName("attributes")]
public Attributes Attributes { get; set; }
[JsonPropertyName("relationships")]
public Relationships Relationships { get; set; }
[JsonPropertyName("type")]
public string Type { get; set; }
}
public sealed class Relationships
{
[JsonPropertyName("user")]
public PatreonRelationshipUser User { get; set; }
}
public sealed class PatreonRelationshipUser
{
[JsonPropertyName("data")]
public PatreonUserData Data { get; set; }
}
public sealed class PatreonUserData
{
[JsonPropertyName("id")]
public string Id { get; set; }
}
[JsonPropertyName("patron_status")]
public string PatronStatus { get; set; }
}
public sealed class Data
{
[JsonPropertyName("id")]
public string Id { get; set; }
[JsonPropertyName("type")]
public string Type { get; set; }
}
public sealed class Address
{
[JsonPropertyName("data")]
public Data Data { get; set; }
}
// public sealed class CurrentlyEntitledTiers
// {
// [JsonPropertyName("data")]
// public List<Datum> Data { get; set; }
// }
// public sealed class Relationships
// {
// [JsonPropertyName("address")]
// public Address Address { get; set; }
//
// // [JsonPropertyName("currently_entitled_tiers")]
// // public CurrentlyEntitledTiers CurrentlyEntitledTiers { get; set; }
// }
public sealed class PatreonResponse
{
[JsonPropertyName("data")]
public List<PatreonMember> Data { get; set; }
[JsonPropertyName("included")]
public List<PatreonUser> Included { get; set; }
[JsonPropertyName("links")]
public PatreonLinks Links { get; set; }
}
public sealed class PatreonLinks
{
[JsonPropertyName("next")]
public string Next { get; set; }
}
public sealed class PatreonUser
{
[JsonPropertyName("attributes")]
public PatreonUserAttributes Attributes { get; set; }
[JsonPropertyName("id")]
public string Id { get; set; }
// public string Type { get; set; }
}
public sealed class PatreonUserAttributes
{
[JsonPropertyName("social_connections")]
public PatreonSocials SocialConnections { get; set; }
}
public sealed class PatreonSocials
{
[JsonPropertyName("discord")]
public DiscordSocial Discord { get; set; }
}
public sealed class DiscordSocial
{
[JsonPropertyName("user_id")]
public string UserId { get; set; }
}
public sealed class PatreonMember
{
[JsonPropertyName("attributes")]
public Attributes Attributes { get; set; }
[JsonPropertyName("relationships")]
public Relationships Relationships { get; set; }
[JsonPropertyName("type")]
public string Type { get; set; }
}
public sealed class Relationships
{
[JsonPropertyName("user")]
public PatreonRelationshipUser User { get; set; }
}
public sealed class PatreonRelationshipUser
{
[JsonPropertyName("data")]
public PatreonUserData Data { get; set; }
}
public sealed class PatreonUserData
{
[JsonPropertyName("id")]
public string Id { get; set; }
}

View File

@@ -1,8 +1,7 @@
namespace NadekoBot.Modules.Utility.Common
namespace NadekoBot.Modules.Utility.Common;
public enum StreamRoleListType
{
public enum StreamRoleListType
{
Whitelist,
Blacklist,
}
}
Whitelist,
Blacklist,
}

View File

@@ -1,189 +1,183 @@
using Discord;
using Discord.Commands;
using NadekoBot.Common;
using NadekoBot.Common.Attributes;
using NadekoBot.Extensions;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using NadekoBot.Services;
using NadekoBot.Modules.Administration.Services;
namespace NadekoBot.Modules.Utility
namespace NadekoBot.Modules.Utility;
public partial class Utility
{
public partial class Utility
public class ConfigCommands : NadekoSubmodule
{
public class ConfigCommands : NadekoSubmodule
private readonly BotConfigService _bss;
private readonly SelfService _selfService;
private readonly IEnumerable<IConfigService> _settingServices;
public ConfigCommands(BotConfigService bss, SelfService selfService, IEnumerable<IConfigService> settingServices)
{
private readonly BotConfigService _bss;
private readonly SelfService _selfService;
private readonly IEnumerable<IConfigService> _settingServices;
_settingServices = settingServices;
_bss = bss;
_selfService = selfService;
}
public ConfigCommands(BotConfigService bss, SelfService selfService, IEnumerable<IConfigService> settingServices)
{
_settingServices = settingServices;
_bss = bss;
_selfService = selfService;
}
[NadekoCommand, Aliases]
[OwnerOnly]
public Task BotConfigEdit()
=> Config("bot");
[NadekoCommand, Aliases]
[OwnerOnly]
public Task BotConfigEdit()
=> Config("bot");
[NadekoCommand, Aliases]
[Priority(0)]
[OwnerOnly]
public Task BotConfigEdit(string prop, [Leftover] string newValue = null)
=> Config("bot", prop, newValue);
[NadekoCommand, Aliases]
[Priority(0)]
[OwnerOnly]
public Task BotConfigEdit(string prop, [Leftover] string newValue = null)
=> Config("bot", prop, newValue);
[NadekoCommand, Aliases]
[OwnerOnly]
public async Task ConfigReload(string name)
{
var setting = _settingServices.FirstOrDefault(x =>
x.Name.StartsWith(name, StringComparison.InvariantCultureIgnoreCase));
[NadekoCommand, Aliases]
[OwnerOnly]
public async Task ConfigReload(string name)
{
var setting = _settingServices.FirstOrDefault(x =>
x.Name.StartsWith(name, StringComparison.InvariantCultureIgnoreCase));
if (setting is null)
{
var configNames = _settingServices.Select(x => x.Name);
var embed = _eb.Create()
.WithErrorColor()
.WithDescription(GetText(strs.config_not_found(Format.Code(name))))
.AddField(GetText(strs.config_list), string.Join("\n", configNames));
await ctx.Channel.EmbedAsync(embed);
return;
}
setting.Reload();
await ctx.OkAsync();
}
[NadekoCommand, Aliases]
[OwnerOnly]
public async Task Config(string name = null, string prop = null, [Leftover] string value = null)
if (setting is null)
{
var configNames = _settingServices.Select(x => x.Name);
// if name is not provided, print available configs
name = name?.ToLowerInvariant();
if (string.IsNullOrWhiteSpace(name))
{
var embed = _eb.Create()
.WithOkColor()
.WithTitle(GetText(strs.config_list))
.WithDescription(string.Join("\n", configNames));
var embed = _eb.Create()
.WithErrorColor()
.WithDescription(GetText(strs.config_not_found(Format.Code(name))))
.AddField(GetText(strs.config_list), string.Join("\n", configNames));
await ctx.Channel.EmbedAsync(embed);
return;
}
var setting = _settingServices.FirstOrDefault(x =>
x.Name.StartsWith(name, StringComparison.InvariantCultureIgnoreCase));
// if config name is not found, print error and the list of configs
if (setting is null)
{
var embed = _eb.Create()
.WithErrorColor()
.WithDescription(GetText(strs.config_not_found(Format.Code(name))))
.AddField(GetText(strs.config_list), string.Join("\n", configNames));
await ctx.Channel.EmbedAsync(embed);
return;
}
name = setting.Name;
// if prop is not sent, then print the list of all props and values in that config
prop = prop?.ToLowerInvariant();
var propNames = setting.GetSettableProps();
if (string.IsNullOrWhiteSpace(prop))
{
var propStrings = GetPropsAndValuesString(setting, propNames);
var embed = _eb.Create()
.WithOkColor()
.WithTitle($"⚙️ {setting.Name}")
.WithDescription(propStrings);
await ctx.Channel.EmbedAsync(embed);
return;
}
// if the prop is invalid -> print error and list of
var exists = propNames.Any(x => x == prop);
if (!exists)
{
var propStrings = GetPropsAndValuesString(setting, propNames);
var propErrorEmbed = _eb.Create()
.WithErrorColor()
.WithDescription(GetText(strs.config_prop_not_found(Format.Code(prop), Format.Code(name))))
.AddField($"⚙️ {setting.Name}", propStrings);
await ctx.Channel.EmbedAsync(propErrorEmbed);
return;
}
// if prop is sent, but value is not, then we have to check
// if prop is valid ->
if (string.IsNullOrWhiteSpace(value))
{
value = setting.GetSetting(prop);
if (string.IsNullOrWhiteSpace(value))
value = "-";
if (prop != "currency.sign")
{
value = Format.Code(Format.Sanitize(value?.TrimTo(1000)), "json");
}
var embed = _eb.Create()
.WithOkColor()
.AddField("Config", Format.Code(setting.Name), true)
.AddField("Prop", Format.Code(prop), true)
.AddField("Value", value);
var comment = setting.GetComment(prop);
if (!string.IsNullOrWhiteSpace(comment))
embed.AddField("Comment", comment);
await ctx.Channel.EmbedAsync(embed);
return;
}
var success = setting.SetSetting(prop, value);
if (!success)
{
await ReplyErrorLocalizedAsync(strs.config_edit_fail(Format.Code(prop), Format.Code(value)));
return;
}
await ctx.OkAsync();
await ctx.Channel.EmbedAsync(embed);
return;
}
private string GetPropsAndValuesString(IConfigService config, IEnumerable<string> names)
setting.Reload();
await ctx.OkAsync();
}
[NadekoCommand, Aliases]
[OwnerOnly]
public async Task Config(string name = null, string prop = null, [Leftover] string value = null)
{
var configNames = _settingServices.Select(x => x.Name);
// if name is not provided, print available configs
name = name?.ToLowerInvariant();
if (string.IsNullOrWhiteSpace(name))
{
var propValues = names.Select(pr =>
{
var val = config.GetSetting(pr);
if (pr != "currency.sign")
val = val?.TrimTo(28);
return val?.Replace("\n", "") ?? "-";
});
var strings = names.Zip(propValues, (name, value) =>
$"{name, -25} = {value}\n");
var embed = _eb.Create()
.WithOkColor()
.WithTitle(GetText(strs.config_list))
.WithDescription(string.Join("\n", configNames));
return Format.Code(string.Concat(strings), "hs");
await ctx.Channel.EmbedAsync(embed);
return;
}
var setting = _settingServices.FirstOrDefault(x =>
x.Name.StartsWith(name, StringComparison.InvariantCultureIgnoreCase));
// if config name is not found, print error and the list of configs
if (setting is null)
{
var embed = _eb.Create()
.WithErrorColor()
.WithDescription(GetText(strs.config_not_found(Format.Code(name))))
.AddField(GetText(strs.config_list), string.Join("\n", configNames));
await ctx.Channel.EmbedAsync(embed);
return;
}
name = setting.Name;
// if prop is not sent, then print the list of all props and values in that config
prop = prop?.ToLowerInvariant();
var propNames = setting.GetSettableProps();
if (string.IsNullOrWhiteSpace(prop))
{
var propStrings = GetPropsAndValuesString(setting, propNames);
var embed = _eb.Create()
.WithOkColor()
.WithTitle($"⚙️ {setting.Name}")
.WithDescription(propStrings);
await ctx.Channel.EmbedAsync(embed);
return;
}
// if the prop is invalid -> print error and list of
var exists = propNames.Any(x => x == prop);
if (!exists)
{
var propStrings = GetPropsAndValuesString(setting, propNames);
var propErrorEmbed = _eb.Create()
.WithErrorColor()
.WithDescription(GetText(strs.config_prop_not_found(Format.Code(prop), Format.Code(name))))
.AddField($"⚙️ {setting.Name}", propStrings);
await ctx.Channel.EmbedAsync(propErrorEmbed);
return;
}
// if prop is sent, but value is not, then we have to check
// if prop is valid ->
if (string.IsNullOrWhiteSpace(value))
{
value = setting.GetSetting(prop);
if (string.IsNullOrWhiteSpace(value))
value = "-";
if (prop != "currency.sign")
{
value = Format.Code(Format.Sanitize(value?.TrimTo(1000)), "json");
}
var embed = _eb.Create()
.WithOkColor()
.AddField("Config", Format.Code(setting.Name), true)
.AddField("Prop", Format.Code(prop), true)
.AddField("Value", value);
var comment = setting.GetComment(prop);
if (!string.IsNullOrWhiteSpace(comment))
embed.AddField("Comment", comment);
await ctx.Channel.EmbedAsync(embed);
return;
}
var success = setting.SetSetting(prop, value);
if (!success)
{
await ReplyErrorLocalizedAsync(strs.config_edit_fail(Format.Code(prop), Format.Code(value)));
return;
}
await ctx.OkAsync();
}
private string GetPropsAndValuesString(IConfigService config, IEnumerable<string> names)
{
var propValues = names.Select(pr =>
{
var val = config.GetSetting(pr);
if (pr != "currency.sign")
val = val?.TrimTo(28);
return val?.Replace("\n", "") ?? "-";
});
var strings = names.Zip(propValues, (name, value) =>
$"{name, -25} = {value}\n");
return Format.Code(string.Concat(strings), "hs");
}
}
}
}

View File

@@ -3,155 +3,152 @@ using Discord.Commands;
using Discord.WebSocket;
using NadekoBot.Extensions;
using NadekoBot.Services;
using System;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using NadekoBot.Common.Attributes;
namespace NadekoBot.Modules.Utility
namespace NadekoBot.Modules.Utility;
public partial class Utility
{
public partial class Utility
[Group]
public class InfoCommands : NadekoSubmodule
{
[Group]
public class InfoCommands : NadekoSubmodule
private readonly DiscordSocketClient _client;
private readonly IStatsService _stats;
public InfoCommands(DiscordSocketClient client, IStatsService stats)
{
private readonly DiscordSocketClient _client;
private readonly IStatsService _stats;
_client = client;
_stats = stats;
}
public InfoCommands(DiscordSocketClient client, IStatsService stats)
{
_client = client;
_stats = stats;
}
[NadekoCommand, Aliases]
[RequireContext(ContextType.Guild)]
public async Task ServerInfo(string guildName = null)
{
var channel = (ITextChannel)ctx.Channel;
guildName = guildName?.ToUpperInvariant();
SocketGuild guild;
[NadekoCommand, Aliases]
[RequireContext(ContextType.Guild)]
public async Task ServerInfo(string guildName = null)
{
var channel = (ITextChannel)ctx.Channel;
guildName = guildName?.ToUpperInvariant();
SocketGuild guild;
if (string.IsNullOrWhiteSpace(guildName))
guild = (SocketGuild)channel.Guild;
else
guild = _client.Guilds.FirstOrDefault(g => g.Name.ToUpperInvariant() == guildName.ToUpperInvariant());
if (string.IsNullOrWhiteSpace(guildName))
guild = (SocketGuild)channel.Guild;
else
guild = _client.Guilds.FirstOrDefault(g => g.Name.ToUpperInvariant() == guildName.ToUpperInvariant());
if (guild is null)
return;
if (guild is null)
return;
var ownername = guild.GetUser(guild.OwnerId);
var textchn = guild.TextChannels.Count;
var voicechn = guild.VoiceChannels.Count;
var channels = $@"{GetText(strs.text_channels(textchn))}
var ownername = guild.GetUser(guild.OwnerId);
var textchn = guild.TextChannels.Count;
var voicechn = guild.VoiceChannels.Count;
var channels = $@"{GetText(strs.text_channels(textchn))}
{GetText(strs.voice_channels(voicechn))}";
var createdAt = new DateTime(2015, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc).AddMilliseconds(guild.Id >> 22);
var features = string.Join(", ", guild.Features);
if (string.IsNullOrWhiteSpace(features))
features = "-";
var createdAt = new DateTime(2015, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc).AddMilliseconds(guild.Id >> 22);
var features = string.Join(", ", guild.Features);
if (string.IsNullOrWhiteSpace(features))
features = "-";
var embed = _eb.Create()
.WithAuthor(GetText(strs.server_info))
.WithTitle(guild.Name)
.AddField(GetText(strs.id), guild.Id.ToString(), true)
.AddField(GetText(strs.owner), ownername.ToString(), true)
.AddField(GetText(strs.members), guild.MemberCount.ToString(), true)
.AddField(GetText(strs.channels), channels, true)
.AddField(GetText(strs.created_at), $"{createdAt:dd.MM.yyyy HH:mm}", true)
.AddField(GetText(strs.roles), (guild.Roles.Count - 1).ToString(), true)
.AddField(GetText(strs.features), features)
.WithOkColor();
var embed = _eb.Create()
.WithAuthor(GetText(strs.server_info))
.WithTitle(guild.Name)
.AddField(GetText(strs.id), guild.Id.ToString(), true)
.AddField(GetText(strs.owner), ownername.ToString(), true)
.AddField(GetText(strs.members), guild.MemberCount.ToString(), true)
.AddField(GetText(strs.channels), channels, true)
.AddField(GetText(strs.created_at), $"{createdAt:dd.MM.yyyy HH:mm}", true)
.AddField(GetText(strs.roles), (guild.Roles.Count - 1).ToString(), true)
.AddField(GetText(strs.features), features)
.WithOkColor();
if (Uri.IsWellFormedUriString(guild.IconUrl, UriKind.Absolute))
embed.WithThumbnailUrl(guild.IconUrl);
if (Uri.IsWellFormedUriString(guild.IconUrl, UriKind.Absolute))
embed.WithThumbnailUrl(guild.IconUrl);
if (guild.Emotes.Any())
{
embed.AddField(GetText(strs.custom_emojis) + $"({guild.Emotes.Count})",
string.Join(" ", guild.Emotes
if (guild.Emotes.Any())
{
embed.AddField(GetText(strs.custom_emojis) + $"({guild.Emotes.Count})",
string.Join(" ", guild.Emotes
.Shuffle()
.Take(20)
.Select(e => $"{e.Name} {e.ToString()}"))
.TrimTo(1020));
}
await ctx.Channel.EmbedAsync(embed).ConfigureAwait(false);
.TrimTo(1020));
}
await ctx.Channel.EmbedAsync(embed).ConfigureAwait(false);
}
[NadekoCommand, Aliases]
[RequireContext(ContextType.Guild)]
public async Task ChannelInfo(ITextChannel channel = null)
[NadekoCommand, Aliases]
[RequireContext(ContextType.Guild)]
public async Task ChannelInfo(ITextChannel channel = null)
{
var ch = channel ?? (ITextChannel)ctx.Channel;
if (ch is null)
return;
var createdAt = new DateTime(2015, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc).AddMilliseconds(ch.Id >> 22);
var usercount = (await ch.GetUsersAsync().FlattenAsync().ConfigureAwait(false)).Count();
var embed = _eb.Create()
.WithTitle(ch.Name)
.WithDescription(ch.Topic?.SanitizeMentions(true))
.AddField(GetText(strs.id), ch.Id.ToString(), true)
.AddField(GetText(strs.created_at), $"{createdAt:dd.MM.yyyy HH:mm}", true)
.AddField(GetText(strs.users), usercount.ToString(), true)
.WithOkColor();
await ctx.Channel.EmbedAsync(embed).ConfigureAwait(false);
}
[NadekoCommand, Aliases]
[RequireContext(ContextType.Guild)]
public async Task UserInfo(IGuildUser usr = null)
{
var user = usr ?? ctx.User as IGuildUser;
if (user is null)
return;
var embed = _eb.Create()
.AddField(GetText(strs.name), $"**{user.Username}**#{user.Discriminator}", true);
if (!string.IsNullOrWhiteSpace(user.Nickname))
{
var ch = channel ?? (ITextChannel)ctx.Channel;
if (ch is null)
return;
var createdAt = new DateTime(2015, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc).AddMilliseconds(ch.Id >> 22);
var usercount = (await ch.GetUsersAsync().FlattenAsync().ConfigureAwait(false)).Count();
var embed = _eb.Create()
.WithTitle(ch.Name)
.WithDescription(ch.Topic?.SanitizeMentions(true))
.AddField(GetText(strs.id), ch.Id.ToString(), true)
.AddField(GetText(strs.created_at), $"{createdAt:dd.MM.yyyy HH:mm}", true)
.AddField(GetText(strs.users), usercount.ToString(), true)
.WithOkColor();
await ctx.Channel.EmbedAsync(embed).ConfigureAwait(false);
embed.AddField(GetText(strs.nickname), user.Nickname, true);
}
embed.AddField(GetText(strs.id), user.Id.ToString(), true)
.AddField(GetText(strs.joined_server), $"{user.JoinedAt?.ToString("dd.MM.yyyy HH:mm") ?? "?"}", true)
.AddField(GetText(strs.joined_discord), $"{user.CreatedAt:dd.MM.yyyy HH:mm}", true)
.AddField(GetText(strs.roles), $"**({user.RoleIds.Count - 1})** - {string.Join("\n", user.GetRoles().Take(10).Where(r => r.Id != r.Guild.EveryoneRole.Id).Select(r => r.Name)).SanitizeMentions(true)}", true)
.WithOkColor();
[NadekoCommand, Aliases]
[RequireContext(ContextType.Guild)]
public async Task UserInfo(IGuildUser usr = null)
var av = user.RealAvatarUrl();
if (av != null && av.IsAbsoluteUri)
embed.WithThumbnailUrl(av.ToString());
await ctx.Channel.EmbedAsync(embed).ConfigureAwait(false);
}
[NadekoCommand, Aliases]
[RequireContext(ContextType.Guild)]
[OwnerOnly]
public async Task Activity(int page = 1)
{
const int activityPerPage = 10;
page -= 1;
if (page < 0)
return;
int startCount = page * activityPerPage;
StringBuilder str = new StringBuilder();
foreach (var kvp in CmdHandler.UserMessagesSent.OrderByDescending(kvp => kvp.Value).Skip(page * activityPerPage).Take(activityPerPage))
{
var user = usr ?? ctx.User as IGuildUser;
if (user is null)
return;
var embed = _eb.Create()
.AddField(GetText(strs.name), $"**{user.Username}**#{user.Discriminator}", true);
if (!string.IsNullOrWhiteSpace(user.Nickname))
{
embed.AddField(GetText(strs.nickname), user.Nickname, true);
}
embed.AddField(GetText(strs.id), user.Id.ToString(), true)
.AddField(GetText(strs.joined_server), $"{user.JoinedAt?.ToString("dd.MM.yyyy HH:mm") ?? "?"}", true)
.AddField(GetText(strs.joined_discord), $"{user.CreatedAt:dd.MM.yyyy HH:mm}", true)
.AddField(GetText(strs.roles), $"**({user.RoleIds.Count - 1})** - {string.Join("\n", user.GetRoles().Take(10).Where(r => r.Id != r.Guild.EveryoneRole.Id).Select(r => r.Name)).SanitizeMentions(true)}", true)
.WithOkColor();
var av = user.RealAvatarUrl();
if (av != null && av.IsAbsoluteUri)
embed.WithThumbnailUrl(av.ToString());
await ctx.Channel.EmbedAsync(embed).ConfigureAwait(false);
str.AppendLine(GetText(strs.activity_line(
++startCount,
Format.Bold(kvp.Key.ToString()),
kvp.Value / _stats.GetUptime().TotalSeconds, kvp.Value)));
}
[NadekoCommand, Aliases]
[RequireContext(ContextType.Guild)]
[OwnerOnly]
public async Task Activity(int page = 1)
{
const int activityPerPage = 10;
page -= 1;
if (page < 0)
return;
int startCount = page * activityPerPage;
StringBuilder str = new StringBuilder();
foreach (var kvp in CmdHandler.UserMessagesSent.OrderByDescending(kvp => kvp.Value).Skip(page * activityPerPage).Take(activityPerPage))
{
str.AppendLine(GetText(strs.activity_line(
++startCount,
Format.Bold(kvp.Key.ToString()),
kvp.Value / _stats.GetUptime().TotalSeconds, kvp.Value)));
}
await ctx.Channel.EmbedAsync(_eb.Create()
.WithTitle(GetText(strs.activity_page(page + 1)))
.WithOkColor()
.WithFooter(GetText(strs.activity_users_total(CmdHandler.UserMessagesSent.Count)))
.WithDescription(str.ToString())).ConfigureAwait(false);
}
await ctx.Channel.EmbedAsync(_eb.Create()
.WithTitle(GetText(strs.activity_page(page + 1)))
.WithOkColor()
.WithFooter(GetText(strs.activity_users_total(CmdHandler.UserMessagesSent.Count)))
.WithDescription(str.ToString())).ConfigureAwait(false);
}
}
}
}

View File

@@ -1,106 +1,103 @@
using System;
using Discord;
using Discord;
using Discord.Commands;
using NadekoBot.Common.Attributes;
using NadekoBot.Common;
using NadekoBot.Modules.Utility.Services;
using NadekoBot.Extensions;
using System.Linq;
using System.Threading.Tasks;
namespace NadekoBot.Modules.Utility
namespace NadekoBot.Modules.Utility;
public partial class Utility
{
public partial class Utility
[Group]
public class InviteCommands : NadekoSubmodule<InviteService>
{
[Group]
public class InviteCommands : NadekoSubmodule<InviteService>
[NadekoCommand, Aliases]
[RequireContext(ContextType.Guild)]
[BotPerm(ChannelPerm.CreateInstantInvite)]
[UserPerm(ChannelPerm.CreateInstantInvite)]
[NadekoOptions(typeof(InviteService.Options))]
public async Task InviteCreate(params string[] args)
{
[NadekoCommand, Aliases]
[RequireContext(ContextType.Guild)]
[BotPerm(ChannelPerm.CreateInstantInvite)]
[UserPerm(ChannelPerm.CreateInstantInvite)]
[NadekoOptions(typeof(InviteService.Options))]
public async Task InviteCreate(params string[] args)
var (opts, success) = OptionsParser.ParseFrom(new InviteService.Options(), args);
if (!success)
return;
var ch = (ITextChannel)ctx.Channel;
var invite = await ch.CreateInviteAsync(opts.Expire, opts.MaxUses, isTemporary: opts.Temporary, isUnique: opts.Unique).ConfigureAwait(false);
await SendConfirmAsync($"{ctx.User.Mention} https://discord.gg/{invite.Code}").ConfigureAwait(false);
}
[NadekoCommand, Aliases]
[RequireContext(ContextType.Guild)]
[BotPerm(ChannelPerm.ManageChannel)]
[UserPerm(ChannelPerm.ManageChannel)]
public async Task InviteList(int page = 1, [Leftover]ITextChannel ch = null)
{
if (--page < 0)
return;
var channel = ch ?? (ITextChannel)ctx.Channel;
var invites = await channel.GetInvitesAsync().ConfigureAwait(false);
await ctx.SendPaginatedConfirmAsync(page, (cur) =>
{
var (opts, success) = OptionsParser.ParseFrom(new InviteService.Options(), args);
if (!success)
return;
var ch = (ITextChannel)ctx.Channel;
var invite = await ch.CreateInviteAsync(opts.Expire, opts.MaxUses, isTemporary: opts.Temporary, isUnique: opts.Unique).ConfigureAwait(false);
await SendConfirmAsync($"{ctx.User.Mention} https://discord.gg/{invite.Code}").ConfigureAwait(false);
}
[NadekoCommand, Aliases]
[RequireContext(ContextType.Guild)]
[BotPerm(ChannelPerm.ManageChannel)]
[UserPerm(ChannelPerm.ManageChannel)]
public async Task InviteList(int page = 1, [Leftover]ITextChannel ch = null)
{
if (--page < 0)
return;
var channel = ch ?? (ITextChannel)ctx.Channel;
var invites = await channel.GetInvitesAsync().ConfigureAwait(false);
await ctx.SendPaginatedConfirmAsync(page, (cur) =>
{
var i = 1;
var invs = invites
.Skip(cur * 9)
.Take(9)
.ToList();
var i = 1;
var invs = invites
.Skip(cur * 9)
.Take(9)
.ToList();
if (!invs.Any())
{
return _eb.Create()
.WithErrorColor()
.WithDescription(GetText(strs.no_invites));
}
if (!invs.Any())
{
return _eb.Create()
.WithErrorColor()
.WithDescription(GetText(strs.no_invites));
}
var embed = _eb.Create().WithOkColor();
foreach (var inv in invites)
{
var expiryString = (inv.MaxAge is null || inv.MaxAge == 0 || inv.CreatedAt is null)
? "∞"
: (inv.CreatedAt.Value.AddSeconds(inv.MaxAge.Value).UtcDateTime - DateTime.UtcNow)
.ToString(@"d\.hh\:mm\:ss");
var creator = inv.Inviter.ToString().TrimTo(25);
var usesString = $"{inv.Uses} / {(inv.MaxUses == 0 ? "" : inv.MaxUses?.ToString())}";
var embed = _eb.Create().WithOkColor();
foreach (var inv in invites)
{
var expiryString = (inv.MaxAge is null || inv.MaxAge == 0 || inv.CreatedAt is null)
? "∞"
: (inv.CreatedAt.Value.AddSeconds(inv.MaxAge.Value).UtcDateTime - DateTime.UtcNow)
.ToString(@"d\.hh\:mm\:ss");
var creator = inv.Inviter.ToString().TrimTo(25);
var usesString = $"{inv.Uses} / {(inv.MaxUses == 0 ? "" : inv.MaxUses?.ToString())}";
var desc = $@"`{GetText(strs.inv_uses)}` **{usesString}**
var desc = $@"`{GetText(strs.inv_uses)}` **{usesString}**
`{GetText(strs.inv_expire)}` **{expiryString}**
{inv.Url} ";
embed.AddField($"#{i++} {creator}", desc);
}
embed.AddField($"#{i++} {creator}", desc);
}
return embed;
return embed;
}, invites.Count, 9).ConfigureAwait(false);
}
}, invites.Count, 9).ConfigureAwait(false);
}
[NadekoCommand, Aliases]
[RequireContext(ContextType.Guild)]
[BotPerm(ChannelPerm.ManageChannel)]
[UserPerm(ChannelPerm.ManageChannel)]
public async Task InviteDelete(int index)
{
if (--index < 0)
return;
[NadekoCommand, Aliases]
[RequireContext(ContextType.Guild)]
[BotPerm(ChannelPerm.ManageChannel)]
[UserPerm(ChannelPerm.ManageChannel)]
public async Task InviteDelete(int index)
{
if (--index < 0)
return;
var ch = (ITextChannel)ctx.Channel;
var ch = (ITextChannel)ctx.Channel;
var invites = await ch.GetInvitesAsync().ConfigureAwait(false);
var invites = await ch.GetInvitesAsync().ConfigureAwait(false);
if (invites.Count <= index)
return;
var inv = invites.ElementAt(index);
await inv.DeleteAsync().ConfigureAwait(false);
if (invites.Count <= index)
return;
var inv = invites.ElementAt(index);
await inv.DeleteAsync().ConfigureAwait(false);
await ReplyAsync(GetText(strs.invite_deleted(Format.Bold(inv.Code))));
}
await ReplyAsync(GetText(strs.invite_deleted(Format.Bold(inv.Code))));
}
}
}
}

View File

@@ -1,54 +1,50 @@
using System.Threading.Tasks;
using Discord.Commands;
using System;
using NadekoBot.Services;
using NadekoBot.Extensions;
using Discord;
using NadekoBot.Common.Attributes;
using NadekoBot.Modules.Utility.Services;
using Serilog;
namespace NadekoBot.Modules.Utility
namespace NadekoBot.Modules.Utility;
public partial class Utility
{
public partial class Utility
[Group]
public class PatreonCommands : NadekoSubmodule<PatreonRewardsService>
{
[Group]
public class PatreonCommands : NadekoSubmodule<PatreonRewardsService>
private readonly IBotCredentials _creds;
public PatreonCommands(IBotCredentials creds)
{
private readonly IBotCredentials _creds;
_creds = creds;
}
public PatreonCommands(IBotCredentials creds)
[NadekoCommand, Aliases]
[RequireContext(ContextType.DM)]
public async Task ClaimPatreonRewards()
{
if (string.IsNullOrWhiteSpace(_creds.Patreon.AccessToken))
{
_creds = creds;
Log.Warning("In order to use patreon reward commands, " +
"you need to specify CampaignId and AccessToken in creds.yml");
return;
}
[NadekoCommand, Aliases]
[RequireContext(ContextType.DM)]
public async Task ClaimPatreonRewards()
if (DateTime.UtcNow.Day < 5)
{
if (string.IsNullOrWhiteSpace(_creds.Patreon.AccessToken))
{
Log.Warning("In order to use patreon reward commands, " +
"you need to specify CampaignId and AccessToken in creds.yml");
return;
}
if (DateTime.UtcNow.Day < 5)
{
await ReplyErrorLocalizedAsync(strs.clpa_too_early).ConfigureAwait(false);
return;
}
await ReplyErrorLocalizedAsync(strs.clpa_too_early).ConfigureAwait(false);
return;
}
var rem = (_service.Interval - (DateTime.UtcNow - _service.LastUpdate));
var helpcmd = Format.Code(Prefix + "donate");
await ctx.Channel.EmbedAsync(_eb.Create().WithOkColor()
.WithDescription(GetText(strs.clpa_obsolete))
.AddField(GetText(strs.clpa_fail_already_title), GetText(strs.clpa_fail_already))
.AddField(GetText(strs.clpa_fail_wait_title), GetText(strs.clpa_fail_wait))
.AddField(GetText(strs.clpa_fail_conn_title), GetText(strs.clpa_fail_conn))
.AddField(GetText(strs.clpa_fail_sup_title), GetText(strs.clpa_fail_sup(helpcmd)))
.WithFooter(GetText(strs.clpa_next_update(rem))));
}
var rem = (_service.Interval - (DateTime.UtcNow - _service.LastUpdate));
var helpcmd = Format.Code(Prefix + "donate");
await ctx.Channel.EmbedAsync(_eb.Create().WithOkColor()
.WithDescription(GetText(strs.clpa_obsolete))
.AddField(GetText(strs.clpa_fail_already_title), GetText(strs.clpa_fail_already))
.AddField(GetText(strs.clpa_fail_wait_title), GetText(strs.clpa_fail_wait))
.AddField(GetText(strs.clpa_fail_conn_title), GetText(strs.clpa_fail_conn))
.AddField(GetText(strs.clpa_fail_sup_title), GetText(strs.clpa_fail_sup(helpcmd)))
.WithFooter(GetText(strs.clpa_next_update(rem))));
}
}
}

View File

@@ -3,10 +3,7 @@ using Discord.Commands;
using NadekoBot.Common;
using NadekoBot.Common.Attributes;
using NadekoBot.Common.Replacements;
using NadekoBot.Db.Models;
using NadekoBot.Extensions;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Threading.Tasks;
using NadekoBot.Common.Yml;
@@ -15,378 +12,377 @@ using NadekoBot.Services.Database.Models;
using NadekoBot.Db;
using YamlDotNet.Serialization;
namespace NadekoBot.Modules.Utility
namespace NadekoBot.Modules.Utility;
public partial class Utility
{
public partial class Utility
[Group]
public class QuoteCommands : NadekoSubmodule
{
[Group]
public class QuoteCommands : NadekoSubmodule
private readonly DbService _db;
private readonly IHttpClientFactory _http;
public QuoteCommands(DbService db, IHttpClientFactory http)
{
private readonly DbService _db;
private readonly IHttpClientFactory _http;
_db = db;
_http = http;
}
public QuoteCommands(DbService db, IHttpClientFactory http)
[NadekoCommand, Aliases]
[RequireContext(ContextType.Guild)]
[Priority(1)]
public Task ListQuotes(OrderType order = OrderType.Keyword)
=> ListQuotes(1, order);
[NadekoCommand, Aliases]
[RequireContext(ContextType.Guild)]
[Priority(0)]
public async Task ListQuotes(int page = 1, OrderType order = OrderType.Keyword)
{
page -= 1;
if (page < 0)
return;
IEnumerable<Quote> quotes;
using (var uow = _db.GetDbContext())
{
_db = db;
_http = http;
quotes = uow.Quotes.GetGroup(ctx.Guild.Id, page, order);
}
[NadekoCommand, Aliases]
[RequireContext(ContextType.Guild)]
[Priority(1)]
public Task ListQuotes(OrderType order = OrderType.Keyword)
=> ListQuotes(1, order);
if (quotes.Any())
await SendConfirmAsync(GetText(strs.quotes_page(page + 1)),
string.Join("\n", quotes.Select(q => $"`#{q.Id}` {Format.Bold(q.Keyword.SanitizeAllMentions()),-20} by {q.AuthorName.SanitizeAllMentions()}")))
.ConfigureAwait(false);
else
await ReplyErrorLocalizedAsync(strs.quotes_page_none).ConfigureAwait(false);
}
[NadekoCommand, Aliases]
[RequireContext(ContextType.Guild)]
[Priority(0)]
public async Task ListQuotes(int page = 1, OrderType order = OrderType.Keyword)
[NadekoCommand, Aliases]
[RequireContext(ContextType.Guild)]
public async Task QuotePrint([Leftover] string keyword)
{
if (string.IsNullOrWhiteSpace(keyword))
return;
keyword = keyword.ToUpperInvariant();
Quote quote;
using (var uow = _db.GetDbContext())
{
page -= 1;
if (page < 0)
return;
IEnumerable<Quote> quotes;
using (var uow = _db.GetDbContext())
{
quotes = uow.Quotes.GetGroup(ctx.Guild.Id, page, order);
}
if (quotes.Any())
await SendConfirmAsync(GetText(strs.quotes_page(page + 1)),
string.Join("\n", quotes.Select(q => $"`#{q.Id}` {Format.Bold(q.Keyword.SanitizeAllMentions()),-20} by {q.AuthorName.SanitizeAllMentions()}")))
.ConfigureAwait(false);
else
await ReplyErrorLocalizedAsync(strs.quotes_page_none).ConfigureAwait(false);
quote = await uow.Quotes.GetRandomQuoteByKeywordAsync(ctx.Guild.Id, keyword);
//if (quote != null)
//{
// quote.UseCount += 1;
// uow.Complete();
//}
}
[NadekoCommand, Aliases]
[RequireContext(ContextType.Guild)]
public async Task QuotePrint([Leftover] string keyword)
if (quote is null)
return;
var rep = new ReplacementBuilder()
.WithDefault(Context)
.Build();
var text = SmartText.CreateFrom(quote.Text);
text = rep.Replace(text);
await ctx.Channel.SendAsync($"`#{quote.Id}` 📣 " + text, true);
}
[NadekoCommand, Aliases]
[RequireContext(ContextType.Guild)]
public async Task QuoteShow(int id)
{
Quote quote;
using (var uow = _db.GetDbContext())
{
if (string.IsNullOrWhiteSpace(keyword))
return;
keyword = keyword.ToUpperInvariant();
Quote quote;
using (var uow = _db.GetDbContext())
{
quote = await uow.Quotes.GetRandomQuoteByKeywordAsync(ctx.Guild.Id, keyword);
//if (quote != null)
//{
// quote.UseCount += 1;
// uow.Complete();
//}
}
if (quote is null)
return;
var rep = new ReplacementBuilder()
.WithDefault(Context)
.Build();
var text = SmartText.CreateFrom(quote.Text);
text = rep.Replace(text);
await ctx.Channel.SendAsync($"`#{quote.Id}` 📣 " + text, true);
quote = uow.Quotes.GetById(id);
if (quote?.GuildId != ctx.Guild.Id)
quote = null;
}
[NadekoCommand, Aliases]
[RequireContext(ContextType.Guild)]
public async Task QuoteShow(int id)
if (quote is null)
{
Quote quote;
using (var uow = _db.GetDbContext())
{
quote = uow.Quotes.GetById(id);
if (quote?.GuildId != ctx.Guild.Id)
quote = null;
}
if (quote is null)
{
await ReplyErrorLocalizedAsync(strs.quotes_notfound);
return;
}
await ShowQuoteData(quote);
await ReplyErrorLocalizedAsync(strs.quotes_notfound);
return;
}
private async Task ShowQuoteData(Quote data)
await ShowQuoteData(quote);
}
private async Task ShowQuoteData(Quote data)
{
await ctx.Channel.EmbedAsync(_eb.Create(ctx)
.WithOkColor()
.WithTitle(GetText(strs.quote_id($"#{data.Id}")))
.AddField(GetText(strs.trigger), data.Keyword)
.AddField(GetText(strs.response), Format.Sanitize(data.Text).Replace("](", "]\\("))
.WithFooter(GetText(strs.created_by($"{data.AuthorName} ({data.AuthorId})")))
).ConfigureAwait(false);
}
[NadekoCommand, Aliases]
[RequireContext(ContextType.Guild)]
public async Task QuoteSearch(string keyword, [Leftover] string text)
{
if (string.IsNullOrWhiteSpace(keyword) || string.IsNullOrWhiteSpace(text))
return;
keyword = keyword.ToUpperInvariant();
Quote keywordquote;
using (var uow = _db.GetDbContext())
{
await ctx.Channel.EmbedAsync(_eb.Create(ctx)
.WithOkColor()
.WithTitle(GetText(strs.quote_id($"#{data.Id}")))
.AddField(GetText(strs.trigger), data.Keyword)
.AddField(GetText(strs.response), Format.Sanitize(data.Text).Replace("](", "]\\("))
.WithFooter(GetText(strs.created_by($"{data.AuthorName} ({data.AuthorId})")))
).ConfigureAwait(false);
keywordquote = await uow.Quotes.SearchQuoteKeywordTextAsync(ctx.Guild.Id, keyword, text);
}
[NadekoCommand, Aliases]
[RequireContext(ContextType.Guild)]
public async Task QuoteSearch(string keyword, [Leftover] string text)
if (keywordquote is null)
return;
await ctx.Channel.SendMessageAsync($"`#{keywordquote.Id}` 💬 " + keyword.ToLowerInvariant() + ": " +
keywordquote.Text.SanitizeAllMentions()).ConfigureAwait(false);
}
[NadekoCommand, Aliases]
[RequireContext(ContextType.Guild)]
public async Task QuoteId(int id)
{
if (id < 0)
return;
Quote quote;
var rep = new ReplacementBuilder()
.WithDefault(Context)
.Build();
using (var uow = _db.GetDbContext())
{
if (string.IsNullOrWhiteSpace(keyword) || string.IsNullOrWhiteSpace(text))
return;
keyword = keyword.ToUpperInvariant();
Quote keywordquote;
using (var uow = _db.GetDbContext())
{
keywordquote = await uow.Quotes.SearchQuoteKeywordTextAsync(ctx.Guild.Id, keyword, text);
}
if (keywordquote is null)
return;
await ctx.Channel.SendMessageAsync($"`#{keywordquote.Id}` 💬 " + keyword.ToLowerInvariant() + ": " +
keywordquote.Text.SanitizeAllMentions()).ConfigureAwait(false);
quote = uow.Quotes.GetById(id);
}
[NadekoCommand, Aliases]
[RequireContext(ContextType.Guild)]
public async Task QuoteId(int id)
if (quote is null || quote.GuildId != ctx.Guild.Id)
{
if (id < 0)
return;
await SendErrorAsync(GetText(strs.quotes_notfound)).ConfigureAwait(false);
return;
}
Quote quote;
var rep = new ReplacementBuilder()
.WithDefault(Context)
.Build();
using (var uow = _db.GetDbContext())
{
quote = uow.Quotes.GetById(id);
}
if (quote is null || quote.GuildId != ctx.Guild.Id)
{
await SendErrorAsync(GetText(strs.quotes_notfound)).ConfigureAwait(false);
return;
}
var infoText = $"`#{quote.Id} added by {quote.AuthorName.SanitizeAllMentions()}` 🗯️ " + quote.Keyword.ToLowerInvariant().SanitizeAllMentions() + ":\n";
var infoText = $"`#{quote.Id} added by {quote.AuthorName.SanitizeAllMentions()}` 🗯️ " + quote.Keyword.ToLowerInvariant().SanitizeAllMentions() + ":\n";
var text = SmartText.CreateFrom(quote.Text);
text = rep.Replace(text);
await ctx.Channel.SendAsync(infoText + text, true);
}
var text = SmartText.CreateFrom(quote.Text);
text = rep.Replace(text);
await ctx.Channel.SendAsync(infoText + text, true);
}
[NadekoCommand, Aliases]
[RequireContext(ContextType.Guild)]
public async Task QuoteAdd(string keyword, [Leftover] string text)
{
if (string.IsNullOrWhiteSpace(keyword) || string.IsNullOrWhiteSpace(text))
return;
[NadekoCommand, Aliases]
[RequireContext(ContextType.Guild)]
public async Task QuoteAdd(string keyword, [Leftover] string text)
{
if (string.IsNullOrWhiteSpace(keyword) || string.IsNullOrWhiteSpace(text))
return;
keyword = keyword.ToUpperInvariant();
keyword = keyword.ToUpperInvariant();
Quote q;
using (var uow = _db.GetDbContext())
{
uow.Quotes.Add(q = new Quote
{
AuthorId = ctx.Message.Author.Id,
AuthorName = ctx.Message.Author.Username,
GuildId = ctx.Guild.Id,
Keyword = keyword,
Text = text,
});
await uow.SaveChangesAsync();
}
await ReplyConfirmLocalizedAsync(strs.quote_added_new(Format.Code(q.Id.ToString()))).ConfigureAwait(false);
}
[NadekoCommand, Aliases]
[RequireContext(ContextType.Guild)]
public async Task QuoteDelete(int id)
Quote q;
using (var uow = _db.GetDbContext())
{
var hasManageMessages = ((IGuildUser)ctx.Message.Author).GuildPermissions.ManageMessages;
var success = false;
string response;
using (var uow = _db.GetDbContext())
uow.Quotes.Add(q = new Quote
{
var q = uow.Quotes.GetById(id);
AuthorId = ctx.Message.Author.Id,
AuthorName = ctx.Message.Author.Username,
GuildId = ctx.Guild.Id,
Keyword = keyword,
Text = text,
});
await uow.SaveChangesAsync();
}
await ReplyConfirmLocalizedAsync(strs.quote_added_new(Format.Code(q.Id.ToString()))).ConfigureAwait(false);
}
if ((q?.GuildId != ctx.Guild.Id) || (!hasManageMessages && q.AuthorId != ctx.Message.Author.Id))
{
response = GetText(strs.quotes_remove_none);
}
else
{
uow.Quotes.Remove(q);
await uow.SaveChangesAsync();
success = true;
response = GetText(strs.quote_deleted(id));
}
[NadekoCommand, Aliases]
[RequireContext(ContextType.Guild)]
public async Task QuoteDelete(int id)
{
var hasManageMessages = ((IGuildUser)ctx.Message.Author).GuildPermissions.ManageMessages;
var success = false;
string response;
using (var uow = _db.GetDbContext())
{
var q = uow.Quotes.GetById(id);
if ((q?.GuildId != ctx.Guild.Id) || (!hasManageMessages && q.AuthorId != ctx.Message.Author.Id))
{
response = GetText(strs.quotes_remove_none);
}
if (success)
await SendConfirmAsync(response).ConfigureAwait(false);
else
await SendErrorAsync(response).ConfigureAwait(false);
}
[NadekoCommand, Aliases]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.ManageMessages)]
public async Task DelAllQuotes([Leftover] string keyword)
{
if (string.IsNullOrWhiteSpace(keyword))
return;
keyword = keyword.ToUpperInvariant();
using (var uow = _db.GetDbContext())
{
uow.Quotes.RemoveAllByKeyword(ctx.Guild.Id, keyword.ToUpperInvariant());
uow.Quotes.Remove(q);
await uow.SaveChangesAsync();
success = true;
response = GetText(strs.quote_deleted(id));
}
await ReplyConfirmLocalizedAsync(strs.quotes_deleted(Format.Bold(keyword.SanitizeAllMentions()))).ConfigureAwait(false);
}
if (success)
await SendConfirmAsync(response).ConfigureAwait(false);
else
await SendErrorAsync(response).ConfigureAwait(false);
}
public class ExportedQuote
[NadekoCommand, Aliases]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.ManageMessages)]
public async Task DelAllQuotes([Leftover] string keyword)
{
if (string.IsNullOrWhiteSpace(keyword))
return;
keyword = keyword.ToUpperInvariant();
using (var uow = _db.GetDbContext())
{
public static ExportedQuote FromModel(Quote quote)
=> new ExportedQuote()
{
Id = ((kwum)quote.Id).ToString(),
An = quote.AuthorName,
Aid = quote.AuthorId,
Txt = quote.Text
};
uow.Quotes.RemoveAllByKeyword(ctx.Guild.Id, keyword.ToUpperInvariant());
public string Id { get; set; }
public string An { get; set; }
public ulong Aid { get; set; }
public string Txt { get; set; }
await uow.SaveChangesAsync();
}
await ReplyConfirmLocalizedAsync(strs.quotes_deleted(Format.Bold(keyword.SanitizeAllMentions()))).ConfigureAwait(false);
}
public class ExportedQuote
{
public static ExportedQuote FromModel(Quote quote)
=> new ExportedQuote()
{
Id = ((kwum)quote.Id).ToString(),
An = quote.AuthorName,
Aid = quote.AuthorId,
Txt = quote.Text
};
public string Id { get; set; }
public string An { get; set; }
public ulong Aid { get; set; }
public string Txt { get; set; }
}
private const string _prependExport =
@"# Keys are keywords, Each key has a LIST of quotes in the following format:
private const string _prependExport =
@"# Keys are keywords, Each key has a LIST of quotes in the following format:
# - id: Alphanumeric id used for commands related to the quote. (Note, when using .quotesimport, a new id will be generated.)
# an: Author name
# aid: Author id
# txt: Quote text
";
private static readonly ISerializer _exportSerializer = new SerializerBuilder()
.WithEventEmitter(args => new MultilineScalarFlowStyleEmitter(args))
.WithNamingConvention(YamlDotNet.Serialization.NamingConventions.CamelCaseNamingConvention.Instance)
.WithIndentedSequences()
.ConfigureDefaultValuesHandling(DefaultValuesHandling.OmitDefaults)
.DisableAliases()
.Build();
private static readonly ISerializer _exportSerializer = new SerializerBuilder()
.WithEventEmitter(args => new MultilineScalarFlowStyleEmitter(args))
.WithNamingConvention(YamlDotNet.Serialization.NamingConventions.CamelCaseNamingConvention.Instance)
.WithIndentedSequences()
.ConfigureDefaultValuesHandling(DefaultValuesHandling.OmitDefaults)
.DisableAliases()
.Build();
[NadekoCommand, Aliases]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.Administrator)]
public async Task QuotesExport()
[NadekoCommand, Aliases]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.Administrator)]
public async Task QuotesExport()
{
IEnumerable<Quote> quotes;
using (var uow = _db.GetDbContext())
{
IEnumerable<Quote> quotes;
using (var uow = _db.GetDbContext())
{
quotes = uow.Quotes
.GetForGuild(ctx.Guild.Id)
.ToList();
}
var crsDict = quotes
.GroupBy(x => x.Keyword)
.ToDictionary(x => x.Key, x => x.Select(ExportedQuote.FromModel));
var text = _prependExport + _exportSerializer
.Serialize(crsDict)
.UnescapeUnicodeCodePoints();
await using var stream = await text.ToStream();
await ctx.Channel.SendFileAsync(stream, "quote-export.yml", text: null);
quotes = uow.Quotes
.GetForGuild(ctx.Guild.Id)
.ToList();
}
[NadekoCommand, Aliases]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.Administrator)]
[Ratelimit(300)]
var crsDict = quotes
.GroupBy(x => x.Keyword)
.ToDictionary(x => x.Key, x => x.Select(ExportedQuote.FromModel));
var text = _prependExport + _exportSerializer
.Serialize(crsDict)
.UnescapeUnicodeCodePoints();
await using var stream = await text.ToStream();
await ctx.Channel.SendFileAsync(stream, "quote-export.yml", text: null);
}
[NadekoCommand, Aliases]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.Administrator)]
[Ratelimit(300)]
#if GLOBAL_NADEKO
[OwnerOnly]
#endif
public async Task QuotesImport([Leftover]string input = null)
public async Task QuotesImport([Leftover]string input = null)
{
input = input?.Trim();
_ = ctx.Channel.TriggerTypingAsync();
if (input is null)
{
input = input?.Trim();
_ = ctx.Channel.TriggerTypingAsync();
if (input is null)
var attachment = ctx.Message.Attachments.FirstOrDefault();
if (attachment is null)
{
var attachment = ctx.Message.Attachments.FirstOrDefault();
if (attachment is null)
{
await ReplyErrorLocalizedAsync(strs.expr_import_no_input);
return;
}
using var client = _http.CreateClient();
input = await client.GetStringAsync(attachment.Url);
if (string.IsNullOrWhiteSpace(input))
{
await ReplyErrorLocalizedAsync(strs.expr_import_no_input);
return;
}
}
var succ = await ImportCrsAsync(ctx.Guild.Id, input);
if (!succ)
{
await ReplyErrorLocalizedAsync(strs.expr_import_invalid_data);
await ReplyErrorLocalizedAsync(strs.expr_import_no_input);
return;
}
using var client = _http.CreateClient();
input = await client.GetStringAsync(attachment.Url);
if (string.IsNullOrWhiteSpace(input))
{
await ReplyErrorLocalizedAsync(strs.expr_import_no_input);
return;
}
await ctx.OkAsync();
}
public async Task<bool> ImportCrsAsync(ulong guildId, string input)
var succ = await ImportCrsAsync(ctx.Guild.Id, input);
if (!succ)
{
Dictionary<string, List<ExportedQuote>> data;
try
{
data = Yaml.Deserializer.Deserialize<Dictionary<string, List<ExportedQuote>>>(input);
if (data.Sum(x => x.Value.Count) == 0)
return false;
}
catch
{
return false;
}
await using var uow = _db.GetDbContext();
foreach (var entry in data)
{
var keyword = entry.Key;
await uow.Quotes
.AddRangeAsync(entry.Value
.Where(quote => !string.IsNullOrWhiteSpace(quote.Txt))
.Select(quote => new Quote()
{
GuildId = guildId,
Keyword = keyword,
Text = quote.Txt,
AuthorId = quote.Aid,
AuthorName = quote.An,
}));
}
await uow.SaveChangesAsync();
return true;
await ReplyErrorLocalizedAsync(strs.expr_import_invalid_data);
return;
}
await ctx.OkAsync();
}
public async Task<bool> ImportCrsAsync(ulong guildId, string input)
{
Dictionary<string, List<ExportedQuote>> data;
try
{
data = Yaml.Deserializer.Deserialize<Dictionary<string, List<ExportedQuote>>>(input);
if (data.Sum(x => x.Value.Count) == 0)
return false;
}
catch
{
return false;
}
await using var uow = _db.GetDbContext();
foreach (var entry in data)
{
var keyword = entry.Key;
await uow.Quotes
.AddRangeAsync(entry.Value
.Where(quote => !string.IsNullOrWhiteSpace(quote.Txt))
.Select(quote => new Quote()
{
GuildId = guildId,
Keyword = keyword,
Text = quote.Txt,
AuthorId = quote.Aid,
AuthorName = quote.An,
}));
}
await uow.SaveChangesAsync();
return true;
}
}
}
}

View File

@@ -1,7 +1,4 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using System.Threading.Tasks;
using Discord;
using Discord.Commands;
using NadekoBot.Common.Attributes;
@@ -12,240 +9,239 @@ using NadekoBot.Extensions;
using NadekoBot.Modules.Administration.Services;
using NadekoBot.Modules.Utility.Services;
namespace NadekoBot.Modules.Utility
namespace NadekoBot.Modules.Utility;
public partial class Utility
{
public partial class Utility
[Group]
public class RemindCommands : NadekoSubmodule<RemindService>
{
[Group]
public class RemindCommands : NadekoSubmodule<RemindService>
private readonly DbService _db;
private readonly GuildTimezoneService _tz;
public RemindCommands(DbService db, GuildTimezoneService tz)
{
private readonly DbService _db;
private readonly GuildTimezoneService _tz;
_db = db;
_tz = tz;
}
public RemindCommands(DbService db, GuildTimezoneService tz)
public enum MeOrHere
{
Me,
Here
}
[NadekoCommand, Aliases]
[Priority(1)]
public async Task Remind(MeOrHere meorhere, [Leftover] string remindString)
{
if (!_service.TryParseRemindMessage(remindString, out var remindData))
{
_db = db;
_tz = tz;
await ReplyErrorLocalizedAsync(strs.remind_invalid);
return;
}
public enum MeOrHere
{
Me,
Here
}
[NadekoCommand, Aliases]
[Priority(1)]
public async Task Remind(MeOrHere meorhere, [Leftover] string remindString)
{
if (!_service.TryParseRemindMessage(remindString, out var remindData))
{
await ReplyErrorLocalizedAsync(strs.remind_invalid);
return;
}
ulong target;
target = meorhere == MeOrHere.Me ? ctx.User.Id : ctx.Channel.Id;
if (!await RemindInternal(target, meorhere == MeOrHere.Me || ctx.Guild is null, remindData.Time, remindData.What)
ulong target;
target = meorhere == MeOrHere.Me ? ctx.User.Id : ctx.Channel.Id;
if (!await RemindInternal(target, meorhere == MeOrHere.Me || ctx.Guild is null, remindData.Time, remindData.What)
.ConfigureAwait(false))
{
await ReplyErrorLocalizedAsync(strs.remind_too_long).ConfigureAwait(false);
}
{
await ReplyErrorLocalizedAsync(strs.remind_too_long).ConfigureAwait(false);
}
}
[NadekoCommand, Aliases]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.ManageMessages)]
[Priority(0)]
public async Task Remind(ITextChannel channel, [Leftover] string remindString)
{
var perms = ((IGuildUser) ctx.User).GetPermissions(channel);
if (!perms.SendMessages || !perms.ViewChannel)
{
await ReplyErrorLocalizedAsync(strs.cant_read_or_send).ConfigureAwait(false);
return;
}
[NadekoCommand, Aliases]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.ManageMessages)]
[Priority(0)]
public async Task Remind(ITextChannel channel, [Leftover] string remindString)
if (!_service.TryParseRemindMessage(remindString, out var remindData))
{
var perms = ((IGuildUser) ctx.User).GetPermissions(channel);
if (!perms.SendMessages || !perms.ViewChannel)
{
await ReplyErrorLocalizedAsync(strs.cant_read_or_send).ConfigureAwait(false);
return;
}
if (!_service.TryParseRemindMessage(remindString, out var remindData))
{
await ReplyErrorLocalizedAsync(strs.remind_invalid);
return;
}
await ReplyErrorLocalizedAsync(strs.remind_invalid);
return;
}
if (!await RemindInternal(channel.Id, false, remindData.Time, remindData.What)
if (!await RemindInternal(channel.Id, false, remindData.Time, remindData.What)
.ConfigureAwait(false))
{
await ReplyErrorLocalizedAsync(strs.remind_too_long).ConfigureAwait(false);
}
}
public enum Server
{
Server = int.MinValue,
Srvr = int.MinValue,
Serv = int.MinValue,
S = int.MinValue,
}
[NadekoCommand, Aliases]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.Administrator)]
[Priority(0)]
public Task RemindList(Server _, int page = 1)
=> RemindList(page, true);
[NadekoCommand, Aliases]
[Priority(1)]
public Task RemindList(int page = 1)
=> RemindList(page, false);
private async Task RemindList(int page, bool isServer)
{
if (--page < 0)
return;
var embed = _eb.Create()
.WithOkColor()
.WithTitle(GetText(isServer ? strs.reminder_server_list : strs.reminder_list));
List<Reminder> rems;
using (var uow = _db.GetDbContext())
{
if (isServer)
{
await ReplyErrorLocalizedAsync(strs.remind_too_long).ConfigureAwait(false);
rems = uow.Reminders
.RemindersForServer(ctx.Guild.Id, page)
.ToList();
}
else
{
rems = uow.Reminders
.RemindersFor(ctx.User.Id, page)
.ToList();
}
}
public enum Server
if (rems.Any())
{
Server = int.MinValue,
Srvr = int.MinValue,
Serv = int.MinValue,
S = int.MinValue,
}
[NadekoCommand, Aliases]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.Administrator)]
[Priority(0)]
public Task RemindList(Server _, int page = 1)
=> RemindList(page, true);
[NadekoCommand, Aliases]
[Priority(1)]
public Task RemindList(int page = 1)
=> RemindList(page, false);
private async Task RemindList(int page, bool isServer)
{
if (--page < 0)
return;
var embed = _eb.Create()
.WithOkColor()
.WithTitle(GetText(isServer ? strs.reminder_server_list : strs.reminder_list));
List<Reminder> rems;
using (var uow = _db.GetDbContext())
var i = 0;
foreach (var rem in rems)
{
if (isServer)
{
rems = uow.Reminders
.RemindersForServer(ctx.Guild.Id, page)
.ToList();
}
else
{
rems = uow.Reminders
.RemindersFor(ctx.User.Id, page)
.ToList();
}
}
if (rems.Any())
{
var i = 0;
foreach (var rem in rems)
{
var when = rem.When;
var diff = when - DateTime.UtcNow;
embed.AddField(
$"#{++i + (page * 10)} {rem.When:HH:mm yyyy-MM-dd} UTC (in {(int) diff.TotalHours}h {(int) diff.Minutes}m)",
$@"`Target:` {(rem.IsPrivate ? "DM" : "Channel")}
var when = rem.When;
var diff = when - DateTime.UtcNow;
embed.AddField(
$"#{++i + (page * 10)} {rem.When:HH:mm yyyy-MM-dd} UTC (in {(int) diff.TotalHours}h {(int) diff.Minutes}m)",
$@"`Target:` {(rem.IsPrivate ? "DM" : "Channel")}
`TargetId:` {rem.ChannelId}
`Message:` {rem.Message?.TrimTo(50)}", false);
}
}
else
{
embed.WithDescription(GetText(strs.reminders_none));
}
embed.AddPaginatedFooter(page + 1, null);
await ctx.Channel.EmbedAsync(embed).ConfigureAwait(false);
}
else
{
embed.WithDescription(GetText(strs.reminders_none));
}
[NadekoCommand, Aliases]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.Administrator)]
[Priority(0)]
public Task RemindDelete(Server _, int index)
=> RemindDelete(index, true);
[NadekoCommand, Aliases]
[Priority(1)]
public Task RemindDelete(int index)
=> RemindDelete(index, false);
private async Task RemindDelete(int index, bool isServer)
{
if (--index < 0)
return;
embed.AddPaginatedFooter(page + 1, null);
await ctx.Channel.EmbedAsync(embed).ConfigureAwait(false);
}
Reminder rem = null;
using (var uow = _db.GetDbContext())
{
var rems = isServer
? uow.Reminders
.RemindersForServer(ctx.Guild.Id, index / 10)
.ToList()
: uow.Reminders
.RemindersFor(ctx.User.Id, index / 10)
.ToList();
[NadekoCommand, Aliases]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.Administrator)]
[Priority(0)]
public Task RemindDelete(Server _, int index)
=> RemindDelete(index, true);
[NadekoCommand, Aliases]
[Priority(1)]
public Task RemindDelete(int index)
=> RemindDelete(index, false);
private async Task RemindDelete(int index, bool isServer)
{
if (--index < 0)
return;
Reminder rem = null;
using (var uow = _db.GetDbContext())
{
var rems = isServer
? uow.Reminders
.RemindersForServer(ctx.Guild.Id, index / 10)
.ToList()
: uow.Reminders
.RemindersFor(ctx.User.Id, index / 10)
.ToList();
var pageIndex = index % 10;
if (rems.Count > pageIndex)
{
rem = rems[pageIndex];
uow.Reminders.Remove(rem);
uow.SaveChanges();
}
}
if (rem is null)
var pageIndex = index % 10;
if (rems.Count > pageIndex)
{
await ReplyErrorLocalizedAsync(strs.reminder_not_exist).ConfigureAwait(false);
}
else
{
await ReplyConfirmLocalizedAsync(strs.reminder_deleted(index + 1));
rem = rems[pageIndex];
uow.Reminders.Remove(rem);
uow.SaveChanges();
}
}
private async Task<bool> RemindInternal(ulong targetId, bool isPrivate, TimeSpan ts, string message)
if (rem is null)
{
var time = DateTime.UtcNow + ts;
if (ts > TimeSpan.FromDays(60))
return false;
if (ctx.Guild != null)
{
var perms = ((IGuildUser) ctx.User).GetPermissions((IGuildChannel) ctx.Channel);
if (!perms.MentionEveryone)
{
message = message.SanitizeAllMentions();
}
}
var rem = new Reminder
{
ChannelId = targetId,
IsPrivate = isPrivate,
When = time,
Message = message,
UserId = ctx.User.Id,
ServerId = ctx.Guild?.Id ?? 0
};
using (var uow = _db.GetDbContext())
{
uow.Reminders.Add(rem);
await uow.SaveChangesAsync();
}
var gTime = ctx.Guild is null
? time
: TimeZoneInfo.ConvertTime(time, _tz.GetTimeZoneOrUtc(ctx.Guild.Id));
try
{
await SendConfirmAsync(
"⏰ " + GetText(strs.remind(
Format.Bold(!isPrivate ? $"<#{targetId}>" : ctx.User.Username),
Format.Bold(message),
$"{ts.Days}d {ts.Hours}h {ts.Minutes}min",
gTime, gTime))).ConfigureAwait(false);
}
catch
{
}
return true;
await ReplyErrorLocalizedAsync(strs.reminder_not_exist).ConfigureAwait(false);
}
else
{
await ReplyConfirmLocalizedAsync(strs.reminder_deleted(index + 1));
}
}
private async Task<bool> RemindInternal(ulong targetId, bool isPrivate, TimeSpan ts, string message)
{
var time = DateTime.UtcNow + ts;
if (ts > TimeSpan.FromDays(60))
return false;
if (ctx.Guild != null)
{
var perms = ((IGuildUser) ctx.User).GetPermissions((IGuildChannel) ctx.Channel);
if (!perms.MentionEveryone)
{
message = message.SanitizeAllMentions();
}
}
var rem = new Reminder
{
ChannelId = targetId,
IsPrivate = isPrivate,
When = time,
Message = message,
UserId = ctx.User.Id,
ServerId = ctx.Guild?.Id ?? 0
};
using (var uow = _db.GetDbContext())
{
uow.Reminders.Add(rem);
await uow.SaveChangesAsync();
}
var gTime = ctx.Guild is null
? time
: TimeZoneInfo.ConvertTime(time, _tz.GetTimeZoneOrUtc(ctx.Guild.Id));
try
{
await SendConfirmAsync(
"⏰ " + GetText(strs.remind(
Format.Bold(!isPrivate ? $"<#{targetId}>" : ctx.User.Username),
Format.Bold(message),
$"{ts.Days}d {ts.Hours}h {ts.Minutes}min",
gTime, gTime))).ConfigureAwait(false);
}
catch
{
}
return true;
}
}
}

View File

@@ -5,203 +5,200 @@ using NadekoBot.Common.Attributes;
using NadekoBot.Common.TypeReaders;
using NadekoBot.Extensions;
using NadekoBot.Modules.Utility.Services;
using System;
using System.Linq;
using System.Threading.Tasks;
using NadekoBot.Common.TypeReaders.Models;
namespace NadekoBot.Modules.Utility
namespace NadekoBot.Modules.Utility;
public partial class Utility
{
public partial class Utility
[Group]
public class RepeatCommands : NadekoSubmodule<RepeaterService>
{
[Group]
public class RepeatCommands : NadekoSubmodule<RepeaterService>
[NadekoCommand, Aliases]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.ManageMessages)]
public async Task RepeatInvoke(int index)
{
[NadekoCommand, Aliases]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.ManageMessages)]
public async Task RepeatInvoke(int index)
{
if (--index < 0)
return;
if (--index < 0)
return;
var success = await _service.TriggerExternal(ctx.Guild.Id, index);
if (!success)
{
await ReplyErrorLocalizedAsync(strs.repeat_invoke_none).ConfigureAwait(false);
}
var success = await _service.TriggerExternal(ctx.Guild.Id, index);
if (!success)
{
await ReplyErrorLocalizedAsync(strs.repeat_invoke_none).ConfigureAwait(false);
}
}
[NadekoCommand, Aliases]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.ManageMessages)]
public async Task RepeatRemove(int index)
{
if (--index < 0)
return;
var removed = await _service.RemoveByIndexAsync(ctx.Guild.Id, index);
if (removed is null)
{
await ReplyErrorLocalizedAsync(strs.repeater_remove_fail).ConfigureAwait(false);
return;
}
var description = GetRepeaterInfoString(removed);
await ctx.Channel.EmbedAsync(_eb.Create()
.WithOkColor()
.WithTitle(GetText(strs.repeater_removed(index + 1)))
.WithDescription(description));
}
[NadekoCommand, Aliases]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.ManageMessages)]
public async Task RepeatRedundant(int index)
{
if (--index < 0)
return;
var result = await _service.ToggleRedundantAsync(ctx.Guild.Id, index);
if (result is null)
{
await ReplyErrorLocalizedAsync(strs.index_out_of_range).ConfigureAwait(false);
return;
}
[NadekoCommand, Aliases]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.ManageMessages)]
public async Task RepeatRemove(int index)
if (result.Value)
{
if (--index < 0)
return;
var removed = await _service.RemoveByIndexAsync(ctx.Guild.Id, index);
if (removed is null)
{
await ReplyErrorLocalizedAsync(strs.repeater_remove_fail).ConfigureAwait(false);
return;
}
var description = GetRepeaterInfoString(removed);
await ctx.Channel.EmbedAsync(_eb.Create()
.WithOkColor()
.WithTitle(GetText(strs.repeater_removed(index + 1)))
.WithDescription(description));
await ReplyErrorLocalizedAsync(strs.repeater_redundant_no(index + 1));
}
[NadekoCommand, Aliases]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.ManageMessages)]
public async Task RepeatRedundant(int index)
else
{
if (--index < 0)
return;
await ReplyConfirmLocalizedAsync(strs.repeater_redundant_yes(index + 1));
}
}
[NadekoCommand, Aliases]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.ManageMessages)]
[Priority(-1)]
public Task Repeat([Leftover]string message)
=> Repeat(null, null, message);
[NadekoCommand, Aliases]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.ManageMessages)]
[Priority(0)]
public Task Repeat(StoopidTime interval, [Leftover]string message)
=> Repeat(null, interval, message);
[NadekoCommand, Aliases]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.ManageMessages)]
[Priority(1)]
public Task Repeat(GuildDateTime dt, [Leftover] string message)
=> Repeat(dt, null, message);
[NadekoCommand, Aliases]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.ManageMessages)]
[Priority(2)]
public async Task Repeat(GuildDateTime? dt, StoopidTime? interval, [Leftover]string message)
{
var startTimeOfDay = dt?.InputTimeUtc.TimeOfDay;
// if interval not null, that means user specified it (don't change it)
var result = await _service.ToggleRedundantAsync(ctx.Guild.Id, index);
if (result is null)
{
await ReplyErrorLocalizedAsync(strs.index_out_of_range).ConfigureAwait(false);
return;
}
if (result.Value)
{
await ReplyErrorLocalizedAsync(strs.repeater_redundant_no(index + 1));
}
else
{
await ReplyConfirmLocalizedAsync(strs.repeater_redundant_yes(index + 1));
}
// if interval is null set the default to:
// if time of day is specified: 1 day
// else 5 minutes
var realInterval = interval?.Time ?? (startTimeOfDay is null
? TimeSpan.FromMinutes(5)
: TimeSpan.FromDays(1));
if (string.IsNullOrWhiteSpace(message)
|| (interval != null &&
(interval.Time > TimeSpan.FromMinutes(25000) || interval.Time < TimeSpan.FromMinutes(1))))
{
return;
}
[NadekoCommand, Aliases]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.ManageMessages)]
[Priority(-1)]
public Task Repeat([Leftover]string message)
=> Repeat(null, null, message);
[NadekoCommand, Aliases]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.ManageMessages)]
[Priority(0)]
public Task Repeat(StoopidTime interval, [Leftover]string message)
=> Repeat(null, interval, message);
message = ((IGuildUser) ctx.User).GuildPermissions.MentionEveryone
? message
: message.SanitizeMentions(true);
[NadekoCommand, Aliases]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.ManageMessages)]
[Priority(1)]
public Task Repeat(GuildDateTime dt, [Leftover] string message)
=> Repeat(dt, null, message);
var runner = await _service.AddRepeaterAsync(
ctx.Channel.Id,
ctx.Guild.Id,
realInterval,
message,
false,
startTimeOfDay
);
[NadekoCommand, Aliases]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.ManageMessages)]
[Priority(2)]
public async Task Repeat(GuildDateTime? dt, StoopidTime? interval, [Leftover]string message)
if (runner is null)
{
var startTimeOfDay = dt?.InputTimeUtc.TimeOfDay;
// if interval not null, that means user specified it (don't change it)
await ReplyErrorLocalizedAsync(strs.repeater_exceed_limit(5));
return;
}
// if interval is null set the default to:
// if time of day is specified: 1 day
// else 5 minutes
var realInterval = interval?.Time ?? (startTimeOfDay is null
? TimeSpan.FromMinutes(5)
: TimeSpan.FromDays(1));
if (string.IsNullOrWhiteSpace(message)
|| (interval != null &&
(interval.Time > TimeSpan.FromMinutes(25000) || interval.Time < TimeSpan.FromMinutes(1))))
{
return;
}
var description = GetRepeaterInfoString(runner);
await ctx.Channel.EmbedAsync(_eb.Create()
.WithOkColor()
.WithTitle(GetText(strs.repeater_created))
.WithDescription(description));
}
message = ((IGuildUser) ctx.User).GuildPermissions.MentionEveryone
? message
: message.SanitizeMentions(true);
[NadekoCommand, Aliases]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.ManageMessages)]
public async Task RepeatList()
{
var repeaters = _service.GetRepeaters(ctx.Guild.Id);
if (repeaters.Count == 0)
{
await ReplyConfirmLocalizedAsync(strs.repeaters_none).ConfigureAwait(false);
return;
}
var embed = _eb.Create()
.WithTitle(GetText(strs.list_of_repeaters))
.WithOkColor();
var runner = await _service.AddRepeaterAsync(
ctx.Channel.Id,
ctx.Guild.Id,
realInterval,
message,
false,
startTimeOfDay
);
if (runner is null)
{
await ReplyErrorLocalizedAsync(strs.repeater_exceed_limit(5));
return;
}
var i = 0;
foreach(var runner in repeaters.OrderBy(r => r.Repeater.Id))
{
var description = GetRepeaterInfoString(runner);
await ctx.Channel.EmbedAsync(_eb.Create()
.WithOkColor()
.WithTitle(GetText(strs.repeater_created))
.WithDescription(description));
}
[NadekoCommand, Aliases]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.ManageMessages)]
public async Task RepeatList()
{
var repeaters = _service.GetRepeaters(ctx.Guild.Id);
if (repeaters.Count == 0)
{
await ReplyConfirmLocalizedAsync(strs.repeaters_none).ConfigureAwait(false);
return;
}
var embed = _eb.Create()
.WithTitle(GetText(strs.list_of_repeaters))
.WithOkColor();
var i = 0;
foreach(var runner in repeaters.OrderBy(r => r.Repeater.Id))
{
var description = GetRepeaterInfoString(runner);
var name = $"#`{++i}`";
embed.AddField(
name,
description
);
}
await ctx.Channel.EmbedAsync(embed).ConfigureAwait(false);
var name = $"#`{++i}`";
embed.AddField(
name,
description
);
}
private string GetRepeaterInfoString(RunningRepeater runner)
{
var intervalString = Format.Bold(runner.Repeater.Interval.ToPrettyStringHM());
var executesIn = runner.NextTime < DateTime.UtcNow
? TimeSpan.Zero
: runner.NextTime - DateTime.UtcNow;
var executesInString = Format.Bold(executesIn.ToPrettyStringHM());
var message = Format.Sanitize(runner.Repeater.Message.TrimTo(50));
await ctx.Channel.EmbedAsync(embed).ConfigureAwait(false);
}
string description = "";
if (_service.IsNoRedundant(runner.Repeater.Id))
{
description = Format.Underline(Format.Bold(GetText(strs.no_redundant))) + "\n\n";
}
private string GetRepeaterInfoString(RunningRepeater runner)
{
var intervalString = Format.Bold(runner.Repeater.Interval.ToPrettyStringHM());
var executesIn = runner.NextTime < DateTime.UtcNow
? TimeSpan.Zero
: runner.NextTime - DateTime.UtcNow;
var executesInString = Format.Bold(executesIn.ToPrettyStringHM());
var message = Format.Sanitize(runner.Repeater.Message.TrimTo(50));
string description = "";
if (_service.IsNoRedundant(runner.Repeater.Id))
{
description = Format.Underline(Format.Bold(GetText(strs.no_redundant))) + "\n\n";
}
description += $"<#{runner.Repeater.ChannelId}>\n" +
$"`{GetText(strs.interval_colon)}` {intervalString}\n" +
$"`{GetText(strs.executes_in_colon)}` {executesInString}\n" +
$"`{GetText(strs.message_colon)}` {message}";
description += $"<#{runner.Repeater.ChannelId}>\n" +
$"`{GetText(strs.interval_colon)}` {intervalString}\n" +
$"`{GetText(strs.executes_in_colon)}` {executesInString}\n" +
$"`{GetText(strs.message_colon)}` {message}";
return description;
}
return description;
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -6,88 +6,87 @@ using NadekoBot.Modules.Utility.Services;
using NadekoBot.Common.TypeReaders;
using NadekoBot.Modules.Utility.Common;
namespace NadekoBot.Modules.Utility
namespace NadekoBot.Modules.Utility;
public partial class Utility
{
public partial class Utility
public class StreamRoleCommands : NadekoSubmodule<StreamRoleService>
{
public class StreamRoleCommands : NadekoSubmodule<StreamRoleService>
[NadekoCommand, Aliases]
[BotPerm(GuildPerm.ManageRoles)]
[UserPerm(GuildPerm.ManageRoles)]
[RequireContext(ContextType.Guild)]
public async Task StreamRole(IRole fromRole, IRole addRole)
{
[NadekoCommand, Aliases]
[BotPerm(GuildPerm.ManageRoles)]
[UserPerm(GuildPerm.ManageRoles)]
[RequireContext(ContextType.Guild)]
public async Task StreamRole(IRole fromRole, IRole addRole)
{
await this._service.SetStreamRole(fromRole, addRole).ConfigureAwait(false);
await this._service.SetStreamRole(fromRole, addRole).ConfigureAwait(false);
await ReplyConfirmLocalizedAsync(strs.stream_role_enabled(Format.Bold(fromRole.ToString()), Format.Bold(addRole.ToString()))).ConfigureAwait(false);
}
await ReplyConfirmLocalizedAsync(strs.stream_role_enabled(Format.Bold(fromRole.ToString()), Format.Bold(addRole.ToString()))).ConfigureAwait(false);
}
[NadekoCommand, Aliases]
[BotPerm(GuildPerm.ManageRoles)]
[UserPerm(GuildPerm.ManageRoles)]
[RequireContext(ContextType.Guild)]
public async Task StreamRole()
{
await this._service.StopStreamRole(ctx.Guild).ConfigureAwait(false);
await ReplyConfirmLocalizedAsync(strs.stream_role_disabled).ConfigureAwait(false);
}
[NadekoCommand, Aliases]
[BotPerm(GuildPerm.ManageRoles)]
[UserPerm(GuildPerm.ManageRoles)]
[RequireContext(ContextType.Guild)]
public async Task StreamRole()
{
await this._service.StopStreamRole(ctx.Guild).ConfigureAwait(false);
await ReplyConfirmLocalizedAsync(strs.stream_role_disabled).ConfigureAwait(false);
}
[NadekoCommand, Aliases]
[BotPerm(GuildPerm.ManageRoles)]
[UserPerm(GuildPerm.ManageRoles)]
[RequireContext(ContextType.Guild)]
public async Task StreamRoleKeyword([Leftover]string keyword = null)
{
string kw = await this._service.SetKeyword(ctx.Guild, keyword).ConfigureAwait(false);
[NadekoCommand, Aliases]
[BotPerm(GuildPerm.ManageRoles)]
[UserPerm(GuildPerm.ManageRoles)]
[RequireContext(ContextType.Guild)]
public async Task StreamRoleKeyword([Leftover]string keyword = null)
{
string kw = await this._service.SetKeyword(ctx.Guild, keyword).ConfigureAwait(false);
if(string.IsNullOrWhiteSpace(keyword))
await ReplyConfirmLocalizedAsync(strs.stream_role_kw_reset).ConfigureAwait(false);
if(string.IsNullOrWhiteSpace(keyword))
await ReplyConfirmLocalizedAsync(strs.stream_role_kw_reset).ConfigureAwait(false);
else
await ReplyConfirmLocalizedAsync(strs.stream_role_kw_set(Format.Bold(kw))).ConfigureAwait(false);
}
[NadekoCommand, Aliases]
[BotPerm(GuildPerm.ManageRoles)]
[UserPerm(GuildPerm.ManageRoles)]
[RequireContext(ContextType.Guild)]
public async Task StreamRoleBlacklist(AddRemove action, [Leftover] IGuildUser user)
{
var success = await this._service.ApplyListAction(StreamRoleListType.Blacklist, ctx.Guild, action, user.Id, user.ToString())
.ConfigureAwait(false);
if(action == AddRemove.Add)
if(success)
await ReplyConfirmLocalizedAsync(strs.stream_role_bl_add(Format.Bold(user.ToString()))).ConfigureAwait(false);
else
await ReplyConfirmLocalizedAsync(strs.stream_role_kw_set(Format.Bold(kw))).ConfigureAwait(false);
}
await ReplyConfirmLocalizedAsync(strs.stream_role_bl_add_fail(Format.Bold(user.ToString()))).ConfigureAwait(false);
else
if (success)
await ReplyConfirmLocalizedAsync(strs.stream_role_bl_rem(Format.Bold(user.ToString()))).ConfigureAwait(false);
else
await ReplyErrorLocalizedAsync(strs.stream_role_bl_rem_fail(Format.Bold(user.ToString()))).ConfigureAwait(false);
}
[NadekoCommand, Aliases]
[BotPerm(GuildPerm.ManageRoles)]
[UserPerm(GuildPerm.ManageRoles)]
[RequireContext(ContextType.Guild)]
public async Task StreamRoleBlacklist(AddRemove action, [Leftover] IGuildUser user)
{
var success = await this._service.ApplyListAction(StreamRoleListType.Blacklist, ctx.Guild, action, user.Id, user.ToString())
.ConfigureAwait(false);
[NadekoCommand, Aliases]
[BotPerm(GuildPerm.ManageRoles)]
[UserPerm(GuildPerm.ManageRoles)]
[RequireContext(ContextType.Guild)]
public async Task StreamRoleWhitelist(AddRemove action, [Leftover] IGuildUser user)
{
var success = await this._service.ApplyListAction(StreamRoleListType.Whitelist, ctx.Guild, action, user.Id, user.ToString())
.ConfigureAwait(false);
if(action == AddRemove.Add)
if(success)
await ReplyConfirmLocalizedAsync(strs.stream_role_bl_add(Format.Bold(user.ToString()))).ConfigureAwait(false);
else
await ReplyConfirmLocalizedAsync(strs.stream_role_bl_add_fail(Format.Bold(user.ToString()))).ConfigureAwait(false);
if (action == AddRemove.Add)
if(success)
await ReplyConfirmLocalizedAsync(strs.stream_role_wl_add(Format.Bold(user.ToString()))).ConfigureAwait(false);
else
if (success)
await ReplyConfirmLocalizedAsync(strs.stream_role_bl_rem(Format.Bold(user.ToString()))).ConfigureAwait(false);
else
await ReplyErrorLocalizedAsync(strs.stream_role_bl_rem_fail(Format.Bold(user.ToString()))).ConfigureAwait(false);
}
[NadekoCommand, Aliases]
[BotPerm(GuildPerm.ManageRoles)]
[UserPerm(GuildPerm.ManageRoles)]
[RequireContext(ContextType.Guild)]
public async Task StreamRoleWhitelist(AddRemove action, [Leftover] IGuildUser user)
{
var success = await this._service.ApplyListAction(StreamRoleListType.Whitelist, ctx.Guild, action, user.Id, user.ToString())
.ConfigureAwait(false);
if (action == AddRemove.Add)
if(success)
await ReplyConfirmLocalizedAsync(strs.stream_role_wl_add(Format.Bold(user.ToString()))).ConfigureAwait(false);
else
await ReplyConfirmLocalizedAsync(strs.stream_role_wl_add_fail(Format.Bold(user.ToString()))).ConfigureAwait(false);
else
if (success)
await ReplyConfirmLocalizedAsync(strs.stream_role_wl_rem(Format.Bold(user.ToString()))).ConfigureAwait(false);
else
await ReplyErrorLocalizedAsync(strs.stream_role_wl_rem_fail(Format.Bold(user.ToString()))).ConfigureAwait(false);
}
await ReplyConfirmLocalizedAsync(strs.stream_role_wl_add_fail(Format.Bold(user.ToString()))).ConfigureAwait(false);
else
if (success)
await ReplyConfirmLocalizedAsync(strs.stream_role_wl_rem(Format.Bold(user.ToString()))).ConfigureAwait(false);
else
await ReplyErrorLocalizedAsync(strs.stream_role_wl_rem_fail(Format.Bold(user.ToString()))).ConfigureAwait(false);
}
}
}

View File

@@ -1,96 +1,92 @@
using Discord;
using Discord.Commands;
using NadekoBot.Common.Attributes;
using NadekoBot.Common;
using NadekoBot.Extensions;
using NadekoBot.Modules.Utility.Services;
using System;
using System.Linq;
using System.Threading.Tasks;
namespace NadekoBot.Modules.Utility
namespace NadekoBot.Modules.Utility;
public partial class Utility
{
public partial class Utility
[Group]
public class UnitConverterCommands : NadekoSubmodule<ConverterService>
{
[Group]
public class UnitConverterCommands : NadekoSubmodule<ConverterService>
[NadekoCommand, Aliases]
public async Task ConvertList()
{
[NadekoCommand, Aliases]
public async Task ConvertList()
var units = _service.Units;
var embed = _eb.Create()
.WithTitle(GetText(strs.convertlist))
.WithOkColor();
foreach (var g in units.GroupBy(x => x.UnitType))
{
var units = _service.Units;
var embed = _eb.Create()
.WithTitle(GetText(strs.convertlist))
.WithOkColor();
foreach (var g in units.GroupBy(x => x.UnitType))
{
embed.AddField(g.Key.ToTitleCase(),
String.Join(", ", g.Select(x => x.Triggers.FirstOrDefault()).OrderBy(x => x)));
}
await ctx.Channel.EmbedAsync(embed).ConfigureAwait(false);
embed.AddField(g.Key.ToTitleCase(),
String.Join(", ", g.Select(x => x.Triggers.FirstOrDefault()).OrderBy(x => x)));
}
await ctx.Channel.EmbedAsync(embed).ConfigureAwait(false);
}
[NadekoCommand, Aliases]
[Priority(0)]
public async Task Convert(string origin, string target, decimal value)
[NadekoCommand, Aliases]
[Priority(0)]
public async Task Convert(string origin, string target, decimal value)
{
var originUnit = _service.Units.FirstOrDefault(x => x.Triggers.Select(y => y.ToUpperInvariant()).Contains(origin.ToUpperInvariant()));
var targetUnit = _service.Units.FirstOrDefault(x => x.Triggers.Select(y => y.ToUpperInvariant()).Contains(target.ToUpperInvariant()));
if (originUnit is null || targetUnit is null)
{
var originUnit = _service.Units.FirstOrDefault(x => x.Triggers.Select(y => y.ToUpperInvariant()).Contains(origin.ToUpperInvariant()));
var targetUnit = _service.Units.FirstOrDefault(x => x.Triggers.Select(y => y.ToUpperInvariant()).Contains(target.ToUpperInvariant()));
if (originUnit is null || targetUnit is null)
await ReplyErrorLocalizedAsync(strs.convert_not_found(Format.Bold(origin), Format.Bold(target))).ConfigureAwait(false);
return;
}
if (originUnit.UnitType != targetUnit.UnitType)
{
await ReplyErrorLocalizedAsync(strs.convert_type_error(Format.Bold(originUnit.Triggers.First()), Format.Bold(targetUnit.Triggers.First()))).ConfigureAwait(false);
return;
}
decimal res;
if (originUnit.Triggers == targetUnit.Triggers) res = value;
else if (originUnit.UnitType == "temperature")
{
//don't really care too much about efficiency, so just convert to Kelvin, then to target
switch (originUnit.Triggers.First().ToUpperInvariant())
{
await ReplyErrorLocalizedAsync(strs.convert_not_found(Format.Bold(origin), Format.Bold(target))).ConfigureAwait(false);
return;
case "C":
res = value + 273.15m; //celcius!
break;
case "F":
res = (value + 459.67m) * (5m / 9m);
break;
default:
res = value;
break;
}
if (originUnit.UnitType != targetUnit.UnitType)
//from Kelvin to target
switch (targetUnit.Triggers.First().ToUpperInvariant())
{
await ReplyErrorLocalizedAsync(strs.convert_type_error(Format.Bold(originUnit.Triggers.First()), Format.Bold(targetUnit.Triggers.First()))).ConfigureAwait(false);
return;
case "C":
res = res - 273.15m; //celcius!
break;
case "F":
res = res * (9m / 5m) - 459.67m;
break;
}
decimal res;
if (originUnit.Triggers == targetUnit.Triggers) res = value;
else if (originUnit.UnitType == "temperature")
}
else
{
if (originUnit.UnitType == "currency")
{
//don't really care too much about efficiency, so just convert to Kelvin, then to target
switch (originUnit.Triggers.First().ToUpperInvariant())
{
case "C":
res = value + 273.15m; //celcius!
break;
case "F":
res = (value + 459.67m) * (5m / 9m);
break;
default:
res = value;
break;
}
//from Kelvin to target
switch (targetUnit.Triggers.First().ToUpperInvariant())
{
case "C":
res = res - 273.15m; //celcius!
break;
case "F":
res = res * (9m / 5m) - 459.67m;
break;
}
res = (value * targetUnit.Modifier) / originUnit.Modifier;
}
else
{
if (originUnit.UnitType == "currency")
{
res = (value * targetUnit.Modifier) / originUnit.Modifier;
}
else
res = (value * originUnit.Modifier) / targetUnit.Modifier;
}
res = Math.Round(res, 4);
await SendConfirmAsync(GetText(strs.convert(value, originUnit.Triggers.Last(), res, targetUnit.Triggers.Last())));
res = (value * originUnit.Modifier) / targetUnit.Modifier;
}
res = Math.Round(res, 4);
await SendConfirmAsync(GetText(strs.convert(value, originUnit.Triggers.Last(), res, targetUnit.Triggers.Last())));
}
}
}

View File

@@ -5,10 +5,7 @@ using NadekoBot.Common;
using NadekoBot.Common.Attributes;
using NadekoBot.Extensions;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Net.Http;
using System.Text;
using System.Text.Json;
@@ -16,510 +13,507 @@ using System.Threading;
using System.Threading.Tasks;
using NadekoBot.Common.Replacements;
using NadekoBot.Services;
using Serilog;
using SystemTextJsonSamples;
using JsonSerializer = Newtonsoft.Json.JsonSerializer;
namespace NadekoBot.Modules.Utility
namespace NadekoBot.Modules.Utility;
public partial class Utility : NadekoModule
{
public partial class Utility : NadekoModule
private readonly DiscordSocketClient _client;
private readonly ICoordinator _coord;
private readonly IStatsService _stats;
private readonly IBotCredentials _creds;
private readonly DownloadTracker _tracker;
private readonly IHttpClientFactory _httpFactory;
public Utility(DiscordSocketClient client, ICoordinator coord,
IStatsService stats, IBotCredentials creds, DownloadTracker tracker,
IHttpClientFactory httpFactory)
{
private readonly DiscordSocketClient _client;
private readonly ICoordinator _coord;
private readonly IStatsService _stats;
private readonly IBotCredentials _creds;
private readonly DownloadTracker _tracker;
private readonly IHttpClientFactory _httpFactory;
_client = client;
_coord = coord;
_stats = stats;
_creds = creds;
_tracker = tracker;
_httpFactory = httpFactory;
}
public Utility(DiscordSocketClient client, ICoordinator coord,
IStatsService stats, IBotCredentials creds, DownloadTracker tracker,
IHttpClientFactory httpFactory)
{
_client = client;
_coord = coord;
_stats = stats;
_creds = creds;
_tracker = tracker;
_httpFactory = httpFactory;
}
[NadekoCommand, Aliases]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.ManageMessages)]
[Priority(1)]
public async Task Say(ITextChannel channel, [Leftover] SmartText message)
{
var rep = new ReplacementBuilder()
.WithDefault(ctx.User, channel, (SocketGuild)ctx.Guild, (DiscordSocketClient)ctx.Client)
.Build();
[NadekoCommand, Aliases]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.ManageMessages)]
[Priority(1)]
public async Task Say(ITextChannel channel, [Leftover] SmartText message)
{
var rep = new ReplacementBuilder()
.WithDefault(ctx.User, channel, (SocketGuild)ctx.Guild, (DiscordSocketClient)ctx.Client)
.Build();
message = rep.Replace(message);
message = rep.Replace(message);
await channel.SendAsync(message, !((IGuildUser)ctx.User).GuildPermissions.MentionEveryone);
}
await channel.SendAsync(message, !((IGuildUser)ctx.User).GuildPermissions.MentionEveryone);
}
[NadekoCommand, Aliases]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.ManageMessages)]
[Priority(0)]
public Task Say([Leftover] SmartText message)
=> Say((ITextChannel)ctx.Channel, message);
[NadekoCommand, Aliases]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.ManageMessages)]
[Priority(0)]
public Task Say([Leftover] SmartText message)
=> Say((ITextChannel)ctx.Channel, message);
[NadekoCommand, Aliases]
[RequireContext(ContextType.Guild)]
public async Task WhosPlaying([Leftover] string game)
[NadekoCommand, Aliases]
[RequireContext(ContextType.Guild)]
public async Task WhosPlaying([Leftover] string game)
{
game = game?.Trim().ToUpperInvariant();
if (string.IsNullOrWhiteSpace(game))
return;
if (!(ctx.Guild is SocketGuild socketGuild))
{
game = game?.Trim().ToUpperInvariant();
if (string.IsNullOrWhiteSpace(game))
return;
if (!(ctx.Guild is SocketGuild socketGuild))
{
Log.Warning("Can't cast guild to socket guild.");
return;
}
var rng = new NadekoRandom();
var arr = await Task.Run(() => socketGuild.Users
.Where(u => u.Activity?.Name?.ToUpperInvariant() == game)
.Select(u => u.Username)
.OrderBy(x => rng.Next())
.Take(60)
.ToArray()).ConfigureAwait(false);
int i = 0;
if (arr.Length == 0)
await ReplyErrorLocalizedAsync(strs.nobody_playing_game).ConfigureAwait(false);
else
{
await SendConfirmAsync("```css\n" + string.Join("\n", arr.GroupBy(item => (i++) / 2)
.Select(ig => string.Concat(ig.Select(el => $"• {el,-27}")))) + "\n```")
.ConfigureAwait(false);
}
Log.Warning("Can't cast guild to socket guild.");
return;
}
[NadekoCommand, Aliases]
[RequireContext(ContextType.Guild)]
[Priority(0)]
public async Task InRole(int page, [Leftover] IRole role = null)
var rng = new NadekoRandom();
var arr = await Task.Run(() => socketGuild.Users
.Where(u => u.Activity?.Name?.ToUpperInvariant() == game)
.Select(u => u.Username)
.OrderBy(x => rng.Next())
.Take(60)
.ToArray()).ConfigureAwait(false);
int i = 0;
if (arr.Length == 0)
await ReplyErrorLocalizedAsync(strs.nobody_playing_game).ConfigureAwait(false);
else
{
if (--page < 0)
return;
await ctx.Channel.TriggerTypingAsync().ConfigureAwait(false);
await _tracker.EnsureUsersDownloadedAsync(ctx.Guild).ConfigureAwait(false);
var users = await ctx.Guild.GetUsersAsync(
#if GLOBAL_NADEKO
CacheMode.CacheOnly
#endif
);
var roleUsers = users
.Where(u => role is null ? u.RoleIds.Count == 1 : u.RoleIds.Contains(role.Id))
.Select(u => $"`{u.Id, 18}` {u}")
.ToArray();
await ctx.SendPaginatedConfirmAsync(page, (cur) =>
{
var pageUsers = roleUsers.Skip(cur * 20)
.Take(20)
.ToList();
if (pageUsers.Count == 0)
return _eb.Create().WithOkColor().WithDescription(GetText(strs.no_user_on_this_page));
return _eb.Create().WithOkColor()
.WithTitle(GetText(strs.inrole_list(Format.Bold(role?.Name ?? "No Role"), roleUsers.Length)))
.WithDescription(string.Join("\n", pageUsers));
}, roleUsers.Length, 20).ConfigureAwait(false);
}
[NadekoCommand, Aliases]
[RequireContext(ContextType.Guild)]
[Priority(1)]
public Task InRole([Leftover] IRole role = null)
=> InRole(1, role);
public enum MeOrBot { Me, Bot }
[NadekoCommand, Aliases]
[RequireContext(ContextType.Guild)]
public async Task CheckPerms(MeOrBot who = MeOrBot.Me)
{
StringBuilder builder = new StringBuilder();
var user = who == MeOrBot.Me
? (IGuildUser)ctx.User
: ((SocketGuild)ctx.Guild).CurrentUser;
var perms = user.GetPermissions((ITextChannel)ctx.Channel);
foreach (var p in perms.GetType().GetProperties().Where(p => !p.GetGetMethod().GetParameters().Any()))
{
builder.AppendLine($"{p.Name} : {p.GetValue(perms, null)}");
}
await SendConfirmAsync(builder.ToString()).ConfigureAwait(false);
}
[NadekoCommand, Aliases]
[RequireContext(ContextType.Guild)]
public async Task UserId([Leftover] IGuildUser target = null)
{
var usr = target ?? ctx.User;
await ReplyConfirmLocalizedAsync(strs.userid("🆔", Format.Bold(usr.ToString()),
Format.Code(usr.Id.ToString())));
}
[NadekoCommand, Aliases]
[RequireContext(ContextType.Guild)]
public async Task RoleId([Leftover] IRole role)
{
await ReplyConfirmLocalizedAsync(strs.roleid("🆔", Format.Bold(role.ToString()),
Format.Code(role.Id.ToString())));
}
[NadekoCommand, Aliases]
public async Task ChannelId()
{
await ReplyConfirmLocalizedAsync(strs.channelid("🆔", Format.Code(ctx.Channel.Id.ToString())));
}
[NadekoCommand, Aliases]
[RequireContext(ContextType.Guild)]
public async Task ServerId()
{
await ReplyConfirmLocalizedAsync(strs.serverid("🆔", Format.Code(ctx.Guild.Id.ToString())));
}
[NadekoCommand, Aliases]
[RequireContext(ContextType.Guild)]
public async Task Roles(IGuildUser target, int page = 1)
{
var guild = ctx.Guild;
const int rolesPerPage = 20;
if (page < 1 || page > 100)
return;
if (target != null)
{
var roles = target.GetRoles().Except(new[] { guild.EveryoneRole }).OrderBy(r => -r.Position).Skip((page - 1) * rolesPerPage).Take(rolesPerPage).ToArray();
if (!roles.Any())
{
await ReplyErrorLocalizedAsync(strs.no_roles_on_page).ConfigureAwait(false);
}
else
{
await SendConfirmAsync(GetText(strs.roles_page(page, Format.Bold(target.ToString()))),
"\n• " + string.Join("\n• ", (IEnumerable<IRole>)roles).SanitizeMentions(true)).ConfigureAwait(false);
}
}
else
{
var roles = guild.Roles.Except(new[] { guild.EveryoneRole }).OrderBy(r => -r.Position).Skip((page - 1) * rolesPerPage).Take(rolesPerPage).ToArray();
if (!roles.Any())
{
await ReplyErrorLocalizedAsync(strs.no_roles_on_page).ConfigureAwait(false);
}
else
{
await SendConfirmAsync(GetText(strs.roles_all_page(page)),
"\n• " + string.Join("\n• ", (IEnumerable<IRole>)roles).SanitizeMentions(true)).ConfigureAwait(false);
}
}
}
[NadekoCommand, Aliases]
[RequireContext(ContextType.Guild)]
public Task Roles(int page = 1) =>
Roles(null, page);
[NadekoCommand, Aliases]
[RequireContext(ContextType.Guild)]
public async Task ChannelTopic([Leftover]ITextChannel channel = null)
{
if (channel is null)
channel = (ITextChannel)ctx.Channel;
var topic = channel.Topic;
if (string.IsNullOrWhiteSpace(topic))
await ReplyErrorLocalizedAsync(strs.no_topic_set).ConfigureAwait(false);
else
await SendConfirmAsync(GetText(strs.channel_topic), topic).ConfigureAwait(false);
}
[NadekoCommand, Aliases]
public async Task Stats()
{
var ownerIds = string.Join("\n", _creds.OwnerIds);
if (string.IsNullOrWhiteSpace(ownerIds))
ownerIds = "-";
await ctx.Channel.EmbedAsync(
_eb.Create().WithOkColor()
.WithAuthor($"NadekoBot v{StatsService.BotVersion}",
"https://nadeko-pictures.nyc3.digitaloceanspaces.com/other/avatar.png",
"https://nadekobot.readthedocs.io/en/latest/")
.AddField(GetText(strs.author), _stats.Author, true)
.AddField(GetText(strs.botid), _client.CurrentUser.Id.ToString(), true)
.AddField(GetText(strs.shard), $"#{_client.ShardId} / {_creds.TotalShards}", true)
.AddField(GetText(strs.commands_ran), _stats.CommandsRan.ToString(), true)
.AddField(GetText(strs.messages), $"{_stats.MessageCounter} ({_stats.MessagesPerSecond:F2}/sec)",
true)
.AddField(GetText(strs.memory), FormattableString.Invariant($"{_stats.GetPrivateMemory():F2} MB"), true)
.AddField(GetText(strs.owner_ids), ownerIds, true)
.AddField(GetText(strs.uptime), _stats.GetUptimeString("\n"), true)
.AddField(GetText(strs.presence),
GetText(strs.presence_txt(
_coord.GetGuildCount(),
_stats.TextChannels,
_stats.VoiceChannels)),
true))
await SendConfirmAsync("```css\n" + string.Join("\n", arr.GroupBy(item => (i++) / 2)
.Select(ig => string.Concat(ig.Select(el => $"• {el,-27}")))) + "\n```")
.ConfigureAwait(false);
}
}
[NadekoCommand, Aliases]
[RequireContext(ContextType.Guild)]
[Priority(0)]
public async Task InRole(int page, [Leftover] IRole role = null)
{
if (--page < 0)
return;
await ctx.Channel.TriggerTypingAsync().ConfigureAwait(false);
await _tracker.EnsureUsersDownloadedAsync(ctx.Guild).ConfigureAwait(false);
[NadekoCommand, Aliases]
public async Task Showemojis([Leftover] string _) // need to have the parameter so that the message.tags gets populated
var users = await ctx.Guild.GetUsersAsync(
#if GLOBAL_NADEKO
CacheMode.CacheOnly
#endif
);
var roleUsers = users
.Where(u => role is null ? u.RoleIds.Count == 1 : u.RoleIds.Contains(role.Id))
.Select(u => $"`{u.Id, 18}` {u}")
.ToArray();
await ctx.SendPaginatedConfirmAsync(page, (cur) =>
{
var tags = ctx.Message.Tags.Where(t => t.Type == TagType.Emoji).Select(t => (Emote)t.Value);
var pageUsers = roleUsers.Skip(cur * 20)
.Take(20)
.ToList();
var result = string.Join("\n", tags.Select(m => GetText(strs.showemojis(m, m.Url))));
if (pageUsers.Count == 0)
return _eb.Create().WithOkColor().WithDescription(GetText(strs.no_user_on_this_page));
if (string.IsNullOrWhiteSpace(result))
await ReplyErrorLocalizedAsync(strs.showemojis_none).ConfigureAwait(false);
return _eb.Create().WithOkColor()
.WithTitle(GetText(strs.inrole_list(Format.Bold(role?.Name ?? "No Role"), roleUsers.Length)))
.WithDescription(string.Join("\n", pageUsers));
}, roleUsers.Length, 20).ConfigureAwait(false);
}
[NadekoCommand, Aliases]
[RequireContext(ContextType.Guild)]
[Priority(1)]
public Task InRole([Leftover] IRole role = null)
=> InRole(1, role);
public enum MeOrBot { Me, Bot }
[NadekoCommand, Aliases]
[RequireContext(ContextType.Guild)]
public async Task CheckPerms(MeOrBot who = MeOrBot.Me)
{
StringBuilder builder = new StringBuilder();
var user = who == MeOrBot.Me
? (IGuildUser)ctx.User
: ((SocketGuild)ctx.Guild).CurrentUser;
var perms = user.GetPermissions((ITextChannel)ctx.Channel);
foreach (var p in perms.GetType().GetProperties().Where(p => !p.GetGetMethod().GetParameters().Any()))
{
builder.AppendLine($"{p.Name} : {p.GetValue(perms, null)}");
}
await SendConfirmAsync(builder.ToString()).ConfigureAwait(false);
}
[NadekoCommand, Aliases]
[RequireContext(ContextType.Guild)]
public async Task UserId([Leftover] IGuildUser target = null)
{
var usr = target ?? ctx.User;
await ReplyConfirmLocalizedAsync(strs.userid("🆔", Format.Bold(usr.ToString()),
Format.Code(usr.Id.ToString())));
}
[NadekoCommand, Aliases]
[RequireContext(ContextType.Guild)]
public async Task RoleId([Leftover] IRole role)
{
await ReplyConfirmLocalizedAsync(strs.roleid("🆔", Format.Bold(role.ToString()),
Format.Code(role.Id.ToString())));
}
[NadekoCommand, Aliases]
public async Task ChannelId()
{
await ReplyConfirmLocalizedAsync(strs.channelid("🆔", Format.Code(ctx.Channel.Id.ToString())));
}
[NadekoCommand, Aliases]
[RequireContext(ContextType.Guild)]
public async Task ServerId()
{
await ReplyConfirmLocalizedAsync(strs.serverid("🆔", Format.Code(ctx.Guild.Id.ToString())));
}
[NadekoCommand, Aliases]
[RequireContext(ContextType.Guild)]
public async Task Roles(IGuildUser target, int page = 1)
{
var guild = ctx.Guild;
const int rolesPerPage = 20;
if (page < 1 || page > 100)
return;
if (target != null)
{
var roles = target.GetRoles().Except(new[] { guild.EveryoneRole }).OrderBy(r => -r.Position).Skip((page - 1) * rolesPerPage).Take(rolesPerPage).ToArray();
if (!roles.Any())
{
await ReplyErrorLocalizedAsync(strs.no_roles_on_page).ConfigureAwait(false);
}
else
await ctx.Channel.SendMessageAsync(result.TrimTo(2000)).ConfigureAwait(false);
{
await SendConfirmAsync(GetText(strs.roles_page(page, Format.Bold(target.ToString()))),
"\n• " + string.Join("\n• ", (IEnumerable<IRole>)roles).SanitizeMentions(true)).ConfigureAwait(false);
}
}
[NadekoCommand, Aliases]
[RequireContext(ContextType.Guild)]
[BotPerm(GuildPerm.ManageEmojis)]
[UserPerm(GuildPerm.ManageEmojis)]
[Priority(2)]
public Task EmojiAdd(string name, Emote emote)
=> EmojiAdd(name, emote.Url);
[NadekoCommand, Aliases]
[RequireContext(ContextType.Guild)]
[BotPerm(GuildPerm.ManageEmojis)]
[UserPerm(GuildPerm.ManageEmojis)]
[Priority(1)]
public Task EmojiAdd(Emote emote)
=> EmojiAdd(emote.Name, emote.Url);
[NadekoCommand, Aliases]
[RequireContext(ContextType.Guild)]
[BotPerm(GuildPerm.ManageEmojis)]
[UserPerm(GuildPerm.ManageEmojis)]
[Priority(0)]
public async Task EmojiAdd(string name, string url = null)
else
{
name = name.Trim(':');
var roles = guild.Roles.Except(new[] { guild.EveryoneRole }).OrderBy(r => -r.Position).Skip((page - 1) * rolesPerPage).Take(rolesPerPage).ToArray();
if (!roles.Any())
{
await ReplyErrorLocalizedAsync(strs.no_roles_on_page).ConfigureAwait(false);
}
else
{
await SendConfirmAsync(GetText(strs.roles_all_page(page)),
"\n• " + string.Join("\n• ", (IEnumerable<IRole>)roles).SanitizeMentions(true)).ConfigureAwait(false);
}
}
}
url ??= ctx.Message.Attachments.FirstOrDefault()?.Url;
[NadekoCommand, Aliases]
[RequireContext(ContextType.Guild)]
public Task Roles(int page = 1) =>
Roles(null, page);
if (url is null)
return;
[NadekoCommand, Aliases]
[RequireContext(ContextType.Guild)]
public async Task ChannelTopic([Leftover]ITextChannel channel = null)
{
if (channel is null)
channel = (ITextChannel)ctx.Channel;
var topic = channel.Topic;
if (string.IsNullOrWhiteSpace(topic))
await ReplyErrorLocalizedAsync(strs.no_topic_set).ConfigureAwait(false);
else
await SendConfirmAsync(GetText(strs.channel_topic), topic).ConfigureAwait(false);
}
[NadekoCommand, Aliases]
public async Task Stats()
{
var ownerIds = string.Join("\n", _creds.OwnerIds);
if (string.IsNullOrWhiteSpace(ownerIds))
ownerIds = "-";
await ctx.Channel.EmbedAsync(
_eb.Create().WithOkColor()
.WithAuthor($"NadekoBot v{StatsService.BotVersion}",
"https://nadeko-pictures.nyc3.digitaloceanspaces.com/other/avatar.png",
"https://nadekobot.readthedocs.io/en/latest/")
.AddField(GetText(strs.author), _stats.Author, true)
.AddField(GetText(strs.botid), _client.CurrentUser.Id.ToString(), true)
.AddField(GetText(strs.shard), $"#{_client.ShardId} / {_creds.TotalShards}", true)
.AddField(GetText(strs.commands_ran), _stats.CommandsRan.ToString(), true)
.AddField(GetText(strs.messages), $"{_stats.MessageCounter} ({_stats.MessagesPerSecond:F2}/sec)",
true)
.AddField(GetText(strs.memory), FormattableString.Invariant($"{_stats.GetPrivateMemory():F2} MB"), true)
.AddField(GetText(strs.owner_ids), ownerIds, true)
.AddField(GetText(strs.uptime), _stats.GetUptimeString("\n"), true)
.AddField(GetText(strs.presence),
GetText(strs.presence_txt(
_coord.GetGuildCount(),
_stats.TextChannels,
_stats.VoiceChannels)),
true))
.ConfigureAwait(false);
}
[NadekoCommand, Aliases]
public async Task Showemojis([Leftover] string _) // need to have the parameter so that the message.tags gets populated
{
var tags = ctx.Message.Tags.Where(t => t.Type == TagType.Emoji).Select(t => (Emote)t.Value);
var result = string.Join("\n", tags.Select(m => GetText(strs.showemojis(m, m.Url))));
if (string.IsNullOrWhiteSpace(result))
await ReplyErrorLocalizedAsync(strs.showemojis_none).ConfigureAwait(false);
else
await ctx.Channel.SendMessageAsync(result.TrimTo(2000)).ConfigureAwait(false);
}
[NadekoCommand, Aliases]
[RequireContext(ContextType.Guild)]
[BotPerm(GuildPerm.ManageEmojis)]
[UserPerm(GuildPerm.ManageEmojis)]
[Priority(2)]
public Task EmojiAdd(string name, Emote emote)
=> EmojiAdd(name, emote.Url);
[NadekoCommand, Aliases]
[RequireContext(ContextType.Guild)]
[BotPerm(GuildPerm.ManageEmojis)]
[UserPerm(GuildPerm.ManageEmojis)]
[Priority(1)]
public Task EmojiAdd(Emote emote)
=> EmojiAdd(emote.Name, emote.Url);
[NadekoCommand, Aliases]
[RequireContext(ContextType.Guild)]
[BotPerm(GuildPerm.ManageEmojis)]
[UserPerm(GuildPerm.ManageEmojis)]
[Priority(0)]
public async Task EmojiAdd(string name, string url = null)
{
name = name.Trim(':');
url ??= ctx.Message.Attachments.FirstOrDefault()?.Url;
if (url is null)
return;
using var http = _httpFactory.CreateClient();
var res = await http.GetAsync(url, HttpCompletionOption.ResponseHeadersRead);
if (!res.IsImage() || res.GetImageSize() is null or > 262_144)
{
await ReplyErrorLocalizedAsync(strs.invalid_emoji_link);
return;
}
await using var imgStream = await res.Content.ReadAsStreamAsync();
Emote em;
try
{
em = await ctx.Guild.CreateEmoteAsync(name, new(imgStream));
}
catch (Exception ex)
{
Log.Warning(ex, "Error adding emoji on server {GuildId}", ctx.Guild.Id);
await ReplyErrorLocalizedAsync(strs.emoji_add_error);
return;
}
await ConfirmLocalizedAsync(strs.emoji_added(em.ToString()));
}
[NadekoCommand, Aliases]
[OwnerOnly]
public async Task ListServers(int page = 1)
using var http = _httpFactory.CreateClient();
var res = await http.GetAsync(url, HttpCompletionOption.ResponseHeadersRead);
if (!res.IsImage() || res.GetImageSize() is null or > 262_144)
{
page -= 1;
if (page < 0)
return;
var guilds = await Task.Run(() => _client.Guilds.OrderBy(g => g.Name).Skip((page) * 15).Take(15)).ConfigureAwait(false);
if (!guilds.Any())
{
await ReplyErrorLocalizedAsync(strs.listservers_none).ConfigureAwait(false);
return;
}
var embed = _eb.Create()
.WithOkColor();
foreach (var guild in guilds)
embed.AddField(guild.Name,
GetText(strs.listservers(guild.Id, guild.MemberCount, guild.OwnerId)),
false);
await ctx.Channel.EmbedAsync(embed);
await ReplyErrorLocalizedAsync(strs.invalid_emoji_link);
return;
}
await using var imgStream = await res.Content.ReadAsStreamAsync();
Emote em;
try
{
em = await ctx.Guild.CreateEmoteAsync(name, new(imgStream));
}
catch (Exception ex)
{
Log.Warning(ex, "Error adding emoji on server {GuildId}", ctx.Guild.Id);
await ReplyErrorLocalizedAsync(strs.emoji_add_error);
return;
}
await ConfirmLocalizedAsync(strs.emoji_added(em.ToString()));
}
[NadekoCommand, Aliases]
[OwnerOnly]
public async Task ListServers(int page = 1)
{
page -= 1;
if (page < 0)
return;
var guilds = await Task.Run(() => _client.Guilds.OrderBy(g => g.Name).Skip((page) * 15).Take(15)).ConfigureAwait(false);
if (!guilds.Any())
{
await ReplyErrorLocalizedAsync(strs.listservers_none).ConfigureAwait(false);
return;
}
var embed = _eb.Create()
.WithOkColor();
foreach (var guild in guilds)
embed.AddField(guild.Name,
GetText(strs.listservers(guild.Id, guild.MemberCount, guild.OwnerId)),
false);
await ctx.Channel.EmbedAsync(embed);
}
[NadekoCommand, Aliases]
[RequireContext(ContextType.Guild)]
public Task ShowEmbed(ulong messageId)
=> ShowEmbed((ITextChannel)ctx.Channel, messageId);
[NadekoCommand, Aliases]
[RequireContext(ContextType.Guild)]
public Task ShowEmbed(ulong messageId)
=> ShowEmbed((ITextChannel)ctx.Channel, messageId);
private static readonly JsonSerializerOptions _showEmbedSerializerOptions = new JsonSerializerOptions()
{
WriteIndented = true,
IgnoreNullValues = true,
PropertyNamingPolicy = LowerCaseNamingPolicy.Default
};
private static readonly JsonSerializerOptions _showEmbedSerializerOptions = new JsonSerializerOptions()
{
WriteIndented = true,
IgnoreNullValues = true,
PropertyNamingPolicy = LowerCaseNamingPolicy.Default
};
[NadekoCommand, Aliases]
[RequireContext(ContextType.Guild)]
public async Task ShowEmbed(ITextChannel ch, ulong messageId)
[NadekoCommand, Aliases]
[RequireContext(ContextType.Guild)]
public async Task ShowEmbed(ITextChannel ch, ulong messageId)
{
var user = (IGuildUser)ctx.User;
var perms = user.GetPermissions(ch);
if (!perms.ReadMessageHistory || !perms.ViewChannel)
{
var user = (IGuildUser)ctx.User;
var perms = user.GetPermissions(ch);
if (!perms.ReadMessageHistory || !perms.ViewChannel)
{
await ReplyErrorLocalizedAsync(strs.insuf_perms_u);
return;
}
var msg = await ch.GetMessageAsync(messageId);
if (msg is null)
{
await ReplyErrorLocalizedAsync(strs.msg_not_found);
return;
}
var embed = msg.Embeds.FirstOrDefault();
if (embed is null)
{
await ReplyErrorLocalizedAsync(strs.not_found);
return;
}
var json = SmartEmbedText.FromEmbed(embed, msg.Content).ToJson(_showEmbedSerializerOptions);
await SendConfirmAsync(Format.Sanitize(json).Replace("](", "]\\("));
await ReplyErrorLocalizedAsync(strs.insuf_perms_u);
return;
}
[NadekoCommand, Aliases]
[RequireContext(ContextType.Guild)]
[OwnerOnly]
public async Task SaveChat(int cnt)
var msg = await ch.GetMessageAsync(messageId);
if (msg is null)
{
var msgs = new List<IMessage>(cnt);
await ctx.Channel.GetMessagesAsync(cnt).ForEachAsync(dled => msgs.AddRange(dled)).ConfigureAwait(false);
await ReplyErrorLocalizedAsync(strs.msg_not_found);
return;
}
var title = $"Chatlog-{ctx.Guild.Name}/#{ctx.Channel.Name}-{DateTime.Now}.txt";
var grouping = msgs.GroupBy(x => $"{x.CreatedAt.Date:dd.MM.yyyy}")
.Select(g => new
var embed = msg.Embeds.FirstOrDefault();
if (embed is null)
{
await ReplyErrorLocalizedAsync(strs.not_found);
return;
}
var json = SmartEmbedText.FromEmbed(embed, msg.Content).ToJson(_showEmbedSerializerOptions);
await SendConfirmAsync(Format.Sanitize(json).Replace("](", "]\\("));
}
[NadekoCommand, Aliases]
[RequireContext(ContextType.Guild)]
[OwnerOnly]
public async Task SaveChat(int cnt)
{
var msgs = new List<IMessage>(cnt);
await ctx.Channel.GetMessagesAsync(cnt).ForEachAsync(dled => msgs.AddRange(dled)).ConfigureAwait(false);
var title = $"Chatlog-{ctx.Guild.Name}/#{ctx.Channel.Name}-{DateTime.Now}.txt";
var grouping = msgs.GroupBy(x => $"{x.CreatedAt.Date:dd.MM.yyyy}")
.Select(g => new
{
date = g.Key,
messages = g.OrderBy(x => x.CreatedAt).Select(s =>
{
date = g.Key,
messages = g.OrderBy(x => x.CreatedAt).Select(s =>
var msg = $"【{s.Timestamp:HH:mm:ss}】{s.Author}:";
if (string.IsNullOrWhiteSpace(s.ToString()))
{
var msg = $"【{s.Timestamp:HH:mm:ss}】{s.Author}:";
if (string.IsNullOrWhiteSpace(s.ToString()))
if (s.Attachments.Any())
{
if (s.Attachments.Any())
{
msg += "FILES_UPLOADED: " + string.Join("\n", s.Attachments.Select(x => x.Url));
}
else if (s.Embeds.Any())
{
msg += "EMBEDS: " + string.Join("\n--------\n", s.Embeds.Select(x => $"Description: {x.Description}"));
}
msg += "FILES_UPLOADED: " + string.Join("\n", s.Attachments.Select(x => x.Url));
}
else
else if (s.Embeds.Any())
{
msg += s.ToString();
msg += "EMBEDS: " + string.Join("\n--------\n", s.Embeds.Select(x => $"Description: {x.Description}"));
}
return msg;
})
});
using (var stream = await JsonConvert.SerializeObject(grouping, Formatting.Indented).ToStream().ConfigureAwait(false))
{
await ctx.User.SendFileAsync(stream, title, title, false).ConfigureAwait(false);
}
}
else
{
msg += s.ToString();
}
return msg;
})
});
using (var stream = await JsonConvert.SerializeObject(grouping, Formatting.Indented).ToStream().ConfigureAwait(false))
{
await ctx.User.SendFileAsync(stream, title, title, false).ConfigureAwait(false);
}
private static SemaphoreSlim sem = new SemaphoreSlim(1, 1);
}
private static SemaphoreSlim sem = new SemaphoreSlim(1, 1);
[NadekoCommand, Aliases]
[NadekoCommand, Aliases]
#if GLOBAL_NADEKO
[Ratelimit(30)]
#endif
public async Task Ping()
public async Task Ping()
{
await sem.WaitAsync(5000).ConfigureAwait(false);
try
{
await sem.WaitAsync(5000).ConfigureAwait(false);
try
{
var sw = Stopwatch.StartNew();
var msg = await ctx.Channel.SendMessageAsync("🏓").ConfigureAwait(false);
sw.Stop();
msg.DeleteAfter(0);
var sw = Stopwatch.StartNew();
var msg = await ctx.Channel.SendMessageAsync("🏓").ConfigureAwait(false);
sw.Stop();
msg.DeleteAfter(0);
await SendConfirmAsync($"{Format.Bold(ctx.User.ToString())} 🏓 {(int)sw.Elapsed.TotalMilliseconds}ms").ConfigureAwait(false);
}
finally
{
sem.Release();
}
await SendConfirmAsync($"{Format.Bold(ctx.User.ToString())} 🏓 {(int)sw.Elapsed.TotalMilliseconds}ms").ConfigureAwait(false);
}
public enum CreateInviteType
finally
{
Any,
New
sem.Release();
}
// [NadekoCommand, Usage, Description, Aliases]
// [RequireContext(ContextType.Guild)]
// public async Task CreateMyInvite(CreateInviteType type = CreateInviteType.Any)
// {
// if (type == CreateInviteType.Any)
// {
// if (_inviteService.TryGetInvite(type, out var code))
// {
// await ReplyErrorLocalizedAsync(strs.your_invite($"https://discord.gg/{code}"));
// return;
// }
// }
//
// var invite = await ((ITextChannel) ctx.Channel).CreateInviteAsync(isUnique: true);
// }
//
// [NadekoCommand, Usage, Description, Aliases]
// [RequireContext(ContextType.Guild)]
// public async Task InviteLb(int page = 1)
// {
// if (--page < 0)
// return;
//
// var inviteUsers = await _inviteService.GetInviteUsersAsync(ctx.Guild.Id);
//
// var embed = _eb.Create()
// .WithOkColor();
//
// await ctx.SendPaginatedConfirmAsync(page, (curPage) =>
// {
// var items = inviteUsers.Skip(curPage * 9).Take(9);
// var i = 0;
// foreach (var item in items)
// embed.AddField($"#{curPage * 9 + ++i} {item.UserName} [{item.User.Id}]", item.InvitedUsers);
//
// return embed;
// }, inviteUsers.Count, 9);
// }
}
}
public enum CreateInviteType
{
Any,
New
}
// [NadekoCommand, Usage, Description, Aliases]
// [RequireContext(ContextType.Guild)]
// public async Task CreateMyInvite(CreateInviteType type = CreateInviteType.Any)
// {
// if (type == CreateInviteType.Any)
// {
// if (_inviteService.TryGetInvite(type, out var code))
// {
// await ReplyErrorLocalizedAsync(strs.your_invite($"https://discord.gg/{code}"));
// return;
// }
// }
//
// var invite = await ((ITextChannel) ctx.Channel).CreateInviteAsync(isUnique: true);
// }
//
// [NadekoCommand, Usage, Description, Aliases]
// [RequireContext(ContextType.Guild)]
// public async Task InviteLb(int page = 1)
// {
// if (--page < 0)
// return;
//
// var inviteUsers = await _inviteService.GetInviteUsersAsync(ctx.Guild.Id);
//
// var embed = _eb.Create()
// .WithOkColor();
//
// await ctx.SendPaginatedConfirmAsync(page, (curPage) =>
// {
// var items = inviteUsers.Skip(curPage * 9).Take(9);
// var i = 0;
// foreach (var item in items)
// embed.AddField($"#{curPage * 9 + ++i} {item.UserName} [{item.User.Id}]", item.InvitedUsers);
//
// return embed;
// }, inviteUsers.Count, 9);
// }
}

View File

@@ -4,25 +4,24 @@ using NadekoBot.Common.Attributes;
using NadekoBot.Modules.Utility.Services;
using System.Threading.Tasks;
namespace NadekoBot.Modules.Utility
{
public partial class Utility
{
[Group]
public class VerboseErrorCommands : NadekoSubmodule<VerboseErrorsService>
{
[NadekoCommand, Aliases]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.ManageMessages)]
public async Task VerboseError(bool? newstate = null)
{
var state = _service.ToggleVerboseErrors(ctx.Guild.Id, newstate);
namespace NadekoBot.Modules.Utility;
if (state)
await ReplyConfirmLocalizedAsync(strs.verbose_errors_enabled).ConfigureAwait(false);
else
await ReplyConfirmLocalizedAsync(strs.verbose_errors_disabled).ConfigureAwait(false);
}
public partial class Utility
{
[Group]
public class VerboseErrorCommands : NadekoSubmodule<VerboseErrorsService>
{
[NadekoCommand, Aliases]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.ManageMessages)]
public async Task VerboseError(bool? newstate = null)
{
var state = _service.ToggleVerboseErrors(ctx.Guild.Id, newstate);
if (state)
await ReplyConfirmLocalizedAsync(strs.verbose_errors_enabled).ConfigureAwait(false);
else
await ReplyConfirmLocalizedAsync(strs.verbose_errors_disabled).ConfigureAwait(false);
}
}
}
}