mirror of
https://gitlab.com/Kwoth/nadekobot.git
synced 2025-09-11 09:48:26 -04:00
Restructured folders and project names, ci should be fixed
This commit is contained in:
69
src/NadekoBot/Modules/Utility/CalcCommands.cs
Normal file
69
src/NadekoBot/Modules/Utility/CalcCommands.cs
Normal file
@@ -0,0 +1,69 @@
|
||||
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
|
||||
{
|
||||
public partial class Utility
|
||||
{
|
||||
[Group]
|
||||
public class CalcCommands : NadekoSubmodule
|
||||
{
|
||||
[NadekoCommand, Usage, Description, 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 ctx.Channel.SendConfirmAsync("⚙ " + GetText("result"), result.ToString()).ConfigureAwait(false);
|
||||
else
|
||||
await ctx.Channel.SendErrorAsync("⚙ " + GetText("error"), expr.Error).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private static void Expr_EvaluateParameter(string name, NCalc.ParameterArgs args)
|
||||
{
|
||||
switch (name.ToLowerInvariant())
|
||||
{
|
||||
case "pi":
|
||||
args.Result = Math.PI;
|
||||
break;
|
||||
case "e":
|
||||
args.Result = Math.E;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
[NadekoCommand, Usage, Description, 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 ctx.Channel.SendConfirmAsync(GetText("calcops", Prefix), string.Join(", ", selection)).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
146
src/NadekoBot/Modules/Utility/CommandMapCommands.cs
Normal file
146
src/NadekoBot/Modules/Utility/CommandMapCommands.cs
Normal file
@@ -0,0 +1,146 @@
|
||||
using Discord;
|
||||
using Discord.Commands;
|
||||
using Discord.WebSocket;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using NadekoBot.Extensions;
|
||||
using NadekoBot.Core.Services;
|
||||
using NadekoBot.Core.Services.Database.Models;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using NadekoBot.Common.Attributes;
|
||||
using NadekoBot.Modules.Utility.Services;
|
||||
|
||||
namespace NadekoBot.Modules.Utility
|
||||
{
|
||||
public partial class Utility
|
||||
{
|
||||
[Group]
|
||||
public class CommandMapCommands : NadekoSubmodule<CommandMapService>
|
||||
{
|
||||
private readonly DbService _db;
|
||||
private readonly DiscordSocketClient _client;
|
||||
|
||||
public CommandMapCommands(DbService db, DiscordSocketClient client)
|
||||
{
|
||||
_db = db;
|
||||
_client = client;
|
||||
}
|
||||
|
||||
[NadekoCommand, Usage, Description, Aliases]
|
||||
[RequireContext(ContextType.Guild)]
|
||||
[UserPerm(GuildPerm.Administrator)]
|
||||
public async Task AliasesClear()
|
||||
{
|
||||
var count = _service.ClearAliases(ctx.Guild.Id);
|
||||
await ReplyConfirmLocalizedAsync("aliases_cleared", count).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
[NadekoCommand, Usage, Description, 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 _))
|
||||
{
|
||||
await ReplyErrorLocalizedAsync("alias_remove_fail", Format.Code(trigger)).ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
|
||||
using (var uow = _db.GetDbContext())
|
||||
{
|
||||
var config = uow.GuildConfigs.ForId(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._context.Set<CommandAlias>().Remove(tr);
|
||||
uow.SaveChanges();
|
||||
}
|
||||
|
||||
await ReplyConfirmLocalizedAsync("alias_removed", Format.Code(trigger)).ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
_service.AliasMaps.AddOrUpdate(ctx.Guild.Id, (_) =>
|
||||
{
|
||||
using (var uow = _db.GetDbContext())
|
||||
{
|
||||
var config = uow.GuildConfigs.ForId(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() },
|
||||
});
|
||||
}, (_, map) =>
|
||||
{
|
||||
using (var uow = _db.GetDbContext())
|
||||
{
|
||||
var config = uow.GuildConfigs.ForId(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._context.RemoveRange(toRemove.ToArray());
|
||||
config.CommandAliases.Add(toAdd);
|
||||
uow.SaveChanges();
|
||||
}
|
||||
map.AddOrUpdate(trigger, mapping, (key, old) => mapping);
|
||||
return map;
|
||||
});
|
||||
|
||||
await ReplyConfirmLocalizedAsync("alias_added", Format.Code(trigger), Format.Code(mapping)).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
|
||||
[NadekoCommand, Usage, Description, 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("aliases_none").ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
|
||||
var arr = maps.ToArray();
|
||||
|
||||
await ctx.SendPaginatedConfirmAsync(page, (curPage) =>
|
||||
{
|
||||
return new EmbedBuilder().WithOkColor()
|
||||
.WithTitle(GetText("alias_list"))
|
||||
.WithDescription(string.Join("\n",
|
||||
arr.Skip(curPage * 10).Take(10).Select(x => $"`{x.Key}` => `{x.Value}`")));
|
||||
|
||||
}, arr.Length, 10).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
12
src/NadekoBot/Modules/Utility/Common/ConvertUnit.cs
Normal file
12
src/NadekoBot/Modules/Utility/Common/ConvertUnit.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace NadekoBot.Modules.Utility.Common
|
||||
{
|
||||
[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; }
|
||||
}
|
||||
}
|
@@ -0,0 +1,19 @@
|
||||
using System;
|
||||
|
||||
namespace NadekoBot.Modules.Utility.Common.Exceptions
|
||||
{
|
||||
public class StreamRoleNotFoundException : Exception
|
||||
{
|
||||
public StreamRoleNotFoundException() : base("Stream role wasn't found.")
|
||||
{
|
||||
}
|
||||
|
||||
public StreamRoleNotFoundException(string message) : base(message)
|
||||
{
|
||||
}
|
||||
|
||||
public StreamRoleNotFoundException(string message, Exception innerException) : base(message, innerException)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,19 @@
|
||||
using System;
|
||||
|
||||
namespace NadekoBot.Modules.Utility.Common.Exceptions
|
||||
{
|
||||
public class StreamRolePermissionException : Exception
|
||||
{
|
||||
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)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
23
src/NadekoBot/Modules/Utility/Common/Patreon/PatreonData.cs
Normal file
23
src/NadekoBot/Modules/Utility/Common/Patreon/PatreonData.cs
Normal file
@@ -0,0 +1,23 @@
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace NadekoBot.Modules.Utility.Common.Patreon
|
||||
{
|
||||
public class PatreonData
|
||||
{
|
||||
public JObject[] Included { get; set; }
|
||||
public JObject[] Data { get; set; }
|
||||
public PatreonDataLinks Links { get; set; }
|
||||
}
|
||||
|
||||
public class PatreonDataLinks
|
||||
{
|
||||
public string first { get; set; }
|
||||
public string next { get; set; }
|
||||
}
|
||||
|
||||
public class PatreonUserAndReward
|
||||
{
|
||||
public PatreonUser User { get; set; }
|
||||
public PatreonPledge Reward { get; set; }
|
||||
}
|
||||
}
|
@@ -0,0 +1,62 @@
|
||||
namespace NadekoBot.Modules.Utility.Common.Patreon
|
||||
{
|
||||
public class Attributes
|
||||
{
|
||||
public int amount_cents { get; set; }
|
||||
public string created_at { get; set; }
|
||||
public object declined_since { get; set; }
|
||||
public bool is_twitch_pledge { get; set; }
|
||||
public bool patron_pays_fees { get; set; }
|
||||
public int? pledge_cap_cents { get; set; }
|
||||
}
|
||||
|
||||
public class Address
|
||||
{
|
||||
public object data { get; set; }
|
||||
}
|
||||
|
||||
public class Data
|
||||
{
|
||||
public string id { get; set; }
|
||||
public string type { get; set; }
|
||||
}
|
||||
|
||||
public class Links
|
||||
{
|
||||
public string related { get; set; }
|
||||
}
|
||||
|
||||
public class Creator
|
||||
{
|
||||
public Data data { get; set; }
|
||||
public Links links { get; set; }
|
||||
}
|
||||
|
||||
public class Patron
|
||||
{
|
||||
public Data data { get; set; }
|
||||
public Links links { get; set; }
|
||||
}
|
||||
|
||||
public class Reward
|
||||
{
|
||||
public Data data { get; set; }
|
||||
public Links links { get; set; }
|
||||
}
|
||||
|
||||
public class Relationships
|
||||
{
|
||||
public Address address { get; set; }
|
||||
public Creator creator { get; set; }
|
||||
public Patron patron { get; set; }
|
||||
public Reward reward { get; set; }
|
||||
}
|
||||
|
||||
public class PatreonPledge
|
||||
{
|
||||
public Attributes attributes { get; set; }
|
||||
public string id { get; set; }
|
||||
public Relationships relationships { get; set; }
|
||||
public string type { get; set; }
|
||||
}
|
||||
}
|
64
src/NadekoBot/Modules/Utility/Common/Patreon/PatreonUser.cs
Normal file
64
src/NadekoBot/Modules/Utility/Common/Patreon/PatreonUser.cs
Normal file
@@ -0,0 +1,64 @@
|
||||
namespace NadekoBot.Modules.Utility.Common.Patreon
|
||||
{
|
||||
public class DiscordConnection
|
||||
{
|
||||
public string user_id { get; set; }
|
||||
}
|
||||
|
||||
public class SocialConnections
|
||||
{
|
||||
public object deviantart { get; set; }
|
||||
public DiscordConnection discord { get; set; }
|
||||
public object facebook { get; set; }
|
||||
public object spotify { get; set; }
|
||||
public object twitch { get; set; }
|
||||
public object twitter { get; set; }
|
||||
public object youtube { get; set; }
|
||||
}
|
||||
|
||||
public class UserAttributes
|
||||
{
|
||||
public string about { get; set; }
|
||||
public string created { get; set; }
|
||||
public object discord_id { get; set; }
|
||||
public string email { get; set; }
|
||||
public object facebook { get; set; }
|
||||
public object facebook_id { get; set; }
|
||||
public string first_name { get; set; }
|
||||
public string full_name { get; set; }
|
||||
public int gender { get; set; }
|
||||
public bool has_password { get; set; }
|
||||
public string image_url { get; set; }
|
||||
public bool is_deleted { get; set; }
|
||||
public bool is_nuked { get; set; }
|
||||
public bool is_suspended { get; set; }
|
||||
public string last_name { get; set; }
|
||||
public SocialConnections social_connections { get; set; }
|
||||
public int status { get; set; }
|
||||
public string thumb_url { get; set; }
|
||||
public object twitch { get; set; }
|
||||
public string twitter { get; set; }
|
||||
public string url { get; set; }
|
||||
public string vanity { get; set; }
|
||||
public object youtube { get; set; }
|
||||
}
|
||||
|
||||
public class Campaign
|
||||
{
|
||||
public Data data { get; set; }
|
||||
public Links links { get; set; }
|
||||
}
|
||||
|
||||
public class UserRelationships
|
||||
{
|
||||
public Campaign campaign { get; set; }
|
||||
}
|
||||
|
||||
public class PatreonUser
|
||||
{
|
||||
public UserAttributes attributes { get; set; }
|
||||
public string id { get; set; }
|
||||
public UserRelationships relationships { get; set; }
|
||||
public string type { get; set; }
|
||||
}
|
||||
}
|
@@ -0,0 +1,8 @@
|
||||
namespace NadekoBot.Modules.Utility.Common
|
||||
{
|
||||
public enum StreamRoleListType
|
||||
{
|
||||
Whitelist,
|
||||
Blacklist,
|
||||
}
|
||||
}
|
189
src/NadekoBot/Modules/Utility/ConfigCommands.cs
Normal file
189
src/NadekoBot/Modules/Utility/ConfigCommands.cs
Normal file
@@ -0,0 +1,189 @@
|
||||
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.Core.Services;
|
||||
using NadekoBot.Modules.Administration.Services;
|
||||
|
||||
namespace NadekoBot.Modules.Utility
|
||||
{
|
||||
public partial class Utility
|
||||
{
|
||||
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)
|
||||
{
|
||||
_settingServices = settingServices;
|
||||
_bss = bss;
|
||||
_selfService = selfService;
|
||||
}
|
||||
|
||||
[NadekoCommand, Usage, Description, Aliases]
|
||||
[OwnerOnly]
|
||||
public Task BotConfigEdit()
|
||||
=> Config("bot");
|
||||
|
||||
[NadekoCommand, Usage, Description, Aliases]
|
||||
[Priority(0)]
|
||||
[OwnerOnly]
|
||||
public Task BotConfigEdit(string prop, [Leftover] string newValue = null)
|
||||
=> Config("bot", prop, newValue);
|
||||
|
||||
[NadekoCommand, Usage, Description, 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 = new EmbedBuilder()
|
||||
.WithErrorColor()
|
||||
.WithDescription(GetText("config_not_found", Format.Code(name)))
|
||||
.AddField(GetText("config_list"), string.Join("\n", configNames));
|
||||
|
||||
await ctx.Channel.EmbedAsync(embed);
|
||||
return;
|
||||
}
|
||||
|
||||
setting.Reload();
|
||||
await ctx.OkAsync();
|
||||
}
|
||||
|
||||
[NadekoCommand, Usage, Description, 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 embed = new EmbedBuilder()
|
||||
.WithOkColor()
|
||||
.WithTitle(GetText("config_list"))
|
||||
.WithDescription(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 = new EmbedBuilder()
|
||||
.WithErrorColor()
|
||||
.WithDescription(GetText("config_not_found", Format.Code(name)))
|
||||
.AddField(GetText("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 = new EmbedBuilder()
|
||||
.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 = new EmbedBuilder()
|
||||
.WithErrorColor()
|
||||
.WithDescription(GetText("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 = new EmbedBuilder()
|
||||
.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("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");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,27 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using NadekoBot.Core.Services.Database.Models;
|
||||
using NadekoBot.Core.Services.Database.Repositories;
|
||||
|
||||
namespace NadekoBot.Modules.Utility.Extensions
|
||||
{
|
||||
public static class StreamRoleExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets full stream role settings for the guild with the specified id.
|
||||
/// </summary>
|
||||
/// <param name="gc"></param>
|
||||
/// <param name="guildId">Id of the guild to get stream role settings for.</param>
|
||||
/// <returns></returns>
|
||||
public static StreamRoleSettings GetStreamRoleSettings(this IGuildConfigRepository gc, ulong guildId)
|
||||
{
|
||||
var conf = gc.ForId(guildId, set => set.Include(y => y.StreamRole)
|
||||
.Include(y => y.StreamRole.Whitelist)
|
||||
.Include(y => y.StreamRole.Blacklist));
|
||||
|
||||
if (conf.StreamRole == null)
|
||||
conf.StreamRole = new StreamRoleSettings();
|
||||
|
||||
return conf.StreamRole;
|
||||
}
|
||||
}
|
||||
}
|
154
src/NadekoBot/Modules/Utility/InfoCommands.cs
Normal file
154
src/NadekoBot/Modules/Utility/InfoCommands.cs
Normal file
@@ -0,0 +1,154 @@
|
||||
using Discord;
|
||||
using Discord.Commands;
|
||||
using Discord.WebSocket;
|
||||
using NadekoBot.Extensions;
|
||||
using NadekoBot.Core.Services;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using NadekoBot.Common.Attributes;
|
||||
|
||||
namespace NadekoBot.Modules.Utility
|
||||
{
|
||||
public partial class Utility
|
||||
{
|
||||
[Group]
|
||||
public class InfoCommands : NadekoSubmodule
|
||||
{
|
||||
private readonly DiscordSocketClient _client;
|
||||
private readonly IStatsService _stats;
|
||||
|
||||
public InfoCommands(DiscordSocketClient client, IStatsService stats)
|
||||
{
|
||||
_client = client;
|
||||
_stats = stats;
|
||||
}
|
||||
|
||||
[NadekoCommand, Usage, Description, 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 (guild == null)
|
||||
return;
|
||||
var ownername = guild.GetUser(guild.OwnerId);
|
||||
var textchn = guild.TextChannels.Count();
|
||||
var voicechn = guild.VoiceChannels.Count();
|
||||
|
||||
var createdAt = new DateTime(2015, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc).AddMilliseconds(guild.Id >> 22);
|
||||
var features = string.Join("\n", guild.Features);
|
||||
if (string.IsNullOrWhiteSpace(features))
|
||||
features = "-";
|
||||
var embed = new EmbedBuilder()
|
||||
.WithAuthor(eab => eab.WithName(GetText("server_info")))
|
||||
.WithTitle(guild.Name)
|
||||
.AddField(fb => fb.WithName(GetText("id")).WithValue(guild.Id.ToString()).WithIsInline(true))
|
||||
.AddField(fb => fb.WithName(GetText("owner")).WithValue(ownername.ToString()).WithIsInline(true))
|
||||
.AddField(fb => fb.WithName(GetText("members")).WithValue(guild.MemberCount.ToString()).WithIsInline(true))
|
||||
.AddField(fb => fb.WithName(GetText("text_channels")).WithValue(textchn.ToString()).WithIsInline(true))
|
||||
.AddField(fb => fb.WithName(GetText("voice_channels")).WithValue(voicechn.ToString()).WithIsInline(true))
|
||||
.AddField(fb => fb.WithName(GetText("created_at")).WithValue($"{createdAt:dd.MM.yyyy HH:mm}").WithIsInline(true))
|
||||
.AddField(fb => fb.WithName(GetText("region")).WithValue(guild.VoiceRegionId.ToString()).WithIsInline(true))
|
||||
.AddField(fb => fb.WithName(GetText("roles")).WithValue((guild.Roles.Count - 1).ToString()).WithIsInline(true))
|
||||
.AddField(fb => fb.WithName(GetText("features")).WithValue(features).WithIsInline(true))
|
||||
.WithColor(NadekoBot.OkColor);
|
||||
if (Uri.IsWellFormedUriString(guild.IconUrl, UriKind.Absolute))
|
||||
embed.WithThumbnailUrl(guild.IconUrl);
|
||||
if (guild.Emotes.Any())
|
||||
{
|
||||
embed.AddField(fb =>
|
||||
fb.WithName(GetText("custom_emojis") + $"({guild.Emotes.Count})")
|
||||
.WithValue(string.Join(" ", guild.Emotes
|
||||
.Shuffle()
|
||||
.Take(20)
|
||||
.Select(e => $"{e.Name} {e.ToString()}"))
|
||||
.TrimTo(1020)));
|
||||
}
|
||||
await ctx.Channel.EmbedAsync(embed).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
[NadekoCommand, Usage, Description, Aliases]
|
||||
[RequireContext(ContextType.Guild)]
|
||||
public async Task ChannelInfo(ITextChannel channel = null)
|
||||
{
|
||||
var ch = channel ?? (ITextChannel)ctx.Channel;
|
||||
if (ch == 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 = new EmbedBuilder()
|
||||
.WithTitle(ch.Name)
|
||||
.WithDescription(ch.Topic?.SanitizeMentions(true))
|
||||
.AddField(fb => fb.WithName(GetText("id")).WithValue(ch.Id.ToString()).WithIsInline(true))
|
||||
.AddField(fb => fb.WithName(GetText("created_at")).WithValue($"{createdAt:dd.MM.yyyy HH:mm}").WithIsInline(true))
|
||||
.AddField(fb => fb.WithName(GetText("users")).WithValue(usercount.ToString()).WithIsInline(true))
|
||||
.WithColor(NadekoBot.OkColor);
|
||||
await ctx.Channel.EmbedAsync(embed).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
[NadekoCommand, Usage, Description, Aliases]
|
||||
[RequireContext(ContextType.Guild)]
|
||||
public async Task UserInfo(IGuildUser usr = null)
|
||||
{
|
||||
var user = usr ?? ctx.User as IGuildUser;
|
||||
|
||||
if (user == null)
|
||||
return;
|
||||
|
||||
var embed = new EmbedBuilder()
|
||||
.AddField(fb => fb.WithName(GetText("name")).WithValue($"**{user.Username}**#{user.Discriminator}").WithIsInline(true));
|
||||
if (!string.IsNullOrWhiteSpace(user.Nickname))
|
||||
{
|
||||
embed.AddField(fb => fb.WithName(GetText("nickname")).WithValue(user.Nickname).WithIsInline(true));
|
||||
}
|
||||
embed.AddField(fb => fb.WithName(GetText("id")).WithValue(user.Id.ToString()).WithIsInline(true))
|
||||
.AddField(fb => fb.WithName(GetText("joined_server")).WithValue($"{user.JoinedAt?.ToString("dd.MM.yyyy HH:mm") ?? "?"}").WithIsInline(true))
|
||||
.AddField(fb => fb.WithName(GetText("joined_discord")).WithValue($"{user.CreatedAt:dd.MM.yyyy HH:mm}").WithIsInline(true))
|
||||
.AddField(fb => fb.WithName(GetText("roles")).WithValue($"**({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)}").WithIsInline(true))
|
||||
.WithColor(NadekoBot.OkColor);
|
||||
|
||||
var av = user.RealAvatarUrl();
|
||||
if (av != null && av.IsAbsoluteUri)
|
||||
embed.WithThumbnailUrl(av.ToString());
|
||||
await ctx.Channel.EmbedAsync(embed).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
[NadekoCommand, Usage, Description, 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("activity_line",
|
||||
++startCount,
|
||||
Format.Bold(kvp.Key.ToString()),
|
||||
kvp.Value / _stats.GetUptime().TotalSeconds, kvp.Value));
|
||||
}
|
||||
|
||||
await ctx.Channel.EmbedAsync(new EmbedBuilder()
|
||||
.WithTitle(GetText("activity_page", page + 1))
|
||||
.WithOkColor()
|
||||
.WithFooter(efb => efb.WithText(GetText("activity_users_total",
|
||||
CmdHandler.UserMessagesSent.Count)))
|
||||
.WithDescription(str.ToString())).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
106
src/NadekoBot/Modules/Utility/InviteCommands.cs
Normal file
106
src/NadekoBot/Modules/Utility/InviteCommands.cs
Normal file
@@ -0,0 +1,106 @@
|
||||
using System;
|
||||
using Discord;
|
||||
using Discord.Commands;
|
||||
using NadekoBot.Common.Attributes;
|
||||
using NadekoBot.Core.Common;
|
||||
using NadekoBot.Core.Modules.Utility.Services;
|
||||
using NadekoBot.Extensions;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace NadekoBot.Modules.Utility
|
||||
{
|
||||
public partial class Utility
|
||||
{
|
||||
[Group]
|
||||
public class InviteCommands : NadekoSubmodule<InviteService>
|
||||
{
|
||||
[NadekoCommand, Usage, Description, 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 ctx.Channel.SendConfirmAsync($"{ctx.User.Mention} https://discord.gg/{invite.Code}").ConfigureAwait(false);
|
||||
}
|
||||
|
||||
[NadekoCommand, Usage, Description, 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();
|
||||
|
||||
if (!invs.Any())
|
||||
{
|
||||
return new EmbedBuilder()
|
||||
.WithErrorColor()
|
||||
.WithDescription(GetText("no_invites"));
|
||||
}
|
||||
|
||||
var embed = new EmbedBuilder().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("inv_uses")}` **{usesString}**
|
||||
`{GetText("inv_expire")}` **{expiryString}**
|
||||
|
||||
{inv.Url} ";
|
||||
embed.AddField($"#{i++} {creator}", desc);
|
||||
}
|
||||
|
||||
return embed;
|
||||
|
||||
}, invites.Count, 9).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
[NadekoCommand, Usage, Description, 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 invites = await ch.GetInvitesAsync().ConfigureAwait(false);
|
||||
|
||||
if (invites.Count <= index)
|
||||
return;
|
||||
var inv = invites.ElementAt(index);
|
||||
await inv.DeleteAsync().ConfigureAwait(false);
|
||||
|
||||
await ReplyAsync(GetText("invite_deleted", Format.Bold(inv.Code.ToString()))).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
50
src/NadekoBot/Modules/Utility/PatreonCommands.cs
Normal file
50
src/NadekoBot/Modules/Utility/PatreonCommands.cs
Normal file
@@ -0,0 +1,50 @@
|
||||
using System.Threading.Tasks;
|
||||
using Discord.Commands;
|
||||
using System;
|
||||
using NadekoBot.Core.Services;
|
||||
using NadekoBot.Extensions;
|
||||
using Discord;
|
||||
using NadekoBot.Common.Attributes;
|
||||
using NadekoBot.Modules.Utility.Services;
|
||||
|
||||
namespace NadekoBot.Modules.Utility
|
||||
{
|
||||
public partial class Utility
|
||||
{
|
||||
[Group]
|
||||
public class PatreonCommands : NadekoSubmodule<PatreonRewardsService>
|
||||
{
|
||||
private readonly IBotCredentials _creds;
|
||||
|
||||
public PatreonCommands(IBotCredentials creds)
|
||||
{
|
||||
_creds = creds;
|
||||
}
|
||||
|
||||
[NadekoCommand, Usage, Description, Aliases]
|
||||
[RequireContext(ContextType.DM)]
|
||||
public async Task ClaimPatreonRewards()
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(_creds.PatreonAccessToken))
|
||||
return;
|
||||
|
||||
if (DateTime.UtcNow.Day < 5)
|
||||
{
|
||||
await ReplyErrorLocalizedAsync("clpa_too_early").ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
|
||||
var rem = (_service.Interval - (DateTime.UtcNow - _service.LastUpdate));
|
||||
var helpcmd = Format.Code(Prefix + "donate");
|
||||
await ctx.Channel.EmbedAsync(new EmbedBuilder().WithOkColor()
|
||||
.WithDescription(GetText("clpa_obsolete"))
|
||||
.AddField(efb => efb.WithName(GetText("clpa_fail_already_title")).WithValue(GetText("clpa_fail_already")))
|
||||
.AddField(efb => efb.WithName(GetText("clpa_fail_wait_title")).WithValue(GetText("clpa_fail_wait")))
|
||||
.AddField(efb => efb.WithName(GetText("clpa_fail_conn_title")).WithValue(GetText("clpa_fail_conn")))
|
||||
.AddField(efb => efb.WithName(GetText("clpa_fail_sup_title")).WithValue(GetText("clpa_fail_sup", helpcmd)))
|
||||
.WithFooter(efb => efb.WithText(GetText("clpa_next_update", rem))))
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
265
src/NadekoBot/Modules/Utility/QuoteCommands.cs
Normal file
265
src/NadekoBot/Modules/Utility/QuoteCommands.cs
Normal file
@@ -0,0 +1,265 @@
|
||||
using Discord;
|
||||
using Discord.Commands;
|
||||
using NadekoBot.Common;
|
||||
using NadekoBot.Common.Attributes;
|
||||
using NadekoBot.Common.Replacements;
|
||||
using NadekoBot.Core.Services;
|
||||
using NadekoBot.Core.Services.Database.Models;
|
||||
using NadekoBot.Extensions;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace NadekoBot.Modules.Utility
|
||||
{
|
||||
public partial class Utility
|
||||
{
|
||||
[Group]
|
||||
public class QuoteCommands : NadekoSubmodule
|
||||
{
|
||||
private readonly DbService _db;
|
||||
|
||||
public QuoteCommands(DbService db)
|
||||
{
|
||||
_db = db;
|
||||
}
|
||||
|
||||
[NadekoCommand, Usage, Description, Aliases]
|
||||
[RequireContext(ContextType.Guild)]
|
||||
[Priority(1)]
|
||||
public Task ListQuotes(OrderType order = OrderType.Keyword)
|
||||
=> ListQuotes(1, order);
|
||||
|
||||
[NadekoCommand, Usage, Description, 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())
|
||||
{
|
||||
quotes = uow.Quotes.GetGroup(ctx.Guild.Id, page, order);
|
||||
}
|
||||
|
||||
if (quotes.Any())
|
||||
await ctx.Channel.SendConfirmAsync(GetText("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("quotes_page_none").ConfigureAwait(false);
|
||||
}
|
||||
|
||||
[NadekoCommand, Usage, Description, 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())
|
||||
{
|
||||
quote = await uow.Quotes.GetRandomQuoteByKeywordAsync(ctx.Guild.Id, keyword);
|
||||
//if (quote != null)
|
||||
//{
|
||||
// quote.UseCount += 1;
|
||||
// uow.Complete();
|
||||
//}
|
||||
}
|
||||
|
||||
if (quote == null)
|
||||
return;
|
||||
|
||||
var rep = new ReplacementBuilder()
|
||||
.WithDefault(Context)
|
||||
.Build();
|
||||
|
||||
if (CREmbed.TryParse(quote.Text, out var crembed))
|
||||
{
|
||||
rep.Replace(crembed);
|
||||
await ctx.Channel.EmbedAsync(crembed.ToEmbed(), $"`#{quote.Id}` 📣 " + crembed.PlainText?.SanitizeAllMentions() ?? "")
|
||||
.ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
await ctx.Channel.SendMessageAsync($"`#{quote.Id}` 📣 " + rep.Replace(quote.Text)?.SanitizeAllMentions()).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
[NadekoCommand, Usage, Description, Aliases]
|
||||
[RequireContext(ContextType.Guild)]
|
||||
public async Task QuoteShow(int id)
|
||||
{
|
||||
Quote quote;
|
||||
using (var uow = _db.GetDbContext())
|
||||
{
|
||||
quote = uow.Quotes.GetById(id);
|
||||
if (quote.GuildId != Context.Guild.Id)
|
||||
quote = null;
|
||||
}
|
||||
|
||||
if (quote is null)
|
||||
{
|
||||
await ReplyErrorLocalizedAsync("quote_no_found_id");
|
||||
return;
|
||||
}
|
||||
|
||||
await ShowQuoteData(quote);
|
||||
}
|
||||
|
||||
private async Task ShowQuoteData(Quote data)
|
||||
{
|
||||
await ctx.Channel.EmbedAsync(new EmbedBuilder()
|
||||
.WithOkColor()
|
||||
.WithTitle(GetText("quote_id", $"#{data.Id}"))
|
||||
.AddField(efb => efb.WithName(GetText("trigger")).WithValue(data.Keyword))
|
||||
.AddField(efb => efb.WithName(GetText("response")).WithValue(data.Text.Length > 1000
|
||||
? GetText("redacted_too_long")
|
||||
: Format.Sanitize(data.Text)))
|
||||
.WithFooter(GetText("created_by", $"{data.AuthorName} ({data.AuthorId})"))
|
||||
).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
[NadekoCommand, Usage, Description, 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())
|
||||
{
|
||||
keywordquote = await uow.Quotes.SearchQuoteKeywordTextAsync(ctx.Guild.Id, keyword, text);
|
||||
}
|
||||
|
||||
if (keywordquote == null)
|
||||
return;
|
||||
|
||||
await ctx.Channel.SendMessageAsync($"`#{keywordquote.Id}` 💬 " + keyword.ToLowerInvariant() + ": " +
|
||||
keywordquote.Text.SanitizeAllMentions()).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
[NadekoCommand, Usage, Description, 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())
|
||||
{
|
||||
quote = uow.Quotes.GetById(id);
|
||||
}
|
||||
|
||||
if (quote is null || quote.GuildId != ctx.Guild.Id)
|
||||
{
|
||||
await ctx.Channel.SendErrorAsync(GetText("quotes_notfound")).ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
|
||||
var infoText = $"`#{quote.Id} added by {quote.AuthorName.SanitizeAllMentions()}` 🗯️ " + quote.Keyword.ToLowerInvariant().SanitizeAllMentions() + ":\n";
|
||||
|
||||
if (CREmbed.TryParse(quote.Text, out var crembed))
|
||||
{
|
||||
rep.Replace(crembed);
|
||||
|
||||
await ctx.Channel.EmbedAsync(crembed.ToEmbed(), infoText + crembed.PlainText?.SanitizeAllMentions())
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
await ctx.Channel.SendMessageAsync(infoText + rep.Replace(quote.Text)?.SanitizeAllMentions())
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
[NadekoCommand, Usage, Description, Aliases]
|
||||
[RequireContext(ContextType.Guild)]
|
||||
public async Task QuoteAdd(string keyword, [Leftover] string text)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(keyword) || string.IsNullOrWhiteSpace(text))
|
||||
return;
|
||||
|
||||
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("quote_added_new", Format.Code(q.Id.ToString())).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
[NadekoCommand, Usage, Description, Aliases]
|
||||
[RequireContext(ContextType.Guild)]
|
||||
public async Task QuoteDelete(int id)
|
||||
{
|
||||
var isAdmin = ((IGuildUser)ctx.Message.Author).GuildPermissions.Administrator;
|
||||
|
||||
var success = false;
|
||||
string response;
|
||||
using (var uow = _db.GetDbContext())
|
||||
{
|
||||
var q = uow.Quotes.GetById(id);
|
||||
|
||||
if ((q?.GuildId != ctx.Guild.Id) || (!isAdmin && q.AuthorId != ctx.Message.Author.Id))
|
||||
{
|
||||
response = GetText("quotes_remove_none");
|
||||
}
|
||||
else
|
||||
{
|
||||
uow.Quotes.Remove(q);
|
||||
await uow.SaveChangesAsync();
|
||||
success = true;
|
||||
response = GetText("quote_deleted", id);
|
||||
}
|
||||
}
|
||||
if (success)
|
||||
await ctx.Channel.SendConfirmAsync(response).ConfigureAwait(false);
|
||||
else
|
||||
await ctx.Channel.SendErrorAsync(response).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
[NadekoCommand, Usage, Description, Aliases]
|
||||
[RequireContext(ContextType.Guild)]
|
||||
[UserPerm(GuildPerm.Administrator)]
|
||||
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());
|
||||
|
||||
await uow.SaveChangesAsync();
|
||||
}
|
||||
|
||||
await ReplyConfirmLocalizedAsync("quotes_deleted", Format.Bold(keyword.SanitizeAllMentions())).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
206
src/NadekoBot/Modules/Utility/RemindCommands.cs
Normal file
206
src/NadekoBot/Modules/Utility/RemindCommands.cs
Normal file
@@ -0,0 +1,206 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Discord;
|
||||
using Discord.Commands;
|
||||
using NadekoBot.Common.Attributes;
|
||||
using NadekoBot.Core.Services;
|
||||
using NadekoBot.Core.Services.Database.Models;
|
||||
using NadekoBot.Extensions;
|
||||
using NadekoBot.Modules.Administration.Services;
|
||||
using NadekoBot.Modules.Utility.Services;
|
||||
|
||||
namespace NadekoBot.Modules.Utility
|
||||
{
|
||||
public partial class Utility
|
||||
{
|
||||
[Group]
|
||||
public class RemindCommands : NadekoSubmodule<RemindService>
|
||||
{
|
||||
private readonly DbService _db;
|
||||
private readonly GuildTimezoneService _tz;
|
||||
|
||||
public RemindCommands(DbService db, GuildTimezoneService tz)
|
||||
{
|
||||
_db = db;
|
||||
_tz = tz;
|
||||
}
|
||||
|
||||
public enum MeOrHere
|
||||
{
|
||||
Me,
|
||||
Here
|
||||
}
|
||||
|
||||
[NadekoCommand, Usage, Description, Aliases]
|
||||
[Priority(1)]
|
||||
public async Task Remind(MeOrHere meorhere, [Leftover] string remindString)
|
||||
{
|
||||
if (!_service.TryParseRemindMessage(remindString, out var remindData))
|
||||
{
|
||||
await ReplyErrorLocalizedAsync("remind_invalid");
|
||||
return;
|
||||
}
|
||||
|
||||
ulong target;
|
||||
target = meorhere == MeOrHere.Me ? ctx.User.Id : ctx.Channel.Id;
|
||||
if (!await RemindInternal(target, meorhere == MeOrHere.Me || ctx.Guild == null, remindData.Time, remindData.What)
|
||||
.ConfigureAwait(false))
|
||||
{
|
||||
await ReplyErrorLocalizedAsync("remind_too_long").ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
[NadekoCommand, Usage, Description, 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("cant_read_or_send").ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_service.TryParseRemindMessage(remindString, out var remindData))
|
||||
{
|
||||
await ReplyErrorLocalizedAsync("remind_invalid");
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
if (!await RemindInternal(channel.Id, false, remindData.Time, remindData.What)
|
||||
.ConfigureAwait(false))
|
||||
{
|
||||
await ReplyErrorLocalizedAsync("remind_too_long").ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
[NadekoCommand, Usage, Description, Aliases]
|
||||
public async Task RemindList(int page = 1)
|
||||
{
|
||||
if (--page < 0)
|
||||
return;
|
||||
|
||||
var embed = new EmbedBuilder()
|
||||
.WithOkColor()
|
||||
.WithTitle(GetText("reminder_list"));
|
||||
|
||||
List<Reminder> rems;
|
||||
using (var uow = _db.GetDbContext())
|
||||
{
|
||||
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")}
|
||||
`TargetId:` {rem.ChannelId}
|
||||
`Message:` {rem.Message?.TrimTo(50)}", false);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
embed.WithDescription(GetText("reminders_none"));
|
||||
}
|
||||
|
||||
embed.AddPaginatedFooter(page + 1, null);
|
||||
await ctx.Channel.EmbedAsync(embed).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
[NadekoCommand, Usage, Description, Aliases]
|
||||
public async Task RemindDelete(int index)
|
||||
{
|
||||
if (--index < 0)
|
||||
return;
|
||||
|
||||
var embed = new EmbedBuilder();
|
||||
|
||||
Reminder rem = null;
|
||||
using (var uow = _db.GetDbContext())
|
||||
{
|
||||
var rems = 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 == null)
|
||||
{
|
||||
await ReplyErrorLocalizedAsync("reminder_not_exist").ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
await ReplyErrorLocalizedAsync("reminder_deleted", index + 1).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
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 == null
|
||||
? time
|
||||
: TimeZoneInfo.ConvertTime(time, _tz.GetTimeZoneOrUtc(ctx.Guild.Id));
|
||||
try
|
||||
{
|
||||
await ctx.Channel.SendConfirmAsync(
|
||||
"⏰ " + GetText("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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
211
src/NadekoBot/Modules/Utility/RepeatCommands.cs
Normal file
211
src/NadekoBot/Modules/Utility/RepeatCommands.cs
Normal file
@@ -0,0 +1,211 @@
|
||||
#nullable enable
|
||||
using Discord;
|
||||
using Discord.Commands;
|
||||
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.Core.Common.TypeReaders.Models;
|
||||
|
||||
namespace NadekoBot.Modules.Utility
|
||||
{
|
||||
public partial class Utility
|
||||
{
|
||||
[Group]
|
||||
public class RepeatCommands : NadekoSubmodule<RepeaterService>
|
||||
{
|
||||
// public override string RunningRepeaterToString() =>
|
||||
// $"{Channel?.Mention ?? $"⚠<#{Repeater.ChannelId}>"} " +
|
||||
// (this.Repeater.NoRedundant ? "| ✍" : "") +
|
||||
// $"| {(int) Repeater.Interval.TotalHours}:{Repeater.Interval:mm} " +
|
||||
// $"| {Repeater.Message.TrimTo(33)}";
|
||||
|
||||
[NadekoCommand, Usage, Description, Aliases]
|
||||
[RequireContext(ContextType.Guild)]
|
||||
[UserPerm(GuildPerm.ManageMessages)]
|
||||
public async Task RepeatInvoke(int index)
|
||||
{
|
||||
if (--index < 0)
|
||||
return;
|
||||
|
||||
var success = await _service.TriggerExternal(ctx.Guild.Id, index);
|
||||
if (!success)
|
||||
{
|
||||
await ReplyErrorLocalizedAsync("repeat_invoke_none").ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
[NadekoCommand, Usage, Description, 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("repeater_remove_failed").ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
|
||||
var description = GetRepeaterInfoString(removed);
|
||||
await ctx.Channel.EmbedAsync(new EmbedBuilder()
|
||||
.WithOkColor()
|
||||
.WithTitle(GetText("repeater_removed", index + 1))
|
||||
.WithDescription(description));
|
||||
}
|
||||
|
||||
[NadekoCommand, Usage, Description, 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("index_out_of_range").ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (result.Value)
|
||||
{
|
||||
await ReplyConfirmLocalizedAsync("repeater_redundant_no", index + 1);
|
||||
}
|
||||
else
|
||||
{
|
||||
await ReplyConfirmLocalizedAsync("repeater_redundant_yes" ,index + 1);
|
||||
}
|
||||
}
|
||||
|
||||
[NadekoCommand, Usage, Description, Aliases]
|
||||
[RequireContext(ContextType.Guild)]
|
||||
[UserPerm(GuildPerm.ManageMessages)]
|
||||
[Priority(-1)]
|
||||
public Task Repeat([Leftover]string message)
|
||||
=> Repeat(null, null, message);
|
||||
|
||||
[NadekoCommand, Usage, Description, Aliases]
|
||||
[RequireContext(ContextType.Guild)]
|
||||
[UserPerm(GuildPerm.ManageMessages)]
|
||||
[Priority(0)]
|
||||
public Task Repeat(StoopidTime interval, [Leftover]string message)
|
||||
=> Repeat(null, interval, message);
|
||||
|
||||
[NadekoCommand, Usage, Description, Aliases]
|
||||
[RequireContext(ContextType.Guild)]
|
||||
[UserPerm(GuildPerm.ManageMessages)]
|
||||
[Priority(1)]
|
||||
public Task Repeat(GuildDateTime dt, [Leftover] string message)
|
||||
=> Repeat(dt, null, message);
|
||||
|
||||
[NadekoCommand, Usage, Description, 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)
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
message = ((IGuildUser) ctx.User).GuildPermissions.MentionEveryone
|
||||
? message
|
||||
: message.SanitizeMentions(true);
|
||||
|
||||
var runner = await _service.AddRepeaterAsync(
|
||||
ctx.Channel.Id,
|
||||
ctx.Guild.Id,
|
||||
realInterval,
|
||||
message,
|
||||
false,
|
||||
startTimeOfDay
|
||||
);
|
||||
|
||||
if (runner is null)
|
||||
{
|
||||
await ReplyErrorLocalizedAsync("repeater_exceed_limit", 5);
|
||||
return;
|
||||
}
|
||||
|
||||
var description = GetRepeaterInfoString(runner);
|
||||
await ctx.Channel.EmbedAsync(new EmbedBuilder()
|
||||
.WithOkColor()
|
||||
.WithTitle(GetText("repeater_created"))
|
||||
.WithDescription(description));
|
||||
}
|
||||
|
||||
[NadekoCommand, Usage, Description, Aliases]
|
||||
[RequireContext(ContextType.Guild)]
|
||||
[UserPerm(GuildPerm.ManageMessages)]
|
||||
public async Task RepeatList()
|
||||
{
|
||||
var repeaters = _service.GetRepeaters(ctx.Guild.Id);
|
||||
if (repeaters.Count == 0)
|
||||
{
|
||||
await ReplyConfirmLocalizedAsync("repeaters_none").ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
|
||||
var embed = new EmbedBuilder()
|
||||
.WithTitle(GetText("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 Context.Channel.EmbedAsync(embed).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private string GetRepeaterInfoString(RunningRepeater runner)
|
||||
{
|
||||
var intervalString = Format.Bold(runner.Repeater.Interval.ToPrettyStringHM());
|
||||
var executesIn = 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("no_redundant:"))) + "\n\n";
|
||||
}
|
||||
|
||||
description += $"<#{runner.Repeater.ChannelId}>\n" +
|
||||
$"`{GetText("interval:")}` {intervalString}\n" +
|
||||
$"`{GetText("executes_in:")}` {executesInString}\n" +
|
||||
$"`{GetText("message:")}` {message}";
|
||||
|
||||
return description;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
113
src/NadekoBot/Modules/Utility/Services/CommandMapService.cs
Normal file
113
src/NadekoBot/Modules/Utility/Services/CommandMapService.cs
Normal file
@@ -0,0 +1,113 @@
|
||||
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.Core.Services;
|
||||
using NadekoBot.Core.Services.Database.Models;
|
||||
using System;
|
||||
using Discord.WebSocket;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace NadekoBot.Modules.Utility.Services
|
||||
{
|
||||
public class CommandMapService : IInputTransformer, INService
|
||||
{
|
||||
|
||||
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)
|
||||
{
|
||||
|
||||
using (var uow = db.GetDbContext())
|
||||
{
|
||||
var guildIds = client.Guilds.Select(x => x.Id).ToList();
|
||||
var configs = uow._context.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))));
|
||||
|
||||
_db = db;
|
||||
}
|
||||
}
|
||||
|
||||
public int ClearAliases(ulong guildId)
|
||||
{
|
||||
AliasMaps.TryRemove(guildId, out _);
|
||||
|
||||
int count;
|
||||
using (var uow = _db.GetDbContext())
|
||||
{
|
||||
var gc = uow.GuildConfigs.ForId(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)
|
||||
{
|
||||
await Task.Yield();
|
||||
|
||||
if (guild == null || string.IsNullOrWhiteSpace(input))
|
||||
return input;
|
||||
|
||||
if (guild != null)
|
||||
{
|
||||
if (AliasMaps.TryGetValue(guild.Id, out ConcurrentDictionary<string, string> maps))
|
||||
{
|
||||
var keys = maps.Keys
|
||||
.OrderByDescending(x => x.Length);
|
||||
|
||||
foreach (var k in keys)
|
||||
{
|
||||
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($"{input} => {newInput}").ConfigureAwait(false);
|
||||
var _ = Task.Run(async () =>
|
||||
{
|
||||
await Task.Delay(1500).ConfigureAwait(false);
|
||||
await toDelete.DeleteAsync(new RequestOptions()
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
105
src/NadekoBot/Modules/Utility/Services/ConverterService.cs
Normal file
105
src/NadekoBot/Modules/Utility/Services/ConverterService.cs
Normal file
@@ -0,0 +1,105 @@
|
||||
using Discord.WebSocket;
|
||||
using NadekoBot.Core.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
|
||||
{
|
||||
public class ConverterService : INService, IUnloadableService
|
||||
{
|
||||
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)
|
||||
{
|
||||
_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
|
||||
}
|
||||
}
|
||||
|
||||
public Task Unload()
|
||||
{
|
||||
_currencyUpdater?.Change(Timeout.Infinite, Timeout.Infinite);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
|
||||
public class Rates
|
||||
{
|
||||
public string Base { get; set; }
|
||||
public DateTime Date { get; set; }
|
||||
[JsonProperty("rates")]
|
||||
public Dictionary<string, decimal> ConversionRates { get; set; }
|
||||
}
|
||||
}
|
33
src/NadekoBot/Modules/Utility/Services/InviteService.cs
Normal file
33
src/NadekoBot/Modules/Utility/Services/InviteService.cs
Normal file
@@ -0,0 +1,33 @@
|
||||
using CommandLine;
|
||||
using NadekoBot.Core.Common;
|
||||
using NadekoBot.Core.Services;
|
||||
|
||||
namespace NadekoBot.Core.Modules.Utility.Services
|
||||
{
|
||||
public class InviteService : INService
|
||||
{
|
||||
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()
|
||||
{
|
||||
if (MaxUses < 0)
|
||||
MaxUses = 0;
|
||||
|
||||
if (Expire < 0)
|
||||
Expire = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
228
src/NadekoBot/Modules/Utility/Services/PatreonRewardsService.cs
Normal file
228
src/NadekoBot/Modules/Utility/Services/PatreonRewardsService.cs
Normal file
@@ -0,0 +1,228 @@
|
||||
using Discord.WebSocket;
|
||||
using NadekoBot.Core.Services;
|
||||
using NadekoBot.Core.Services.Database.Models;
|
||||
using NadekoBot.Modules.Utility.Common.Patreon;
|
||||
using Newtonsoft.Json;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Discord;
|
||||
using NadekoBot.Core.Modules.Gambling.Services;
|
||||
using NadekoBot.Extensions;
|
||||
using Serilog;
|
||||
|
||||
namespace NadekoBot.Modules.Utility.Services
|
||||
{
|
||||
public class PatreonRewardsService : INService, IUnloadableService
|
||||
{
|
||||
private readonly SemaphoreSlim getPledgesLocker = new SemaphoreSlim(1, 1);
|
||||
|
||||
private PatreonUserAndReward[] _pledges;
|
||||
|
||||
private readonly Timer _updater;
|
||||
private readonly SemaphoreSlim claimLockJustInCase = new SemaphoreSlim(1, 1);
|
||||
|
||||
public TimeSpan Interval { get; } = TimeSpan.FromMinutes(3);
|
||||
private readonly IBotCredentials _creds;
|
||||
private readonly DbService _db;
|
||||
private readonly ICurrencyService _currency;
|
||||
private readonly GamblingConfigService _gamblingConfigService;
|
||||
private readonly IHttpClientFactory _httpFactory;
|
||||
private readonly DiscordSocketClient _client;
|
||||
|
||||
public DateTime LastUpdate { get; private set; } = DateTime.UtcNow;
|
||||
|
||||
public PatreonRewardsService(IBotCredentials creds, DbService db,
|
||||
ICurrencyService currency, IHttpClientFactory factory,
|
||||
DiscordSocketClient client, GamblingConfigService gamblingConfigService)
|
||||
{
|
||||
_creds = creds;
|
||||
_db = db;
|
||||
_currency = currency;
|
||||
_gamblingConfigService = gamblingConfigService;
|
||||
_httpFactory = factory;
|
||||
_client = client;
|
||||
|
||||
if (client.ShardId == 0)
|
||||
_updater = new Timer(async _ => await RefreshPledges().ConfigureAwait(false),
|
||||
null, TimeSpan.Zero, Interval);
|
||||
}
|
||||
|
||||
public async Task RefreshPledges()
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(_creds.PatreonAccessToken)
|
||||
|| string.IsNullOrWhiteSpace(_creds.PatreonAccessToken))
|
||||
return;
|
||||
|
||||
if (DateTime.UtcNow.Day < 5)
|
||||
return;
|
||||
|
||||
LastUpdate = DateTime.UtcNow;
|
||||
await getPledgesLocker.WaitAsync().ConfigureAwait(false);
|
||||
try
|
||||
{
|
||||
var rewards = new List<PatreonPledge>();
|
||||
var users = new List<PatreonUser>();
|
||||
using (var http = _httpFactory.CreateClient())
|
||||
{
|
||||
http.DefaultRequestHeaders.Clear();
|
||||
http.DefaultRequestHeaders.Add("Authorization", "Bearer " + _creds.PatreonAccessToken);
|
||||
var data = new PatreonData()
|
||||
{
|
||||
Links = new PatreonDataLinks()
|
||||
{
|
||||
next = $"https://api.patreon.com/oauth2/api/campaigns/{_creds.PatreonCampaignId}/pledges"
|
||||
}
|
||||
};
|
||||
do
|
||||
{
|
||||
var res = await http.GetStringAsync(data.Links.next)
|
||||
.ConfigureAwait(false);
|
||||
data = JsonConvert.DeserializeObject<PatreonData>(res);
|
||||
var pledgers = data.Data.Where(x => x["type"].ToString() == "pledge");
|
||||
rewards.AddRange(pledgers.Select(x => JsonConvert.DeserializeObject<PatreonPledge>(x.ToString()))
|
||||
.Where(x => x.attributes.declined_since == null));
|
||||
if (data.Included != null)
|
||||
{
|
||||
users.AddRange(data.Included
|
||||
.Where(x => x["type"].ToString() == "user")
|
||||
.Select(x => JsonConvert.DeserializeObject<PatreonUser>(x.ToString())));
|
||||
}
|
||||
} while (!string.IsNullOrWhiteSpace(data.Links.next));
|
||||
}
|
||||
var toSet = rewards.Join(users, (r) => r.relationships?.patron?.data?.id, (u) => u.id, (x, y) => new PatreonUserAndReward()
|
||||
{
|
||||
User = y,
|
||||
Reward = x,
|
||||
}).ToArray();
|
||||
|
||||
_pledges = toSet;
|
||||
|
||||
foreach (var pledge in _pledges)
|
||||
{
|
||||
var userIdStr = pledge.User.attributes?.social_connections?.discord?.user_id;
|
||||
if (userIdStr != null && ulong.TryParse(userIdStr, out var userId))
|
||||
{
|
||||
await ClaimReward(userId);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Warning(ex, "Error refreshing patreon pledges");
|
||||
}
|
||||
finally
|
||||
{
|
||||
getPledgesLocker.Release();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public async Task<int> ClaimReward(ulong userId)
|
||||
{
|
||||
await claimLockJustInCase.WaitAsync().ConfigureAwait(false);
|
||||
var settings = _gamblingConfigService.Data;
|
||||
var now = DateTime.UtcNow;
|
||||
try
|
||||
{
|
||||
var datas = _pledges?.Where(x => x.User.attributes?.social_connections?.discord?.user_id == userId.ToString())
|
||||
?? Enumerable.Empty<PatreonUserAndReward>();
|
||||
|
||||
var totalAmount = 0;
|
||||
foreach (var data in datas)
|
||||
{
|
||||
var amount = (int)(data.Reward.attributes.amount_cents * settings.PatreonCurrencyPerCent);
|
||||
|
||||
using (var uow = _db.GetDbContext())
|
||||
{
|
||||
var users = uow._context.Set<RewardedUser>();
|
||||
var usr = users.FirstOrDefault(x => x.PatreonUserId == data.User.id);
|
||||
|
||||
if (usr == null)
|
||||
{
|
||||
users.Add(new RewardedUser()
|
||||
{
|
||||
PatreonUserId = data.User.id,
|
||||
LastReward = now,
|
||||
AmountRewardedThisMonth = amount,
|
||||
});
|
||||
|
||||
await uow.SaveChangesAsync();
|
||||
|
||||
await _currency.AddAsync(userId, "Patreon reward - new", amount, gamble: true);
|
||||
totalAmount += amount;
|
||||
|
||||
Log.Information($"Sending new currency reward to {userId}");
|
||||
await SendMessageToUser(userId, $"Thank you for your pledge! " +
|
||||
$"You've been awarded **{amount}**{settings.Currency.Sign} !");
|
||||
continue;
|
||||
}
|
||||
|
||||
if (usr.LastReward.Month != now.Month)
|
||||
{
|
||||
usr.LastReward = now;
|
||||
usr.AmountRewardedThisMonth = amount;
|
||||
|
||||
await uow.SaveChangesAsync();
|
||||
|
||||
await _currency.AddAsync(userId, "Patreon reward - recurring", amount, gamble: true);
|
||||
totalAmount += amount;
|
||||
Log.Information($"Sending recurring currency reward to {userId}");
|
||||
await SendMessageToUser(userId, $"Thank you for your continued support! " +
|
||||
$"You've been awarded **{amount}**{settings.Currency.Sign} for this month's support!");
|
||||
continue;
|
||||
}
|
||||
|
||||
if (usr.AmountRewardedThisMonth < amount)
|
||||
{
|
||||
var toAward = amount - usr.AmountRewardedThisMonth;
|
||||
|
||||
usr.LastReward = now;
|
||||
usr.AmountRewardedThisMonth = amount;
|
||||
await uow.SaveChangesAsync();
|
||||
|
||||
await _currency.AddAsync(userId, "Patreon reward - update", toAward, gamble: true);
|
||||
totalAmount += toAward;
|
||||
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} !");
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return totalAmount;
|
||||
}
|
||||
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(message);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignored
|
||||
}
|
||||
}
|
||||
|
||||
public Task Unload()
|
||||
{
|
||||
_updater?.Change(Timeout.Infinite, Timeout.Infinite);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
}
|
180
src/NadekoBot/Modules/Utility/Services/RemindService.cs
Normal file
180
src/NadekoBot/Modules/Utility/Services/RemindService.cs
Normal file
@@ -0,0 +1,180 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using Discord;
|
||||
using Discord.WebSocket;
|
||||
using NadekoBot.Extensions;
|
||||
using NadekoBot.Core.Services;
|
||||
using NadekoBot.Core.Services.Database.Models;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Serilog;
|
||||
|
||||
namespace NadekoBot.Modules.Utility.Services
|
||||
{
|
||||
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;
|
||||
|
||||
public RemindService(DiscordSocketClient client, DbService db, IBotCredentials creds)
|
||||
{
|
||||
_client = client;
|
||||
_db = db;
|
||||
_creds = creds;
|
||||
_ = 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._context.Set<Reminder>()
|
||||
.RemoveRange(reminders);
|
||||
|
||||
await uow.SaveChangesAsync();
|
||||
}
|
||||
}
|
||||
|
||||
private Task<List<Reminder>> GetRemindersBeforeAsync(DateTime now)
|
||||
{
|
||||
using (var uow = _db.GetDbContext())
|
||||
{
|
||||
return uow._context.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 == null)
|
||||
return;
|
||||
ch = await user.GetOrCreateDMChannelAsync().ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
ch = _client.GetGuild(r.ServerId)?.GetTextChannel(r.ChannelId);
|
||||
}
|
||||
if (ch == null)
|
||||
return;
|
||||
|
||||
await ch.EmbedAsync(new EmbedBuilder()
|
||||
.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})"); }
|
||||
}
|
||||
}
|
||||
}
|
425
src/NadekoBot/Modules/Utility/Services/RepeaterService.cs
Normal file
425
src/NadekoBot/Modules/Utility/Services/RepeaterService.cs
Normal file
@@ -0,0 +1,425 @@
|
||||
#nullable enable
|
||||
using Discord.WebSocket;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using NadekoBot.Core.Services;
|
||||
using NadekoBot.Core.Services.Database.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 Serilog;
|
||||
|
||||
namespace NadekoBot.Modules.Utility.Services
|
||||
{
|
||||
public sealed class RepeaterService : IReadyExecutor, INService
|
||||
{
|
||||
public const int MAX_REPEATERS = 5;
|
||||
|
||||
private readonly DbService _db;
|
||||
private readonly IBotCredentials _creds;
|
||||
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)
|
||||
{
|
||||
_db = db;
|
||||
_creds = creds;
|
||||
_client = client;
|
||||
|
||||
var uow = _db.GetDbContext();
|
||||
var shardRepeaters = uow
|
||||
._context
|
||||
.Set<Repeater>()
|
||||
.FromSqlInterpolated($@"select * from repeaters
|
||||
where ((guildid >> 22) % {_creds.TotalShards}) == {_client.ShardId};")
|
||||
.AsNoTracking()
|
||||
.ToList();
|
||||
|
||||
_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));
|
||||
}
|
||||
|
||||
public Task OnReadyAsync()
|
||||
{
|
||||
_ = 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);
|
||||
continue;
|
||||
}
|
||||
|
||||
// collect (remove) all repeaters which need to run (3 seconds tolerance)
|
||||
var now = DateTime.UtcNow + TimeSpan.FromSeconds(3);
|
||||
|
||||
var toExecute = new List<RunningRepeater>();
|
||||
while (true)
|
||||
{
|
||||
lock (_repeaterQueue)
|
||||
{
|
||||
var current = _repeaterQueue.First;
|
||||
if (current is null || current.Value.NextTime > now)
|
||||
break;
|
||||
|
||||
toExecute.Add(current.Value);
|
||||
_repeaterQueue.RemoveFirst();
|
||||
}
|
||||
}
|
||||
|
||||
// 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)
|
||||
{
|
||||
await RemoveRepeaterInternal(rep.Repeater);
|
||||
return;
|
||||
}
|
||||
|
||||
rep.UpdateNextTime();
|
||||
AddToQueue(rep);
|
||||
}
|
||||
|
||||
public async Task<bool> TriggerExternal(ulong guildId, int index)
|
||||
{
|
||||
using var uow = _db.GetDbContext();
|
||||
|
||||
var toTrigger = await uow._context.Repeaters
|
||||
.AsNoTracking()
|
||||
.Skip(index)
|
||||
.FirstOrDefaultAsyncEF(x => x.GuildId == guildId);
|
||||
|
||||
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 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)
|
||||
channel = await _client.Rest.GetChannelAsync(repeater.ChannelId) as ITextChannel;
|
||||
|
||||
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
|
||||
{
|
||||
IMessage newMsg;
|
||||
if (CREmbed.TryParse(repeater.Message, out var crEmbed))
|
||||
{
|
||||
rep.Replace(crEmbed);
|
||||
newMsg = await channel.EmbedAsync(crEmbed);
|
||||
}
|
||||
else
|
||||
{
|
||||
newMsg = await channel.SendMessageAsync(rep.Replace(repeater.Message));
|
||||
}
|
||||
|
||||
_ = 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._context
|
||||
.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._context.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._context.Repeaters.AsNoTracking().CountAsyncEF() < MAX_REPEATERS)
|
||||
uow._context.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._context.Repeaters
|
||||
.AsNoTracking()
|
||||
.Skip(index)
|
||||
.FirstOrDefaultAsyncEF(x => x.GuildId == guildId);
|
||||
|
||||
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._context.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._context
|
||||
.Repeaters
|
||||
.AsQueryable()
|
||||
.Skip(index)
|
||||
.FirstOrDefaultAsyncEF(x => x.GuildId == guildId);
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
96
src/NadekoBot/Modules/Utility/Services/RunningRepeater.cs
Normal file
96
src/NadekoBot/Modules/Utility/Services/RunningRepeater.cs
Normal file
@@ -0,0 +1,96 @@
|
||||
using System;
|
||||
using NadekoBot.Core.Services.Database.Models;
|
||||
|
||||
namespace NadekoBot.Modules.Utility.Services
|
||||
{
|
||||
public sealed class RunningRepeater
|
||||
{
|
||||
public DateTime NextTime { get; private set; }
|
||||
|
||||
public Repeater Repeater { get; }
|
||||
public int ErrorCount { get; set; }
|
||||
|
||||
public RunningRepeater(Repeater repeater)
|
||||
{
|
||||
this.Repeater = repeater;
|
||||
NextTime = CalculateInitialExecution();
|
||||
}
|
||||
|
||||
public void UpdateNextTime()
|
||||
{
|
||||
NextTime = DateTime.UtcNow + Repeater.Interval;
|
||||
}
|
||||
|
||||
private DateTime CalculateInitialExecution()
|
||||
{
|
||||
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)
|
||||
{
|
||||
// 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);
|
||||
}
|
||||
|
||||
// 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)
|
||||
{
|
||||
return initialDateTime;
|
||||
}
|
||||
|
||||
// else calculate based on minutes difference
|
||||
|
||||
// get the difference
|
||||
var diff = DateTime.UtcNow - initialDateTime;
|
||||
|
||||
// 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
|
||||
|
||||
var initialIntervalMultiplier = 1 - (triggerCount - Math.Truncate(triggerCount));
|
||||
return DateTime.UtcNow + (Repeater.Interval * initialIntervalMultiplier);
|
||||
}
|
||||
}
|
||||
}
|
321
src/NadekoBot/Modules/Utility/Services/StreamRoleService.cs
Normal file
321
src/NadekoBot/Modules/Utility/Services/StreamRoleService.cs
Normal file
@@ -0,0 +1,321 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Discord;
|
||||
using Discord.WebSocket;
|
||||
using NadekoBot.Extensions;
|
||||
using NadekoBot.Core.Services;
|
||||
using NadekoBot.Core.Services.Database.Models;
|
||||
using NadekoBot.Modules.Utility.Extensions;
|
||||
using NadekoBot.Common.TypeReaders;
|
||||
using NadekoBot.Modules.Utility.Common;
|
||||
using NadekoBot.Modules.Utility.Common.Exceptions;
|
||||
using Discord.Net;
|
||||
using Serilog;
|
||||
|
||||
namespace NadekoBot.Modules.Utility.Services
|
||||
{
|
||||
public class StreamRoleService : INService, IUnloadableService
|
||||
{
|
||||
private readonly DbService _db;
|
||||
private readonly DiscordSocketClient _client;
|
||||
private readonly ConcurrentDictionary<ulong, StreamRoleSettings> guildSettings;
|
||||
|
||||
public StreamRoleService(DiscordSocketClient client, DbService db, NadekoBot 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 () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
await Task.WhenAll(client.Guilds.Select(g => RescanUsers(g))).ConfigureAwait(false);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignored
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public Task Unload()
|
||||
{
|
||||
_client.GuildMemberUpdated -= Client_GuildMemberUpdated;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
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.GuildConfigs.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._context.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.GuildConfigs.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.GuildConfigs.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.GuildConfigs.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.GuildConfigs.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)
|
||||
{
|
||||
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 null)
|
||||
&& setting.Enabled
|
||||
&& setting.Blacklist.All(x => x.UserId != user.Id)
|
||||
&& user.RoleIds.Contains(setting.FromRoleId))
|
||||
{
|
||||
try
|
||||
{
|
||||
addRole = addRole ?? user.Guild.GetRole(setting.AddRoleId);
|
||||
if (addRole == 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(setting.AddRoleId))
|
||||
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 == 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 == 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);
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,79 @@
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Discord;
|
||||
using Discord.Commands;
|
||||
using NadekoBot.Common.Collections;
|
||||
using NadekoBot.Extensions;
|
||||
using NadekoBot.Modules.Help.Services;
|
||||
using NadekoBot.Core.Services;
|
||||
|
||||
namespace NadekoBot.Modules.Utility.Services
|
||||
{
|
||||
public class VerboseErrorsService : INService, IUnloadableService
|
||||
{
|
||||
private readonly ConcurrentHashSet<ulong> guildsEnabled;
|
||||
private readonly DbService _db;
|
||||
private readonly CommandHandler _ch;
|
||||
private readonly HelpService _hs;
|
||||
|
||||
public VerboseErrorsService(NadekoBot bot, DbService db, CommandHandler ch, HelpService hs)
|
||||
{
|
||||
_db = db;
|
||||
_ch = ch;
|
||||
_hs = hs;
|
||||
|
||||
_ch.CommandErrored += LogVerboseError;
|
||||
|
||||
guildsEnabled = new ConcurrentHashSet<ulong>(bot
|
||||
.AllGuildConfigs
|
||||
.Where(x => x.VerboseErrors)
|
||||
.Select(x => x.GuildId));
|
||||
}
|
||||
|
||||
public Task Unload()
|
||||
{
|
||||
_ch.CommandErrored -= LogVerboseError;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private async Task LogVerboseError(CommandInfo cmd, ITextChannel channel, string reason)
|
||||
{
|
||||
if (channel == 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.GuildConfigs.ForId(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;
|
||||
}
|
||||
}
|
||||
}
|
93
src/NadekoBot/Modules/Utility/StreamRoleCommands.cs
Normal file
93
src/NadekoBot/Modules/Utility/StreamRoleCommands.cs
Normal file
@@ -0,0 +1,93 @@
|
||||
using Discord;
|
||||
using Discord.Commands;
|
||||
using System.Threading.Tasks;
|
||||
using NadekoBot.Common.Attributes;
|
||||
using NadekoBot.Modules.Utility.Services;
|
||||
using NadekoBot.Common.TypeReaders;
|
||||
using NadekoBot.Modules.Utility.Common;
|
||||
|
||||
namespace NadekoBot.Modules.Utility
|
||||
{
|
||||
public partial class Utility
|
||||
{
|
||||
public class StreamRoleCommands : NadekoSubmodule<StreamRoleService>
|
||||
{
|
||||
[NadekoCommand, Usage, Description, 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 ReplyConfirmLocalizedAsync("stream_role_enabled", Format.Bold(fromRole.ToString()), Format.Bold(addRole.ToString())).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
[NadekoCommand, Usage, Description, Aliases]
|
||||
[BotPerm(GuildPerm.ManageRoles)]
|
||||
[UserPerm(GuildPerm.ManageRoles)]
|
||||
[RequireContext(ContextType.Guild)]
|
||||
public async Task StreamRole()
|
||||
{
|
||||
await this._service.StopStreamRole(ctx.Guild).ConfigureAwait(false);
|
||||
await ReplyConfirmLocalizedAsync("stream_role_disabled").ConfigureAwait(false);
|
||||
}
|
||||
|
||||
[NadekoCommand, Usage, Description, 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("stream_role_kw_reset").ConfigureAwait(false);
|
||||
else
|
||||
await ReplyConfirmLocalizedAsync("stream_role_kw_set", Format.Bold(kw)).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
[NadekoCommand, Usage, Description, 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("stream_role_bl_add", Format.Bold(user.ToString())).ConfigureAwait(false);
|
||||
else
|
||||
await ReplyConfirmLocalizedAsync("stream_role_bl_add_fail", Format.Bold(user.ToString())).ConfigureAwait(false);
|
||||
else
|
||||
if (success)
|
||||
await ReplyConfirmLocalizedAsync("stream_role_bl_rem", Format.Bold(user.ToString())).ConfigureAwait(false);
|
||||
else
|
||||
await ReplyErrorLocalizedAsync("stream_role_bl_rem_fail", Format.Bold(user.ToString())).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
[NadekoCommand, Usage, Description, 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("stream_role_wl_add", Format.Bold(user.ToString())).ConfigureAwait(false);
|
||||
else
|
||||
await ReplyConfirmLocalizedAsync("stream_role_wl_add_fail", Format.Bold(user.ToString())).ConfigureAwait(false);
|
||||
else
|
||||
if (success)
|
||||
await ReplyConfirmLocalizedAsync("stream_role_wl_rem", Format.Bold(user.ToString())).ConfigureAwait(false);
|
||||
else
|
||||
await ReplyErrorLocalizedAsync("stream_role_wl_rem_fail", Format.Bold(user.ToString())).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
91
src/NadekoBot/Modules/Utility/UnitConversionCommands.cs
Normal file
91
src/NadekoBot/Modules/Utility/UnitConversionCommands.cs
Normal file
@@ -0,0 +1,91 @@
|
||||
using Discord;
|
||||
using Discord.Commands;
|
||||
using NadekoBot.Common.Attributes;
|
||||
using NadekoBot.Core.Common;
|
||||
using NadekoBot.Extensions;
|
||||
using NadekoBot.Modules.Utility.Services;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace NadekoBot.Modules.Utility
|
||||
{
|
||||
public partial class Utility
|
||||
{
|
||||
[Group]
|
||||
public class UnitConverterCommands : NadekoSubmodule<ConverterService>
|
||||
{
|
||||
[NadekoCommand, Usage, Description, Aliases]
|
||||
public async Task ConvertList()
|
||||
{
|
||||
var units = _service.Units;
|
||||
var res = units.GroupBy(x => x.UnitType)
|
||||
.Aggregate(new EmbedBuilder().WithTitle(GetText("convertlist"))
|
||||
.WithOkColor(),
|
||||
(embed, g) => embed.AddField(efb =>
|
||||
efb.WithName(g.Key.ToTitleCase())
|
||||
.WithValue(String.Join(", ", g.Select(x => x.Triggers.FirstOrDefault())
|
||||
.OrderBy(x => x)))));
|
||||
await ctx.Channel.EmbedAsync(res).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
[NadekoCommand, Usage, Description, 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 == null || targetUnit == null)
|
||||
{
|
||||
await ReplyErrorLocalizedAsync("convert_not_found", Format.Bold(origin), Format.Bold(target)).ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
if (originUnit.UnitType != targetUnit.UnitType)
|
||||
{
|
||||
await ReplyErrorLocalizedAsync("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())
|
||||
{
|
||||
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;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (originUnit.UnitType == "currency")
|
||||
{
|
||||
res = (value * targetUnit.Modifier) / originUnit.Modifier;
|
||||
}
|
||||
else
|
||||
res = (value * originUnit.Modifier) / targetUnit.Modifier;
|
||||
}
|
||||
res = Math.Round(res, 4);
|
||||
|
||||
await ctx.Channel.SendConfirmAsync(GetText("convert", value, originUnit.Triggers.Last(), res, targetUnit.Triggers.Last())).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
430
src/NadekoBot/Modules/Utility/Utility.cs
Normal file
430
src/NadekoBot/Modules/Utility/Utility.cs
Normal file
@@ -0,0 +1,430 @@
|
||||
using Discord;
|
||||
using Discord.Commands;
|
||||
using Discord.WebSocket;
|
||||
using NadekoBot.Common;
|
||||
using NadekoBot.Common.Attributes;
|
||||
using NadekoBot.Core.Services;
|
||||
using NadekoBot.Core.Services.Impl;
|
||||
using NadekoBot.Extensions;
|
||||
using Newtonsoft.Json;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using NadekoBot.Common.Replacements;
|
||||
using NadekoBot.Core.Common;
|
||||
using Serilog;
|
||||
|
||||
namespace NadekoBot.Modules.Utility
|
||||
{
|
||||
public partial class Utility : NadekoModule
|
||||
{
|
||||
private readonly DiscordSocketClient _client;
|
||||
private readonly IStatsService _stats;
|
||||
private readonly IBotCredentials _creds;
|
||||
private readonly NadekoBot _bot;
|
||||
private readonly DownloadTracker _tracker;
|
||||
|
||||
public Utility(NadekoBot nadeko, DiscordSocketClient client,
|
||||
IStatsService stats, IBotCredentials creds, DownloadTracker tracker)
|
||||
{
|
||||
_client = client;
|
||||
_stats = stats;
|
||||
_creds = creds;
|
||||
_bot = nadeko;
|
||||
_tracker = tracker;
|
||||
}
|
||||
|
||||
|
||||
[NadekoCommand, Usage, Description, Aliases]
|
||||
[RequireContext(ContextType.Guild)]
|
||||
[UserPerm(GuildPerm.ManageMessages)]
|
||||
[Priority(1)]
|
||||
public async Task Say(ITextChannel channel, [Leftover] string message)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(message))
|
||||
return;
|
||||
|
||||
var rep = new ReplacementBuilder()
|
||||
.WithDefault(ctx.User, channel, (SocketGuild)ctx.Guild, (DiscordSocketClient)ctx.Client)
|
||||
.Build();
|
||||
|
||||
if (CREmbed.TryParse(message, out var embedData))
|
||||
{
|
||||
rep.Replace(embedData);
|
||||
await channel.EmbedAsync(embedData, sanitizeAll: !((IGuildUser)Context.User).GuildPermissions.MentionEveryone).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
var msg = rep.Replace(message);
|
||||
if (!string.IsNullOrWhiteSpace(msg))
|
||||
{
|
||||
await channel.SendConfirmAsync(msg).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[NadekoCommand, Usage, Description, Aliases]
|
||||
[RequireContext(ContextType.Guild)]
|
||||
[UserPerm(GuildPerm.ManageMessages)]
|
||||
[Priority(0)]
|
||||
public Task Say([Leftover] string message) =>
|
||||
Say((ITextChannel)ctx.Channel, message);
|
||||
|
||||
[NadekoCommand, Usage, Description, 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))
|
||||
{
|
||||
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("nobody_playing_game").ConfigureAwait(false);
|
||||
else
|
||||
{
|
||||
await ctx.Channel.SendConfirmAsync("```css\n" + string.Join("\n", arr.GroupBy(item => (i++) / 2)
|
||||
.Select(ig => string.Concat(ig.Select(el => $"• {el,-27}")))) + "\n```")
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
[NadekoCommand, Usage, Description, Aliases]
|
||||
[RequireContext(ContextType.Guild)]
|
||||
[Priority(0)]
|
||||
public async Task InRole(int page, [Leftover] IRole role = null)
|
||||
{
|
||||
if (--page < 0)
|
||||
return;
|
||||
|
||||
await Context.Channel.TriggerTypingAsync().ConfigureAwait(false);
|
||||
await _tracker.EnsureUsersDownloadedAsync(ctx.Guild).ConfigureAwait(false);
|
||||
|
||||
var users = await ctx.Guild.GetUsersAsync();
|
||||
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 new EmbedBuilder().WithOkColor().WithDescription(GetText("no_user_on_this_page"));
|
||||
|
||||
return new EmbedBuilder().WithOkColor()
|
||||
.WithTitle(GetText("inrole_list", Format.Bold(role?.Name ?? "No Role")) + $" - {roleUsers.Length}")
|
||||
.WithDescription(string.Join("\n", pageUsers));
|
||||
}, roleUsers.Length, 20).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
[NadekoCommand, Usage, Description, Aliases]
|
||||
[RequireContext(ContextType.Guild)]
|
||||
[Priority(1)]
|
||||
public Task InRole([Leftover] IRole role = null)
|
||||
=> InRole(1, role);
|
||||
|
||||
public enum MeOrBot { Me, Bot }
|
||||
|
||||
[NadekoCommand, Usage, Description, 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 ctx.Channel.SendConfirmAsync(builder.ToString()).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
[NadekoCommand, Usage, Description, Aliases]
|
||||
[RequireContext(ContextType.Guild)]
|
||||
public async Task UserId([Leftover] IGuildUser target = null)
|
||||
{
|
||||
var usr = target ?? ctx.User;
|
||||
await ReplyConfirmLocalizedAsync("userid", "🆔", Format.Bold(usr.ToString()),
|
||||
Format.Code(usr.Id.ToString())).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
[NadekoCommand, Usage, Description, Aliases]
|
||||
[RequireContext(ContextType.Guild)]
|
||||
public async Task RoleId([Leftover] IRole role)
|
||||
{
|
||||
await ReplyConfirmLocalizedAsync("roleid", "🆔", Format.Bold(role.ToString()),
|
||||
Format.Code(role.Id.ToString())).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
[NadekoCommand, Usage, Description, Aliases]
|
||||
public async Task ChannelId()
|
||||
{
|
||||
await ReplyConfirmLocalizedAsync("channelid", "🆔", Format.Code(ctx.Channel.Id.ToString()))
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
[NadekoCommand, Usage, Description, Aliases]
|
||||
[RequireContext(ContextType.Guild)]
|
||||
public async Task ServerId()
|
||||
{
|
||||
await ReplyConfirmLocalizedAsync("serverid", "🆔", Format.Code(ctx.Guild.Id.ToString()))
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
[NadekoCommand, Usage, Description, Aliases]
|
||||
[RequireContext(ContextType.Guild)]
|
||||
public async Task Roles(IGuildUser target, int page = 1)
|
||||
{
|
||||
var channel = (ITextChannel)ctx.Channel;
|
||||
var guild = channel.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("no_roles_on_page").ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
await channel.SendConfirmAsync(GetText("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("no_roles_on_page").ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
await channel.SendConfirmAsync(GetText("roles_all_page", page),
|
||||
"\n• " + string.Join("\n• ", (IEnumerable<IRole>)roles).SanitizeMentions(true)).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[NadekoCommand, Usage, Description, Aliases]
|
||||
[RequireContext(ContextType.Guild)]
|
||||
public Task Roles(int page = 1) =>
|
||||
Roles(null, page);
|
||||
|
||||
[NadekoCommand, Usage, Description, Aliases]
|
||||
[RequireContext(ContextType.Guild)]
|
||||
public async Task ChannelTopic([Leftover]ITextChannel channel = null)
|
||||
{
|
||||
if (channel == null)
|
||||
channel = (ITextChannel)ctx.Channel;
|
||||
|
||||
var topic = channel.Topic;
|
||||
if (string.IsNullOrWhiteSpace(topic))
|
||||
await ReplyErrorLocalizedAsync("no_topic_set").ConfigureAwait(false);
|
||||
else
|
||||
await ctx.Channel.SendConfirmAsync(GetText("channel_topic"), topic).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
[NadekoCommand, Usage, Description, Aliases]
|
||||
public async Task Stats()
|
||||
{
|
||||
var ownerIds = string.Join("\n", _creds.OwnerIds);
|
||||
if (string.IsNullOrWhiteSpace(ownerIds))
|
||||
ownerIds = "-";
|
||||
|
||||
await ctx.Channel.EmbedAsync(
|
||||
new EmbedBuilder().WithOkColor()
|
||||
.WithAuthor(eab => eab.WithName($"NadekoBot v{StatsService.BotVersion}")
|
||||
.WithUrl("http://nadekobot.readthedocs.io/en/latest/")
|
||||
.WithIconUrl("https://nadeko-pictures.nyc3.digitaloceanspaces.com/other/avatar.png"))
|
||||
.AddField(efb => efb.WithName(GetText("author")).WithValue(_stats.Author).WithIsInline(true))
|
||||
.AddField(efb => efb.WithName(GetText("botid")).WithValue(_client.CurrentUser.Id.ToString()).WithIsInline(true))
|
||||
.AddField(efb => efb.WithName(GetText("shard")).WithValue($"#{_client.ShardId} / {_creds.TotalShards}").WithIsInline(true))
|
||||
.AddField(efb => efb.WithName(GetText("commands_ran")).WithValue(_stats.CommandsRan.ToString()).WithIsInline(true))
|
||||
.AddField(efb => efb.WithName(GetText("messages")).WithValue($"{_stats.MessageCounter} ({_stats.MessagesPerSecond:F2}/sec)").WithIsInline(true))
|
||||
.AddField(efb => efb.WithName(GetText("memory")).WithValue($"{_stats.Heap} MB").WithIsInline(true))
|
||||
.AddField(efb => efb.WithName(GetText("owner_ids")).WithValue(ownerIds).WithIsInline(true))
|
||||
.AddField(efb => efb.WithName(GetText("uptime")).WithValue(_stats.GetUptimeString("\n")).WithIsInline(true))
|
||||
.AddField(efb => efb.WithName(GetText("presence")).WithValue(
|
||||
GetText("presence_txt",
|
||||
_bot.GuildCount, _stats.TextChannels, _stats.VoiceChannels)).WithIsInline(true))).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
[NadekoCommand, Usage, Description, 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("showemojis", m, m.Url)));
|
||||
|
||||
if (string.IsNullOrWhiteSpace(result))
|
||||
await ReplyErrorLocalizedAsync("showemojis_none").ConfigureAwait(false);
|
||||
else
|
||||
await ctx.Channel.SendMessageAsync(result.TrimTo(2000)).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
[NadekoCommand, Usage, Description, 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("listservers_none").ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
|
||||
await ctx.Channel.EmbedAsync(guilds.Aggregate(new EmbedBuilder().WithOkColor(),
|
||||
(embed, g) => embed.AddField(efb => efb.WithName(g.Name)
|
||||
.WithValue(
|
||||
GetText("listservers", g.Id, g.MemberCount,
|
||||
g.OwnerId))
|
||||
.WithIsInline(false))))
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
|
||||
[NadekoCommand, Usage, Description, 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 =>
|
||||
{
|
||||
var msg = $"【{s.Timestamp:HH:mm:ss}】{s.Author}:";
|
||||
if (string.IsNullOrWhiteSpace(s.ToString()))
|
||||
{
|
||||
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}"));
|
||||
}
|
||||
}
|
||||
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);
|
||||
|
||||
[NadekoCommand, Usage, Description, Aliases]
|
||||
#if GLOBAL_NADEKO
|
||||
[Ratelimit(30)]
|
||||
#endif
|
||||
public async Task Ping()
|
||||
{
|
||||
await sem.WaitAsync(5000).ConfigureAwait(false);
|
||||
try
|
||||
{
|
||||
var sw = Stopwatch.StartNew();
|
||||
var msg = await ctx.Channel.SendMessageAsync("🏓").ConfigureAwait(false);
|
||||
sw.Stop();
|
||||
msg.DeleteAfter(0);
|
||||
|
||||
await ctx.Channel.SendConfirmAsync($"{Format.Bold(ctx.User.ToString())} 🏓 {(int)sw.Elapsed.TotalMilliseconds}ms").ConfigureAwait(false);
|
||||
}
|
||||
finally
|
||||
{
|
||||
sem.Release();
|
||||
}
|
||||
}
|
||||
|
||||
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 ReplyConfirmLocalizedAsync("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 = new EmbedBuilder()
|
||||
// .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);
|
||||
// }
|
||||
}
|
||||
}
|
28
src/NadekoBot/Modules/Utility/VerboseErrorCommands.cs
Normal file
28
src/NadekoBot/Modules/Utility/VerboseErrorCommands.cs
Normal file
@@ -0,0 +1,28 @@
|
||||
using Discord;
|
||||
using Discord.Commands;
|
||||
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, Usage, Description, 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("verbose_errors_enabled").ConfigureAwait(false);
|
||||
else
|
||||
await ReplyConfirmLocalizedAsync("verbose_errors_disabled").ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user