From 56f4c39ed3dfd9916adac99e6aad76a9f056a305 Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Thu, 8 Jun 2017 06:37:01 +0200 Subject: [PATCH 001/346] Fixed api link for omdb --- src/NadekoBot/Modules/Searches/Commands/OMDB/OmdbProvider.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/NadekoBot/Modules/Searches/Commands/OMDB/OmdbProvider.cs b/src/NadekoBot/Modules/Searches/Commands/OMDB/OmdbProvider.cs index a3d56fcf..59b8a6a1 100644 --- a/src/NadekoBot/Modules/Searches/Commands/OMDB/OmdbProvider.cs +++ b/src/NadekoBot/Modules/Searches/Commands/OMDB/OmdbProvider.cs @@ -10,7 +10,7 @@ namespace NadekoBot.Modules.Searches.Commands.OMDB { public static class OmdbProvider { - private const string queryUrl = "http://www.omdbapi.com/?t={0}&y=&plot=full&r=json"; + private const string queryUrl = "http://omdbapi.nadekobot.me/?t={0}&y=&plot=full&r=json"; public static async Task FindMovie(string name) { From 31fa8552777b382f47e83984877793522f746561 Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Sun, 11 Jun 2017 11:44:30 +0200 Subject: [PATCH 002/346] Update help --- src/NadekoBot/Modules/Help/Help.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/NadekoBot/Modules/Help/Help.cs b/src/NadekoBot/Modules/Help/Help.cs index 1208260b..e5b7c462 100644 --- a/src/NadekoBot/Modules/Help/Help.cs +++ b/src/NadekoBot/Modules/Help/Help.cs @@ -159,8 +159,8 @@ namespace NadekoBot.Modules.Help public async Task Guide() { await ConfirmLocalized("guide", - "http://nadekobot.readthedocs.io/en/latest/Commands%20List/", - "http://nadekobot.readthedocs.io/en/latest/").ConfigureAwait(false); + "http://nadekobot.readthedocs.io/en/1.3x/Commands%20List/", + "http://nadekobot.readthedocs.io/en/1.3x/").ConfigureAwait(false); } [NadekoCommand, Usage, Description, Aliases] From b381ee00b6ca69d05cbac4193fa022e128f43f09 Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Mon, 19 Jun 2017 15:42:10 +0200 Subject: [PATCH 003/346] Huge changes to make shards run in separate processes --- .../ModuleBehaviors/IEarlyBlockingExecutor.cs | 2 +- .../ModuleBehaviors/ILateBlocker.cs | 2 +- .../ModuleBehaviors/ILateExecutor.cs | 2 +- .../ShardCom/IShardComMessage.cs | 16 +++ .../DataStructures/ShardCom/ShardComClient.cs | 22 +++ .../DataStructures/ShardCom/ShardComServer.cs | 36 +++++ .../TypeReaders/GuildTypeReader.cs | 4 +- .../Commands/SelfAssignedRolesCommand.cs | 2 +- .../Administration/Commands/SelfCommands.cs | 54 +++---- .../Commands/TimeZoneCommands.cs | 2 +- .../CustomReactions/CustomReactions.cs | 4 +- .../Modules/Gambling/Commands/AnimalRacing.cs | 8 +- .../Gambling/Commands/CurrencyEvents.cs | 8 +- .../Modules/Gambling/Commands/FlowerShop.cs | 4 +- .../Modules/Games/Commands/Acropobia.cs | 8 +- .../Games/Commands/Hangman/HangmanGame.cs | 4 +- .../Modules/Games/Commands/HangmanCommands.cs | 4 +- .../Games/Commands/Models/TypingGame.cs | 4 +- .../Modules/Games/Commands/PollCommands.cs | 4 +- .../Games/Commands/SpeedTypingCommands.cs | 4 +- .../Modules/Games/Commands/TicTacToe.cs | 8 +- .../Games/Commands/Trivia/TriviaGame.cs | 4 +- .../Modules/Games/Commands/TriviaCommands.cs | 4 +- src/NadekoBot/Modules/Music/Music.cs | 4 +- src/NadekoBot/Modules/NadekoModule.cs | 7 +- .../Permissions/Commands/FilterCommands.cs | 2 +- .../Utility/Commands/CommandMapCommands.cs | 4 +- .../Modules/Utility/Commands/InfoCommands.cs | 4 +- .../Utility/Commands/RepeatCommands.cs | 4 +- src/NadekoBot/Modules/Utility/Utility.cs | 80 ++++++----- src/NadekoBot/NadekoBot.cs | 136 ++++++++++++------ src/NadekoBot/Program.cs | 11 +- src/NadekoBot/Properties/launchSettings.json | 8 ++ .../Administration/AutoAssignRoleService.cs | 4 +- .../Administration/GameVoiceChannelService.cs | 4 +- .../Administration/LogCommandService.cs | 80 +++++------ .../Services/Administration/MuteService.cs | 4 +- .../Administration/PlayingRotateService.cs | 26 ++-- .../Administration/ProtectionService.cs | 4 +- .../Administration/RatelimitService.cs | 4 +- .../Services/Administration/SelfService.cs | 24 ++-- .../Services/Administration/VcRoleService.cs | 4 +- .../Services/Administration/VplusTService.cs | 4 +- .../ClashOfClans/ClashOfClansService.cs | 4 +- src/NadekoBot/Services/CommandHandler.cs | 4 +- .../CustomReactions/CustomReactionsService.cs | 9 +- .../Services/CustomReactions/Extensions.cs | 12 +- .../Repositories/IGuildConfigRepository.cs | 2 +- .../Impl/GuildConfigRepository.cs | 6 +- .../Discord/SocketMessageEventWrapper.cs | 4 +- .../Services/Games/ChatterbotService.cs | 6 +- src/NadekoBot/Services/Games/GamesService.cs | 4 +- src/NadekoBot/Services/Games/Poll.cs | 4 +- src/NadekoBot/Services/Games/PollService.cs | 6 +- .../Services/GreetSettingsService.cs | 4 +- src/NadekoBot/Services/Help/HelpService.cs | 2 +- src/NadekoBot/Services/IBotCredentials.cs | 1 + src/NadekoBot/Services/IImagesService.cs | 2 +- src/NadekoBot/Services/Impl/ImagesService.cs | 8 +- src/NadekoBot/Services/Impl/StatsService.cs | 53 +++---- src/NadekoBot/Services/LogSetup.cs | 28 ++++ .../Services/Permissions/CmdCdService.cs | 2 +- .../Services/Permissions/FilterService.cs | 2 +- .../Permissions/GlobalPermissionService.cs | 2 +- .../Permissions/PermissionsService.cs | 5 +- .../Services/Searches/SearchesService.cs | 4 +- .../Searches/StreamNotificationService.cs | 4 +- .../Utility/MessageRepeaterService.cs | 2 +- .../Services/Utility/RemindService.cs | 4 +- .../Services/Utility/RepeatRunner.cs | 2 +- .../Services/Utility/UtilityService.cs | 4 +- src/NadekoBot/ShardsCoordinator.cs | 95 ++++++++++++ src/NadekoBot/_Extensions/Extensions.cs | 4 +- 73 files changed, 586 insertions(+), 331 deletions(-) create mode 100644 src/NadekoBot/DataStructures/ShardCom/IShardComMessage.cs create mode 100644 src/NadekoBot/DataStructures/ShardCom/ShardComClient.cs create mode 100644 src/NadekoBot/DataStructures/ShardCom/ShardComServer.cs create mode 100644 src/NadekoBot/Properties/launchSettings.json create mode 100644 src/NadekoBot/Services/LogSetup.cs create mode 100644 src/NadekoBot/ShardsCoordinator.cs diff --git a/src/NadekoBot/DataStructures/ModuleBehaviors/IEarlyBlockingExecutor.cs b/src/NadekoBot/DataStructures/ModuleBehaviors/IEarlyBlockingExecutor.cs index f28eaf4f..a3e004b1 100644 --- a/src/NadekoBot/DataStructures/ModuleBehaviors/IEarlyBlockingExecutor.cs +++ b/src/NadekoBot/DataStructures/ModuleBehaviors/IEarlyBlockingExecutor.cs @@ -13,6 +13,6 @@ namespace NadekoBot.DataStructures.ModuleBehaviors /// Try to execute some logic within some module's service. /// /// Whether it should block other command executions after it. - Task TryExecuteEarly(DiscordShardedClient client, IGuild guild, IUserMessage msg); + Task TryExecuteEarly(DiscordSocketClient client, IGuild guild, IUserMessage msg); } } diff --git a/src/NadekoBot/DataStructures/ModuleBehaviors/ILateBlocker.cs b/src/NadekoBot/DataStructures/ModuleBehaviors/ILateBlocker.cs index 3b3fc020..68f33206 100644 --- a/src/NadekoBot/DataStructures/ModuleBehaviors/ILateBlocker.cs +++ b/src/NadekoBot/DataStructures/ModuleBehaviors/ILateBlocker.cs @@ -6,7 +6,7 @@ namespace NadekoBot.DataStructures.ModuleBehaviors { public interface ILateBlocker { - Task TryBlockLate(DiscordShardedClient client, IUserMessage msg, IGuild guild, + Task TryBlockLate(DiscordSocketClient client, IUserMessage msg, IGuild guild, IMessageChannel channel, IUser user, string moduleName, string commandName); } } diff --git a/src/NadekoBot/DataStructures/ModuleBehaviors/ILateExecutor.cs b/src/NadekoBot/DataStructures/ModuleBehaviors/ILateExecutor.cs index 3cf11603..a7b3e52e 100644 --- a/src/NadekoBot/DataStructures/ModuleBehaviors/ILateExecutor.cs +++ b/src/NadekoBot/DataStructures/ModuleBehaviors/ILateExecutor.cs @@ -9,6 +9,6 @@ namespace NadekoBot.DataStructures.ModuleBehaviors /// public interface ILateExecutor { - Task LateExecute(DiscordShardedClient client, IGuild guild, IUserMessage msg); + Task LateExecute(DiscordSocketClient client, IGuild guild, IUserMessage msg); } } diff --git a/src/NadekoBot/DataStructures/ShardCom/IShardComMessage.cs b/src/NadekoBot/DataStructures/ShardCom/IShardComMessage.cs new file mode 100644 index 00000000..9fb1f5d0 --- /dev/null +++ b/src/NadekoBot/DataStructures/ShardCom/IShardComMessage.cs @@ -0,0 +1,16 @@ +using Discord; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace NadekoBot.DataStructures.ShardCom +{ + public class ShardComMessage + { + public int ShardId { get; set; } + public ConnectionState ConnectionState { get; set; } + public int Guilds { get; set; } + } +} diff --git a/src/NadekoBot/DataStructures/ShardCom/ShardComClient.cs b/src/NadekoBot/DataStructures/ShardCom/ShardComClient.cs new file mode 100644 index 00000000..a8857f3e --- /dev/null +++ b/src/NadekoBot/DataStructures/ShardCom/ShardComClient.cs @@ -0,0 +1,22 @@ +using Newtonsoft.Json; +using System; +using System.Net; +using System.Net.Sockets; +using System.Text; +using System.Threading.Tasks; + +namespace NadekoBot.DataStructures.ShardCom +{ + public class ShardComClient + { + public async Task Send(ShardComMessage data) + { + var msg = JsonConvert.SerializeObject(data); + using (var client = new UdpClient()) + { + var bytes = Encoding.UTF8.GetBytes(msg); + await client.SendAsync(bytes, bytes.Length, IPAddress.Loopback.ToString(), ShardComServer.Port).ConfigureAwait(false); + } + } + } +} diff --git a/src/NadekoBot/DataStructures/ShardCom/ShardComServer.cs b/src/NadekoBot/DataStructures/ShardCom/ShardComServer.cs new file mode 100644 index 00000000..61c35a85 --- /dev/null +++ b/src/NadekoBot/DataStructures/ShardCom/ShardComServer.cs @@ -0,0 +1,36 @@ +using Newtonsoft.Json; +using System; +using System.Net; +using System.Net.Sockets; +using System.Text; +using System.Threading.Tasks; + +namespace NadekoBot.DataStructures.ShardCom +{ + public class ShardComServer : IDisposable + { + public const int Port = 5664; + private readonly UdpClient _client = new UdpClient(Port); + + public void Start() + { + Task.Run(async () => + { + var ip = new IPEndPoint(IPAddress.Any, 0); + while (true) + { + var recv = await _client.ReceiveAsync(); + var data = Encoding.UTF8.GetString(recv.Buffer); + var _ = OnDataReceived(JsonConvert.DeserializeObject(data)); + } + }); + } + + public void Dispose() + { + _client.Dispose(); + } + + public event Func OnDataReceived = delegate { return Task.CompletedTask; }; + } +} diff --git a/src/NadekoBot/DataStructures/TypeReaders/GuildTypeReader.cs b/src/NadekoBot/DataStructures/TypeReaders/GuildTypeReader.cs index 63971f5a..3bb72d4c 100644 --- a/src/NadekoBot/DataStructures/TypeReaders/GuildTypeReader.cs +++ b/src/NadekoBot/DataStructures/TypeReaders/GuildTypeReader.cs @@ -7,9 +7,9 @@ namespace NadekoBot.TypeReaders { public class GuildTypeReader : TypeReader { - private readonly DiscordShardedClient _client; + private readonly DiscordSocketClient _client; - public GuildTypeReader(DiscordShardedClient client) + public GuildTypeReader(DiscordSocketClient client) { _client = client; } diff --git a/src/NadekoBot/Modules/Administration/Commands/SelfAssignedRolesCommand.cs b/src/NadekoBot/Modules/Administration/Commands/SelfAssignedRolesCommand.cs index 9815fa1b..3ad63210 100644 --- a/src/NadekoBot/Modules/Administration/Commands/SelfAssignedRolesCommand.cs +++ b/src/NadekoBot/Modules/Administration/Commands/SelfAssignedRolesCommand.cs @@ -140,7 +140,7 @@ namespace NadekoBot.Modules.Administration await uow.CompleteAsync(); } - await Context.Channel.SendPaginatedConfirmAsync((DiscordShardedClient)Context.Client, page, (curPage) => + await Context.Channel.SendPaginatedConfirmAsync((DiscordSocketClient)Context.Client, page, (curPage) => { return new EmbedBuilder() .WithTitle(GetText("self_assign_list", roleCnt)) diff --git a/src/NadekoBot/Modules/Administration/Commands/SelfCommands.cs b/src/NadekoBot/Modules/Administration/Commands/SelfCommands.cs index aa99eeab..5e5ecb33 100644 --- a/src/NadekoBot/Modules/Administration/Commands/SelfCommands.cs +++ b/src/NadekoBot/Modules/Administration/Commands/SelfCommands.cs @@ -13,6 +13,7 @@ using NadekoBot.Services; using NadekoBot.Services.Database.Models; using Microsoft.EntityFrameworkCore; using NadekoBot.Services.Administration; +using System.Diagnostics; namespace NadekoBot.Modules.Administration { @@ -25,10 +26,10 @@ namespace NadekoBot.Modules.Administration private static readonly object _locker = new object(); private readonly SelfService _service; - private readonly DiscordShardedClient _client; + private readonly DiscordSocketClient _client; private readonly IImagesService _images; - public SelfCommands(DbService db, SelfService service, DiscordShardedClient client, + public SelfCommands(DbService db, SelfService service, DiscordSocketClient client, IImagesService images) { _db = db; @@ -204,28 +205,29 @@ namespace NadekoBot.Modules.Administration } - [NadekoCommand, Usage, Description, Aliases] - [OwnerOnly] - public async Task ConnectShard(int shardid) - { - var shard = _client.GetShard(shardid); + //todo 2 shard commands + //[NadekoCommand, Usage, Description, Aliases] + //[OwnerOnly] + //public async Task ConnectShard(int shardid) + //{ + // var shard = _client.GetShard(shardid); - if (shard == null) - { - await ReplyErrorLocalized("no_shard_id").ConfigureAwait(false); - return; - } - try - { - await ReplyConfirmLocalized("shard_reconnecting", Format.Bold("#" + shardid)).ConfigureAwait(false); - await shard.StartAsync().ConfigureAwait(false); - await ReplyConfirmLocalized("shard_reconnected", Format.Bold("#" + shardid)).ConfigureAwait(false); - } - catch (Exception ex) - { - _log.Warn(ex); - } - } + // if (shard == null) + // { + // await ReplyErrorLocalized("no_shard_id").ConfigureAwait(false); + // return; + // } + // try + // { + // await ReplyConfirmLocalized("shard_reconnecting", Format.Bold("#" + shardid)).ConfigureAwait(false); + // await shard.StartAsync().ConfigureAwait(false); + // await ReplyConfirmLocalized("shard_reconnected", Format.Bold("#" + shardid)).ConfigureAwait(false); + // } + // catch (Exception ex) + // { + // _log.Warn(ex); + // } + //} [NadekoCommand, Usage, Description, Aliases] [OwnerOnly] @@ -417,8 +419,10 @@ namespace NadekoBot.Modules.Administration [OwnerOnly] public async Task ReloadImages() { - var time = _images.Reload(); - await ReplyConfirmLocalized("images_loaded", time.TotalSeconds.ToString("F3")).ConfigureAwait(false); + var sw = Stopwatch.StartNew(); + _images.Reload(); + sw.Stop(); + await ReplyConfirmLocalized("images_loaded", sw.Elapsed.TotalSeconds.ToString("F3")).ConfigureAwait(false); } private static UserStatus SettableUserStatusToUserStatus(SettableUserStatus sus) diff --git a/src/NadekoBot/Modules/Administration/Commands/TimeZoneCommands.cs b/src/NadekoBot/Modules/Administration/Commands/TimeZoneCommands.cs index e054288c..c97fdbca 100644 --- a/src/NadekoBot/Modules/Administration/Commands/TimeZoneCommands.cs +++ b/src/NadekoBot/Modules/Administration/Commands/TimeZoneCommands.cs @@ -36,7 +36,7 @@ namespace NadekoBot.Modules.Administration .ToArray(); var timezonesPerPage = 20; - await Context.Channel.SendPaginatedConfirmAsync((DiscordShardedClient)Context.Client, page, + await Context.Channel.SendPaginatedConfirmAsync((DiscordSocketClient)Context.Client, page, (curPage) => new EmbedBuilder() .WithOkColor() .WithTitle(GetText("timezones_available")) diff --git a/src/NadekoBot/Modules/CustomReactions/CustomReactions.cs b/src/NadekoBot/Modules/CustomReactions/CustomReactions.cs index 5dac3b9c..ae033cad 100644 --- a/src/NadekoBot/Modules/CustomReactions/CustomReactions.cs +++ b/src/NadekoBot/Modules/CustomReactions/CustomReactions.cs @@ -17,10 +17,10 @@ namespace NadekoBot.Modules.CustomReactions private readonly IBotCredentials _creds; private readonly DbService _db; private readonly CustomReactionsService _crs; - private readonly DiscordShardedClient _client; + private readonly DiscordSocketClient _client; public CustomReactions(IBotCredentials creds, DbService db, CustomReactionsService crs, - DiscordShardedClient client) + DiscordSocketClient client) { _creds = creds; _db = db; diff --git a/src/NadekoBot/Modules/Gambling/Commands/AnimalRacing.cs b/src/NadekoBot/Modules/Gambling/Commands/AnimalRacing.cs index 4db985e5..46cd25b3 100644 --- a/src/NadekoBot/Modules/Gambling/Commands/AnimalRacing.cs +++ b/src/NadekoBot/Modules/Gambling/Commands/AnimalRacing.cs @@ -22,12 +22,12 @@ namespace NadekoBot.Modules.Gambling { private readonly BotConfig _bc; private readonly CurrencyService _cs; - private readonly DiscordShardedClient _client; + private readonly DiscordSocketClient _client; public static ConcurrentDictionary AnimalRaces { get; } = new ConcurrentDictionary(); - public AnimalRacing(BotConfig bc, CurrencyService cs, DiscordShardedClient client) + public AnimalRacing(BotConfig bc, CurrencyService cs, DiscordSocketClient client) { _bc = bc; _cs = cs; @@ -82,14 +82,14 @@ namespace NadekoBot.Modules.Gambling private readonly ITextChannel _raceChannel; private readonly BotConfig _bc; private readonly CurrencyService _cs; - private readonly DiscordShardedClient _client; + private readonly DiscordSocketClient _client; private readonly ILocalization _localization; private readonly NadekoStrings _strings; public bool Started { get; private set; } public AnimalRace(ulong serverId, ITextChannel channel, string prefix, BotConfig bc, - CurrencyService cs, DiscordShardedClient client, ILocalization localization, + CurrencyService cs, DiscordSocketClient client, ILocalization localization, NadekoStrings strings) { _prefix = prefix; diff --git a/src/NadekoBot/Modules/Gambling/Commands/CurrencyEvents.cs b/src/NadekoBot/Modules/Gambling/Commands/CurrencyEvents.cs index 76369c15..644823bc 100644 --- a/src/NadekoBot/Modules/Gambling/Commands/CurrencyEvents.cs +++ b/src/NadekoBot/Modules/Gambling/Commands/CurrencyEvents.cs @@ -34,11 +34,11 @@ namespace NadekoBot.Modules.Gambling .ToArray(); private string _secretCode = string.Empty; - private readonly DiscordShardedClient _client; + private readonly DiscordSocketClient _client; private readonly BotConfig _bc; private readonly CurrencyService _cs; - public CurrencyEvents(DiscordShardedClient client, BotConfig bc, CurrencyService cs) + public CurrencyEvents(DiscordSocketClient client, BotConfig bc, CurrencyService cs) { _client = client; _bc = bc; @@ -151,7 +151,7 @@ namespace NadekoBot.Modules.Gambling { private readonly ConcurrentHashSet _flowerReactionAwardedUsers = new ConcurrentHashSet(); private readonly Logger _log; - private readonly DiscordShardedClient _client; + private readonly DiscordSocketClient _client; private readonly CurrencyService _cs; private IUserMessage StartingMessage { get; set; } @@ -159,7 +159,7 @@ namespace NadekoBot.Modules.Gambling private CancellationTokenSource Source { get; } private CancellationToken CancelToken { get; } - public FlowerReactionEvent(DiscordShardedClient client, CurrencyService cs) + public FlowerReactionEvent(DiscordSocketClient client, CurrencyService cs) { _log = LogManager.GetCurrentClassLogger(); _client = client; diff --git a/src/NadekoBot/Modules/Gambling/Commands/FlowerShop.cs b/src/NadekoBot/Modules/Gambling/Commands/FlowerShop.cs index dfc8d648..dd4fd309 100644 --- a/src/NadekoBot/Modules/Gambling/Commands/FlowerShop.cs +++ b/src/NadekoBot/Modules/Gambling/Commands/FlowerShop.cs @@ -22,7 +22,7 @@ namespace NadekoBot.Modules.Gambling private readonly BotConfig _bc; private readonly DbService _db; private readonly CurrencyService _cs; - private readonly DiscordShardedClient _client; + private readonly DiscordSocketClient _client; public enum Role { @@ -34,7 +34,7 @@ namespace NadekoBot.Modules.Gambling List } - public FlowerShop(BotConfig bc, DbService db, CurrencyService cs, DiscordShardedClient client) + public FlowerShop(BotConfig bc, DbService db, CurrencyService cs, DiscordSocketClient client) { _db = db; _bc = bc; diff --git a/src/NadekoBot/Modules/Games/Commands/Acropobia.cs b/src/NadekoBot/Modules/Games/Commands/Acropobia.cs index 0d8e337f..44b64808 100644 --- a/src/NadekoBot/Modules/Games/Commands/Acropobia.cs +++ b/src/NadekoBot/Modules/Games/Commands/Acropobia.cs @@ -20,12 +20,12 @@ namespace NadekoBot.Modules.Games [Group] public class Acropobia : NadekoSubmodule { - private readonly DiscordShardedClient _client; + private readonly DiscordSocketClient _client; //channelId, game public static ConcurrentDictionary AcrophobiaGames { get; } = new ConcurrentDictionary(); - public Acropobia(DiscordShardedClient client) + public Acropobia(DiscordSocketClient client) { _client = client; } @@ -86,10 +86,10 @@ namespace NadekoBot.Modules.Games //text, votes private readonly ConcurrentDictionary _votes = new ConcurrentDictionary(); private readonly Logger _log; - private readonly DiscordShardedClient _client; + private readonly DiscordSocketClient _client; private readonly NadekoStrings _strings; - public AcrophobiaGame(DiscordShardedClient client, NadekoStrings strings, ITextChannel channel, int time) + public AcrophobiaGame(DiscordSocketClient client, NadekoStrings strings, ITextChannel channel, int time) { _log = LogManager.GetCurrentClassLogger(); _client = client; diff --git a/src/NadekoBot/Modules/Games/Commands/Hangman/HangmanGame.cs b/src/NadekoBot/Modules/Games/Commands/Hangman/HangmanGame.cs index 24ca87c9..eddd00fc 100644 --- a/src/NadekoBot/Modules/Games/Commands/Hangman/HangmanGame.cs +++ b/src/NadekoBot/Modules/Games/Commands/Hangman/HangmanGame.cs @@ -56,7 +56,7 @@ namespace NadekoBot.Modules.Games.Hangman public class HangmanGame: IDisposable { private readonly Logger _log; - private readonly DiscordShardedClient _client; + private readonly DiscordSocketClient _client; public IMessageChannel GameChannel { get; } public HashSet Guesses { get; } = new HashSet(); @@ -82,7 +82,7 @@ namespace NadekoBot.Modules.Games.Hangman public event Action OnEnded; - public HangmanGame(DiscordShardedClient client, IMessageChannel channel, string type) + public HangmanGame(DiscordSocketClient client, IMessageChannel channel, string type) { _log = LogManager.GetCurrentClassLogger(); _client = client; diff --git a/src/NadekoBot/Modules/Games/Commands/HangmanCommands.cs b/src/NadekoBot/Modules/Games/Commands/HangmanCommands.cs index 633be81f..c515c0b8 100644 --- a/src/NadekoBot/Modules/Games/Commands/HangmanCommands.cs +++ b/src/NadekoBot/Modules/Games/Commands/HangmanCommands.cs @@ -15,9 +15,9 @@ namespace NadekoBot.Modules.Games [Group] public class HangmanCommands : NadekoSubmodule { - private readonly DiscordShardedClient _client; + private readonly DiscordSocketClient _client; - public HangmanCommands(DiscordShardedClient client) + public HangmanCommands(DiscordSocketClient client) { _client = client; } diff --git a/src/NadekoBot/Modules/Games/Commands/Models/TypingGame.cs b/src/NadekoBot/Modules/Games/Commands/Models/TypingGame.cs index db670fd2..92b01eb3 100644 --- a/src/NadekoBot/Modules/Games/Commands/Models/TypingGame.cs +++ b/src/NadekoBot/Modules/Games/Commands/Models/TypingGame.cs @@ -20,13 +20,13 @@ namespace NadekoBot.Modules.Games.Models public bool IsActive { get; private set; } private readonly Stopwatch sw; private readonly List finishedUserIds; - private readonly DiscordShardedClient _client; + private readonly DiscordSocketClient _client; private readonly GamesService _games; private readonly string _prefix; private Logger _log { get; } - public TypingGame(GamesService games, DiscordShardedClient client, ITextChannel channel, string prefix) //kek@prefix + public TypingGame(GamesService games, DiscordSocketClient client, ITextChannel channel, string prefix) //kek@prefix { _log = LogManager.GetCurrentClassLogger(); _games = games; diff --git a/src/NadekoBot/Modules/Games/Commands/PollCommands.cs b/src/NadekoBot/Modules/Games/Commands/PollCommands.cs index 487473f0..1cf5b951 100644 --- a/src/NadekoBot/Modules/Games/Commands/PollCommands.cs +++ b/src/NadekoBot/Modules/Games/Commands/PollCommands.cs @@ -13,10 +13,10 @@ namespace NadekoBot.Modules.Games [Group] public class PollCommands : NadekoSubmodule { - private readonly DiscordShardedClient _client; + private readonly DiscordSocketClient _client; private readonly PollService _polls; - public PollCommands(DiscordShardedClient client, PollService polls) + public PollCommands(DiscordSocketClient client, PollService polls) { _client = client; _polls = polls; diff --git a/src/NadekoBot/Modules/Games/Commands/SpeedTypingCommands.cs b/src/NadekoBot/Modules/Games/Commands/SpeedTypingCommands.cs index 72663094..6cce5191 100644 --- a/src/NadekoBot/Modules/Games/Commands/SpeedTypingCommands.cs +++ b/src/NadekoBot/Modules/Games/Commands/SpeedTypingCommands.cs @@ -20,9 +20,9 @@ namespace NadekoBot.Modules.Games { public static ConcurrentDictionary RunningContests = new ConcurrentDictionary(); private readonly GamesService _games; - private readonly DiscordShardedClient _client; + private readonly DiscordSocketClient _client; - public SpeedTypingCommands(DiscordShardedClient client, GamesService games) + public SpeedTypingCommands(DiscordSocketClient client, GamesService games) { _games = games; _client = client; diff --git a/src/NadekoBot/Modules/Games/Commands/TicTacToe.cs b/src/NadekoBot/Modules/Games/Commands/TicTacToe.cs index 7de40565..6f626325 100644 --- a/src/NadekoBot/Modules/Games/Commands/TicTacToe.cs +++ b/src/NadekoBot/Modules/Games/Commands/TicTacToe.cs @@ -21,9 +21,9 @@ namespace NadekoBot.Modules.Games private static readonly Dictionary _games = new Dictionary(); private readonly SemaphoreSlim _sem = new SemaphoreSlim(1, 1); - private readonly DiscordShardedClient _client; + private readonly DiscordSocketClient _client; - public TicTacToeCommands(DiscordShardedClient client) + public TicTacToeCommands(DiscordSocketClient client) { _client = client; } @@ -87,9 +87,9 @@ namespace NadekoBot.Modules.Games private IUserMessage _previousMessage; private Timer _timeoutTimer; private readonly NadekoStrings _strings; - private readonly DiscordShardedClient _client; + private readonly DiscordSocketClient _client; - public TicTacToe(NadekoStrings strings, DiscordShardedClient client, ITextChannel channel, IGuildUser firstUser) + public TicTacToe(NadekoStrings strings, DiscordSocketClient client, ITextChannel channel, IGuildUser firstUser) { _channel = channel; _strings = strings; diff --git a/src/NadekoBot/Modules/Games/Commands/Trivia/TriviaGame.cs b/src/NadekoBot/Modules/Games/Commands/Trivia/TriviaGame.cs index ea6e1e2c..b1b7d477 100644 --- a/src/NadekoBot/Modules/Games/Commands/Trivia/TriviaGame.cs +++ b/src/NadekoBot/Modules/Games/Commands/Trivia/TriviaGame.cs @@ -20,7 +20,7 @@ namespace NadekoBot.Modules.Games.Trivia private readonly SemaphoreSlim _guessLock = new SemaphoreSlim(1, 1); private readonly Logger _log; private readonly NadekoStrings _strings; - private readonly DiscordShardedClient _client; + private readonly DiscordSocketClient _client; private readonly BotConfig _bc; private readonly CurrencyService _cs; @@ -43,7 +43,7 @@ namespace NadekoBot.Modules.Games.Trivia public int WinRequirement { get; } - public TriviaGame(NadekoStrings strings, DiscordShardedClient client, BotConfig bc, + public TriviaGame(NadekoStrings strings, DiscordSocketClient client, BotConfig bc, CurrencyService cs, IGuild guild, ITextChannel channel, bool showHints, int winReq, bool isPokemon) { diff --git a/src/NadekoBot/Modules/Games/Commands/TriviaCommands.cs b/src/NadekoBot/Modules/Games/Commands/TriviaCommands.cs index d38e04dd..ac9cdcc3 100644 --- a/src/NadekoBot/Modules/Games/Commands/TriviaCommands.cs +++ b/src/NadekoBot/Modules/Games/Commands/TriviaCommands.cs @@ -18,12 +18,12 @@ namespace NadekoBot.Modules.Games public class TriviaCommands : NadekoSubmodule { private readonly CurrencyService _cs; - private readonly DiscordShardedClient _client; + private readonly DiscordSocketClient _client; private readonly BotConfig _bc; public static ConcurrentDictionary RunningTrivias { get; } = new ConcurrentDictionary(); - public TriviaCommands(DiscordShardedClient client, BotConfig bc, CurrencyService cs) + public TriviaCommands(DiscordSocketClient client, BotConfig bc, CurrencyService cs) { _cs = cs; _client = client; diff --git a/src/NadekoBot/Modules/Music/Music.cs b/src/NadekoBot/Modules/Music/Music.cs index afd60bb2..ebec33c5 100644 --- a/src/NadekoBot/Modules/Music/Music.cs +++ b/src/NadekoBot/Modules/Music/Music.cs @@ -22,12 +22,12 @@ namespace NadekoBot.Modules.Music public class Music : NadekoTopLevelModule { private static MusicService _music; - private readonly DiscordShardedClient _client; + private readonly DiscordSocketClient _client; private readonly IBotCredentials _creds; private readonly IGoogleApiService _google; private readonly DbService _db; - public Music(DiscordShardedClient client, IBotCredentials creds, IGoogleApiService google, + public Music(DiscordSocketClient client, IBotCredentials creds, IGoogleApiService google, DbService db, MusicService music) { _client = client; diff --git a/src/NadekoBot/Modules/NadekoModule.cs b/src/NadekoBot/Modules/NadekoModule.cs index dae471d4..45abaea9 100644 --- a/src/NadekoBot/Modules/NadekoModule.cs +++ b/src/NadekoBot/Modules/NadekoModule.cs @@ -86,13 +86,12 @@ namespace NadekoBot.Modules var text = GetText(textKey, replacements); return Context.Channel.SendConfirmAsync(Context.User.Mention + " " + text); } - - // todo maybe make this generic and use - // TypeConverter typeConverter = TypeDescriptor.GetConverter(propType); + + // TypeConverter typeConverter = TypeDescriptor.GetConverter(propType); ? public async Task GetUserInputAsync(ulong userId, ulong channelId) { var userInputTask = new TaskCompletionSource(); - var dsc = (DiscordShardedClient)Context.Client; + var dsc = (DiscordSocketClient)Context.Client; try { dsc.MessageReceived += MessageReceived; diff --git a/src/NadekoBot/Modules/Permissions/Commands/FilterCommands.cs b/src/NadekoBot/Modules/Permissions/Commands/FilterCommands.cs index 371678ff..1329fc0b 100644 --- a/src/NadekoBot/Modules/Permissions/Commands/FilterCommands.cs +++ b/src/NadekoBot/Modules/Permissions/Commands/FilterCommands.cs @@ -195,7 +195,7 @@ namespace NadekoBot.Modules.Permissions var fws = fwHash.ToArray(); - await channel.SendPaginatedConfirmAsync((DiscordShardedClient)Context.Client, + await channel.SendPaginatedConfirmAsync((DiscordSocketClient)Context.Client, page, (curPage) => new EmbedBuilder() diff --git a/src/NadekoBot/Modules/Utility/Commands/CommandMapCommands.cs b/src/NadekoBot/Modules/Utility/Commands/CommandMapCommands.cs index d38cfb90..03316135 100644 --- a/src/NadekoBot/Modules/Utility/Commands/CommandMapCommands.cs +++ b/src/NadekoBot/Modules/Utility/Commands/CommandMapCommands.cs @@ -21,9 +21,9 @@ namespace NadekoBot.Modules.Utility { private readonly CommandMapService _service; private readonly DbService _db; - private readonly DiscordShardedClient _client; + private readonly DiscordSocketClient _client; - public CommandMapCommands(CommandMapService service, DbService db, DiscordShardedClient client) + public CommandMapCommands(CommandMapService service, DbService db, DiscordSocketClient client) { _service = service; _db = db; diff --git a/src/NadekoBot/Modules/Utility/Commands/InfoCommands.cs b/src/NadekoBot/Modules/Utility/Commands/InfoCommands.cs index 3a5b8fe8..e3c625c4 100644 --- a/src/NadekoBot/Modules/Utility/Commands/InfoCommands.cs +++ b/src/NadekoBot/Modules/Utility/Commands/InfoCommands.cs @@ -16,10 +16,10 @@ namespace NadekoBot.Modules.Utility [Group] public class InfoCommands : NadekoSubmodule { - private readonly DiscordShardedClient _client; + private readonly DiscordSocketClient _client; private readonly IStatsService _stats; - public InfoCommands(DiscordShardedClient client, IStatsService stats, CommandHandler ch) + public InfoCommands(DiscordSocketClient client, IStatsService stats, CommandHandler ch) { _client = client; _stats = stats; diff --git a/src/NadekoBot/Modules/Utility/Commands/RepeatCommands.cs b/src/NadekoBot/Modules/Utility/Commands/RepeatCommands.cs index 6561712d..89619c1c 100644 --- a/src/NadekoBot/Modules/Utility/Commands/RepeatCommands.cs +++ b/src/NadekoBot/Modules/Utility/Commands/RepeatCommands.cs @@ -22,10 +22,10 @@ namespace NadekoBot.Modules.Utility public class RepeatCommands : NadekoSubmodule { private readonly MessageRepeaterService _service; - private readonly DiscordShardedClient _client; + private readonly DiscordSocketClient _client; private readonly DbService _db; - public RepeatCommands(MessageRepeaterService service, DiscordShardedClient client, DbService db) + public RepeatCommands(MessageRepeaterService service, DiscordSocketClient client, DbService db) { _service = service; _client = client; diff --git a/src/NadekoBot/Modules/Utility/Utility.cs b/src/NadekoBot/Modules/Utility/Utility.cs index 4774a266..4fecdf5e 100644 --- a/src/NadekoBot/Modules/Utility/Utility.cs +++ b/src/NadekoBot/Modules/Utility/Utility.cs @@ -24,15 +24,17 @@ namespace NadekoBot.Modules.Utility public partial class Utility : NadekoTopLevelModule { private static ConcurrentDictionary _rotatingRoleColors = new ConcurrentDictionary(); - private readonly DiscordShardedClient _client; + private readonly DiscordSocketClient _client; private readonly IStatsService _stats; private readonly IBotCredentials _creds; + private readonly NadekoBot _bot; - public Utility(DiscordShardedClient client, IStatsService stats, IBotCredentials creds) + public Utility(NadekoBot bot, DiscordSocketClient client, IStatsService stats, IBotCredentials creds) { _client = client; _stats = stats; _creds = creds; + _bot = bot; } //[NadekoCommand, Usage, Description, Aliases] @@ -352,56 +354,56 @@ namespace NadekoBot.Modules.Utility await Context.Channel.SendConfirmAsync($"{Context.User.Mention} https://discord.gg/{invite.Code}"); } + //todo 2 shard commands + //[NadekoCommand, Usage, Description, Aliases] + //public async Task ShardStats(int page = 1) + //{ + // if (--page < 0) + // return; - [NadekoCommand, Usage, Description, Aliases] - public async Task ShardStats(int page = 1) - { - if (--page < 0) - return; + // var status = string.Join(", ", _client.Shards.GroupBy(x => x.ConnectionState) + // .Select(x => $"{x.Count()} {x.Key}") + // .ToArray()); - var status = string.Join(", ", _client.Shards.GroupBy(x => x.ConnectionState) - .Select(x => $"{x.Count()} {x.Key}") - .ToArray()); - - var allShardStrings = _client.Shards - .Select(x => - GetText("shard_stats_txt", x.ShardId.ToString(), - Format.Bold(x.ConnectionState.ToString()), Format.Bold(x.Guilds.Count.ToString()))) - .ToArray(); + // var allShardStrings = _client.Shards + // .Select(x => + // GetText("shard_stats_txt", x.ShardId.ToString(), + // Format.Bold(x.ConnectionState.ToString()), Format.Bold(x.Guilds.Count.ToString()))) + // .ToArray(); - await Context.Channel.SendPaginatedConfirmAsync(_client, page, (curPage) => - { + // await Context.Channel.SendPaginatedConfirmAsync(_client, page, (curPage) => + // { - var str = string.Join("\n", allShardStrings.Skip(25 * curPage).Take(25)); + // var str = string.Join("\n", allShardStrings.Skip(25 * curPage).Take(25)); - if (string.IsNullOrWhiteSpace(str)) - str = GetText("no_shards_on_page"); + // if (string.IsNullOrWhiteSpace(str)) + // str = GetText("no_shards_on_page"); - return new EmbedBuilder() - .WithAuthor(a => a.WithName(GetText("shard_stats"))) - .WithTitle(status) - .WithOkColor() - .WithDescription(str); - }, allShardStrings.Length / 25); - } + // return new EmbedBuilder() + // .WithAuthor(a => a.WithName(GetText("shard_stats"))) + // .WithTitle(status) + // .WithOkColor() + // .WithDescription(str); + // }, allShardStrings.Length / 25); + //} - [NadekoCommand, Usage, Description, Aliases] - public async Task ShardId(IGuild guild) - { - var shardId = _client.GetShardIdFor(guild); + //[NadekoCommand, Usage, Description, Aliases] + //public async Task ShardId(IGuild guild) + //{ + // var shardId = _client.GetShardIdFor(guild); - await Context.Channel.SendConfirmAsync(shardId.ToString()).ConfigureAwait(false); - } + // await Context.Channel.SendConfirmAsync(shardId.ToString()).ConfigureAwait(false); + //} [NadekoCommand, Usage, Description, Aliases] public async Task Stats() { - var shardId = Context.Guild != null - ? _client.GetShardIdFor(Context.Guild) - : 0; - + //var shardId = Context.Guild != null + // ? _client.GetShardIdFor(Context.Guild) + // : 0; + await Context.Channel.EmbedAsync( new EmbedBuilder().WithOkColor() .WithAuthor(eab => eab.WithName($"NadekoBot v{StatsService.BotVersion}") @@ -409,7 +411,7 @@ namespace NadekoBot.Modules.Utility .WithIconUrl("https://cdn.discordapp.com/avatars/116275390695079945/b21045e778ef21c96d175400e779f0fb.jpg")) .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($"#{shardId} / {_client.Shards.Count}").WithIsInline(true)) + .AddField(efb => efb.WithName(GetText("shard")).WithValue($"#{_bot.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)) diff --git a/src/NadekoBot/NadekoBot.cs b/src/NadekoBot/NadekoBot.cs index 44802af6..1c086a7e 100644 --- a/src/NadekoBot/NadekoBot.cs +++ b/src/NadekoBot/NadekoBot.cs @@ -27,8 +27,7 @@ using NadekoBot.Services.Utility; using NadekoBot.Services.Help; using System.IO; using NadekoBot.Services.Pokemon; -using NadekoBot.DataStructures; -using NadekoBot.Extensions; +using NadekoBot.DataStructures.ShardCom; namespace NadekoBot { @@ -45,47 +44,74 @@ namespace NadekoBot public static Color OkColor { get; private set; } public static Color ErrorColor { get; private set; } - public ImmutableArray AllGuildConfigs { get; } + public ImmutableArray AllGuildConfigs { get; private set; } public BotConfig BotConfig { get; } public DbService Db { get; } public CommandService CommandService { get; } public CommandHandler CommandHandler { get; private set; } - public Localization Localization { get; } - public NadekoStrings Strings { get; } - public StatsService Stats { get; } + public Localization Localization { get; private set; } + public NadekoStrings Strings { get; private set; } + public StatsService Stats { get; private set; } public ImagesService Images { get; } public CurrencyService Currency { get; } public GoogleApiService GoogleApi { get; } - public DiscordShardedClient Client { get; } + public DiscordSocketClient Client { get; } public bool Ready { get; private set; } public INServiceProvider Services { get; private set; } public BotCredentials Credentials { get; } - public NadekoBot() + private const string _mutexName = @"Global\nadeko_shards_lock"; + private readonly Semaphore sem = new Semaphore(1, 1, _mutexName); + public int ShardId { get; } + private readonly Thread waitForParentKill; + private readonly ShardComClient _comClient = new ShardComClient(); + + public NadekoBot(int shardId, int parentProcessId) { - SetupLogger(); + if (shardId < 0) + throw new ArgumentOutOfRangeException(nameof(shardId)); + + ShardId = shardId; + + LogSetup.SetupLogger(); _log = LogManager.GetCurrentClassLogger(); TerribleElevatedPermissionCheck(); - + + waitForParentKill = new Thread(new ThreadStart(() => + { + try + { + var p = Process.GetProcessById(parentProcessId); + if (p == null) + return; + p.WaitForExit(); + } + finally + { + Environment.Exit(10); + } + })); + waitForParentKill.Start(); + Credentials = new BotCredentials(); Db = new DbService(Credentials); using (var uow = Db.UnitOfWork) { - AllGuildConfigs = uow.GuildConfigs.GetAllGuildConfigs().ToImmutableArray(); BotConfig = uow.BotConfig.GetOrCreate(); OkColor = new Color(Convert.ToUInt32(BotConfig.OkColor, 16)); ErrorColor = new Color(Convert.ToUInt32(BotConfig.ErrorColor, 16)); } - Client = new DiscordShardedClient(new DiscordSocketConfig + Client = new DiscordSocketClient(new DiscordSocketConfig { MessageCacheSize = 10, LogLevel = LogSeverity.Warning, - TotalShards = Credentials.TotalShards, ConnectionTimeout = int.MaxValue, + TotalShards = Credentials.TotalShards, + ShardId = shardId, AlwaysDownloadUsers = false, }); @@ -96,21 +122,45 @@ namespace NadekoBot }); //foundation services - Localization = new Localization(BotConfig.Locale, AllGuildConfigs.ToDictionary(x => x.GuildId, x => x.Locale), Db); - Strings = new NadekoStrings(Localization); - CommandHandler = new CommandHandler(Client, Db, BotConfig, AllGuildConfigs, CommandService, Credentials, this); - Stats = new StatsService(Client, CommandHandler, Credentials); Images = new ImagesService(); Currency = new CurrencyService(BotConfig, Db); GoogleApi = new GoogleApiService(Credentials); + StartSendingData(); + #if GLOBAL_NADEKO Client.Log += Client_Log; #endif } + private void StartSendingData() + { + Task.Run(async () => + { + while (true) + { + await _comClient.Send(new ShardComMessage() + { + ConnectionState = Client.ConnectionState, + Guilds = Client.ConnectionState == ConnectionState.Connected ? Client.Guilds.Count : 0, + ShardId = Client.ShardId, + }); + await Task.Delay(1000); + } + }); + } + private void AddServices() { + using (var uow = Db.UnitOfWork) + { + AllGuildConfigs = uow.GuildConfigs.GetAllGuildConfigs(Client.Guilds.Select(x => (long)x.Id).ToList()).ToImmutableArray(); + } + Localization = new Localization(BotConfig.Locale, AllGuildConfigs.ToDictionary(x => x.GuildId, x => x.Locale), Db); + Strings = new NadekoStrings(Localization); + CommandHandler = new CommandHandler(Client, Db, BotConfig, AllGuildConfigs, CommandService, Credentials, this); + Stats = new StatsService(Client, CommandHandler, Credentials); + var soundcloudApiService = new SoundCloudApiService(Credentials); #region help @@ -185,7 +235,7 @@ namespace NadekoBot .Add(Credentials) .Add(CommandService) .Add(Strings) - .Add(Client) + .Add(Client) .Add(BotConfig) .Add(Currency) .Add(CommandHandler) @@ -242,19 +292,30 @@ namespace NadekoBot CommandService.AddTypeReader(new GuildDateTimeTypeReader(guildTimezoneService)); } - private async Task LoginAsync(string token) + private Task LoginAsync(string token) { - _log.Info("Logging in..."); //connect - await Client.LoginAsync(TokenType.Bot, token).ConfigureAwait(false); - await Client.StartAsync().ConfigureAwait(false); - - _log.Info("Waiting for all shards to connect..."); - while (!Client.Shards.All(x => x.ConnectionState == ConnectionState.Connected)) + try { sem.WaitOne(); } catch (AbandonedMutexException) { } + _log.Info("Shard {0} logging in ...", ShardId); + try { - _log.Info("Connecting... {0}/{1}", Client.Shards.Count(x => x.ConnectionState == ConnectionState.Connected), Client.Shards.Count); - await Task.Delay(1000).ConfigureAwait(false); + Client.LoginAsync(TokenType.Bot, token).GetAwaiter().GetResult(); + Client.StartAsync().GetAwaiter().GetResult(); + while (Client.ConnectionState != ConnectionState.Connected) + Task.Delay(100).GetAwaiter().GetResult(); } + finally + { + _log.Info("Shard {0} logged in ...", ShardId); + sem.Release(); + } + return Task.CompletedTask; + //_log.Info("Waiting for all shards to connect..."); + //while (!Client.Shards.All(x => x.ConnectionState == ConnectionState.Connected)) + //{ + // _log.Info("Connecting... {0}/{1}", Client.Shards.Count(x => x.ConnectionState == ConnectionState.Connected), Client.Shards.Count); + // await Task.Delay(1000).ConfigureAwait(false); + //} } public async Task RunAsync(params string[] args) @@ -265,11 +326,11 @@ namespace NadekoBot await LoginAsync(Credentials.Token).ConfigureAwait(false); - _log.Info("Loading services..."); + _log.Info($"Shard {ShardId} loading services..."); AddServices(); sw.Stop(); - _log.Info($"Connected in {sw.Elapsed.TotalSeconds:F2} s"); + _log.Info($"Shard {ShardId} connected in {sw.Elapsed.TotalSeconds:F2} s"); var stats = Services.GetService(); stats.Initialize(); @@ -298,7 +359,8 @@ namespace NadekoBot .ForEach(x => CommandService.RemoveModuleAsync(x)); #endif Ready = true; - _log.Info(await stats.Print().ConfigureAwait(false)); + _log.Info($"Shard {ShardId} ready."); + //_log.Info(await stats.Print().ConfigureAwait(false)); } private Task Client_Log(LogMessage arg) @@ -330,19 +392,5 @@ namespace NadekoBot Environment.Exit(2); } } - - private static void SetupLogger() - { - var logConfig = new LoggingConfiguration(); - var consoleTarget = new ColoredConsoleTarget() - { - Layout = @"${date:format=HH\:mm\:ss} ${logger} | ${message}" - }; - logConfig.AddTarget("Console", consoleTarget); - - logConfig.LoggingRules.Add(new LoggingRule("*", LogLevel.Debug, consoleTarget)); - - LogManager.Configuration = logConfig; - } } } diff --git a/src/NadekoBot/Program.cs b/src/NadekoBot/Program.cs index 0c8832e2..20f3a052 100644 --- a/src/NadekoBot/Program.cs +++ b/src/NadekoBot/Program.cs @@ -2,7 +2,14 @@ { public class Program { - public static void Main(string[] args) => - new NadekoBot().RunAndBlockAsync(args).GetAwaiter().GetResult(); + public static void Main(string[] args) + { + if (args.Length == 0) + return; + if (args[0].ToLowerInvariant() == "main") + new ShardsCoordinator().RunAndBlockAsync(args).GetAwaiter().GetResult(); + else if (int.TryParse(args[0], out int shardId) && int.TryParse(args[1], out int parentProcessId)) + new NadekoBot(shardId, parentProcessId).RunAndBlockAsync(args).GetAwaiter().GetResult(); + } } } diff --git a/src/NadekoBot/Properties/launchSettings.json b/src/NadekoBot/Properties/launchSettings.json new file mode 100644 index 00000000..77336a17 --- /dev/null +++ b/src/NadekoBot/Properties/launchSettings.json @@ -0,0 +1,8 @@ +{ + "profiles": { + "NadekoBot": { + "commandName": "Project", + "commandLineArgs": "main" + } + } +} \ No newline at end of file diff --git a/src/NadekoBot/Services/Administration/AutoAssignRoleService.cs b/src/NadekoBot/Services/Administration/AutoAssignRoleService.cs index 14679b6d..7c8b8711 100644 --- a/src/NadekoBot/Services/Administration/AutoAssignRoleService.cs +++ b/src/NadekoBot/Services/Administration/AutoAssignRoleService.cs @@ -12,12 +12,12 @@ namespace NadekoBot.Services.Administration public class AutoAssignRoleService { private readonly Logger _log; - private readonly DiscordShardedClient _client; + private readonly DiscordSocketClient _client; //guildid/roleid public ConcurrentDictionary AutoAssignedRoles { get; } - public AutoAssignRoleService(DiscordShardedClient client, IEnumerable gcs) + public AutoAssignRoleService(DiscordSocketClient client, IEnumerable gcs) { _log = LogManager.GetCurrentClassLogger(); _client = client; diff --git a/src/NadekoBot/Services/Administration/GameVoiceChannelService.cs b/src/NadekoBot/Services/Administration/GameVoiceChannelService.cs index 14f9c6c6..52ea08f7 100644 --- a/src/NadekoBot/Services/Administration/GameVoiceChannelService.cs +++ b/src/NadekoBot/Services/Administration/GameVoiceChannelService.cs @@ -16,9 +16,9 @@ namespace NadekoBot.Services.Administration private readonly Logger _log; private readonly DbService _db; - private readonly DiscordShardedClient _client; + private readonly DiscordSocketClient _client; - public GameVoiceChannelService(DiscordShardedClient client, DbService db, IEnumerable gcs) + public GameVoiceChannelService(DiscordSocketClient client, DbService db, IEnumerable gcs) { _log = LogManager.GetCurrentClassLogger(); _db = db; diff --git a/src/NadekoBot/Services/Administration/LogCommandService.cs b/src/NadekoBot/Services/Administration/LogCommandService.cs index 84555345..420ada80 100644 --- a/src/NadekoBot/Services/Administration/LogCommandService.cs +++ b/src/NadekoBot/Services/Administration/LogCommandService.cs @@ -16,7 +16,7 @@ namespace NadekoBot.Services.Administration public class LogCommandService { - private readonly DiscordShardedClient _client; + private readonly DiscordSocketClient _client; private readonly Logger _log; private string PrettyCurrentTime => $"【{DateTime.UtcNow:HH:mm:ss}】"; @@ -31,7 +31,7 @@ namespace NadekoBot.Services.Administration private readonly MuteService _mute; private readonly ProtectionService _prot; - public LogCommandService(DiscordShardedClient client, NadekoStrings strings, + public LogCommandService(DiscordSocketClient client, NadekoStrings strings, IEnumerable gcs, DbService db, MuteService mute, ProtectionService prot) { _client = client; @@ -74,7 +74,7 @@ namespace NadekoBot.Services.Administration _client.UserUnbanned += _client_UserUnbanned; _client.UserJoined += _client_UserJoined; _client.UserLeft += _client_UserLeft; - _client.UserPresenceUpdated += _client_UserPresenceUpdated; + //_client.UserPresenceUpdated += _client_UserPresenceUpdated; _client.UserVoiceStateUpdated += _client_UserVoiceStateUpdated; _client.UserVoiceStateUpdated += _client_UserVoiceStateUpdated_TTS; _client.GuildMemberUpdated += _client_GuildUserUpdated; @@ -576,48 +576,48 @@ namespace NadekoBot.Services.Administration return Task.CompletedTask; } - private Task _client_UserPresenceUpdated(Optional optGuild, SocketUser usr, SocketPresence before, SocketPresence after) - { - var _ = Task.Run(async () => - { - try - { - var guild = optGuild.GetValueOrDefault() ?? (usr as SocketGuildUser)?.Guild; + //private Task _client_UserPresenceUpdated(Optional optGuild, SocketUser usr, SocketPresence before, SocketPresence after) + //{ + // var _ = Task.Run(async () => + // { + // try + // { + // var guild = optGuild.GetValueOrDefault() ?? (usr as SocketGuildUser)?.Guild; - if (guild == null) - return; + // if (guild == null) + // return; - if (!GuildLogSettings.TryGetValue(guild.Id, out LogSetting logSetting) - || (logSetting.LogUserPresenceId == null) - || before.Status == after.Status) - return; + // if (!GuildLogSettings.TryGetValue(guild.Id, out LogSetting logSetting) + // || (logSetting.LogUserPresenceId == null) + // || before.Status == after.Status) + // return; - ITextChannel logChannel; - if ((logChannel = await TryGetLogChannel(guild, logSetting, LogType.UserPresence)) == null) - return; - string str = ""; - if (before.Status != after.Status) - str = "🎭" + Format.Code(PrettyCurrentTime) + - GetText(logChannel.Guild, "user_status_change", - "👤" + Format.Bold(usr.Username), - Format.Bold(after.Status.ToString())); + // ITextChannel logChannel; + // if ((logChannel = await TryGetLogChannel(guild, logSetting, LogType.UserPresence)) == null) + // return; + // string str = ""; + // if (before.Status != after.Status) + // str = "🎭" + Format.Code(PrettyCurrentTime) + + // GetText(logChannel.Guild, "user_status_change", + // "👤" + Format.Bold(usr.Username), + // Format.Bold(after.Status.ToString())); - //if (before.Game?.Name != after.Game?.Name) - //{ - // if (str != "") - // str += "\n"; - // str += $"👾`{prettyCurrentTime}`👤__**{usr.Username}**__ is now playing **{after.Game?.Name}**."; - //} + // //if (before.Game?.Name != after.Game?.Name) + // //{ + // // if (str != "") + // // str += "\n"; + // // str += $"👾`{prettyCurrentTime}`👤__**{usr.Username}**__ is now playing **{after.Game?.Name}**."; + // //} - PresenceUpdates.AddOrUpdate(logChannel, new List() { str }, (id, list) => { list.Add(str); return list; }); - } - catch - { - // ignored - } - }); - return Task.CompletedTask; - } + // PresenceUpdates.AddOrUpdate(logChannel, new List() { str }, (id, list) => { list.Add(str); return list; }); + // } + // catch + // { + // // ignored + // } + // }); + // return Task.CompletedTask; + //} private Task _client_UserLeft(IGuildUser usr) { diff --git a/src/NadekoBot/Services/Administration/MuteService.cs b/src/NadekoBot/Services/Administration/MuteService.cs index 7ed9062a..87bde743 100644 --- a/src/NadekoBot/Services/Administration/MuteService.cs +++ b/src/NadekoBot/Services/Administration/MuteService.cs @@ -33,10 +33,10 @@ namespace NadekoBot.Services.Administration private static readonly OverwritePermissions denyOverwrite = new OverwritePermissions(sendMessages: PermValue.Deny, attachFiles: PermValue.Deny); private readonly Logger _log = LogManager.GetCurrentClassLogger(); - private readonly DiscordShardedClient _client; + private readonly DiscordSocketClient _client; private readonly DbService _db; - public MuteService(DiscordShardedClient client, IEnumerable gcs, DbService db) + public MuteService(DiscordSocketClient client, IEnumerable gcs, DbService db) { _client = client; _db = db; diff --git a/src/NadekoBot/Services/Administration/PlayingRotateService.cs b/src/NadekoBot/Services/Administration/PlayingRotateService.cs index 7662b7ca..c7a1bf41 100644 --- a/src/NadekoBot/Services/Administration/PlayingRotateService.cs +++ b/src/NadekoBot/Services/Administration/PlayingRotateService.cs @@ -17,7 +17,7 @@ namespace NadekoBot.Services.Administration public List RotatingStatusMessages { get; } public volatile bool RotatingStatuses; private readonly Timer _t; - private readonly DiscordShardedClient _client; + private readonly DiscordSocketClient _client; private readonly BotConfig _bc; private readonly MusicService _music; private readonly Logger _log; @@ -27,7 +27,7 @@ namespace NadekoBot.Services.Administration public int Index { get; set; } } - public PlayingRotateService(DiscordShardedClient client, BotConfig bc, MusicService music) + public PlayingRotateService(DiscordSocketClient client, BotConfig bc, MusicService music) { _client = client; _bc = bc; @@ -36,7 +36,7 @@ namespace NadekoBot.Services.Administration RotatingStatusMessages = _bc.RotatingStatusMessages; RotatingStatuses = _bc.RotatingStatuses; - + _t = new Timer(async (objState) => { try @@ -52,17 +52,12 @@ namespace NadekoBot.Services.Administration var status = RotatingStatusMessages[state.Index++].Status; if (string.IsNullOrWhiteSpace(status)) return; - PlayingPlaceholders.ForEach(e => status = status.Replace(e.Key, e.Value(_client,_music))); - var shards = _client.Shards; - for (int i = 0; i < shards.Count; i++) + PlayingPlaceholders.ForEach(e => status = status.Replace(e.Key, e.Value(_client, _music))); + ShardSpecificPlaceholders.ForEach(e => status = status.Replace(e.Key, e.Value(client))); + try { await client.SetGameAsync(status).ConfigureAwait(false); } + catch (Exception ex) { - var curShard = shards.ElementAt(i); - ShardSpecificPlaceholders.ForEach(e => status = status.Replace(e.Key, e.Value(curShard))); - try { await shards.ElementAt(i).SetGameAsync(status).ConfigureAwait(false); } - catch (Exception ex) - { - _log.Warn(ex); - } + _log.Warn(ex); } } catch (Exception ex) @@ -72,8 +67,8 @@ namespace NadekoBot.Services.Administration }, new TimerState(), TimeSpan.FromMinutes(1), TimeSpan.FromMinutes(1)); } - public Dictionary> PlayingPlaceholders { get; } = - new Dictionary> { + public Dictionary> PlayingPlaceholders { get; } = + new Dictionary> { { "%servers%", (c, ms) => c.Guilds.Count.ToString()}, { "%users%", (c, ms) => c.Guilds.Sum(s => s.Users.Count).ToString()}, { "%playing%", (c, ms) => { @@ -90,7 +85,6 @@ namespace NadekoBot.Services.Administration }, { "%queued%", (c, ms) => ms.MusicPlayers.Sum(kvp => kvp.Value.Playlist.Count).ToString()}, { "%time%", (c, ms) => DateTime.Now.ToString("HH:mm " + TimeZoneInfo.Local.StandardName.GetInitials()) }, - { "%shardcount%", (c, ms) => c.Shards.Count.ToString() }, }; public Dictionary> ShardSpecificPlaceholders { get; } = diff --git a/src/NadekoBot/Services/Administration/ProtectionService.cs b/src/NadekoBot/Services/Administration/ProtectionService.cs index 23bca7ad..6a2ae99b 100644 --- a/src/NadekoBot/Services/Administration/ProtectionService.cs +++ b/src/NadekoBot/Services/Administration/ProtectionService.cs @@ -21,10 +21,10 @@ namespace NadekoBot.Services.Administration public event Func OnAntiProtectionTriggered = delegate { return Task.CompletedTask; }; private readonly Logger _log; - private readonly DiscordShardedClient _client; + private readonly DiscordSocketClient _client; private readonly MuteService _mute; - public ProtectionService(DiscordShardedClient client, IEnumerable gcs, MuteService mute) + public ProtectionService(DiscordSocketClient client, IEnumerable gcs, MuteService mute) { _log = LogManager.GetCurrentClassLogger(); _client = client; diff --git a/src/NadekoBot/Services/Administration/RatelimitService.cs b/src/NadekoBot/Services/Administration/RatelimitService.cs index b344930f..a0f4171f 100644 --- a/src/NadekoBot/Services/Administration/RatelimitService.cs +++ b/src/NadekoBot/Services/Administration/RatelimitService.cs @@ -19,9 +19,9 @@ namespace NadekoBot.Services.Administration public ConcurrentDictionary> IgnoredUsers = new ConcurrentDictionary>(); private readonly Logger _log; - private readonly DiscordShardedClient _client; + private readonly DiscordSocketClient _client; - public SlowmodeService(DiscordShardedClient client, IEnumerable gcs) + public SlowmodeService(DiscordSocketClient client, IEnumerable gcs) { _log = LogManager.GetCurrentClassLogger(); _client = client; diff --git a/src/NadekoBot/Services/Administration/SelfService.cs b/src/NadekoBot/Services/Administration/SelfService.cs index f3148d5a..8e8a4a13 100644 --- a/src/NadekoBot/Services/Administration/SelfService.cs +++ b/src/NadekoBot/Services/Administration/SelfService.cs @@ -23,11 +23,11 @@ namespace NadekoBot.Services.Administration private readonly Logger _log; private readonly ILocalization _localization; private readonly NadekoStrings _strings; - private readonly DiscordShardedClient _client; + private readonly DiscordSocketClient _client; private readonly IBotCredentials _creds; private ImmutableArray> ownerChannels = new ImmutableArray>(); - public SelfService(DiscordShardedClient client, NadekoBot bot, CommandHandler cmdHandler, DbService db, + public SelfService(DiscordSocketClient client, NadekoBot bot, CommandHandler cmdHandler, DbService db, BotConfig bc, ILocalization localization, NadekoStrings strings, IBotCredentials creds) { _bot = bot; @@ -69,10 +69,7 @@ namespace NadekoBot.Services.Administration LoadOwnerChannels(); - if (!ownerChannels.Any()) - _log.Warn("No owner channels created! Make sure you've specified correct OwnerId in the credentials.json file."); - else - _log.Info($"Created {ownerChannels.Length} out of {_creds.OwnerIds.Length} owner message channels."); + }); } @@ -81,11 +78,9 @@ namespace NadekoBot.Services.Administration var hs = new HashSet(_creds.OwnerIds); var channels = new Dictionary>(); - foreach (var s in _client.Shards) + if (hs.Count > 0) { - if (hs.Count == 0) - break; - foreach (var g in s.Guilds) + foreach (var g in _client.Guilds) { if (hs.Count == 0) break; @@ -101,14 +96,19 @@ namespace NadekoBot.Services.Administration } } } - + ownerChannels = channels.OrderBy(x => _creds.OwnerIds.IndexOf(x.Key)) .Select(x => x.Value) .ToImmutableArray(); + + if (!ownerChannels.Any()) + _log.Warn("No owner channels created! Make sure you've specified correct OwnerId in the credentials.json file."); + else + _log.Info($"Created {ownerChannels.Length} out of {_creds.OwnerIds.Length} owner message channels."); } // forwards dms - public async Task LateExecute(DiscordShardedClient client, IGuild guild, IUserMessage msg) + public async Task LateExecute(DiscordSocketClient client, IGuild guild, IUserMessage msg) { if (msg.Channel is IDMChannel && ForwardDMs && ownerChannels.Length > 0) { diff --git a/src/NadekoBot/Services/Administration/VcRoleService.cs b/src/NadekoBot/Services/Administration/VcRoleService.cs index 49dbe9ff..10252833 100644 --- a/src/NadekoBot/Services/Administration/VcRoleService.cs +++ b/src/NadekoBot/Services/Administration/VcRoleService.cs @@ -14,11 +14,11 @@ namespace NadekoBot.Services.Administration { private readonly Logger _log; private readonly DbService _db; - private readonly DiscordShardedClient _client; + private readonly DiscordSocketClient _client; public ConcurrentDictionary> VcRoles { get; } - public VcRoleService(DiscordShardedClient client, IEnumerable gcs, DbService db) + public VcRoleService(DiscordSocketClient client, IEnumerable gcs, DbService db) { _log = LogManager.GetCurrentClassLogger(); _db = db; diff --git a/src/NadekoBot/Services/Administration/VplusTService.cs b/src/NadekoBot/Services/Administration/VplusTService.cs index 3d9e86cd..b4e3122d 100644 --- a/src/NadekoBot/Services/Administration/VplusTService.cs +++ b/src/NadekoBot/Services/Administration/VplusTService.cs @@ -20,12 +20,12 @@ namespace NadekoBot.Services.Administration public readonly ConcurrentHashSet VoicePlusTextCache; private readonly ConcurrentDictionary _guildLockObjects = new ConcurrentDictionary(); - private readonly DiscordShardedClient _client; + private readonly DiscordSocketClient _client; private readonly NadekoStrings _strings; private readonly DbService _db; private readonly Logger _log; - public VplusTService(DiscordShardedClient client, IEnumerable gcs, NadekoStrings strings, + public VplusTService(DiscordSocketClient client, IEnumerable gcs, NadekoStrings strings, DbService db) { _client = client; diff --git a/src/NadekoBot/Services/ClashOfClans/ClashOfClansService.cs b/src/NadekoBot/Services/ClashOfClans/ClashOfClansService.cs index edb78512..042afa1b 100644 --- a/src/NadekoBot/Services/ClashOfClans/ClashOfClansService.cs +++ b/src/NadekoBot/Services/ClashOfClans/ClashOfClansService.cs @@ -17,7 +17,7 @@ namespace NadekoBot.Services.ClashOfClans // shouldn't be here public class ClashOfClansService { - private readonly DiscordShardedClient _client; + private readonly DiscordSocketClient _client; private readonly DbService _db; private readonly ILocalization _localization; private readonly NadekoStrings _strings; @@ -25,7 +25,7 @@ namespace NadekoBot.Services.ClashOfClans public ConcurrentDictionary> ClashWars { get; set; } - public ClashOfClansService(DiscordShardedClient client, DbService db, ILocalization localization, NadekoStrings strings) + public ClashOfClansService(DiscordSocketClient client, DbService db, ILocalization localization, NadekoStrings strings) { _client = client; _db = db; diff --git a/src/NadekoBot/Services/CommandHandler.cs b/src/NadekoBot/Services/CommandHandler.cs index 2177ce01..a1de8871 100644 --- a/src/NadekoBot/Services/CommandHandler.cs +++ b/src/NadekoBot/Services/CommandHandler.cs @@ -28,7 +28,7 @@ namespace NadekoBot.Services { public const int GlobalCommandsCooldown = 750; - private readonly DiscordShardedClient _client; + private readonly DiscordSocketClient _client; private readonly CommandService _commandService; private readonly Logger _log; private readonly IBotCredentials _creds; @@ -48,7 +48,7 @@ namespace NadekoBot.Services public ConcurrentHashSet UsersOnShortCooldown { get; } = new ConcurrentHashSet(); private readonly Timer _clearUsersOnShortCooldown; - public CommandHandler(DiscordShardedClient client, DbService db, BotConfig bc, IEnumerable gcs, CommandService commandService, IBotCredentials credentials, NadekoBot bot) + public CommandHandler(DiscordSocketClient client, DbService db, BotConfig bc, IEnumerable gcs, CommandService commandService, IBotCredentials credentials, NadekoBot bot) { _client = client; _commandService = commandService; diff --git a/src/NadekoBot/Services/CustomReactions/CustomReactionsService.cs b/src/NadekoBot/Services/CustomReactions/CustomReactionsService.cs index 206b0bd9..bb7fb8d9 100644 --- a/src/NadekoBot/Services/CustomReactions/CustomReactionsService.cs +++ b/src/NadekoBot/Services/CustomReactions/CustomReactionsService.cs @@ -22,13 +22,13 @@ namespace NadekoBot.Services.CustomReactions private readonly Logger _log; private readonly DbService _db; - private readonly DiscordShardedClient _client; + private readonly DiscordSocketClient _client; private readonly PermissionService _perms; private readonly CommandHandler _cmd; private readonly BotConfig _bc; public CustomReactionsService(PermissionService perms, DbService db, - DiscordShardedClient client, CommandHandler cmd, BotConfig bc) + DiscordSocketClient client, CommandHandler cmd, BotConfig bc) { _log = LogManager.GetCurrentClassLogger(); _db = db; @@ -37,15 +37,12 @@ namespace NadekoBot.Services.CustomReactions _cmd = cmd; _bc = bc; - var sw = Stopwatch.StartNew(); using (var uow = _db.UnitOfWork) { var items = uow.CustomReactions.GetAll(); GuildReactions = new ConcurrentDictionary(items.Where(g => g.GuildId != null && g.GuildId != 0).GroupBy(k => k.GuildId.Value).ToDictionary(g => g.Key, g => g.ToArray())); GlobalReactions = items.Where(g => g.GuildId == null || g.GuildId == 0).ToArray(); } - sw.Stop(); - _log.Debug($"Loaded in {sw.Elapsed.TotalSeconds:F2}s"); } public void ClearStats() => ReactionStats.Clear(); @@ -98,7 +95,7 @@ namespace NadekoBot.Services.CustomReactions return greaction; } - public async Task TryExecuteEarly(DiscordShardedClient client, IGuild guild, IUserMessage msg) + public async Task TryExecuteEarly(DiscordSocketClient client, IGuild guild, IUserMessage msg) { // maybe this message is a custom reaction var cr = await Task.Run(() => TryGetCustomReaction(msg)).ConfigureAwait(false); diff --git a/src/NadekoBot/Services/CustomReactions/Extensions.cs b/src/NadekoBot/Services/CustomReactions/Extensions.cs index e2c5481f..4d6d4d06 100644 --- a/src/NadekoBot/Services/CustomReactions/Extensions.cs +++ b/src/NadekoBot/Services/CustomReactions/Extensions.cs @@ -40,7 +40,7 @@ namespace NadekoBot.Services.CustomReactions } }, }; - public static Dictionary> placeholders = new Dictionary>() + public static Dictionary> placeholders = new Dictionary>() { {"%mention%", (ctx, client) => { return $"<@{client.CurrentUser.Id}>"; } }, {"%user%", (ctx, client) => { return ctx.Author.Mention; } }, @@ -94,7 +94,7 @@ namespace NadekoBot.Services.CustomReactions } } }; - private static string ResolveTriggerString(this string str, IUserMessage ctx, DiscordShardedClient client) + private static string ResolveTriggerString(this string str, IUserMessage ctx, DiscordSocketClient client) { foreach (var ph in placeholders) { @@ -104,7 +104,7 @@ namespace NadekoBot.Services.CustomReactions return str; } - private static async Task ResolveResponseStringAsync(this string str, IUserMessage ctx, DiscordShardedClient client, string resolvedTrigger) + private static async Task ResolveResponseStringAsync(this string str, IUserMessage ctx, DiscordSocketClient client, string resolvedTrigger) { foreach (var ph in placeholders) { @@ -127,13 +127,13 @@ namespace NadekoBot.Services.CustomReactions return str; } - public static string TriggerWithContext(this CustomReaction cr, IUserMessage ctx, DiscordShardedClient client) + public static string TriggerWithContext(this CustomReaction cr, IUserMessage ctx, DiscordSocketClient client) => cr.Trigger.ResolveTriggerString(ctx, client); - public static Task ResponseWithContextAsync(this CustomReaction cr, IUserMessage ctx, DiscordShardedClient client) + public static Task ResponseWithContextAsync(this CustomReaction cr, IUserMessage ctx, DiscordSocketClient client) => cr.Response.ResolveResponseStringAsync(ctx, client, cr.Trigger.ResolveTriggerString(ctx, client)); - public static async Task Send(this CustomReaction cr, IUserMessage context, DiscordShardedClient client, CustomReactionsService crs) + public static async Task Send(this CustomReaction cr, IUserMessage context, DiscordSocketClient client, CustomReactionsService crs) { var channel = cr.DmResponse ? await context.Author.CreateDMChannelAsync() : context.Channel; diff --git a/src/NadekoBot/Services/Database/Repositories/IGuildConfigRepository.cs b/src/NadekoBot/Services/Database/Repositories/IGuildConfigRepository.cs index cca54609..52332cfb 100644 --- a/src/NadekoBot/Services/Database/Repositories/IGuildConfigRepository.cs +++ b/src/NadekoBot/Services/Database/Repositories/IGuildConfigRepository.cs @@ -11,7 +11,7 @@ namespace NadekoBot.Services.Database.Repositories GuildConfig For(ulong guildId, Func, IQueryable> includes = null); GuildConfig LogSettingsFor(ulong guildId); IEnumerable OldPermissionsForAll(); - IEnumerable GetAllGuildConfigs(); + IEnumerable GetAllGuildConfigs(List availableGuilds); IEnumerable GetAllFollowedStreams(); void SetCleverbotEnabled(ulong id, bool cleverbotEnabled); IEnumerable Permissionsv2ForAll(); diff --git a/src/NadekoBot/Services/Database/Repositories/Impl/GuildConfigRepository.cs b/src/NadekoBot/Services/Database/Repositories/Impl/GuildConfigRepository.cs index f3ed3566..25dd52fe 100644 --- a/src/NadekoBot/Services/Database/Repositories/Impl/GuildConfigRepository.cs +++ b/src/NadekoBot/Services/Database/Repositories/Impl/GuildConfigRepository.cs @@ -24,8 +24,10 @@ namespace NadekoBot.Services.Database.Repositories.Impl } }; - public IEnumerable GetAllGuildConfigs() => - _set.Include(gc => gc.LogSetting) + public IEnumerable GetAllGuildConfigs(List availableGuilds) => + _set + .Where(gc => availableGuilds.Contains((long)gc.GuildId)) + .Include(gc => gc.LogSetting) .ThenInclude(ls => ls.IgnoredChannels) .Include(gc => gc.MutedUsers) .Include(gc => gc.CommandAliases) diff --git a/src/NadekoBot/Services/Discord/SocketMessageEventWrapper.cs b/src/NadekoBot/Services/Discord/SocketMessageEventWrapper.cs index 58b3a1e5..4903fd66 100644 --- a/src/NadekoBot/Services/Discord/SocketMessageEventWrapper.cs +++ b/src/NadekoBot/Services/Discord/SocketMessageEventWrapper.cs @@ -12,7 +12,7 @@ namespace NadekoBot.Services.Discord public event Action OnReactionRemoved = delegate { }; public event Action OnReactionsCleared = delegate { }; - public ReactionEventWrapper(DiscordShardedClient client, IUserMessage msg) + public ReactionEventWrapper(DiscordSocketClient client, IUserMessage msg) { Message = msg ?? throw new ArgumentNullException(nameof(msg)); _client = client; @@ -69,7 +69,7 @@ namespace NadekoBot.Services.Discord } private bool disposing = false; - private readonly DiscordShardedClient _client; + private readonly DiscordSocketClient _client; public void Dispose() { diff --git a/src/NadekoBot/Services/Games/ChatterbotService.cs b/src/NadekoBot/Services/Games/ChatterbotService.cs index 8039b79d..ad295dec 100644 --- a/src/NadekoBot/Services/Games/ChatterbotService.cs +++ b/src/NadekoBot/Services/Games/ChatterbotService.cs @@ -15,14 +15,14 @@ namespace NadekoBot.Services.Games { public class ChatterBotService : IEarlyBlockingExecutor { - private readonly DiscordShardedClient _client; + private readonly DiscordSocketClient _client; private readonly Logger _log; private readonly PermissionService _perms; private readonly CommandHandler _cmd; public ConcurrentDictionary> ChatterBotGuilds { get; } - public ChatterBotService(DiscordShardedClient client, PermissionService perms, IEnumerable gcs, CommandHandler cmd) + public ChatterBotService(DiscordSocketClient client, PermissionService perms, IEnumerable gcs, CommandHandler cmd) { _client = client; _log = LogManager.GetCurrentClassLogger(); @@ -83,7 +83,7 @@ namespace NadekoBot.Services.Games return true; } - public async Task TryExecuteEarly(DiscordShardedClient client, IGuild guild, IUserMessage usrMsg) + public async Task TryExecuteEarly(DiscordSocketClient client, IGuild guild, IUserMessage usrMsg) { if (!(guild is SocketGuild sg)) return false; diff --git a/src/NadekoBot/Services/Games/GamesService.cs b/src/NadekoBot/Services/Games/GamesService.cs index 24139906..e677990b 100644 --- a/src/NadekoBot/Services/Games/GamesService.cs +++ b/src/NadekoBot/Services/Games/GamesService.cs @@ -23,7 +23,7 @@ namespace NadekoBot.Services.Games public readonly ImmutableArray EightBallResponses; private readonly Timer _t; - private readonly DiscordShardedClient _client; + private readonly DiscordSocketClient _client; private readonly NadekoStrings _strings; private readonly IImagesService _images; private readonly Logger _log; @@ -33,7 +33,7 @@ namespace NadekoBot.Services.Games public List TypingArticles { get; } = new List(); - public GamesService(DiscordShardedClient client, BotConfig bc, IEnumerable gcs, + public GamesService(DiscordSocketClient client, BotConfig bc, IEnumerable gcs, NadekoStrings strings, IImagesService images, CommandHandler cmdHandler) { _bc = bc; diff --git a/src/NadekoBot/Services/Games/Poll.cs b/src/NadekoBot/Services/Games/Poll.cs index 8feb4cb5..1a6bbf20 100644 --- a/src/NadekoBot/Services/Games/Poll.cs +++ b/src/NadekoBot/Services/Games/Poll.cs @@ -18,7 +18,7 @@ namespace NadekoBot.Services.Games private string[] answers { get; } private readonly ConcurrentDictionary _participants = new ConcurrentDictionary(); private readonly string _question; - private readonly DiscordShardedClient _client; + private readonly DiscordSocketClient _client; private readonly NadekoStrings _strings; private bool running = false; private HashSet _guildUsers; @@ -27,7 +27,7 @@ namespace NadekoBot.Services.Games public bool IsPublic { get; } - public Poll(DiscordShardedClient client, NadekoStrings strings, IUserMessage umsg, string question, IEnumerable enumerable, bool isPublic = false) + public Poll(DiscordSocketClient client, NadekoStrings strings, IUserMessage umsg, string question, IEnumerable enumerable, bool isPublic = false) { _client = client; _strings = strings; diff --git a/src/NadekoBot/Services/Games/PollService.cs b/src/NadekoBot/Services/Games/PollService.cs index a421bd77..ce8852a7 100644 --- a/src/NadekoBot/Services/Games/PollService.cs +++ b/src/NadekoBot/Services/Games/PollService.cs @@ -13,10 +13,10 @@ namespace NadekoBot.Services.Games { public ConcurrentDictionary ActivePolls = new ConcurrentDictionary(); private readonly Logger _log; - private readonly DiscordShardedClient _client; + private readonly DiscordSocketClient _client; private readonly NadekoStrings _strings; - public PollService(DiscordShardedClient client, NadekoStrings strings) + public PollService(DiscordSocketClient client, NadekoStrings strings) { _log = LogManager.GetCurrentClassLogger(); _client = client; @@ -45,7 +45,7 @@ namespace NadekoBot.Services.Games return false; } - public async Task TryExecuteEarly(DiscordShardedClient client, IGuild guild, IUserMessage msg) + public async Task TryExecuteEarly(DiscordSocketClient client, IGuild guild, IUserMessage msg) { if (guild == null) { diff --git a/src/NadekoBot/Services/GreetSettingsService.cs b/src/NadekoBot/Services/GreetSettingsService.cs index 6e2e630d..3296c602 100644 --- a/src/NadekoBot/Services/GreetSettingsService.cs +++ b/src/NadekoBot/Services/GreetSettingsService.cs @@ -17,10 +17,10 @@ namespace NadekoBot.Services private readonly DbService _db; public readonly ConcurrentDictionary GuildConfigsCache; - private readonly DiscordShardedClient _client; + private readonly DiscordSocketClient _client; private readonly Logger _log; - public GreetSettingsService(DiscordShardedClient client, IEnumerable guildConfigs, DbService db) + public GreetSettingsService(DiscordSocketClient client, IEnumerable guildConfigs, DbService db) { _db = db; _client = client; diff --git a/src/NadekoBot/Services/Help/HelpService.cs b/src/NadekoBot/Services/Help/HelpService.cs index 25a91f16..83b64927 100644 --- a/src/NadekoBot/Services/Help/HelpService.cs +++ b/src/NadekoBot/Services/Help/HelpService.cs @@ -24,7 +24,7 @@ namespace NadekoBot.Services.Help _strings = strings; } - public async Task LateExecute(DiscordShardedClient client, IGuild guild, IUserMessage msg) + public async Task LateExecute(DiscordSocketClient client, IGuild guild, IUserMessage msg) { try { diff --git a/src/NadekoBot/Services/IBotCredentials.cs b/src/NadekoBot/Services/IBotCredentials.cs index c6c42f87..7fa661bd 100644 --- a/src/NadekoBot/Services/IBotCredentials.cs +++ b/src/NadekoBot/Services/IBotCredentials.cs @@ -20,6 +20,7 @@ namespace NadekoBot.Services string OsuApiKey { get; } bool IsOwner(IUser u); + int TotalShards { get; } } public class DBConfig diff --git a/src/NadekoBot/Services/IImagesService.cs b/src/NadekoBot/Services/IImagesService.cs index 0a76175d..31e6c32a 100644 --- a/src/NadekoBot/Services/IImagesService.cs +++ b/src/NadekoBot/Services/IImagesService.cs @@ -18,6 +18,6 @@ namespace NadekoBot.Services ImmutableArray WifeMatrix { get; } ImmutableArray RategirlDot { get; } - TimeSpan Reload(); + void Reload(); } } diff --git a/src/NadekoBot/Services/Impl/ImagesService.cs b/src/NadekoBot/Services/Impl/ImagesService.cs index 4bce6148..95930464 100644 --- a/src/NadekoBot/Services/Impl/ImagesService.cs +++ b/src/NadekoBot/Services/Impl/ImagesService.cs @@ -47,12 +47,10 @@ namespace NadekoBot.Services.Impl this.Reload(); } - public TimeSpan Reload() + public void Reload() { try { - _log.Info("Loading images..."); - var sw = Stopwatch.StartNew(); Heads = File.ReadAllBytes(_headsPath).ToImmutableArray(); Tails = File.ReadAllBytes(_tailsPath).ToImmutableArray(); @@ -79,10 +77,6 @@ namespace NadekoBot.Services.Impl WifeMatrix = File.ReadAllBytes(_wifeMatrixPath).ToImmutableArray(); RategirlDot = File.ReadAllBytes(_rategirlDot).ToImmutableArray(); - - sw.Stop(); - _log.Info($"Images loaded after {sw.Elapsed.TotalSeconds:F2}s!"); - return sw.Elapsed; } catch (Exception ex) { diff --git a/src/NadekoBot/Services/Impl/StatsService.cs b/src/NadekoBot/Services/Impl/StatsService.cs index bcd4a092..5d93196d 100644 --- a/src/NadekoBot/Services/Impl/StatsService.cs +++ b/src/NadekoBot/Services/Impl/StatsService.cs @@ -13,7 +13,7 @@ namespace NadekoBot.Services.Impl { public class StatsService : IStatsService { - private readonly DiscordShardedClient _client; + private readonly DiscordSocketClient _client; private readonly IBotCredentials _creds; private readonly DateTime _started; @@ -36,7 +36,7 @@ namespace NadekoBot.Services.Impl private readonly Timer _carbonitexTimer; - public StatsService(DiscordShardedClient client, CommandHandler cmdHandler, IBotCredentials creds) + public StatsService(DiscordSocketClient client, CommandHandler cmdHandler, IBotCredentials creds) { _client = client; _creds = creds; @@ -121,31 +121,32 @@ namespace NadekoBot.Services.Impl return Task.CompletedTask; }; - _carbonitexTimer = new Timer(async (state) => - { - if (string.IsNullOrWhiteSpace(_creds.CarbonKey)) - return; - try - { - using (var http = new HttpClient()) - { - using (var content = new FormUrlEncodedContent( - new Dictionary { - { "servercount", _client.Guilds.Count.ToString() }, - { "key", _creds.CarbonKey }})) - { - content.Headers.Clear(); - content.Headers.Add("Content-Type", "application/x-www-form-urlencoded"); + //todo carbonitex update + //_carbonitexTimer = new Timer(async (state) => + //{ + // if (string.IsNullOrWhiteSpace(_creds.CarbonKey)) + // return; + // try + // { + // using (var http = new HttpClient()) + // { + // using (var content = new FormUrlEncodedContent( + // new Dictionary { + // { "servercount", _client.Guilds.Count.ToString() }, + // { "key", _creds.CarbonKey }})) + // { + // content.Headers.Clear(); + // content.Headers.Add("Content-Type", "application/x-www-form-urlencoded"); - await http.PostAsync("https://www.carbonitex.net/discord/data/botdata.php", content).ConfigureAwait(false); - } - } - } - catch - { - // ignored - } - }, null, TimeSpan.FromHours(1), TimeSpan.FromHours(1)); + // await http.PostAsync("https://www.carbonitex.net/discord/data/botdata.php", content).ConfigureAwait(false); + // } + // } + // } + // catch + // { + // // ignored + // } + //}, null, TimeSpan.FromHours(1), TimeSpan.FromHours(1)); } public void Initialize() diff --git a/src/NadekoBot/Services/LogSetup.cs b/src/NadekoBot/Services/LogSetup.cs new file mode 100644 index 00000000..159b447e --- /dev/null +++ b/src/NadekoBot/Services/LogSetup.cs @@ -0,0 +1,28 @@ +using NLog; +using NLog.Config; +using NLog.Targets; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace NadekoBot.Services +{ + public class LogSetup + { + public static void SetupLogger() + { + var logConfig = new LoggingConfiguration(); + var consoleTarget = new ColoredConsoleTarget() + { + Layout = @"${date:format=HH\:mm\:ss} ${logger} | ${message}" + }; + logConfig.AddTarget("Console", consoleTarget); + + logConfig.LoggingRules.Add(new LoggingRule("*", LogLevel.Debug, consoleTarget)); + + LogManager.Configuration = logConfig; + } + } +} diff --git a/src/NadekoBot/Services/Permissions/CmdCdService.cs b/src/NadekoBot/Services/Permissions/CmdCdService.cs index 0a1cbf47..7a44a292 100644 --- a/src/NadekoBot/Services/Permissions/CmdCdService.cs +++ b/src/NadekoBot/Services/Permissions/CmdCdService.cs @@ -21,7 +21,7 @@ namespace NadekoBot.Services.Permissions v => new ConcurrentHashSet(v.CommandCooldowns))); } - public Task TryBlockLate(DiscordShardedClient client, IUserMessage msg, IGuild guild, + public Task TryBlockLate(DiscordSocketClient client, IUserMessage msg, IGuild guild, IMessageChannel channel, IUser user, string moduleName, string commandName) { if (guild == null) diff --git a/src/NadekoBot/Services/Permissions/FilterService.cs b/src/NadekoBot/Services/Permissions/FilterService.cs index 2dbd375d..adbe0aee 100644 --- a/src/NadekoBot/Services/Permissions/FilterService.cs +++ b/src/NadekoBot/Services/Permissions/FilterService.cs @@ -41,7 +41,7 @@ namespace NadekoBot.Services.Permissions return words; } - public FilterService(DiscordShardedClient _client, IEnumerable gcs) + public FilterService(DiscordSocketClient _client, IEnumerable gcs) { _log = LogManager.GetCurrentClassLogger(); diff --git a/src/NadekoBot/Services/Permissions/GlobalPermissionService.cs b/src/NadekoBot/Services/Permissions/GlobalPermissionService.cs index 633a81a3..419f77f3 100644 --- a/src/NadekoBot/Services/Permissions/GlobalPermissionService.cs +++ b/src/NadekoBot/Services/Permissions/GlobalPermissionService.cs @@ -19,7 +19,7 @@ namespace NadekoBot.Services.Permissions BlockedCommands = new ConcurrentHashSet(bc.BlockedCommands.Select(x => x.Name)); } - public async Task TryBlockLate(DiscordShardedClient client, IUserMessage msg, IGuild guild, IMessageChannel channel, IUser user, string moduleName, string commandName) + public async Task TryBlockLate(DiscordSocketClient client, IUserMessage msg, IGuild guild, IMessageChannel channel, IUser user, string moduleName, string commandName) { await Task.Yield(); commandName = commandName.ToLowerInvariant(); diff --git a/src/NadekoBot/Services/Permissions/PermissionsService.cs b/src/NadekoBot/Services/Permissions/PermissionsService.cs index 765cb131..27b547be 100644 --- a/src/NadekoBot/Services/Permissions/PermissionsService.cs +++ b/src/NadekoBot/Services/Permissions/PermissionsService.cs @@ -1,4 +1,5 @@ -using Microsoft.EntityFrameworkCore; + +using Microsoft.EntityFrameworkCore; using NadekoBot.DataStructures.ModuleBehaviors; using NadekoBot.Services.Database.Models; using NLog; @@ -183,7 +184,7 @@ WHERE secondaryTargetName LIKE '.%' OR }); } - public async Task TryBlockLate(DiscordShardedClient client, IUserMessage msg, IGuild guild, IMessageChannel channel, IUser user, string moduleName, string commandName) + public async Task TryBlockLate(DiscordSocketClient client, IUserMessage msg, IGuild guild, IMessageChannel channel, IUser user, string moduleName, string commandName) { await Task.Yield(); if (guild == null) diff --git a/src/NadekoBot/Services/Searches/SearchesService.cs b/src/NadekoBot/Services/Searches/SearchesService.cs index 864c9522..8c98a22e 100644 --- a/src/NadekoBot/Services/Searches/SearchesService.cs +++ b/src/NadekoBot/Services/Searches/SearchesService.cs @@ -15,7 +15,7 @@ namespace NadekoBot.Services.Searches { public class SearchesService { - private readonly DiscordShardedClient _client; + private readonly DiscordSocketClient _client; private readonly IGoogleApiService _google; private readonly DbService _db; private readonly Logger _log; @@ -31,7 +31,7 @@ namespace NadekoBot.Services.Searches public List WowJokes { get; } = new List(); public List MagicItems { get; } = new List(); - public SearchesService(DiscordShardedClient client, IGoogleApiService google, DbService db) + public SearchesService(DiscordSocketClient client, IGoogleApiService google, DbService db) { _client = client; _google = google; diff --git a/src/NadekoBot/Services/Searches/StreamNotificationService.cs b/src/NadekoBot/Services/Searches/StreamNotificationService.cs index 3fdbb9ac..9b8480fc 100644 --- a/src/NadekoBot/Services/Searches/StreamNotificationService.cs +++ b/src/NadekoBot/Services/Searches/StreamNotificationService.cs @@ -20,10 +20,10 @@ namespace NadekoBot.Services.Searches private readonly ConcurrentDictionary _cachedStatuses = new ConcurrentDictionary(); private readonly DbService _db; - private readonly DiscordShardedClient _client; + private readonly DiscordSocketClient _client; private readonly NadekoStrings _strings; - public StreamNotificationService(DbService db, DiscordShardedClient client, NadekoStrings strings) + public StreamNotificationService(DbService db, DiscordSocketClient client, NadekoStrings strings) { _db = db; _client = client; diff --git a/src/NadekoBot/Services/Utility/MessageRepeaterService.cs b/src/NadekoBot/Services/Utility/MessageRepeaterService.cs index 57873ef1..cc402aa8 100644 --- a/src/NadekoBot/Services/Utility/MessageRepeaterService.cs +++ b/src/NadekoBot/Services/Utility/MessageRepeaterService.cs @@ -15,7 +15,7 @@ namespace NadekoBot.Services.Utility public ConcurrentDictionary> Repeaters { get; set; } public bool RepeaterReady { get; private set; } - public MessageRepeaterService(NadekoBot bot, DiscordShardedClient client, IEnumerable gcs) + public MessageRepeaterService(NadekoBot bot, DiscordSocketClient client, IEnumerable gcs) { var _ = Task.Run(async () => { diff --git a/src/NadekoBot/Services/Utility/RemindService.cs b/src/NadekoBot/Services/Utility/RemindService.cs index a6dd276b..545e8648 100644 --- a/src/NadekoBot/Services/Utility/RemindService.cs +++ b/src/NadekoBot/Services/Utility/RemindService.cs @@ -30,10 +30,10 @@ namespace NadekoBot.Services.Utility private readonly CancellationTokenSource cancelSource; private readonly CancellationToken cancelAllToken; private readonly BotConfig _config; - private readonly DiscordShardedClient _client; + private readonly DiscordSocketClient _client; private readonly DbService _db; - public RemindService(DiscordShardedClient client, BotConfig config, DbService db) + public RemindService(DiscordSocketClient client, BotConfig config, DbService db) { _config = config; _client = client; diff --git a/src/NadekoBot/Services/Utility/RepeatRunner.cs b/src/NadekoBot/Services/Utility/RepeatRunner.cs index 8c96dc8b..abfa21b1 100644 --- a/src/NadekoBot/Services/Utility/RepeatRunner.cs +++ b/src/NadekoBot/Services/Utility/RepeatRunner.cs @@ -22,7 +22,7 @@ namespace NadekoBot.Services.Utility private IUserMessage oldMsg = null; private Timer _t; - public RepeatRunner(DiscordShardedClient client, Repeater repeater) + public RepeatRunner(DiscordSocketClient client, Repeater repeater) { _log = LogManager.GetCurrentClassLogger(); Repeater = repeater; diff --git a/src/NadekoBot/Services/Utility/UtilityService.cs b/src/NadekoBot/Services/Utility/UtilityService.cs index 660984b2..14af97e8 100644 --- a/src/NadekoBot/Services/Utility/UtilityService.cs +++ b/src/NadekoBot/Services/Utility/UtilityService.cs @@ -13,9 +13,9 @@ namespace NadekoBot.Services.Utility { public readonly ConcurrentDictionary> Subscribers = new ConcurrentDictionary>(); - private DiscordShardedClient _client; + private DiscordSocketClient _client; - public CrossServerTextService(IEnumerable guildConfigs, DiscordShardedClient client) + public CrossServerTextService(IEnumerable guildConfigs, DiscordSocketClient client) { _client = client; _client.MessageReceived += Client_MessageReceived; diff --git a/src/NadekoBot/ShardsCoordinator.cs b/src/NadekoBot/ShardsCoordinator.cs new file mode 100644 index 00000000..6db3aade --- /dev/null +++ b/src/NadekoBot/ShardsCoordinator.cs @@ -0,0 +1,95 @@ +using NadekoBot.DataStructures.ShardCom; +using NadekoBot.Services; +using NadekoBot.Services.Impl; +using NLog; +using System; +using System.Diagnostics; +using System.Linq; +using System.Threading.Tasks; + +namespace NadekoBot +{ + public class ShardsCoordinator + { + private readonly BotCredentials Credentials; + private Process[] ShardProcesses; + private ShardComMessage[] Statuses; + private readonly Logger _log; + private readonly ShardComServer _comServer; + + public ShardsCoordinator() + { + LogSetup.SetupLogger(); + Credentials = new BotCredentials(); + ShardProcesses = new Process[Credentials.TotalShards]; + Statuses = new ShardComMessage[Credentials.TotalShards]; + _log = LogManager.GetCurrentClassLogger(); + + _comServer = new ShardComServer(); + _comServer.Start(); + + _comServer.OnDataReceived += _comServer_OnDataReceived; + } + + private Task _comServer_OnDataReceived(ShardComMessage msg) + { + Statuses[msg.ShardId] = msg; + if (msg.ConnectionState == Discord.ConnectionState.Disconnected || msg.ConnectionState == Discord.ConnectionState.Disconnecting) + _log.Error("!!! SHARD {0} IS IN {1} STATE", msg.ShardId, msg.ConnectionState); + return Task.CompletedTask; + } + + public async Task RunAsync(params string[] args) + { + var curProcessId = Process.GetCurrentProcess().Id; + for (int i = 0; i < Credentials.TotalShards; i++) + { + var p = Process.Start(new ProcessStartInfo() + { + FileName = "dotnet", + Arguments = $"run -c Debug -- {i} {curProcessId}", + }); + await Task.Delay(5000); + + //Task.Run(() => { while (!p.HasExited) _log.Info($"S-{i}|" + p.StandardOutput.ReadLine()); }); + //Task.Run(() => { while (!p.HasExited) _log.Error($"S-{i}|" + p.StandardError.ReadLine()); }); + } + } + + public async Task RunAndBlockAsync(params string[] args) + { + try + { + await RunAsync(args).ConfigureAwait(false); + } + catch (Exception ex) + { + _log.Error(ex); + } + await Task.Run(() => + { + string input; + while ((input = Console.ReadLine()?.ToLowerInvariant()) != "quit") + { + switch (input) + { + case "ls": + var groupStr = string.Join(",", Statuses + .Where(x => x != null) + .GroupBy(x => x.ConnectionState) + .Select(x => x.Count() + " " + x.Key)); + _log.Info(string.Join("\n", Statuses.Select(x => $"Shard {x.ShardId} is in {x.ConnectionState.ToString()} state with {x.Guilds} servers")) + "\n" + groupStr); + break; + default: + break; + } + } + }); + foreach (var p in ShardProcesses) + { + try { p.Kill(); } catch { } + try { p.Dispose(); } catch { } + } + } + } +} diff --git a/src/NadekoBot/_Extensions/Extensions.cs b/src/NadekoBot/_Extensions/Extensions.cs index cafb5d32..10e97e14 100644 --- a/src/NadekoBot/_Extensions/Extensions.cs +++ b/src/NadekoBot/_Extensions/Extensions.cs @@ -70,7 +70,7 @@ namespace NadekoBot.Extensions /// /// danny kamisama /// - public static async Task SendPaginatedConfirmAsync(this IMessageChannel channel, DiscordShardedClient client, int currentPage, Func pageFunc, int? lastPage = null, bool addPaginatedFooter = true) + public static async Task SendPaginatedConfirmAsync(this IMessageChannel channel, DiscordSocketClient client, int currentPage, Func pageFunc, int? lastPage = null, bool addPaginatedFooter = true) { var embed = pageFunc(currentPage); @@ -134,7 +134,7 @@ namespace NadekoBot.Extensions return embed.WithFooter(efb => efb.WithText(curPage.ToString())); } - public static ReactionEventWrapper OnReaction(this IUserMessage msg, DiscordShardedClient client, Action reactionAdded, Action reactionRemoved = null) + public static ReactionEventWrapper OnReaction(this IUserMessage msg, DiscordSocketClient client, Action reactionAdded, Action reactionRemoved = null) { if (reactionRemoved == null) reactionRemoved = delegate { }; From 808dca8ec473be82de2e832ee1b4a9c9943c72fb Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Mon, 19 Jun 2017 17:57:12 +0200 Subject: [PATCH 004/346] nerfed prune speed a bit to hopefully prevent ratelimits? --- .../Services/Administration/PruneService.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/NadekoBot/Services/Administration/PruneService.cs b/src/NadekoBot/Services/Administration/PruneService.cs index 0929c1a6..e1078102 100644 --- a/src/NadekoBot/Services/Administration/PruneService.cs +++ b/src/NadekoBot/Services/Administration/PruneService.cs @@ -12,7 +12,7 @@ namespace NadekoBot.Services.Administration public class PruneService { //channelids where prunes are currently occuring - private ConcurrentHashSet _pruningChannels = new ConcurrentHashSet(); + private ConcurrentHashSet _pruningGuilds = new ConcurrentHashSet(); private readonly TimeSpan twoWeeks = TimeSpan.FromDays(14); public async Task PruneWhere(ITextChannel channel, int amount, Func predicate) @@ -21,14 +21,14 @@ namespace NadekoBot.Services.Administration if (amount <= 0) throw new ArgumentOutOfRangeException(nameof(amount)); - if (!_pruningChannels.Add(channel.Id)) + if (!_pruningGuilds.Add(channel.GuildId)) return; try { IMessage[] msgs; IMessage lastMessage = null; - msgs = (await channel.GetMessagesAsync().Flatten()).Where(predicate).Take(amount).ToArray(); + msgs = (await channel.GetMessagesAsync(50).Flatten()).Where(predicate).Take(amount).ToArray(); while (amount > 0 && msgs.Any()) { lastMessage = msgs[msgs.Length - 1]; @@ -52,9 +52,9 @@ namespace NadekoBot.Services.Administration //this isn't good, because this still work as if i want to remove only specific user's messages from the last //100 messages, Maybe this needs to be reduced by msgs.Length instead of 100 - amount -= 100; + amount -= 50; if(amount > 0) - msgs = (await channel.GetMessagesAsync(lastMessage, Direction.Before).Flatten()).Where(predicate).Take(amount).ToArray(); + msgs = (await channel.GetMessagesAsync(lastMessage, Direction.Before, 50).Flatten()).Where(predicate).Take(amount).ToArray(); } } catch @@ -63,7 +63,7 @@ namespace NadekoBot.Services.Administration } finally { - _pruningChannels.TryRemove(channel.Id); + _pruningGuilds.TryRemove(channel.GuildId); } } } From 01cf59d83eee75817deca4c8f0f1e7d6278f9c7a Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Tue, 20 Jun 2017 04:23:11 +0200 Subject: [PATCH 005/346] Sharding over processes almost done --- .../Commands/CrossServerTextChannel.cs | 64 --------------- src/NadekoBot/Modules/Utility/Utility.cs | 79 +------------------ src/NadekoBot/NadekoBot.cs | 63 +++++++++------ src/NadekoBot/Program.cs | 8 +- src/NadekoBot/Properties/launchSettings.json | 3 +- .../Repositories/IGuildConfigRepository.cs | 4 +- .../Repositories/IReminderRepository.cs | 4 +- .../Impl/GuildConfigRepository.cs | 9 ++- .../Repositories/Impl/ReminderRepository.cs | 8 ++ .../Permissions/PermissionsService.cs | 23 +++--- .../Services/Searches/AnimeSearchService.cs | 1 + .../Searches/StreamNotificationService.cs | 2 +- .../Services/Utility/ConverterService.cs | 1 + .../Services/Utility/PatreonRewardsService.cs | 1 + .../Services/Utility/RemindService.cs | 4 +- .../Services/Utility/UtilityService.cs | 69 ---------------- src/NadekoBot/ShardsCoordinator.cs | 8 +- .../_strings/ResponseStrings.en-US.json | 3 - 18 files changed, 85 insertions(+), 269 deletions(-) delete mode 100644 src/NadekoBot/Modules/Utility/Commands/CrossServerTextChannel.cs delete mode 100644 src/NadekoBot/Services/Utility/UtilityService.cs diff --git a/src/NadekoBot/Modules/Utility/Commands/CrossServerTextChannel.cs b/src/NadekoBot/Modules/Utility/Commands/CrossServerTextChannel.cs deleted file mode 100644 index de917054..00000000 --- a/src/NadekoBot/Modules/Utility/Commands/CrossServerTextChannel.cs +++ /dev/null @@ -1,64 +0,0 @@ -using Discord; -using Discord.Commands; -using NadekoBot.Attributes; -using NadekoBot.Extensions; -using NadekoBot.Services; -using NadekoBot.Services.Utility; -using System.Collections.Concurrent; -using System.Threading.Tasks; - -namespace NadekoBot.Modules.Utility -{ - public partial class Utility - { - [Group] - public class CrossServerTextChannel : NadekoSubmodule - { - private readonly CrossServerTextService _service; - - public CrossServerTextChannel(CrossServerTextService service) - { - _service = service; - } - - [NadekoCommand, Usage, Description, Aliases] - [RequireContext(ContextType.Guild)] - [OwnerOnly] - public async Task Scsc() - { - var token = new NadekoRandom().Next(); - var set = new ConcurrentHashSet(); - if (_service.Subscribers.TryAdd(token, set)) - { - set.Add((ITextChannel) Context.Channel); - await ((IGuildUser) Context.User).SendConfirmAsync(GetText("csc_token"), token.ToString()) - .ConfigureAwait(false); - } - } - - [NadekoCommand, Usage, Description, Aliases] - [RequireContext(ContextType.Guild)] - [RequireUserPermission(GuildPermission.ManageGuild)] - public async Task Jcsc(int token) - { - ConcurrentHashSet set; - if (!_service.Subscribers.TryGetValue(token, out set)) - return; - set.Add((ITextChannel) Context.Channel); - await ReplyConfirmLocalized("csc_join").ConfigureAwait(false); - } - - [NadekoCommand, Usage, Description, Aliases] - [RequireContext(ContextType.Guild)] - [RequireUserPermission(GuildPermission.ManageGuild)] - public async Task Lcsc() - { - foreach (var subscriber in _service.Subscribers) - { - subscriber.Value.TryRemove((ITextChannel) Context.Channel); - } - await ReplyConfirmLocalized("csc_leave").ConfigureAwait(false); - } - } - } -} \ No newline at end of file diff --git a/src/NadekoBot/Modules/Utility/Utility.cs b/src/NadekoBot/Modules/Utility/Utility.cs index 4fecdf5e..caf0e6b0 100644 --- a/src/NadekoBot/Modules/Utility/Utility.cs +++ b/src/NadekoBot/Modules/Utility/Utility.cs @@ -35,84 +35,7 @@ namespace NadekoBot.Modules.Utility _stats = stats; _creds = creds; _bot = bot; - } - - //[NadekoCommand, Usage, Description, Aliases] - //[RequireContext(ContextType.Guild)] - //public async Task Midorina([Remainder] string arg) - //{ - // var channel = (ITextChannel)Context.Channel; - - // var roleNames = arg?.Split(';'); - - // if (roleNames == null || roleNames.Length == 0) - // return; - - // var j = 0; - // var roles = roleNames.Select(x => Context.Guild.Roles.FirstOrDefault(r => String.Compare(r.Name, x, StringComparison.OrdinalIgnoreCase) == 0)) - // .Where(x => x != null) - // .Take(10) - // .ToArray(); - - // var rnd = new NadekoRandom(); - // var reactions = new[] { "🎬", "🐧", "🌍", "🌺", "🚀", "☀", "🌲", "🍒", "🐾", "🏀" } - // .OrderBy(x => rnd.Next()) - // .ToArray(); - - // var roleStrings = roles - // .Select(x => $"{reactions[j++]} -> {x.Name}"); - - // var msg = await Context.Channel.SendConfirmAsync("Pick a Role", - // string.Join("\n", roleStrings)).ConfigureAwait(false); - - // for (int i = 0; i < roles.Length; i++) - // { - // try { await msg.AddReactionAsync(reactions[i]).ConfigureAwait(false); } - // catch (Exception ex) { _log.Warn(ex); } - // await Task.Delay(1000).ConfigureAwait(false); - // } - - // msg.OnReaction((r) => Task.Run(async () => - // { - // try - // { - // var usr = r.User.GetValueOrDefault() as IGuildUser; - - // if (usr == null) - // return; - - // var index = Array.IndexOf(reactions, r.Emoji.Name); - // if (index == -1) - // return; - - // await usr.RemoveRolesAsync(roles[index]); - // } - // catch (Exception ex) - // { - // _log.Warn(ex); - // } - // }), (r) => Task.Run(async () => - // { - // try - // { - // var usr = r.User.GetValueOrDefault() as IGuildUser; - - // if (usr == null) - // return; - - // var index = Array.IndexOf(reactions, r.Emoji.Name); - // if (index == -1) - // return; - - // await usr.RemoveRolesAsync(roles[index]); - // } - // catch (Exception ex) - // { - // _log.Warn(ex); - // } - // })); - //} - + } [NadekoCommand, Usage, Description, Aliases] [RequireContext(ContextType.Guild)] diff --git a/src/NadekoBot/NadekoBot.cs b/src/NadekoBot/NadekoBot.cs index 1c086a7e..24398fe3 100644 --- a/src/NadekoBot/NadekoBot.cs +++ b/src/NadekoBot/NadekoBot.cs @@ -65,7 +65,8 @@ namespace NadekoBot private const string _mutexName = @"Global\nadeko_shards_lock"; private readonly Semaphore sem = new Semaphore(1, 1, _mutexName); public int ShardId { get; } - private readonly Thread waitForParentKill; + public ShardsCoordinator ShardCoord { get; private set; } + private readonly ShardComClient _comClient = new ShardComClient(); public NadekoBot(int shardId, int parentProcessId) @@ -79,22 +80,6 @@ namespace NadekoBot _log = LogManager.GetCurrentClassLogger(); TerribleElevatedPermissionCheck(); - waitForParentKill = new Thread(new ThreadStart(() => - { - try - { - var p = Process.GetProcessById(parentProcessId); - if (p == null) - return; - p.WaitForExit(); - } - finally - { - Environment.Exit(10); - } - })); - waitForParentKill.Start(); - Credentials = new BotCredentials(); Db = new DbService(Credentials); @@ -120,13 +105,12 @@ namespace NadekoBot CaseSensitiveCommands = false, DefaultRunMode = RunMode.Async, }); - - //foundation services + Images = new ImagesService(); Currency = new CurrencyService(BotConfig, Db); GoogleApi = new GoogleApiService(Credentials); - StartSendingData(); + SetupShard(shardId, parentProcessId); #if GLOBAL_NADEKO Client.Log += Client_Log; @@ -152,9 +136,10 @@ namespace NadekoBot private void AddServices() { + var startingGuildIdList = Client.Guilds.Select(x => (long)x.Id).ToList(); using (var uow = Db.UnitOfWork) { - AllGuildConfigs = uow.GuildConfigs.GetAllGuildConfigs(Client.Guilds.Select(x => (long)x.Id).ToList()).ToImmutableArray(); + AllGuildConfigs = uow.GuildConfigs.GetAllGuildConfigs(startingGuildIdList).ToImmutableArray(); } Localization = new Localization(BotConfig.Locale, AllGuildConfigs.ToDictionary(x => x.GuildId, x => x.Locale), Db); Strings = new NadekoStrings(Localization); @@ -170,8 +155,7 @@ namespace NadekoBot //module services //todo 90 - autodiscover, DI, and add instead of manual like this #region utility - var crossServerTextService = new CrossServerTextService(AllGuildConfigs, Client); - var remindService = new RemindService(Client, BotConfig, Db); + var remindService = new RemindService(Client, BotConfig, Db, startingGuildIdList); var repeaterService = new MessageRepeaterService(this, Client, AllGuildConfigs); var converterService = new ConverterService(Db); var commandMapService = new CommandMapService(AllGuildConfigs); @@ -181,7 +165,7 @@ namespace NadekoBot #endregion #region permissions - var permissionsService = new PermissionService(Db, BotConfig, CommandHandler); + var permissionsService = new PermissionService(Client, Db, BotConfig, CommandHandler); var blacklistService = new BlacklistService(BotConfig); var cmdcdsService = new CmdCdService(AllGuildConfigs); var filterService = new FilterService(Client, AllGuildConfigs); @@ -241,7 +225,6 @@ namespace NadekoBot .Add(CommandHandler) .Add(Db) //modules - .Add(crossServerTextService) .Add(commandMapService) .Add(remindService) .Add(repeaterService) @@ -375,7 +358,10 @@ namespace NadekoBot public async Task RunAndBlockAsync(params string[] args) { await RunAsync(args).ConfigureAwait(false); - await Task.Delay(-1).ConfigureAwait(false); + if (ShardCoord != null) + await ShardCoord.RunAndBlockAsync(); + else + await Task.Delay(-1).ConfigureAwait(false); } private void TerribleElevatedPermissionCheck() @@ -392,5 +378,30 @@ namespace NadekoBot Environment.Exit(2); } } + + private void SetupShard(int shardId, int parentProcessId) + { + if (shardId != 0) + { + new Thread(new ThreadStart(() => + { + try + { + var p = Process.GetProcessById(parentProcessId); + if (p == null) + return; + p.WaitForExit(); + } + finally + { + Environment.Exit(10); + } + })).Start(); + } + else + { + ShardCoord = new ShardsCoordinator(); + } + } } } diff --git a/src/NadekoBot/Program.cs b/src/NadekoBot/Program.cs index 20f3a052..a45affcc 100644 --- a/src/NadekoBot/Program.cs +++ b/src/NadekoBot/Program.cs @@ -4,12 +4,10 @@ { public static void Main(string[] args) { - if (args.Length == 0) - return; - if (args[0].ToLowerInvariant() == "main") - new ShardsCoordinator().RunAndBlockAsync(args).GetAwaiter().GetResult(); - else if (int.TryParse(args[0], out int shardId) && int.TryParse(args[1], out int parentProcessId)) + if (args.Length == 2 && int.TryParse(args[0], out int shardId) && int.TryParse(args[1], out int parentProcessId)) new NadekoBot(shardId, parentProcessId).RunAndBlockAsync(args).GetAwaiter().GetResult(); + else + new NadekoBot(0, 0).RunAndBlockAsync(args).GetAwaiter().GetResult(); } } } diff --git a/src/NadekoBot/Properties/launchSettings.json b/src/NadekoBot/Properties/launchSettings.json index 77336a17..b532770a 100644 --- a/src/NadekoBot/Properties/launchSettings.json +++ b/src/NadekoBot/Properties/launchSettings.json @@ -1,8 +1,7 @@ { "profiles": { "NadekoBot": { - "commandName": "Project", - "commandLineArgs": "main" + "commandName": "Project" } } } \ No newline at end of file diff --git a/src/NadekoBot/Services/Database/Repositories/IGuildConfigRepository.cs b/src/NadekoBot/Services/Database/Repositories/IGuildConfigRepository.cs index 52332cfb..e56a9026 100644 --- a/src/NadekoBot/Services/Database/Repositories/IGuildConfigRepository.cs +++ b/src/NadekoBot/Services/Database/Repositories/IGuildConfigRepository.cs @@ -12,9 +12,9 @@ namespace NadekoBot.Services.Database.Repositories GuildConfig LogSettingsFor(ulong guildId); IEnumerable OldPermissionsForAll(); IEnumerable GetAllGuildConfigs(List availableGuilds); - IEnumerable GetAllFollowedStreams(); + IEnumerable GetAllFollowedStreams(List included); void SetCleverbotEnabled(ulong id, bool cleverbotEnabled); - IEnumerable Permissionsv2ForAll(); + IEnumerable Permissionsv2ForAll(List include); GuildConfig GcWithPermissionsv2For(ulong guildId); } } diff --git a/src/NadekoBot/Services/Database/Repositories/IReminderRepository.cs b/src/NadekoBot/Services/Database/Repositories/IReminderRepository.cs index 7c643ec7..07eec33f 100644 --- a/src/NadekoBot/Services/Database/Repositories/IReminderRepository.cs +++ b/src/NadekoBot/Services/Database/Repositories/IReminderRepository.cs @@ -1,9 +1,11 @@ using NadekoBot.Services.Database.Models; +using System.Collections; +using System.Collections.Generic; namespace NadekoBot.Services.Database.Repositories { public interface IReminderRepository : IRepository { - + IEnumerable GetIncludedReminders(List guildIds); } } diff --git a/src/NadekoBot/Services/Database/Repositories/Impl/GuildConfigRepository.cs b/src/NadekoBot/Services/Database/Repositories/Impl/GuildConfigRepository.cs index 25dd52fe..37d2af60 100644 --- a/src/NadekoBot/Services/Database/Repositories/Impl/GuildConfigRepository.cs +++ b/src/NadekoBot/Services/Database/Repositories/Impl/GuildConfigRepository.cs @@ -136,9 +136,10 @@ namespace NadekoBot.Services.Database.Repositories.Impl return query.ToList(); } - public IEnumerable Permissionsv2ForAll() + public IEnumerable Permissionsv2ForAll(List include) { var query = _set + .Where(x => include.Contains((long)x.GuildId)) .Include(gc => gc.Permissions); return query.ToList(); @@ -169,8 +170,10 @@ namespace NadekoBot.Services.Database.Repositories.Impl return config; } - public IEnumerable GetAllFollowedStreams() => - _set.Include(gc => gc.FollowedStreams) + public IEnumerable GetAllFollowedStreams(List included) => + _set + .Where(gc => included.Contains((long)gc.GuildId)) + .Include(gc => gc.FollowedStreams) .SelectMany(gc => gc.FollowedStreams) .ToList(); diff --git a/src/NadekoBot/Services/Database/Repositories/Impl/ReminderRepository.cs b/src/NadekoBot/Services/Database/Repositories/Impl/ReminderRepository.cs index fc7c28ff..b1a0e2a0 100644 --- a/src/NadekoBot/Services/Database/Repositories/Impl/ReminderRepository.cs +++ b/src/NadekoBot/Services/Database/Repositories/Impl/ReminderRepository.cs @@ -1,5 +1,8 @@ using NadekoBot.Services.Database.Models; using Microsoft.EntityFrameworkCore; +using System; +using System.Collections.Generic; +using System.Linq; namespace NadekoBot.Services.Database.Repositories.Impl { @@ -8,5 +11,10 @@ namespace NadekoBot.Services.Database.Repositories.Impl public ReminderRepository(DbContext context) : base(context) { } + + public IEnumerable GetIncludedReminders(List guildIds) + { + return _set.Where(x => guildIds.Contains((long)x.ServerId)).ToList(); + } } } diff --git a/src/NadekoBot/Services/Permissions/PermissionsService.cs b/src/NadekoBot/Services/Permissions/PermissionsService.cs index 27b547be..7ca39a03 100644 --- a/src/NadekoBot/Services/Permissions/PermissionsService.cs +++ b/src/NadekoBot/Services/Permissions/PermissionsService.cs @@ -24,7 +24,7 @@ namespace NadekoBot.Services.Permissions public ConcurrentDictionary Cache { get; } = new ConcurrentDictionary(); - public PermissionService(DbService db, BotConfig bc, CommandHandler cmd) + public PermissionService(DiscordSocketClient client, DbService db, BotConfig bc, CommandHandler cmd) { _log = LogManager.GetCurrentClassLogger(); _db = db; @@ -32,18 +32,23 @@ namespace NadekoBot.Services.Permissions var sw = Stopwatch.StartNew(); TryMigratePermissions(bc); - using (var uow = _db.UnitOfWork) + + client.Ready += delegate { - foreach (var x in uow.GuildConfigs.Permissionsv2ForAll()) + using (var uow = _db.UnitOfWork) { - Cache.TryAdd(x.GuildId, new PermissionCache() + foreach (var x in uow.GuildConfigs.Permissionsv2ForAll(client.Guilds.Select(x => (long)x.Id).ToList())) { - Verbose = x.VerbosePermissions, - PermRole = x.PermissionRole, - Permissions = new PermissionsCollection(x.Permissions) - }); + Cache.TryAdd(x.GuildId, new PermissionCache() + { + Verbose = x.VerbosePermissions, + PermRole = x.PermissionRole, + Permissions = new PermissionsCollection(x.Permissions) + }); + } } - } + return Task.CompletedTask; + }; sw.Stop(); _log.Debug($"Loaded in {sw.Elapsed.TotalSeconds:F2}s"); diff --git a/src/NadekoBot/Services/Searches/AnimeSearchService.cs b/src/NadekoBot/Services/Searches/AnimeSearchService.cs index 71820506..2e4ef756 100644 --- a/src/NadekoBot/Services/Searches/AnimeSearchService.cs +++ b/src/NadekoBot/Services/Searches/AnimeSearchService.cs @@ -11,6 +11,7 @@ using System.Threading.Tasks; namespace NadekoBot.Services.Searches { + //todo move to the website public class AnimeSearchService { private readonly Timer _anilistTokenRefresher; diff --git a/src/NadekoBot/Services/Searches/StreamNotificationService.cs b/src/NadekoBot/Services/Searches/StreamNotificationService.cs index 9b8480fc..a5846362 100644 --- a/src/NadekoBot/Services/Searches/StreamNotificationService.cs +++ b/src/NadekoBot/Services/Searches/StreamNotificationService.cs @@ -35,7 +35,7 @@ namespace NadekoBot.Services.Searches IEnumerable streams; using (var uow = _db.UnitOfWork) { - streams = uow.GuildConfigs.GetAllFollowedStreams(); + streams = uow.GuildConfigs.GetAllFollowedStreams(client.Guilds.Select(x => (long)x.Id).ToList()); } await Task.WhenAll(streams.Select(async fs => diff --git a/src/NadekoBot/Services/Utility/ConverterService.cs b/src/NadekoBot/Services/Utility/ConverterService.cs index 38fc7f9a..b7388335 100644 --- a/src/NadekoBot/Services/Utility/ConverterService.cs +++ b/src/NadekoBot/Services/Utility/ConverterService.cs @@ -11,6 +11,7 @@ using System.Threading.Tasks; namespace NadekoBot.Services.Utility { + //todo periodically load from the database, update only on shard 0 public class ConverterService { public List Units { get; set; } = new List(); diff --git a/src/NadekoBot/Services/Utility/PatreonRewardsService.cs b/src/NadekoBot/Services/Utility/PatreonRewardsService.cs index e8202d84..0f7105b7 100644 --- a/src/NadekoBot/Services/Utility/PatreonRewardsService.cs +++ b/src/NadekoBot/Services/Utility/PatreonRewardsService.cs @@ -12,6 +12,7 @@ using System.Threading.Tasks; namespace NadekoBot.Services.Utility { + //todo periodically load from the database, update only on shard 0 public class PatreonRewardsService { private readonly SemaphoreSlim getPledgesLocker = new SemaphoreSlim(1, 1); diff --git a/src/NadekoBot/Services/Utility/RemindService.cs b/src/NadekoBot/Services/Utility/RemindService.cs index 545e8648..16d6d451 100644 --- a/src/NadekoBot/Services/Utility/RemindService.cs +++ b/src/NadekoBot/Services/Utility/RemindService.cs @@ -33,7 +33,7 @@ namespace NadekoBot.Services.Utility private readonly DiscordSocketClient _client; private readonly DbService _db; - public RemindService(DiscordSocketClient client, BotConfig config, DbService db) + public RemindService(DiscordSocketClient client, BotConfig config, DbService db, List guilds) { _config = config; _client = client; @@ -45,7 +45,7 @@ namespace NadekoBot.Services.Utility List reminders; using (var uow = _db.UnitOfWork) { - reminders = uow.Reminders.GetAll().ToList(); + reminders = uow.Reminders.GetIncludedReminders(guilds).ToList(); } RemindMessageFormat = _config.RemindMessageFormat; diff --git a/src/NadekoBot/Services/Utility/UtilityService.cs b/src/NadekoBot/Services/Utility/UtilityService.cs deleted file mode 100644 index 14af97e8..00000000 --- a/src/NadekoBot/Services/Utility/UtilityService.cs +++ /dev/null @@ -1,69 +0,0 @@ -using Discord; -using Discord.WebSocket; -using NadekoBot.Extensions; -using NadekoBot.Services.Database.Models; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; - -namespace NadekoBot.Services.Utility -{ - public class CrossServerTextService - { - public readonly ConcurrentDictionary> Subscribers = - new ConcurrentDictionary>(); - private DiscordSocketClient _client; - - public CrossServerTextService(IEnumerable guildConfigs, DiscordSocketClient client) - { - _client = client; - _client.MessageReceived += Client_MessageReceived; - } - - private Task Client_MessageReceived(SocketMessage imsg) - { - var _ = Task.Run(async () => { - try - { - if (imsg.Author.IsBot) - return; - var msg = imsg as IUserMessage; - if (msg == null) - return; - var channel = imsg.Channel as ITextChannel; - if (channel == null) - return; - if (msg.Author.Id == _client.CurrentUser.Id) return; - foreach (var subscriber in Subscribers) - { - var set = subscriber.Value; - if (!set.Contains(channel)) - continue; - foreach (var chan in set.Except(new[] { channel })) - { - try - { - await chan.SendMessageAsync(GetMessage(channel, (IGuildUser)msg.Author, - msg)).ConfigureAwait(false); - } - catch - { - // ignored - } - } - } - } - catch - { - // ignored - } - }); - - return Task.CompletedTask; - } - - private string GetMessage(ITextChannel channel, IGuildUser user, IUserMessage message) => - $"**{channel.Guild.Name} | {channel.Name}** `{user.Username}`: " + message.Content.SanitizeMentions(); - } -} diff --git a/src/NadekoBot/ShardsCoordinator.cs b/src/NadekoBot/ShardsCoordinator.cs index 6db3aade..acdac540 100644 --- a/src/NadekoBot/ShardsCoordinator.cs +++ b/src/NadekoBot/ShardsCoordinator.cs @@ -39,10 +39,10 @@ namespace NadekoBot return Task.CompletedTask; } - public async Task RunAsync(params string[] args) + public async Task RunAsync() { var curProcessId = Process.GetCurrentProcess().Id; - for (int i = 0; i < Credentials.TotalShards; i++) + for (int i = 1; i < Credentials.TotalShards; i++) { var p = Process.Start(new ProcessStartInfo() { @@ -56,11 +56,11 @@ namespace NadekoBot } } - public async Task RunAndBlockAsync(params string[] args) + public async Task RunAndBlockAsync() { try { - await RunAsync(args).ConfigureAwait(false); + await RunAsync().ConfigureAwait(false); } catch (Exception ex) { diff --git a/src/NadekoBot/_strings/ResponseStrings.en-US.json b/src/NadekoBot/_strings/ResponseStrings.en-US.json index 98df2300..d1863403 100644 --- a/src/NadekoBot/_strings/ResponseStrings.en-US.json +++ b/src/NadekoBot/_strings/ResponseStrings.en-US.json @@ -613,9 +613,6 @@ "utility_convert_not_found": "Cannot convert {0} to {1}: units not found", "utility_convert_type_error": "Cannot convert {0} to {1}: types of unit are not equal", "utility_created_at": "Created at", - "utility_csc_join": "Joined cross server channel.", - "utility_csc_leave": "Left cross server channel.", - "utility_csc_token": "This is your CSC token", "utility_custom_emojis": "Custom emojis", "utility_error": "Error", "utility_features": "Features", From d5f4dcdf2099b765d91e202cc352f06b06d20c4d Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Tue, 20 Jun 2017 04:24:24 +0200 Subject: [PATCH 006/346] removed soundcloud id support --- src/NadekoBot/Services/IBotCredentials.cs | 1 - src/NadekoBot/Services/Impl/BotCredentials.cs | 15 ++------------- 2 files changed, 2 insertions(+), 14 deletions(-) diff --git a/src/NadekoBot/Services/IBotCredentials.cs b/src/NadekoBot/Services/IBotCredentials.cs index c6c42f87..f7711b83 100644 --- a/src/NadekoBot/Services/IBotCredentials.cs +++ b/src/NadekoBot/Services/IBotCredentials.cs @@ -16,7 +16,6 @@ namespace NadekoBot.Services string CarbonKey { get; } DBConfig Db { get; } - string SoundCloudClientId { get; } string OsuApiKey { get; } bool IsOwner(IUser u); diff --git a/src/NadekoBot/Services/Impl/BotCredentials.cs b/src/NadekoBot/Services/Impl/BotCredentials.cs index 828ed573..92ae20c1 100644 --- a/src/NadekoBot/Services/Impl/BotCredentials.cs +++ b/src/NadekoBot/Services/Impl/BotCredentials.cs @@ -25,17 +25,6 @@ namespace NadekoBot.Services.Impl public string LoLApiKey { get; } public string OsuApiKey { get; } - private string _soundcloudClientId; - public string SoundCloudClientId { - get { - return string.IsNullOrWhiteSpace(_soundcloudClientId) - ? "d0bd7768e3a1a2d15430f0dccb871117" - : _soundcloudClientId; - } - private set { - _soundcloudClientId = value; - } - } public DBConfig Db { get; } public int TotalShards { get; } @@ -81,8 +70,8 @@ namespace NadekoBot.Services.Impl ulong.TryParse(data[nameof(ClientId)], out clId); ClientId = clId; - var scId = data[nameof(SoundCloudClientId)]; - SoundCloudClientId = scId; + //var scId = data[nameof(SoundCloudClientId)]; + //SoundCloudClientId = scId; //SoundCloudClientId = string.IsNullOrWhiteSpace(scId) // ? // : scId; From f26c7a32ec4edaa10ad63dd41043534166099277 Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Tue, 20 Jun 2017 04:25:23 +0200 Subject: [PATCH 007/346] Fixed? word and invite filters --- src/NadekoBot/Services/Permissions/FilterService.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/NadekoBot/Services/Permissions/FilterService.cs b/src/NadekoBot/Services/Permissions/FilterService.cs index 2dbd375d..0d3513ad 100644 --- a/src/NadekoBot/Services/Permissions/FilterService.cs +++ b/src/NadekoBot/Services/Permissions/FilterService.cs @@ -67,7 +67,7 @@ namespace NadekoBot.Services.Permissions public async Task TryBlockEarly(IGuild guild, IUserMessage msg) => !(msg.Author is IGuildUser gu) //it's never filtered outside of guilds, and never block administrators ? false - : gu.GuildPermissions.Administrator && (await FilterInvites(guild, msg) || await FilterWords(guild, msg)); + : !gu.GuildPermissions.Administrator && (await FilterInvites(guild, msg) || await FilterWords(guild, msg)); public async Task FilterWords(IGuild guild, IUserMessage usrMsg) { From 95a7da32009a766877d833f66dc6fbea651c45a9 Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Tue, 20 Jun 2017 04:27:50 +0200 Subject: [PATCH 008/346] woops --- src/NadekoBot/Services/Music/SoundCloudApiService.cs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/NadekoBot/Services/Music/SoundCloudApiService.cs b/src/NadekoBot/Services/Music/SoundCloudApiService.cs index 55a343f8..1b2ecac0 100644 --- a/src/NadekoBot/Services/Music/SoundCloudApiService.cs +++ b/src/NadekoBot/Services/Music/SoundCloudApiService.cs @@ -19,8 +19,6 @@ namespace NadekoBot.Services.Music { if (string.IsNullOrWhiteSpace(url)) throw new ArgumentNullException(nameof(url)); - if (string.IsNullOrWhiteSpace(_creds.SoundCloudClientId)) - throw new ArgumentNullException(nameof(_creds.SoundCloudClientId)); string response = ""; @@ -44,8 +42,6 @@ namespace NadekoBot.Services.Music { if (string.IsNullOrWhiteSpace(query)) throw new ArgumentNullException(nameof(query)); - if (string.IsNullOrWhiteSpace(_creds.SoundCloudClientId)) - throw new ArgumentNullException(nameof(_creds.SoundCloudClientId)); var response = ""; using (var http = new HttpClient()) From 5d136ffd2c0e505afaf56ba307bf8036aa7afca5 Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Tue, 20 Jun 2017 17:14:55 +0200 Subject: [PATCH 009/346] need this to get proper timings on command executions --- src/NadekoBot/NadekoBot.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/NadekoBot/NadekoBot.cs b/src/NadekoBot/NadekoBot.cs index 44802af6..136a49ce 100644 --- a/src/NadekoBot/NadekoBot.cs +++ b/src/NadekoBot/NadekoBot.cs @@ -92,7 +92,7 @@ namespace NadekoBot CommandService = new CommandService(new CommandServiceConfig() { CaseSensitiveCommands = false, - DefaultRunMode = RunMode.Async, + DefaultRunMode = RunMode.Sync, }); //foundation services From 4b954df56d5587315526d58398acdc318aed48ea Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Tue, 20 Jun 2017 22:01:27 +0200 Subject: [PATCH 010/346] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index ae4aa579..825366c3 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ ![img](https://ci.appveyor.com/api/projects/status/gmu6b3ltc80hr3k9?svg=true) [![Discord](https://discordapp.com/api/guilds/117523346618318850/widget.png)](https://discord.gg/nadekobot) [![Documentation Status](https://readthedocs.org/projects/nadekobot/badge/?version=latest)](http://nadekobot.readthedocs.io/en/latest/?badge=latest) -[![nadeko0](https://cdn.discordapp.com/attachments/266240393639755778/281920716809699328/part1.png)](http://nadekobot.xyz) +[![nadeko0](https://cdn.discordapp.com/attachments/266240393639755778/281920716809699328/part1.png)](https://nadekobot.me) [![nadeko1](https://cdn.discordapp.com/attachments/266240393639755778/281920134967328768/part2.png)](https://discordapp.com/oauth2/authorize?client_id=170254782546575360&scope=bot&permissions=66186303) [![nadeko2](https://cdn.discordapp.com/attachments/266240393639755778/281920161311883264/part3.png)](http://nadekobot.readthedocs.io/en/latest/Commands%20List/) From c709680413235ab995f45bddcbc8269bced2c31b Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Wed, 21 Jun 2017 15:14:08 +0200 Subject: [PATCH 011/346] fix --- .../DataStructures/Shard0Precondition.cs | 22 +++++++++++++++++++ src/NadekoBot/NadekoBot.cs | 3 ++- 2 files changed, 24 insertions(+), 1 deletion(-) create mode 100644 src/NadekoBot/DataStructures/Shard0Precondition.cs diff --git a/src/NadekoBot/DataStructures/Shard0Precondition.cs b/src/NadekoBot/DataStructures/Shard0Precondition.cs new file mode 100644 index 00000000..eaa5c591 --- /dev/null +++ b/src/NadekoBot/DataStructures/Shard0Precondition.cs @@ -0,0 +1,22 @@ +using Discord.Commands; +using Discord.WebSocket; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace NadekoBot.DataStructures +{ + public class Shard0Precondition : PreconditionAttribute + { + public override Task CheckPermissions(ICommandContext context, CommandInfo command, IServiceProvider services) + { + var c = (DiscordSocketClient)context.Client; + if (c.ShardId == 0) + return Task.FromResult(PreconditionResult.FromSuccess()); + else + return Task.FromResult(PreconditionResult.FromError("Must be ran from shard #0")); + } + } +} diff --git a/src/NadekoBot/NadekoBot.cs b/src/NadekoBot/NadekoBot.cs index a7a32fce..d6b675de 100644 --- a/src/NadekoBot/NadekoBot.cs +++ b/src/NadekoBot/NadekoBot.cs @@ -224,7 +224,7 @@ namespace NadekoBot .Add(Currency) .Add(CommandHandler) .Add(Db) - //modules + //modules .Add(commandMapService) .Add(remindService) .Add(repeaterService) @@ -261,6 +261,7 @@ namespace NadekoBot .Add(filterService) .Add(globalPermsService) .Add(pokemonService) + .Add(this) .Build(); CommandHandler.AddServices(Services); From 4862564c74236a8f6393e5a4694b00fb0be87f1b Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Wed, 21 Jun 2017 23:12:24 +0200 Subject: [PATCH 012/346] disabled private poll, removed shardid command --- .../Modules/Games/Commands/PollCommands.cs | 7 ++- src/NadekoBot/Modules/Utility/Utility.cs | 62 +++++++++---------- src/NadekoBot/Services/IBotCredentials.cs | 2 + src/NadekoBot/Services/Impl/BotCredentials.cs | 19 ++++-- src/NadekoBot/ShardsCoordinator.cs | 11 ++-- src/NadekoBot/credentials_example.json | 3 +- 6 files changed, 56 insertions(+), 48 deletions(-) diff --git a/src/NadekoBot/Modules/Games/Commands/PollCommands.cs b/src/NadekoBot/Modules/Games/Commands/PollCommands.cs index 1cf5b951..27dbb03c 100644 --- a/src/NadekoBot/Modules/Games/Commands/PollCommands.cs +++ b/src/NadekoBot/Modules/Games/Commands/PollCommands.cs @@ -44,9 +44,14 @@ namespace NadekoBot.Modules.Games await Context.Channel.EmbedAsync(poll.GetStats(GetText("current_poll_results"))); } - + //todo enable private polls, or completely remove them private async Task InternalStartPoll(string arg, bool isPublic = false) { + if (isPublic == false) + { + await ReplyErrorLocalized($"Temporarily disabled. Use `{Prefix}ppoll`"); + return; + } if(await _polls.StartPoll((ITextChannel)Context.Channel, Context.Message, arg, isPublic) == false) await ReplyErrorLocalized("poll_already_running").ConfigureAwait(false); } diff --git a/src/NadekoBot/Modules/Utility/Utility.cs b/src/NadekoBot/Modules/Utility/Utility.cs index caf0e6b0..1b0276e6 100644 --- a/src/NadekoBot/Modules/Utility/Utility.cs +++ b/src/NadekoBot/Modules/Utility/Utility.cs @@ -18,6 +18,7 @@ using Discord.WebSocket; using System.Diagnostics; using Color = Discord.Color; using NadekoBot.Services; +using NadekoBot.DataStructures; namespace NadekoBot.Modules.Utility { @@ -277,48 +278,41 @@ namespace NadekoBot.Modules.Utility await Context.Channel.SendConfirmAsync($"{Context.User.Mention} https://discord.gg/{invite.Code}"); } - //todo 2 shard commands - //[NadekoCommand, Usage, Description, Aliases] - //public async Task ShardStats(int page = 1) - //{ - // if (--page < 0) - // return; - // var status = string.Join(", ", _client.Shards.GroupBy(x => x.ConnectionState) - // .Select(x => $"{x.Count()} {x.Key}") - // .ToArray()); + [NadekoCommand, Usage, Description, Aliases] + [Shard0Precondition] + public async Task ShardStats(int page = 1) + { + if (--page < 0) + return; - // var allShardStrings = _client.Shards - // .Select(x => - // GetText("shard_stats_txt", x.ShardId.ToString(), - // Format.Bold(x.ConnectionState.ToString()), Format.Bold(x.Guilds.Count.ToString()))) - // .ToArray(); + var status = string.Join(", ", _bot.ShardCoord.Statuses.GroupBy(x => x.ConnectionState) + .Select(x => $"{x.Count()} {x.Key}") + .ToArray()); + + var allShardStrings = _bot.ShardCoord.Statuses + .Select(x => + GetText("shard_stats_txt", x.ShardId.ToString(), + Format.Bold(x.ConnectionState.ToString()), Format.Bold(x.Guilds.ToString()))) + .ToArray(); - // await Context.Channel.SendPaginatedConfirmAsync(_client, page, (curPage) => - // { + await Context.Channel.SendPaginatedConfirmAsync(_client, page, (curPage) => + { - // var str = string.Join("\n", allShardStrings.Skip(25 * curPage).Take(25)); + var str = string.Join("\n", allShardStrings.Skip(25 * curPage).Take(25)); - // if (string.IsNullOrWhiteSpace(str)) - // str = GetText("no_shards_on_page"); + if (string.IsNullOrWhiteSpace(str)) + str = GetText("no_shards_on_page"); - // return new EmbedBuilder() - // .WithAuthor(a => a.WithName(GetText("shard_stats"))) - // .WithTitle(status) - // .WithOkColor() - // .WithDescription(str); - // }, allShardStrings.Length / 25); - //} - - //[NadekoCommand, Usage, Description, Aliases] - //public async Task ShardId(IGuild guild) - //{ - // var shardId = _client.GetShardIdFor(guild); - - // await Context.Channel.SendConfirmAsync(shardId.ToString()).ConfigureAwait(false); - //} + return new EmbedBuilder() + .WithAuthor(a => a.WithName(GetText("shard_stats"))) + .WithTitle(status) + .WithOkColor() + .WithDescription(str); + }, allShardStrings.Length / 25); + } [NadekoCommand, Usage, Description, Aliases] public async Task Stats() diff --git a/src/NadekoBot/Services/IBotCredentials.cs b/src/NadekoBot/Services/IBotCredentials.cs index d7a7073c..e0c271b5 100644 --- a/src/NadekoBot/Services/IBotCredentials.cs +++ b/src/NadekoBot/Services/IBotCredentials.cs @@ -20,6 +20,8 @@ namespace NadekoBot.Services bool IsOwner(IUser u); int TotalShards { get; } + string ShardRunCommand { get; } + string ShardRunArguments { get; } } public class DBConfig diff --git a/src/NadekoBot/Services/Impl/BotCredentials.cs b/src/NadekoBot/Services/Impl/BotCredentials.cs index 92ae20c1..3517a298 100644 --- a/src/NadekoBot/Services/Impl/BotCredentials.cs +++ b/src/NadekoBot/Services/Impl/BotCredentials.cs @@ -30,20 +30,22 @@ namespace NadekoBot.Services.Impl public int TotalShards { get; } public string CarbonKey { get; } - public string credsFileName { get; } = Path.Combine(Directory.GetCurrentDirectory(), "credentials.json"); + private readonly string _credsFileName = Path.Combine(Directory.GetCurrentDirectory(), "credentials.json"); public string PatreonAccessToken { get; } + public string ShardRunCommand { get; } + public string ShardRunArguments { get; } public BotCredentials() { _log = LogManager.GetCurrentClassLogger(); try { File.WriteAllText("./credentials_example.json", JsonConvert.SerializeObject(new CredentialsModel(), Formatting.Indented)); } catch { } - if(!File.Exists(credsFileName)) + if(!File.Exists(_credsFileName)) _log.Warn($"credentials.json is missing. Attempting to load creds from environment variables prefixed with 'NadekoBot_'. Example is in {Path.GetFullPath("./credentials_example.json")}"); try { var configBuilder = new ConfigurationBuilder(); - configBuilder.AddJsonFile(credsFileName, true) + configBuilder.AddJsonFile(_credsFileName, true) .AddEnvironmentVariables("NadekoBot_"); var data = configBuilder.Build(); @@ -61,13 +63,19 @@ namespace NadekoBot.Services.Impl MashapeKey = data[nameof(MashapeKey)]; OsuApiKey = data[nameof(OsuApiKey)]; PatreonAccessToken = data[nameof(PatreonAccessToken)]; + ShardRunCommand = data[nameof(ShardRunCommand)]; + ShardRunArguments = data[nameof(ShardRunArguments)]; + + if (string.IsNullOrWhiteSpace(ShardRunCommand)) + ShardRunCommand = "dotnet"; + if (string.IsNullOrWhiteSpace(ShardRunArguments)) + ShardRunArguments = "run -c Release -- {0} {1}"; int ts = 1; int.TryParse(data[nameof(TotalShards)], out ts); TotalShards = ts < 1 ? 1 : ts; - ulong clId = 0; - ulong.TryParse(data[nameof(ClientId)], out clId); + ulong.TryParse(data[nameof(ClientId)], out ulong clId); ClientId = clId; //var scId = data[nameof(SoundCloudClientId)]; @@ -107,6 +115,7 @@ namespace NadekoBot.Services.Impl public DBConfig Db { get; set; } = new DBConfig("sqlite", "Filename=./data/NadekoBot.db"); public int TotalShards { get; set; } = 1; public string PatreonAccessToken { get; set; } = ""; + public string ShardRunCommand { get; set; } = ""; } private class DbModel diff --git a/src/NadekoBot/ShardsCoordinator.cs b/src/NadekoBot/ShardsCoordinator.cs index acdac540..395ad71d 100644 --- a/src/NadekoBot/ShardsCoordinator.cs +++ b/src/NadekoBot/ShardsCoordinator.cs @@ -13,7 +13,7 @@ namespace NadekoBot { private readonly BotCredentials Credentials; private Process[] ShardProcesses; - private ShardComMessage[] Statuses; + public ShardComMessage[] Statuses { get; } private readonly Logger _log; private readonly ShardComServer _comServer; @@ -35,7 +35,7 @@ namespace NadekoBot { Statuses[msg.ShardId] = msg; if (msg.ConnectionState == Discord.ConnectionState.Disconnected || msg.ConnectionState == Discord.ConnectionState.Disconnecting) - _log.Error("!!! SHARD {0} IS IN {1} STATE", msg.ShardId, msg.ConnectionState); + _log.Error("!!! SHARD {0} IS IN {1} STATE", msg.ShardId, msg.ConnectionState.ToString()); return Task.CompletedTask; } @@ -46,13 +46,10 @@ namespace NadekoBot { var p = Process.Start(new ProcessStartInfo() { - FileName = "dotnet", - Arguments = $"run -c Debug -- {i} {curProcessId}", + FileName = Credentials.ShardRunCommand, + Arguments = string.Format(Credentials.ShardRunArguments, i, curProcessId) }); await Task.Delay(5000); - - //Task.Run(() => { while (!p.HasExited) _log.Info($"S-{i}|" + p.StandardOutput.ReadLine()); }); - //Task.Run(() => { while (!p.HasExited) _log.Error($"S-{i}|" + p.StandardError.ReadLine()); }); } } diff --git a/src/NadekoBot/credentials_example.json b/src/NadekoBot/credentials_example.json index 912b452a..7c7e4095 100644 --- a/src/NadekoBot/credentials_example.json +++ b/src/NadekoBot/credentials_example.json @@ -15,5 +15,6 @@ "ConnectionString": "Filename=./data/NadekoBot.db" }, "TotalShards": 1, - "PatreonAccessToken": "" + "PatreonAccessToken": "", + "ShardRunCommand": "" } \ No newline at end of file From 85e183999141682cca9ed773ae5ac08688f10367 Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Wed, 21 Jun 2017 23:31:47 +0200 Subject: [PATCH 013/346] don't load owner channels --- src/NadekoBot/Services/Administration/SelfService.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/NadekoBot/Services/Administration/SelfService.cs b/src/NadekoBot/Services/Administration/SelfService.cs index 8e8a4a13..0c5b6e48 100644 --- a/src/NadekoBot/Services/Administration/SelfService.cs +++ b/src/NadekoBot/Services/Administration/SelfService.cs @@ -67,9 +67,8 @@ namespace NadekoBot.Services.Administration _client.Guilds.SelectMany(g => g.Users); - LoadOwnerChannels(); - - + //todo load owner channels + //LoadOwnerChannels(); }); } From 4b57d874bac04818d44a643eb51521cef7272829 Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Wed, 21 Jun 2017 23:34:29 +0200 Subject: [PATCH 014/346] Fixed compile error --- src/NadekoBot/NadekoBot.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/NadekoBot/NadekoBot.cs b/src/NadekoBot/NadekoBot.cs index d6b675de..9c1df1ab 100644 --- a/src/NadekoBot/NadekoBot.cs +++ b/src/NadekoBot/NadekoBot.cs @@ -28,6 +28,7 @@ using NadekoBot.Services.Help; using System.IO; using NadekoBot.Services.Pokemon; using NadekoBot.DataStructures.ShardCom; +using NadekoBot.DataStructures; namespace NadekoBot { @@ -335,13 +336,14 @@ namespace NadekoBot // .Select(x => x.Key + $"({x.Count()})"))); //unload modules which are not available on the public bot -#if GLOBAL_NADEKO +#if GLOBAL_NADEKO CommandService .Modules .ToArray() .Where(x => x.Preconditions.Any(y => y.GetType() == typeof(NoPublicBot))) .ForEach(x => CommandService.RemoveModuleAsync(x)); #endif + Ready = true; _log.Info($"Shard {ShardId} ready."); //_log.Info(await stats.Print().ConfigureAwait(false)); From f1bcbd91a3634cd8efa7f7dffd4154a45141aa24 Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Wed, 21 Jun 2017 23:36:00 +0200 Subject: [PATCH 015/346] Another one --- src/NadekoBot/NadekoBot.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/NadekoBot/NadekoBot.cs b/src/NadekoBot/NadekoBot.cs index 9c1df1ab..e1223ec9 100644 --- a/src/NadekoBot/NadekoBot.cs +++ b/src/NadekoBot/NadekoBot.cs @@ -29,6 +29,7 @@ using System.IO; using NadekoBot.Services.Pokemon; using NadekoBot.DataStructures.ShardCom; using NadekoBot.DataStructures; +using NadekoBot.Extensions; namespace NadekoBot { From d2ea530c10541ae266d4fbc0c3b4f8b38cf70beb Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Wed, 21 Jun 2017 23:49:13 +0200 Subject: [PATCH 016/346] fix shard status nullrefs --- src/NadekoBot/Modules/Utility/Utility.cs | 8 +++--- src/NadekoBot/ShardsCoordinator.cs | 32 +++++++++++++++--------- 2 files changed, 25 insertions(+), 15 deletions(-) diff --git a/src/NadekoBot/Modules/Utility/Utility.cs b/src/NadekoBot/Modules/Utility/Utility.cs index 1b0276e6..c26e584e 100644 --- a/src/NadekoBot/Modules/Utility/Utility.cs +++ b/src/NadekoBot/Modules/Utility/Utility.cs @@ -285,12 +285,14 @@ namespace NadekoBot.Modules.Utility { if (--page < 0) return; - - var status = string.Join(", ", _bot.ShardCoord.Statuses.GroupBy(x => x.ConnectionState) + var statuses = _bot.ShardCoord.Statuses.ToArray(); + var status = string.Join(", ", statuses + .Where(x => x != null) + .GroupBy(x => x.ConnectionState) .Select(x => $"{x.Count()} {x.Key}") .ToArray()); - var allShardStrings = _bot.ShardCoord.Statuses + var allShardStrings = statuses .Select(x => GetText("shard_stats_txt", x.ShardId.ToString(), Format.Bold(x.ConnectionState.ToString()), Format.Bold(x.Guilds.ToString()))) diff --git a/src/NadekoBot/ShardsCoordinator.cs b/src/NadekoBot/ShardsCoordinator.cs index 395ad71d..8ce8f6f6 100644 --- a/src/NadekoBot/ShardsCoordinator.cs +++ b/src/NadekoBot/ShardsCoordinator.cs @@ -65,22 +65,30 @@ namespace NadekoBot } await Task.Run(() => { - string input; - while ((input = Console.ReadLine()?.ToLowerInvariant()) != "quit") + try { - switch (input) + string input; + while ((input = Console.ReadLine()?.ToLowerInvariant()) != "quit") { - case "ls": - var groupStr = string.Join(",", Statuses - .Where(x => x != null) - .GroupBy(x => x.ConnectionState) - .Select(x => x.Count() + " " + x.Key)); - _log.Info(string.Join("\n", Statuses.Select(x => $"Shard {x.ShardId} is in {x.ConnectionState.ToString()} state with {x.Guilds} servers")) + "\n" + groupStr); - break; - default: - break; + switch (input) + { + case "ls": + var groupStr = string.Join(",", Statuses + .ToArray() + .Where(x => x != null) + .GroupBy(x => x.ConnectionState) + .Select(x => x.Count() + " " + x.Key)); + _log.Info(string.Join("\n", Statuses.Select(x => $"Shard {x.ShardId} is in {x.ConnectionState.ToString()} state with {x.Guilds} servers")) + "\n" + groupStr); + break; + default: + break; + } } } + catch (Exception ex) + { + _log.Warn(ex); + } }); foreach (var p in ShardProcesses) { From ee72962ee5da3e4c01f404a712e5a3fa894f149f Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Thu, 22 Jun 2017 00:12:29 +0200 Subject: [PATCH 017/346] Possible database lock fix --- src/NadekoBot/Modules/Utility/Utility.cs | 5 +++-- src/NadekoBot/Services/Database/NadekoContext.cs | 5 +++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/NadekoBot/Modules/Utility/Utility.cs b/src/NadekoBot/Modules/Utility/Utility.cs index c26e584e..51f24efc 100644 --- a/src/NadekoBot/Modules/Utility/Utility.cs +++ b/src/NadekoBot/Modules/Utility/Utility.cs @@ -285,9 +285,10 @@ namespace NadekoBot.Modules.Utility { if (--page < 0) return; - var statuses = _bot.ShardCoord.Statuses.ToArray(); + var statuses = _bot.ShardCoord.Statuses.ToArray() + .Where(x => x != null); + var status = string.Join(", ", statuses - .Where(x => x != null) .GroupBy(x => x.ConnectionState) .Select(x => $"{x.Count()} {x.Key}") .ToArray()); diff --git a/src/NadekoBot/Services/Database/NadekoContext.cs b/src/NadekoBot/Services/Database/NadekoContext.cs index 9c084867..b61c9aa9 100644 --- a/src/NadekoBot/Services/Database/NadekoContext.cs +++ b/src/NadekoBot/Services/Database/NadekoContext.cs @@ -18,8 +18,9 @@ namespace NadekoBot.Services.Database public NadekoContext Create(DbContextFactoryOptions options) { var optionsBuilder = new DbContextOptionsBuilder(); - optionsBuilder.UseSqlite("Filename=./data/NadekoBot.db"); - return new NadekoContext(optionsBuilder.Options); + optionsBuilder.UseSqlite("Filename=./data/NadekoBot.db;Default Command Timeout=60000;Busy Timeout=60000"); + var ctx = new NadekoContext(optionsBuilder.Options); + return ctx; } } From d3c598ae01ddf5bfd37c57073e87130498d958b9 Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Thu, 22 Jun 2017 00:22:50 +0200 Subject: [PATCH 018/346] Command Timeout set to 60 --- src/NadekoBot/Services/Database/NadekoContext.cs | 3 ++- src/NadekoBot/Services/DbService.cs | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/NadekoBot/Services/Database/NadekoContext.cs b/src/NadekoBot/Services/Database/NadekoContext.cs index b61c9aa9..238130ed 100644 --- a/src/NadekoBot/Services/Database/NadekoContext.cs +++ b/src/NadekoBot/Services/Database/NadekoContext.cs @@ -18,8 +18,9 @@ namespace NadekoBot.Services.Database public NadekoContext Create(DbContextFactoryOptions options) { var optionsBuilder = new DbContextOptionsBuilder(); - optionsBuilder.UseSqlite("Filename=./data/NadekoBot.db;Default Command Timeout=60000;Busy Timeout=60000"); + optionsBuilder.UseSqlite("Filename=./data/NadekoBot.db"); var ctx = new NadekoContext(optionsBuilder.Options); + ctx.Database.SetCommandTimeout(60); return ctx; } } diff --git a/src/NadekoBot/Services/DbService.cs b/src/NadekoBot/Services/DbService.cs index 81e56d1d..c8fab899 100644 --- a/src/NadekoBot/Services/DbService.cs +++ b/src/NadekoBot/Services/DbService.cs @@ -32,6 +32,7 @@ namespace NadekoBot.Services public NadekoContext GetDbContext() { var context = new NadekoContext(options); + context.Database.SetCommandTimeout(60); context.Database.Migrate(); context.EnsureSeedData(); From 2cad7f4475be6cb340de64d46f12b72ba3bec027 Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Thu, 22 Jun 2017 00:47:29 +0200 Subject: [PATCH 019/346] small fixes --- src/NadekoBot/NadekoBot.cs | 5 +++-- src/NadekoBot/ShardsCoordinator.cs | 16 ++++++++-------- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/src/NadekoBot/NadekoBot.cs b/src/NadekoBot/NadekoBot.cs index e1223ec9..53288c5c 100644 --- a/src/NadekoBot/NadekoBot.cs +++ b/src/NadekoBot/NadekoBot.cs @@ -292,7 +292,7 @@ namespace NadekoBot } finally { - _log.Info("Shard {0} logged in ...", ShardId); + _log.Info("Shard {0} logged in.", ShardId); sem.Release(); } return Task.CompletedTask; @@ -306,6 +306,7 @@ namespace NadekoBot public async Task RunAsync(params string[] args) { + if(ShardId == 0) _log.Info("Starting NadekoBot v" + StatsService.BotVersion); var sw = Stopwatch.StartNew(); @@ -316,7 +317,7 @@ namespace NadekoBot AddServices(); sw.Stop(); - _log.Info($"Shard {ShardId} connected in {sw.Elapsed.TotalSeconds:F2} s"); + _log.Info($"Shard {ShardId} connected in {sw.Elapsed.TotalSeconds:F2}s"); var stats = Services.GetService(); stats.Initialize(); diff --git a/src/NadekoBot/ShardsCoordinator.cs b/src/NadekoBot/ShardsCoordinator.cs index 8ce8f6f6..9f848389 100644 --- a/src/NadekoBot/ShardsCoordinator.cs +++ b/src/NadekoBot/ShardsCoordinator.cs @@ -49,7 +49,7 @@ namespace NadekoBot FileName = Credentials.ShardRunCommand, Arguments = string.Format(Credentials.ShardRunArguments, i, curProcessId) }); - await Task.Delay(5000); + await Task.Delay(6500); } } @@ -65,10 +65,10 @@ namespace NadekoBot } await Task.Run(() => { - try + string input; + while ((input = Console.ReadLine()?.ToLowerInvariant()) != "quit") { - string input; - while ((input = Console.ReadLine()?.ToLowerInvariant()) != "quit") + try { switch (input) { @@ -84,10 +84,10 @@ namespace NadekoBot break; } } - } - catch (Exception ex) - { - _log.Warn(ex); + catch (Exception ex) + { + _log.Warn(ex); + } } }); foreach (var p in ShardProcesses) From 0aa65b29534dc1fe376cb4b44b789d87409c381a Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Thu, 22 Jun 2017 03:39:26 +0200 Subject: [PATCH 020/346] Fixes to remind and shardstats --- .../Modules/Utility/Commands/Remind.cs | 4 +- src/NadekoBot/NadekoBot.cs | 3 + .../Services/Administration/SelfService.cs | 58 +++++++++---------- src/NadekoBot/ShardsCoordinator.cs | 5 +- 4 files changed, 38 insertions(+), 32 deletions(-) diff --git a/src/NadekoBot/Modules/Utility/Commands/Remind.cs b/src/NadekoBot/Modules/Utility/Commands/Remind.cs index 448ec424..ce046467 100644 --- a/src/NadekoBot/Modules/Utility/Commands/Remind.cs +++ b/src/NadekoBot/Modules/Utility/Commands/Remind.cs @@ -33,7 +33,7 @@ namespace NadekoBot.Modules.Utility [NadekoCommand, Usage, Description, Aliases] [RequireContext(ContextType.Guild)] - [Priority(1)] + [Priority(0)] public async Task Remind(MeOrHere meorhere, string timeStr, [Remainder] string message) { ulong target; @@ -44,7 +44,7 @@ namespace NadekoBot.Modules.Utility [NadekoCommand, Usage, Description, Aliases] [RequireContext(ContextType.Guild)] [RequireUserPermission(GuildPermission.ManageMessages)] - [Priority(0)] + [Priority(1)] public async Task Remind(ITextChannel channel, string timeStr, [Remainder] string message) { var perms = ((IGuildUser)Context.User).GetPermissions((ITextChannel)channel); diff --git a/src/NadekoBot/NadekoBot.cs b/src/NadekoBot/NadekoBot.cs index 53288c5c..e6dc2d5c 100644 --- a/src/NadekoBot/NadekoBot.cs +++ b/src/NadekoBot/NadekoBot.cs @@ -363,10 +363,13 @@ namespace NadekoBot public async Task RunAndBlockAsync(params string[] args) { await RunAsync(args).ConfigureAwait(false); + StartSendingData(); if (ShardCoord != null) await ShardCoord.RunAndBlockAsync(); else + { await Task.Delay(-1).ConfigureAwait(false); + } } private void TerribleElevatedPermissionCheck() diff --git a/src/NadekoBot/Services/Administration/SelfService.cs b/src/NadekoBot/Services/Administration/SelfService.cs index 0c5b6e48..0a288a0b 100644 --- a/src/NadekoBot/Services/Administration/SelfService.cs +++ b/src/NadekoBot/Services/Administration/SelfService.cs @@ -72,39 +72,39 @@ namespace NadekoBot.Services.Administration }); } - private void LoadOwnerChannels() - { - var hs = new HashSet(_creds.OwnerIds); - var channels = new Dictionary>(); + //private void LoadOwnerChannels() + //{ + // var hs = new HashSet(_creds.OwnerIds); + // var channels = new Dictionary>(); - if (hs.Count > 0) - { - foreach (var g in _client.Guilds) - { - if (hs.Count == 0) - break; + // if (hs.Count > 0) + // { + // foreach (var g in _client.Guilds) + // { + // if (hs.Count == 0) + // break; - foreach (var u in g.Users) - { - if (hs.Remove(u.Id)) - { - channels.Add(u.Id, new AsyncLazy(async () => await u.CreateDMChannelAsync())); - if (hs.Count == 0) - break; - } - } - } - } + // foreach (var u in g.Users) + // { + // if (hs.Remove(u.Id)) + // { + // channels.Add(u.Id, new AsyncLazy(async () => await u.CreateDMChannelAsync())); + // if (hs.Count == 0) + // break; + // } + // } + // } + // } - ownerChannels = channels.OrderBy(x => _creds.OwnerIds.IndexOf(x.Key)) - .Select(x => x.Value) - .ToImmutableArray(); + // ownerChannels = channels.OrderBy(x => _creds.OwnerIds.IndexOf(x.Key)) + // .Select(x => x.Value) + // .ToImmutableArray(); - if (!ownerChannels.Any()) - _log.Warn("No owner channels created! Make sure you've specified correct OwnerId in the credentials.json file."); - else - _log.Info($"Created {ownerChannels.Length} out of {_creds.OwnerIds.Length} owner message channels."); - } + // if (!ownerChannels.Any()) + // _log.Warn("No owner channels created! Make sure you've specified correct OwnerId in the credentials.json file."); + // else + // _log.Info($"Created {ownerChannels.Length} out of {_creds.OwnerIds.Length} owner message channels."); + //} // forwards dms public async Task LateExecute(DiscordSocketClient client, IGuild guild, IUserMessage msg) diff --git a/src/NadekoBot/ShardsCoordinator.cs b/src/NadekoBot/ShardsCoordinator.cs index 9f848389..4ed8247f 100644 --- a/src/NadekoBot/ShardsCoordinator.cs +++ b/src/NadekoBot/ShardsCoordinator.cs @@ -78,7 +78,10 @@ namespace NadekoBot .Where(x => x != null) .GroupBy(x => x.ConnectionState) .Select(x => x.Count() + " " + x.Key)); - _log.Info(string.Join("\n", Statuses.Select(x => $"Shard {x.ShardId} is in {x.ConnectionState.ToString()} state with {x.Guilds} servers")) + "\n" + groupStr); + _log.Info(string.Join("\n", Statuses + .ToArray() + .Where(x => x != null) + .Select(x => $"Shard {x.ShardId} is in {x.ConnectionState.ToString()} state with {x.Guilds} servers")) + "\n" + groupStr); break; default: break; From bed3001ce1b15cbbdc8c83dc61c8102e6d9050ea Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Thu, 22 Jun 2017 22:16:58 +0200 Subject: [PATCH 021/346] Database should be faster, disabled unit converter temporarily --- .../Utility/Commands/UnitConversion.cs | 180 +++++++++--------- src/NadekoBot/NadekoBot.cs | 4 +- src/NadekoBot/Services/DbService.cs | 11 ++ src/NadekoBot/Services/Impl/BotCredentials.cs | 2 +- 4 files changed, 104 insertions(+), 93 deletions(-) diff --git a/src/NadekoBot/Modules/Utility/Commands/UnitConversion.cs b/src/NadekoBot/Modules/Utility/Commands/UnitConversion.cs index 9b9c121c..603093c4 100644 --- a/src/NadekoBot/Modules/Utility/Commands/UnitConversion.cs +++ b/src/NadekoBot/Modules/Utility/Commands/UnitConversion.cs @@ -1,94 +1,94 @@ -using Discord; -using Discord.Commands; -using NadekoBot.Attributes; -using NadekoBot.Extensions; -using NadekoBot.Services.Utility; -using System; -using System.Linq; -using System.Threading.Tasks; +//using Discord; +//using Discord.Commands; +//using NadekoBot.Attributes; +//using NadekoBot.Extensions; +//using NadekoBot.Services.Utility; +//using System; +//using System.Linq; +//using System.Threading.Tasks; -namespace NadekoBot.Modules.Utility -{ - public partial class Utility - { - [Group] - public class UnitConverterCommands : NadekoSubmodule - { - private readonly ConverterService _service; +//namespace NadekoBot.Modules.Utility +//{ +// public partial class Utility +// { +// [Group] +// public class UnitConverterCommands : NadekoSubmodule +// { +// private readonly ConverterService _service; - public UnitConverterCommands(ConverterService service) - { - _service = service; - } +// public UnitConverterCommands(ConverterService service) +// { +// _service = service; +// } - [NadekoCommand, Usage, Description, Aliases] - public async Task ConvertList() - { - var res = _service.Units.GroupBy(x => x.UnitType) - .Aggregate(new EmbedBuilder().WithTitle(GetText("convertlist")) - .WithColor(NadekoBot.OkColor), - (embed, g) => embed.AddField(efb => - efb.WithName(g.Key.ToTitleCase()) - .WithValue(String.Join(", ", g.Select(x => x.Triggers.FirstOrDefault()) - .OrderBy(x => x))))); - await Context.Channel.EmbedAsync(res); - } - [NadekoCommand, Usage, Description, Aliases] - public async Task Convert(string origin, string target, decimal value) - { - var originUnit = _service.Units.Find(x => x.Triggers.Select(y => y.ToLowerInvariant()).Contains(origin.ToLowerInvariant())); - var targetUnit = _service.Units.Find(x => x.Triggers.Select(y => y.ToLowerInvariant()).Contains(target.ToLowerInvariant())); - if (originUnit == null || targetUnit == null) - { - await ReplyErrorLocalized("convert_not_found", Format.Bold(origin), Format.Bold(target)).ConfigureAwait(false); - return; - } - if (originUnit.UnitType != targetUnit.UnitType) - { - await ReplyErrorLocalized("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); +// [NadekoCommand, Usage, Description, Aliases] +// public async Task ConvertList() +// { +// var res = _service.Units.GroupBy(x => x.UnitType) +// .Aggregate(new EmbedBuilder().WithTitle(GetText("convertlist")) +// .WithColor(NadekoBot.OkColor), +// (embed, g) => embed.AddField(efb => +// efb.WithName(g.Key.ToTitleCase()) +// .WithValue(String.Join(", ", g.Select(x => x.Triggers.FirstOrDefault()) +// .OrderBy(x => x))))); +// await Context.Channel.EmbedAsync(res); +// } +// [NadekoCommand, Usage, Description, Aliases] +// public async Task Convert(string origin, string target, decimal value) +// { +// var originUnit = _service.Units.Find(x => x.Triggers.Select(y => y.ToLowerInvariant()).Contains(origin.ToLowerInvariant())); +// var targetUnit = _service.Units.Find(x => x.Triggers.Select(y => y.ToLowerInvariant()).Contains(target.ToLowerInvariant())); +// if (originUnit == null || targetUnit == null) +// { +// await ReplyErrorLocalized("convert_not_found", Format.Bold(origin), Format.Bold(target)).ConfigureAwait(false); +// return; +// } +// if (originUnit.UnitType != targetUnit.UnitType) +// { +// await ReplyErrorLocalized("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 Context.Channel.SendConfirmAsync(GetText("convert", value, (originUnit.Triggers.First()).SnPl(value.IsInteger() ? (int)value : 2), res, (targetUnit.Triggers.First() + "s").SnPl(res.IsInteger() ? (int)res : 2))); - } - } - } -} \ No newline at end of file +// await Context.Channel.SendConfirmAsync(GetText("convert", value, (originUnit.Triggers.First()).SnPl(value.IsInteger() ? (int)value : 2), res, (targetUnit.Triggers.First() + "s").SnPl(res.IsInteger() ? (int)res : 2))); +// } +// } +// } +//} \ No newline at end of file diff --git a/src/NadekoBot/NadekoBot.cs b/src/NadekoBot/NadekoBot.cs index e6dc2d5c..8f6e926b 100644 --- a/src/NadekoBot/NadekoBot.cs +++ b/src/NadekoBot/NadekoBot.cs @@ -159,7 +159,7 @@ namespace NadekoBot #region utility var remindService = new RemindService(Client, BotConfig, Db, startingGuildIdList); var repeaterService = new MessageRepeaterService(this, Client, AllGuildConfigs); - var converterService = new ConverterService(Db); + //var converterService = new ConverterService(Db); var commandMapService = new CommandMapService(AllGuildConfigs); var patreonRewardsService = new PatreonRewardsService(Credentials, Db, Currency); var verboseErrorsService = new VerboseErrorsService(AllGuildConfigs, Db, CommandHandler, helpService); @@ -230,7 +230,7 @@ namespace NadekoBot .Add(commandMapService) .Add(remindService) .Add(repeaterService) - .Add(converterService) + //.Add(converterService) .Add(verboseErrorsService) .Add(patreonRewardsService) .Add(pruneService) diff --git a/src/NadekoBot/Services/DbService.cs b/src/NadekoBot/Services/DbService.cs index c8fab899..6e9532ca 100644 --- a/src/NadekoBot/Services/DbService.cs +++ b/src/NadekoBot/Services/DbService.cs @@ -36,6 +36,17 @@ namespace NadekoBot.Services context.Database.Migrate(); context.EnsureSeedData(); + //set important sqlite stuffs + var conn = context.Database.GetDbConnection(); + conn.Open(); + + context.Database.ExecuteSqlCommand("PRAGMA journal_mode=WAL"); + using (var com = conn.CreateCommand()) + { + com.CommandText = "PRAGMA journal_mode=WAL; PRAGMA synchronous=OFF"; + com.ExecuteNonQuery(); + } + return context; } diff --git a/src/NadekoBot/Services/Impl/BotCredentials.cs b/src/NadekoBot/Services/Impl/BotCredentials.cs index 3517a298..b54bed23 100644 --- a/src/NadekoBot/Services/Impl/BotCredentials.cs +++ b/src/NadekoBot/Services/Impl/BotCredentials.cs @@ -89,7 +89,7 @@ namespace NadekoBot.Services.Impl ? "sqlite" : dbSection["Type"], string.IsNullOrWhiteSpace(dbSection["ConnectionString"]) - ? "Filename=./data/NadekoBot.db" + ? "Filename=./data/NadekoBot.db" : dbSection["ConnectionString"]); } catch (Exception ex) From 741538a982a635e799ebc2b8475e16589ebcdcb0 Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Thu, 22 Jun 2017 23:59:54 +0200 Subject: [PATCH 022/346] Slightly faster startup and database access. Shard 0 will now report total guild count --- .../Utility/Commands/UnitConversion.cs | 2 +- src/NadekoBot/Modules/Utility/Utility.cs | 14 +- src/NadekoBot/NadekoBot.cs | 296 +++++++++--------- .../Services/Administration/SelfService.cs | 8 +- .../ClashOfClans/ClashOfClansService.cs | 32 +- .../CustomReactions/CustomReactionsService.cs | 14 +- .../Repositories/IClashOfClansRepository.cs | 2 +- .../Impl/ClashOfClansRepository.cs | 6 +- src/NadekoBot/Services/IStatsService.cs | 1 + src/NadekoBot/Services/Impl/StatsService.cs | 58 ++-- .../Permissions/PermissionsService.cs | 41 +-- .../Searches/StreamNotificationService.cs | 4 +- .../Services/Utility/RemindService.cs | 11 +- src/NadekoBot/ShardsCoordinator.cs | 4 + 14 files changed, 252 insertions(+), 241 deletions(-) diff --git a/src/NadekoBot/Modules/Utility/Commands/UnitConversion.cs b/src/NadekoBot/Modules/Utility/Commands/UnitConversion.cs index 603093c4..f325c402 100644 --- a/src/NadekoBot/Modules/Utility/Commands/UnitConversion.cs +++ b/src/NadekoBot/Modules/Utility/Commands/UnitConversion.cs @@ -6,7 +6,7 @@ //using System; //using System.Linq; //using System.Threading.Tasks; - +////todo Rewrite //namespace NadekoBot.Modules.Utility //{ // public partial class Utility diff --git a/src/NadekoBot/Modules/Utility/Utility.cs b/src/NadekoBot/Modules/Utility/Utility.cs index 51f24efc..3ae25163 100644 --- a/src/NadekoBot/Modules/Utility/Utility.cs +++ b/src/NadekoBot/Modules/Utility/Utility.cs @@ -319,11 +319,7 @@ namespace NadekoBot.Modules.Utility [NadekoCommand, Usage, Description, Aliases] public async Task Stats() - { - //var shardId = Context.Guild != null - // ? _client.GetShardIdFor(Context.Guild) - // : 0; - + { await Context.Channel.EmbedAsync( new EmbedBuilder().WithOkColor() .WithAuthor(eab => eab.WithName($"NadekoBot v{StatsService.BotVersion}") @@ -339,13 +335,7 @@ namespace NadekoBot.Modules.Utility .AddField(efb => efb.WithName(GetText("uptime")).WithValue(_stats.GetUptimeString("\n")).WithIsInline(true)) .AddField(efb => efb.WithName(GetText("presence")).WithValue( GetText("presence_txt", - _client.Guilds.Count, _stats.TextChannels, _stats.VoiceChannels)).WithIsInline(true)) -#if !GLOBAL_NADEKO - //.WithFooter(efb => efb.WithText(GetText("stats_songs", - // _music.MusicPlayers.Count(mp => mp.Value.CurrentSong != null), - // _music.MusicPlayers.Sum(mp => mp.Value.Playlist.Count)))) -#endif - ); + _stats.GuildCount, _stats.TextChannels, _stats.VoiceChannels)).WithIsInline(true))); } [NadekoCommand, Usage, Description, Aliases] diff --git a/src/NadekoBot/NadekoBot.cs b/src/NadekoBot/NadekoBot.cs index 8f6e926b..9025a0a3 100644 --- a/src/NadekoBot/NadekoBot.cs +++ b/src/NadekoBot/NadekoBot.cs @@ -139,169 +139,179 @@ namespace NadekoBot private void AddServices() { var startingGuildIdList = Client.Guilds.Select(x => (long)x.Id).ToList(); + + //this unit of work will be used for initialization of all modules too, to prevent multiple queries from running using (var uow = Db.UnitOfWork) { AllGuildConfigs = uow.GuildConfigs.GetAllGuildConfigs(startingGuildIdList).ToImmutableArray(); + + Localization = new Localization(BotConfig.Locale, AllGuildConfigs.ToDictionary(x => x.GuildId, x => x.Locale), Db); + Strings = new NadekoStrings(Localization); + CommandHandler = new CommandHandler(Client, Db, BotConfig, AllGuildConfigs, CommandService, Credentials, this); + Stats = new StatsService(Client, CommandHandler, Credentials, ShardCoord); + + var soundcloudApiService = new SoundCloudApiService(Credentials); + + #region help + var helpService = new HelpService(BotConfig, CommandHandler, Strings); + #endregion + + //module services + //todo 90 - autodiscover, DI, and add instead of manual like this + #region utility + var remindService = new RemindService(Client, BotConfig, Db, startingGuildIdList, uow); + var repeaterService = new MessageRepeaterService(this, Client, AllGuildConfigs); + //var converterService = new ConverterService(Db); + var commandMapService = new CommandMapService(AllGuildConfigs); + var patreonRewardsService = new PatreonRewardsService(Credentials, Db, Currency); + var verboseErrorsService = new VerboseErrorsService(AllGuildConfigs, Db, CommandHandler, helpService); + var pruneService = new PruneService(); + #endregion + + #region permissions + var permissionsService = new PermissionService(Client, Db, BotConfig, CommandHandler); + var blacklistService = new BlacklistService(BotConfig); + var cmdcdsService = new CmdCdService(AllGuildConfigs); + var filterService = new FilterService(Client, AllGuildConfigs); + var globalPermsService = new GlobalPermissionService(BotConfig); + #endregion + + #region Searches + var searchesService = new SearchesService(Client, GoogleApi, Db); + var streamNotificationService = new StreamNotificationService(Db, Client, Strings); + var animeSearchService = new AnimeSearchService(); + #endregion + + var clashService = new ClashOfClansService(Client, Db, Localization, Strings, uow, startingGuildIdList); + var musicService = new MusicService(GoogleApi, Strings, Localization, Db, soundcloudApiService, Credentials, AllGuildConfigs); + var crService = new CustomReactionsService(permissionsService, Db, Client, CommandHandler, BotConfig, uow); + + #region Games + var gamesService = new GamesService(Client, BotConfig, AllGuildConfigs, Strings, Images, CommandHandler); + var chatterBotService = new ChatterBotService(Client, permissionsService, AllGuildConfigs, CommandHandler); + var pollService = new PollService(Client, Strings); + #endregion + + #region administration + var administrationService = new AdministrationService(AllGuildConfigs, CommandHandler); + var greetSettingsService = new GreetSettingsService(Client, AllGuildConfigs, Db); + var selfService = new SelfService(Client, this, CommandHandler, Db, BotConfig, Localization, Strings, Credentials); + var vcRoleService = new VcRoleService(Client, AllGuildConfigs, Db); + var vPlusTService = new VplusTService(Client, AllGuildConfigs, Strings, Db); + var muteService = new MuteService(Client, AllGuildConfigs, Db); + var ratelimitService = new SlowmodeService(Client, AllGuildConfigs); + var protectionService = new ProtectionService(Client, AllGuildConfigs, muteService); + var playingRotateService = new PlayingRotateService(Client, BotConfig, musicService); + var gameVcService = new GameVoiceChannelService(Client, Db, AllGuildConfigs); + var autoAssignRoleService = new AutoAssignRoleService(Client, AllGuildConfigs); + var logCommandService = new LogCommandService(Client, Strings, AllGuildConfigs, Db, muteService, protectionService); + var guildTimezoneService = new GuildTimezoneService(AllGuildConfigs, Db); + #endregion + + #region pokemon + var pokemonService = new PokemonService(); + #endregion + + + + //initialize Services + Services = new NServiceProvider.ServiceProviderBuilder() + .Add(Localization) + .Add(Stats) + .Add(Images) + .Add(GoogleApi) + .Add(Stats) + .Add(Credentials) + .Add(CommandService) + .Add(Strings) + .Add(Client) + .Add(BotConfig) + .Add(Currency) + .Add(CommandHandler) + .Add(Db) + //modules + .Add(commandMapService) + .Add(remindService) + .Add(repeaterService) + //.Add(converterService) + .Add(verboseErrorsService) + .Add(patreonRewardsService) + .Add(pruneService) + .Add(searchesService) + .Add(streamNotificationService) + .Add(animeSearchService) + .Add(clashService) + .Add(musicService) + .Add(greetSettingsService) + .Add(crService) + .Add(helpService) + .Add(gamesService) + .Add(chatterBotService) + .Add(pollService) + .Add(administrationService) + .Add(selfService) + .Add(vcRoleService) + .Add(vPlusTService) + .Add(muteService) + .Add(ratelimitService) + .Add(playingRotateService) + .Add(gameVcService) + .Add(autoAssignRoleService) + .Add(protectionService) + .Add(logCommandService) + .Add(guildTimezoneService) + .Add(permissionsService) + .Add(blacklistService) + .Add(cmdcdsService) + .Add(filterService) + .Add(globalPermsService) + .Add(pokemonService) + .Add(this) + .Build(); + + + CommandHandler.AddServices(Services); + + //setup typereaders + CommandService.AddTypeReader(new PermissionActionTypeReader()); + CommandService.AddTypeReader(new CommandTypeReader(CommandService, CommandHandler)); + CommandService.AddTypeReader(new CommandOrCrTypeReader(crService, CommandService, CommandHandler)); + CommandService.AddTypeReader(new ModuleTypeReader(CommandService)); + CommandService.AddTypeReader(new ModuleOrCrTypeReader(CommandService)); + CommandService.AddTypeReader(new GuildTypeReader(Client)); + CommandService.AddTypeReader(new GuildDateTimeTypeReader(guildTimezoneService)); + } - Localization = new Localization(BotConfig.Locale, AllGuildConfigs.ToDictionary(x => x.GuildId, x => x.Locale), Db); - Strings = new NadekoStrings(Localization); - CommandHandler = new CommandHandler(Client, Db, BotConfig, AllGuildConfigs, CommandService, Credentials, this); - Stats = new StatsService(Client, CommandHandler, Credentials); - - var soundcloudApiService = new SoundCloudApiService(Credentials); - - #region help - var helpService = new HelpService(BotConfig, CommandHandler, Strings); - #endregion - - //module services - //todo 90 - autodiscover, DI, and add instead of manual like this - #region utility - var remindService = new RemindService(Client, BotConfig, Db, startingGuildIdList); - var repeaterService = new MessageRepeaterService(this, Client, AllGuildConfigs); - //var converterService = new ConverterService(Db); - var commandMapService = new CommandMapService(AllGuildConfigs); - var patreonRewardsService = new PatreonRewardsService(Credentials, Db, Currency); - var verboseErrorsService = new VerboseErrorsService(AllGuildConfigs, Db, CommandHandler, helpService); - var pruneService = new PruneService(); - #endregion - - #region permissions - var permissionsService = new PermissionService(Client, Db, BotConfig, CommandHandler); - var blacklistService = new BlacklistService(BotConfig); - var cmdcdsService = new CmdCdService(AllGuildConfigs); - var filterService = new FilterService(Client, AllGuildConfigs); - var globalPermsService = new GlobalPermissionService(BotConfig); - #endregion - - #region Searches - var searchesService = new SearchesService(Client, GoogleApi, Db); - var streamNotificationService = new StreamNotificationService(Db, Client, Strings); - var animeSearchService = new AnimeSearchService(); - #endregion - - var clashService = new ClashOfClansService(Client, Db, Localization, Strings); - var musicService = new MusicService(GoogleApi, Strings, Localization, Db, soundcloudApiService, Credentials, AllGuildConfigs); - var crService = new CustomReactionsService(permissionsService, Db, Client, CommandHandler, BotConfig); - - #region Games - var gamesService = new GamesService(Client, BotConfig, AllGuildConfigs, Strings, Images, CommandHandler); - var chatterBotService = new ChatterBotService(Client, permissionsService, AllGuildConfigs, CommandHandler); - var pollService = new PollService(Client, Strings); - #endregion - - #region administration - var administrationService = new AdministrationService(AllGuildConfigs, CommandHandler); - var greetSettingsService = new GreetSettingsService(Client, AllGuildConfigs, Db); - var selfService = new SelfService(Client, this, CommandHandler, Db, BotConfig, Localization, Strings, Credentials); - var vcRoleService = new VcRoleService(Client, AllGuildConfigs, Db); - var vPlusTService = new VplusTService(Client, AllGuildConfigs, Strings, Db); - var muteService = new MuteService(Client, AllGuildConfigs, Db); - var ratelimitService = new SlowmodeService(Client, AllGuildConfigs); - var protectionService = new ProtectionService(Client, AllGuildConfigs, muteService); - var playingRotateService = new PlayingRotateService(Client, BotConfig, musicService); - var gameVcService = new GameVoiceChannelService(Client, Db, AllGuildConfigs); - var autoAssignRoleService = new AutoAssignRoleService(Client, AllGuildConfigs); - var logCommandService = new LogCommandService(Client, Strings, AllGuildConfigs, Db, muteService, protectionService); - var guildTimezoneService = new GuildTimezoneService(AllGuildConfigs, Db); - #endregion - - #region pokemon - var pokemonService = new PokemonService(); - #endregion - - - //initialize Services - Services = new NServiceProvider.ServiceProviderBuilder() - .Add(Localization) - .Add(Stats) - .Add(Images) - .Add(GoogleApi) - .Add(Stats) - .Add(Credentials) - .Add(CommandService) - .Add(Strings) - .Add(Client) - .Add(BotConfig) - .Add(Currency) - .Add(CommandHandler) - .Add(Db) - //modules - .Add(commandMapService) - .Add(remindService) - .Add(repeaterService) - //.Add(converterService) - .Add(verboseErrorsService) - .Add(patreonRewardsService) - .Add(pruneService) - .Add(searchesService) - .Add(streamNotificationService) - .Add(animeSearchService) - .Add(clashService) - .Add(musicService) - .Add(greetSettingsService) - .Add(crService) - .Add(helpService) - .Add(gamesService) - .Add(chatterBotService) - .Add(pollService) - .Add(administrationService) - .Add(selfService) - .Add(vcRoleService) - .Add(vPlusTService) - .Add(muteService) - .Add(ratelimitService) - .Add(playingRotateService) - .Add(gameVcService) - .Add(autoAssignRoleService) - .Add(protectionService) - .Add(logCommandService) - .Add(guildTimezoneService) - .Add(permissionsService) - .Add(blacklistService) - .Add(cmdcdsService) - .Add(filterService) - .Add(globalPermsService) - .Add(pokemonService) - .Add(this) - .Build(); - - CommandHandler.AddServices(Services); - - //setup typereaders - CommandService.AddTypeReader(new PermissionActionTypeReader()); - CommandService.AddTypeReader(new CommandTypeReader(CommandService, CommandHandler)); - CommandService.AddTypeReader(new CommandOrCrTypeReader(crService, CommandService, CommandHandler)); - CommandService.AddTypeReader(new ModuleTypeReader(CommandService)); - CommandService.AddTypeReader(new ModuleOrCrTypeReader(CommandService)); - CommandService.AddTypeReader(new GuildTypeReader(Client)); - CommandService.AddTypeReader(new GuildDateTimeTypeReader(guildTimezoneService)); } - private Task LoginAsync(string token) + private async Task LoginAsync(string token) { + var clientReady = new TaskCompletionSource(); + + Task SetClientReady() + { + clientReady.TrySetResult(true); + return Task.CompletedTask; + } + //connect try { sem.WaitOne(); } catch (AbandonedMutexException) { } + _log.Info("Shard {0} logging in ...", ShardId); + try { Client.LoginAsync(TokenType.Bot, token).GetAwaiter().GetResult(); Client.StartAsync().GetAwaiter().GetResult(); - while (Client.ConnectionState != ConnectionState.Connected) - Task.Delay(100).GetAwaiter().GetResult(); + Client.Ready += SetClientReady; + await clientReady.Task.ConfigureAwait(false); + Client.Ready -= SetClientReady; } finally { _log.Info("Shard {0} logged in.", ShardId); sem.Release(); } - return Task.CompletedTask; - //_log.Info("Waiting for all shards to connect..."); - //while (!Client.Shards.All(x => x.ConnectionState == ConnectionState.Connected)) - //{ - // _log.Info("Connecting... {0}/{1}", Client.Shards.Count(x => x.ConnectionState == ConnectionState.Connected), Client.Shards.Count); - // await Task.Delay(1000).ConfigureAwait(false); - //} } public async Task RunAsync(params string[] args) diff --git a/src/NadekoBot/Services/Administration/SelfService.cs b/src/NadekoBot/Services/Administration/SelfService.cs index 0a288a0b..336f9970 100644 --- a/src/NadekoBot/Services/Administration/SelfService.cs +++ b/src/NadekoBot/Services/Administration/SelfService.cs @@ -39,12 +39,8 @@ namespace NadekoBot.Services.Administration _client = client; _creds = creds; - using (var uow = _db.UnitOfWork) - { - var config = uow.BotConfig.GetOrCreate(); - ForwardDMs = config.ForwardMessages; - ForwardDMsToAllOwners = config.ForwardToAllOwners; - } + ForwardDMs = bc.ForwardMessages; + ForwardDMsToAllOwners = bc.ForwardToAllOwners; var _ = Task.Run(async () => { diff --git a/src/NadekoBot/Services/ClashOfClans/ClashOfClansService.cs b/src/NadekoBot/Services/ClashOfClans/ClashOfClansService.cs index 042afa1b..dcdd6376 100644 --- a/src/NadekoBot/Services/ClashOfClans/ClashOfClansService.cs +++ b/src/NadekoBot/Services/ClashOfClans/ClashOfClansService.cs @@ -1,6 +1,7 @@ using Discord; using Discord.WebSocket; using NadekoBot.Extensions; +using NadekoBot.Services.Database; using NadekoBot.Services.Database.Models; using System; using System.Collections.Concurrent; @@ -25,28 +26,27 @@ namespace NadekoBot.Services.ClashOfClans public ConcurrentDictionary> ClashWars { get; set; } - public ClashOfClansService(DiscordSocketClient client, DbService db, ILocalization localization, NadekoStrings strings) + public ClashOfClansService(DiscordSocketClient client, DbService db, + ILocalization localization, NadekoStrings strings, IUnitOfWork uow, + List guilds) { _client = client; _db = db; _localization = localization; _strings = strings; - using (var uow = _db.UnitOfWork) - { - ClashWars = new ConcurrentDictionary>( - uow.ClashOfClans - .GetAllWars() - .Select(cw => - { - cw.Channel = _client.GetGuild(cw.GuildId)? - .GetTextChannel(cw.ChannelId); - return cw; - }) - .Where(cw => cw.Channel != null) - .GroupBy(cw => cw.GuildId) - .ToDictionary(g => g.Key, g => g.ToList())); - } + ClashWars = new ConcurrentDictionary>( + uow.ClashOfClans + .GetAllWars(guilds) + .Select(cw => + { + cw.Channel = _client.GetGuild(cw.GuildId)? + .GetTextChannel(cw.ChannelId); + return cw; + }) + .Where(cw => cw.Channel != null) + .GroupBy(cw => cw.GuildId) + .ToDictionary(g => g.Key, g => g.ToList())); checkWarTimer = new Timer(async _ => { diff --git a/src/NadekoBot/Services/CustomReactions/CustomReactionsService.cs b/src/NadekoBot/Services/CustomReactions/CustomReactionsService.cs index bb7fb8d9..ecf0dede 100644 --- a/src/NadekoBot/Services/CustomReactions/CustomReactionsService.cs +++ b/src/NadekoBot/Services/CustomReactions/CustomReactionsService.cs @@ -10,6 +10,7 @@ using System; using System.Threading.Tasks; using NadekoBot.Services.Permissions; using NadekoBot.Extensions; +using NadekoBot.Services.Database; namespace NadekoBot.Services.CustomReactions { @@ -28,7 +29,7 @@ namespace NadekoBot.Services.CustomReactions private readonly BotConfig _bc; public CustomReactionsService(PermissionService perms, DbService db, - DiscordSocketClient client, CommandHandler cmd, BotConfig bc) + DiscordSocketClient client, CommandHandler cmd, BotConfig bc, IUnitOfWork uow) { _log = LogManager.GetCurrentClassLogger(); _db = db; @@ -36,13 +37,10 @@ namespace NadekoBot.Services.CustomReactions _perms = perms; _cmd = cmd; _bc = bc; - - using (var uow = _db.UnitOfWork) - { - var items = uow.CustomReactions.GetAll(); - GuildReactions = new ConcurrentDictionary(items.Where(g => g.GuildId != null && g.GuildId != 0).GroupBy(k => k.GuildId.Value).ToDictionary(g => g.Key, g => g.ToArray())); - GlobalReactions = items.Where(g => g.GuildId == null || g.GuildId == 0).ToArray(); - } + + var items = uow.CustomReactions.GetAll(); + GuildReactions = new ConcurrentDictionary(items.Where(g => g.GuildId != null && g.GuildId != 0).GroupBy(k => k.GuildId.Value).ToDictionary(g => g.Key, g => g.ToArray())); + GlobalReactions = items.Where(g => g.GuildId == null || g.GuildId == 0).ToArray(); } public void ClearStats() => ReactionStats.Clear(); diff --git a/src/NadekoBot/Services/Database/Repositories/IClashOfClansRepository.cs b/src/NadekoBot/Services/Database/Repositories/IClashOfClansRepository.cs index 756e9789..14edcea8 100644 --- a/src/NadekoBot/Services/Database/Repositories/IClashOfClansRepository.cs +++ b/src/NadekoBot/Services/Database/Repositories/IClashOfClansRepository.cs @@ -5,6 +5,6 @@ namespace NadekoBot.Services.Database.Repositories { public interface IClashOfClansRepository : IRepository { - IEnumerable GetAllWars(); + IEnumerable GetAllWars(List guilds); } } diff --git a/src/NadekoBot/Services/Database/Repositories/Impl/ClashOfClansRepository.cs b/src/NadekoBot/Services/Database/Repositories/Impl/ClashOfClansRepository.cs index 54a391fa..828c4bce 100644 --- a/src/NadekoBot/Services/Database/Repositories/Impl/ClashOfClansRepository.cs +++ b/src/NadekoBot/Services/Database/Repositories/Impl/ClashOfClansRepository.cs @@ -11,9 +11,11 @@ namespace NadekoBot.Services.Database.Repositories.Impl { } - public IEnumerable GetAllWars() + public IEnumerable GetAllWars(List guilds) { - var toReturn = _set.Include(cw => cw.Bases) + var toReturn = _set + .Where(cw => guilds.Contains((long)cw.GuildId)) + .Include(cw => cw.Bases) .ToList(); toReturn.ForEach(cw => cw.Bases = cw.Bases.Where(w => w.SequenceNumber != null).OrderBy(w => w.SequenceNumber).ToList()); return toReturn; diff --git a/src/NadekoBot/Services/IStatsService.cs b/src/NadekoBot/Services/IStatsService.cs index a7735fc8..d187c413 100644 --- a/src/NadekoBot/Services/IStatsService.cs +++ b/src/NadekoBot/Services/IStatsService.cs @@ -13,6 +13,7 @@ namespace NadekoBot.Services double MessagesPerSecond { get; } long TextChannels { get; } long VoiceChannels { get; } + int GuildCount { get; } TimeSpan GetUptime(); string GetUptimeString(string separator = ", "); diff --git a/src/NadekoBot/Services/Impl/StatsService.cs b/src/NadekoBot/Services/Impl/StatsService.cs index 5d93196d..8cce6936 100644 --- a/src/NadekoBot/Services/Impl/StatsService.cs +++ b/src/NadekoBot/Services/Impl/StatsService.cs @@ -35,11 +35,16 @@ namespace NadekoBot.Services.Impl public long CommandsRan => Interlocked.Read(ref _commandsRan); private readonly Timer _carbonitexTimer; + private readonly ShardsCoordinator _sc; - public StatsService(DiscordSocketClient client, CommandHandler cmdHandler, IBotCredentials creds) + public int GuildCount => + _sc?.GuildCount ?? _client.Guilds.Count(); + + public StatsService(DiscordSocketClient client, CommandHandler cmdHandler, IBotCredentials creds, ShardsCoordinator sc) { _client = client; _creds = creds; + _sc = sc; _started = DateTime.UtcNow; _client.MessageReceived += _ => Task.FromResult(Interlocked.Increment(ref _messageCounter)); @@ -122,31 +127,34 @@ namespace NadekoBot.Services.Impl }; //todo carbonitex update - //_carbonitexTimer = new Timer(async (state) => - //{ - // if (string.IsNullOrWhiteSpace(_creds.CarbonKey)) - // return; - // try - // { - // using (var http = new HttpClient()) - // { - // using (var content = new FormUrlEncodedContent( - // new Dictionary { - // { "servercount", _client.Guilds.Count.ToString() }, - // { "key", _creds.CarbonKey }})) - // { - // content.Headers.Clear(); - // content.Headers.Add("Content-Type", "application/x-www-form-urlencoded"); + if (sc != null) + { + _carbonitexTimer = new Timer(async (state) => + { + if (string.IsNullOrWhiteSpace(_creds.CarbonKey)) + return; + try + { + using (var http = new HttpClient()) + { + using (var content = new FormUrlEncodedContent( + new Dictionary { + { "servercount", sc.GuildCount.ToString() }, + { "key", _creds.CarbonKey }})) + { + content.Headers.Clear(); + content.Headers.Add("Content-Type", "application/x-www-form-urlencoded"); - // await http.PostAsync("https://www.carbonitex.net/discord/data/botdata.php", content).ConfigureAwait(false); - // } - // } - // } - // catch - // { - // // ignored - // } - //}, null, TimeSpan.FromHours(1), TimeSpan.FromHours(1)); + await http.PostAsync("https://www.carbonitex.net/discord/data/botdata.php", content).ConfigureAwait(false); + } + } + } + catch + { + // ignored + } + }, null, TimeSpan.FromHours(1), TimeSpan.FromHours(1)); + } } public void Initialize() diff --git a/src/NadekoBot/Services/Permissions/PermissionsService.cs b/src/NadekoBot/Services/Permissions/PermissionsService.cs index 7ca39a03..00794d2d 100644 --- a/src/NadekoBot/Services/Permissions/PermissionsService.cs +++ b/src/NadekoBot/Services/Permissions/PermissionsService.cs @@ -11,6 +11,7 @@ using System.Threading.Tasks; using Discord; using Discord.WebSocket; using NadekoBot.Extensions; +using NadekoBot.Services; namespace NadekoBot.Services.Permissions { @@ -31,24 +32,21 @@ namespace NadekoBot.Services.Permissions _cmd = cmd; var sw = Stopwatch.StartNew(); - TryMigratePermissions(bc); + if (client.ShardId == 0) + TryMigratePermissions(bc); - client.Ready += delegate + using (var uow = _db.UnitOfWork) { - using (var uow = _db.UnitOfWork) + foreach (var x in uow.GuildConfigs.Permissionsv2ForAll(client.Guilds.ToArray().Select(x => (long)x.Id).ToList())) { - foreach (var x in uow.GuildConfigs.Permissionsv2ForAll(client.Guilds.Select(x => (long)x.Id).ToList())) + Cache.TryAdd(x.GuildId, new PermissionCache() { - Cache.TryAdd(x.GuildId, new PermissionCache() - { - Verbose = x.VerbosePermissions, - PermRole = x.PermissionRole, - Permissions = new PermissionsCollection(x.Permissions) - }); - } + Verbose = x.VerbosePermissions, + PermRole = x.PermissionRole, + Permissions = new PermissionsCollection(x.Permissions) + }); } - return Task.CompletedTask; - }; + } sw.Stop(); _log.Debug($"Loaded in {sw.Elapsed.TotalSeconds:F2}s"); @@ -74,10 +72,9 @@ namespace NadekoBot.Services.Permissions private void TryMigratePermissions(BotConfig bc) { var log = LogManager.GetCurrentClassLogger(); - using (var uow = _db.UnitOfWork) + if (bc.PermissionVersion <= 1) { - var _bc = uow.BotConfig.GetOrCreate(); - if (_bc.PermissionVersion <= 1) + using (var uow = _db.UnitOfWork) { log.Info("Permission version is 1, upgrading to 2."); var oldCache = new ConcurrentDictionary(uow.GuildConfigs @@ -132,9 +129,13 @@ namespace NadekoBot.Services.Permissions log.Info("Permission migration to v2 is done."); } - _bc.PermissionVersion = 2; + bc.PermissionVersion = 2; + uow.Complete(); } - if (_bc.PermissionVersion <= 2) + } + if (bc.PermissionVersion <= 2) + { + using (var uow = _db.UnitOfWork) { var oldPrefixes = new[] { ".", ";", "!!", "!m", "!", "+", "-", "$", ">" }; uow._context.Database.ExecuteSqlCommand( @@ -150,9 +151,9 @@ WHERE secondaryTargetName LIKE '.%' OR secondaryTargetName LIKE '>%' OR secondaryTargetName LIKE '-%' OR secondaryTargetName LIKE '!%';"); - _bc.PermissionVersion = 3; + bc.PermissionVersion = 3; + uow.Complete(); } - uow.Complete(); } } diff --git a/src/NadekoBot/Services/Searches/StreamNotificationService.cs b/src/NadekoBot/Services/Searches/StreamNotificationService.cs index a5846362..c077e4e1 100644 --- a/src/NadekoBot/Services/Searches/StreamNotificationService.cs +++ b/src/NadekoBot/Services/Searches/StreamNotificationService.cs @@ -1,6 +1,8 @@ using Discord; using Discord.WebSocket; using NadekoBot.Extensions; +using NadekoBot.Services; +using NadekoBot.Services.Database; using NadekoBot.Services.Database.Models; using Newtonsoft.Json; using System; @@ -73,7 +75,7 @@ namespace NadekoBot.Services.Searches })); firstStreamNotifPass = false; - }, null, TimeSpan.Zero, TimeSpan.FromSeconds(60)); + }, null, TimeSpan.FromSeconds(60), TimeSpan.FromSeconds(60)); } public async Task GetStreamStatus(FollowedStream stream, bool checkCache = true) diff --git a/src/NadekoBot/Services/Utility/RemindService.cs b/src/NadekoBot/Services/Utility/RemindService.cs index 16d6d451..8629712d 100644 --- a/src/NadekoBot/Services/Utility/RemindService.cs +++ b/src/NadekoBot/Services/Utility/RemindService.cs @@ -1,6 +1,7 @@ using Discord; using Discord.WebSocket; using NadekoBot.Extensions; +using NadekoBot.Services.Database; using NadekoBot.Services.Database.Models; using NLog; using System; @@ -33,7 +34,8 @@ namespace NadekoBot.Services.Utility private readonly DiscordSocketClient _client; private readonly DbService _db; - public RemindService(DiscordSocketClient client, BotConfig config, DbService db, List guilds) + public RemindService(DiscordSocketClient client, BotConfig config, DbService db, + List guilds, IUnitOfWork uow) { _config = config; _client = client; @@ -42,11 +44,8 @@ namespace NadekoBot.Services.Utility cancelSource = new CancellationTokenSource(); cancelAllToken = cancelSource.Token; - List reminders; - using (var uow = _db.UnitOfWork) - { - reminders = uow.Reminders.GetIncludedReminders(guilds).ToList(); - } + + var reminders = uow.Reminders.GetIncludedReminders(guilds).ToList(); RemindMessageFormat = _config.RemindMessageFormat; foreach (var r in reminders) diff --git a/src/NadekoBot/ShardsCoordinator.cs b/src/NadekoBot/ShardsCoordinator.cs index 4ed8247f..3fe6da58 100644 --- a/src/NadekoBot/ShardsCoordinator.cs +++ b/src/NadekoBot/ShardsCoordinator.cs @@ -14,6 +14,10 @@ namespace NadekoBot private readonly BotCredentials Credentials; private Process[] ShardProcesses; public ShardComMessage[] Statuses { get; } + public int GuildCount => Statuses.ToArray() + .Where(x => x != null) + .Sum(x => x.Guilds); + private readonly Logger _log; private readonly ShardComServer _comServer; From a8f2ca60c2a349780800cbec87adc52d239ef6df Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Sat, 24 Jun 2017 01:41:24 +0200 Subject: [PATCH 023/346] Reenabled converter commands. Improved rewards reload on bots with multiple shards. --- .gitignore | 1 + .../Utility/Commands/UnitConversion.cs | 181 +++++++++--------- src/NadekoBot/NadekoBot.cs | 5 +- src/NadekoBot/NadekoBot.csproj | 1 + src/NadekoBot/Services/Impl/StatsService.cs | 1 - .../Services/Utility/ConverterService.cs | 88 +++++---- .../Services/Utility/PatreonRewardsService.cs | 97 +++++----- 7 files changed, 200 insertions(+), 174 deletions(-) diff --git a/.gitignore b/.gitignore index 0fa1ef5e..5169035d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ #Manually added files +patreon_rewards.json command_errors*.txt src/NadekoBot/Command Errors*.txt diff --git a/src/NadekoBot/Modules/Utility/Commands/UnitConversion.cs b/src/NadekoBot/Modules/Utility/Commands/UnitConversion.cs index f325c402..1e7f5d22 100644 --- a/src/NadekoBot/Modules/Utility/Commands/UnitConversion.cs +++ b/src/NadekoBot/Modules/Utility/Commands/UnitConversion.cs @@ -1,94 +1,93 @@ -//using Discord; -//using Discord.Commands; -//using NadekoBot.Attributes; -//using NadekoBot.Extensions; -//using NadekoBot.Services.Utility; -//using System; -//using System.Linq; -//using System.Threading.Tasks; -////todo Rewrite -//namespace NadekoBot.Modules.Utility -//{ -// public partial class Utility -// { -// [Group] -// public class UnitConverterCommands : NadekoSubmodule -// { -// private readonly ConverterService _service; +using Discord; +using Discord.Commands; +using NadekoBot.Attributes; +using NadekoBot.Extensions; +using NadekoBot.Services.Utility; +using System; +using System.Linq; +using System.Threading.Tasks; +namespace NadekoBot.Modules.Utility +{ + public partial class Utility + { + [Group] + public class UnitConverterCommands : NadekoSubmodule + { + private readonly ConverterService _service; -// public UnitConverterCommands(ConverterService service) -// { -// _service = service; -// } + public UnitConverterCommands(ConverterService service) + { + _service = service; + } -// [NadekoCommand, Usage, Description, Aliases] -// public async Task ConvertList() -// { -// var res = _service.Units.GroupBy(x => x.UnitType) -// .Aggregate(new EmbedBuilder().WithTitle(GetText("convertlist")) -// .WithColor(NadekoBot.OkColor), -// (embed, g) => embed.AddField(efb => -// efb.WithName(g.Key.ToTitleCase()) -// .WithValue(String.Join(", ", g.Select(x => x.Triggers.FirstOrDefault()) -// .OrderBy(x => x))))); -// await Context.Channel.EmbedAsync(res); -// } -// [NadekoCommand, Usage, Description, Aliases] -// public async Task Convert(string origin, string target, decimal value) -// { -// var originUnit = _service.Units.Find(x => x.Triggers.Select(y => y.ToLowerInvariant()).Contains(origin.ToLowerInvariant())); -// var targetUnit = _service.Units.Find(x => x.Triggers.Select(y => y.ToLowerInvariant()).Contains(target.ToLowerInvariant())); -// if (originUnit == null || targetUnit == null) -// { -// await ReplyErrorLocalized("convert_not_found", Format.Bold(origin), Format.Bold(target)).ConfigureAwait(false); -// return; -// } -// if (originUnit.UnitType != targetUnit.UnitType) -// { -// await ReplyErrorLocalized("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); + [NadekoCommand, Usage, Description, Aliases] + public async Task ConvertList() + { + var res = _service.Units.GroupBy(x => x.UnitType) + .Aggregate(new EmbedBuilder().WithTitle(GetText("convertlist")) + .WithColor(NadekoBot.OkColor), + (embed, g) => embed.AddField(efb => + efb.WithName(g.Key.ToTitleCase()) + .WithValue(String.Join(", ", g.Select(x => x.Triggers.FirstOrDefault()) + .OrderBy(x => x))))); + await Context.Channel.EmbedAsync(res); + } + [NadekoCommand, Usage, Description, Aliases] + public async Task Convert(string origin, string target, decimal value) + { + var originUnit = _service.Units.Find(x => x.Triggers.Select(y => y.ToLowerInvariant()).Contains(origin.ToLowerInvariant())); + var targetUnit = _service.Units.Find(x => x.Triggers.Select(y => y.ToLowerInvariant()).Contains(target.ToLowerInvariant())); + if (originUnit == null || targetUnit == null) + { + await ReplyErrorLocalized("convert_not_found", Format.Bold(origin), Format.Bold(target)).ConfigureAwait(false); + return; + } + if (originUnit.UnitType != targetUnit.UnitType) + { + await ReplyErrorLocalized("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 Context.Channel.SendConfirmAsync(GetText("convert", value, (originUnit.Triggers.First()).SnPl(value.IsInteger() ? (int)value : 2), res, (targetUnit.Triggers.First() + "s").SnPl(res.IsInteger() ? (int)res : 2))); -// } -// } -// } -//} \ No newline at end of file + await Context.Channel.SendConfirmAsync(GetText("convert", value, (originUnit.Triggers.First()).SnPl(value.IsInteger() ? (int)value : 2), res, (targetUnit.Triggers.First() + "s").SnPl(res.IsInteger() ? (int)res : 2))); + } + } + } +} \ No newline at end of file diff --git a/src/NadekoBot/NadekoBot.cs b/src/NadekoBot/NadekoBot.cs index 9025a0a3..2ee3d1b3 100644 --- a/src/NadekoBot/NadekoBot.cs +++ b/src/NadekoBot/NadekoBot.cs @@ -290,7 +290,10 @@ namespace NadekoBot Task SetClientReady() { - clientReady.TrySetResult(true); + var _ = Task.Run(() => + { + clientReady.TrySetResult(true); + }); return Task.CompletedTask; } diff --git a/src/NadekoBot/NadekoBot.csproj b/src/NadekoBot/NadekoBot.csproj index 0cc722bc..18ad4911 100644 --- a/src/NadekoBot/NadekoBot.csproj +++ b/src/NadekoBot/NadekoBot.csproj @@ -90,5 +90,6 @@ + diff --git a/src/NadekoBot/Services/Impl/StatsService.cs b/src/NadekoBot/Services/Impl/StatsService.cs index 8cce6936..64507106 100644 --- a/src/NadekoBot/Services/Impl/StatsService.cs +++ b/src/NadekoBot/Services/Impl/StatsService.cs @@ -126,7 +126,6 @@ namespace NadekoBot.Services.Impl return Task.CompletedTask; }; - //todo carbonitex update if (sc != null) { _carbonitexTimer = new Timer(async (state) => diff --git a/src/NadekoBot/Services/Utility/ConverterService.cs b/src/NadekoBot/Services/Utility/ConverterService.cs index b7388335..1edb470a 100644 --- a/src/NadekoBot/Services/Utility/ConverterService.cs +++ b/src/NadekoBot/Services/Utility/ConverterService.cs @@ -1,4 +1,6 @@ -using NadekoBot.Services.Database.Models; +using Discord.WebSocket; +using NadekoBot.Services; +using NadekoBot.Services.Database.Models; using Newtonsoft.Json; using NLog; using System; @@ -11,27 +13,29 @@ using System.Threading.Tasks; namespace NadekoBot.Services.Utility { - //todo periodically load from the database, update only on shard 0 public class ConverterService { - public List Units { get; set; } = new List(); + public List Units { get; } = new List(); private readonly Logger _log; - private Timer _timer; + private readonly Timer _currencyUpdater; + private readonly Timer _currencyLoader; private readonly TimeSpan _updateInterval = new TimeSpan(12, 0, 0); private readonly DbService _db; - public ConverterService(DbService db) + public ConverterService(DiscordSocketClient client, DbService db) { _log = LogManager.GetCurrentClassLogger(); _db = db; try { - var data = JsonConvert.DeserializeObject>(File.ReadAllText("data/units.json")).Select(u => new ConvertUnit() - { - Modifier = u.Modifier, - UnitType = u.UnitType, - InternalTrigger = string.Join("|", u.Triggers) - }).ToArray(); + var data = JsonConvert.DeserializeObject>( + File.ReadAllText("data/units.json")) + .Select(u => new ConvertUnit() + { + Modifier = u.Modifier, + UnitType = u.UnitType, + InternalTrigger = string.Join("|", u.Triggers) + }).ToArray(); using (var uow = _db.UnitOfWork) { @@ -48,10 +52,10 @@ namespace NadekoBot.Services.Utility _log.Warn("Could not load units: " + ex.Message); } - _timer = new Timer(async (obj) => await UpdateCurrency(), null, _updateInterval, _updateInterval); + _currencyUpdater = new Timer(async (shouldLoad) => await UpdateCurrency((bool)shouldLoad), client.ShardId == 0, _updateInterval, _updateInterval); } - public static async Task UpdateCurrencyRates() + private async Task GetCurrencyRates() { using (var http = new HttpClient()) { @@ -60,38 +64,48 @@ namespace NadekoBot.Services.Utility } } - public async Task UpdateCurrency() + private async Task UpdateCurrency(bool shouldLoad) { try { - var currencyRates = await UpdateCurrencyRates(); var unitTypeString = "currency"; - var range = currencyRates.ConversionRates.Select(u => new ConvertUnit() + if (shouldLoad) { - InternalTrigger = u.Key, - Modifier = u.Value, - UnitType = unitTypeString - }).ToArray(); - var baseType = new ConvertUnit() - { - Triggers = new[] { currencyRates.Base }, - Modifier = decimal.One, - UnitType = unitTypeString - }; - var toRemove = Units.Where(u => u.UnitType == unitTypeString); + var currencyRates = await GetCurrencyRates(); + var baseType = new ConvertUnit() + { + Triggers = new[] { currencyRates.Base }, + Modifier = decimal.One, + UnitType = unitTypeString + }; + var range = currencyRates.ConversionRates.Select(u => new ConvertUnit() + { + InternalTrigger = u.Key, + Modifier = u.Value, + UnitType = unitTypeString + }).ToArray(); + var toRemove = Units.Where(u => u.UnitType == unitTypeString); - using (var uow = _db.UnitOfWork) - { - uow.ConverterUnits.RemoveRange(toRemove.ToArray()); - uow.ConverterUnits.Add(baseType); - uow.ConverterUnits.AddRange(range); + using (var uow = _db.UnitOfWork) + { + uow.ConverterUnits.RemoveRange(toRemove.ToArray()); + uow.ConverterUnits.Add(baseType); + uow.ConverterUnits.AddRange(range); - await uow.CompleteAsync().ConfigureAwait(false); + await uow.CompleteAsync().ConfigureAwait(false); + } + Units.RemoveAll(u => u.UnitType == unitTypeString); + Units.Add(baseType); + Units.AddRange(range); + } + else + { + using (var uow = _db.UnitOfWork) + { + Units.RemoveAll(u => u.UnitType == unitTypeString); + Units.AddRange(uow.ConverterUnits.GetAll().ToArray()); + } } - Units.RemoveAll(u => u.UnitType == unitTypeString); - Units.Add(baseType); - Units.AddRange(range); - _log.Info("Updated Currency"); } catch { diff --git a/src/NadekoBot/Services/Utility/PatreonRewardsService.cs b/src/NadekoBot/Services/Utility/PatreonRewardsService.cs index 0f7105b7..f3811c6d 100644 --- a/src/NadekoBot/Services/Utility/PatreonRewardsService.cs +++ b/src/NadekoBot/Services/Utility/PatreonRewardsService.cs @@ -1,12 +1,15 @@ -using NadekoBot.Services.Database.Models; +using Discord.WebSocket; +using NadekoBot.Services.Database.Models; using NadekoBot.Services.Utility.Patreon; using Newtonsoft.Json; using NLog; using System; using System.Collections.Generic; using System.Collections.Immutable; +using System.IO; using System.Linq; using System.Net.Http; +using System.Text; using System.Threading; using System.Threading.Tasks; @@ -24,12 +27,13 @@ namespace NadekoBot.Services.Utility private readonly SemaphoreSlim claimLockJustInCase = new SemaphoreSlim(1, 1); private readonly Logger _log; - public readonly TimeSpan Interval = TimeSpan.FromMinutes(15); - private IBotCredentials _creds; + public readonly TimeSpan Interval = TimeSpan.FromMinutes(3); + private readonly IBotCredentials _creds; private readonly DbService _db; private readonly CurrencyService _currency; - public PatreonRewardsService(IBotCredentials creds, DbService db, CurrencyService currency) + public PatreonRewardsService(IBotCredentials creds, DbService db, CurrencyService currency, + DiscordSocketClient client) { _creds = creds; _db = db; @@ -37,58 +41,63 @@ namespace NadekoBot.Services.Utility if (string.IsNullOrWhiteSpace(creds.PatreonAccessToken)) return; _log = LogManager.GetCurrentClassLogger(); - Updater = new Timer(async (_) => await LoadPledges(), null, TimeSpan.Zero, Interval); + Updater = new Timer(async (load) => await RefreshPledges((bool)load), client.ShardId == 0, TimeSpan.Zero, Interval); } - public async Task LoadPledges() + public async Task RefreshPledges(bool shouldLoad) { - LastUpdate = DateTime.UtcNow; - await getPledgesLocker.WaitAsync(1000).ConfigureAwait(false); - try + if (shouldLoad) { - var rewards = new List(); - var users = new List(); - using (var http = new HttpClient()) + LastUpdate = DateTime.UtcNow; + await getPledgesLocker.WaitAsync().ConfigureAwait(false); + try { - http.DefaultRequestHeaders.Clear(); - http.DefaultRequestHeaders.Add("Authorization", "Bearer " + _creds.PatreonAccessToken); - var data = new PatreonData() + var rewards = new List(); + var users = new List(); + using (var http = new HttpClient()) { - Links = new PatreonDataLinks() + http.DefaultRequestHeaders.Clear(); + http.DefaultRequestHeaders.Add("Authorization", "Bearer " + _creds.PatreonAccessToken); + var data = new PatreonData() { - next = "https://api.patreon.com/oauth2/api/campaigns/334038/pledges" - } - }; - do + Links = new PatreonDataLinks() + { + next = "https://api.patreon.com/oauth2/api/campaigns/334038/pledges" + } + }; + do + { + var res = await http.GetStringAsync(data.Links.next) + .ConfigureAwait(false); + data = JsonConvert.DeserializeObject(res); + var pledgers = data.Data.Where(x => x["type"].ToString() == "pledge"); + rewards.AddRange(pledgers.Select(x => JsonConvert.DeserializeObject(x.ToString())) + .Where(x => x.attributes.declined_since == null)); + users.AddRange(data.Included + .Where(x => x["type"].ToString() == "user") + .Select(x => JsonConvert.DeserializeObject(x.ToString()))); + } while (!string.IsNullOrWhiteSpace(data.Links.next)); + } + Pledges = rewards.Join(users, (r) => r.relationships?.patron?.data?.id, (u) => u.id, (x, y) => new PatreonUserAndReward() { - var res = await http.GetStringAsync(data.Links.next) - .ConfigureAwait(false); - data = JsonConvert.DeserializeObject(res); - var pledgers = data.Data.Where(x => x["type"].ToString() == "pledge"); - rewards.AddRange(pledgers.Select(x => JsonConvert.DeserializeObject(x.ToString())) - .Where(x => x.attributes.declined_since == null)); - users.AddRange(data.Included - .Where(x => x["type"].ToString() == "user") - .Select(x => JsonConvert.DeserializeObject(x.ToString()))); - } while (!string.IsNullOrWhiteSpace(data.Links.next)); + User = y, + Reward = x, + }).ToImmutableArray(); + File.WriteAllText("./patreon_rewards.json", JsonConvert.SerializeObject(Pledges)); } - Pledges = rewards.Join(users, (r) => r.relationships?.patron?.data?.id, (u) => u.id, (x, y) => new PatreonUserAndReward() + catch (Exception ex) { - User = y, - Reward = x, - }).ToImmutableArray(); - } - catch (Exception ex) - { - _log.Warn(ex); - } - finally - { - var _ = Task.Run(async () => + _log.Warn(ex); + } + finally { - await Task.Delay(TimeSpan.FromMinutes(5)).ConfigureAwait(false); getPledgesLocker.Release(); - }); + } + } + else + { + Pledges = JsonConvert.DeserializeObject(File.ReadAllText("./patreon_rewards.json")) + .ToImmutableArray(); } } From 7ad5c5e02bf5cebf16141247f31e5eacdce0fd75 Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Sat, 24 Jun 2017 05:24:43 +0200 Subject: [PATCH 024/346] Patreon rewards fix finished --- .../Modules/Utility/Commands/PatreonCommands.cs | 5 ++++- src/NadekoBot/NadekoBot.cs | 2 +- src/NadekoBot/Services/Utility/PatreonRewardsService.cs | 9 ++++++++- 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/src/NadekoBot/Modules/Utility/Commands/PatreonCommands.cs b/src/NadekoBot/Modules/Utility/Commands/PatreonCommands.cs index 509419fd..293e2c42 100644 --- a/src/NadekoBot/Modules/Utility/Commands/PatreonCommands.cs +++ b/src/NadekoBot/Modules/Utility/Commands/PatreonCommands.cs @@ -34,7 +34,9 @@ namespace NadekoBot.Modules.Utility [OwnerOnly] public async Task PatreonRewardsReload() { - await _patreon.LoadPledges().ConfigureAwait(false); + if (string.IsNullOrWhiteSpace(_creds.PatreonAccessToken)) + return; + await _patreon.RefreshPledges(true).ConfigureAwait(false); await Context.Channel.SendConfirmAsync("👌").ConfigureAwait(false); } @@ -44,6 +46,7 @@ namespace NadekoBot.Modules.Utility { if (string.IsNullOrWhiteSpace(_creds.PatreonAccessToken)) return; + if (DateTime.UtcNow.Day < 5) { await ReplyErrorLocalized("clpa_too_early").ConfigureAwait(false); diff --git a/src/NadekoBot/NadekoBot.cs b/src/NadekoBot/NadekoBot.cs index 2ee3d1b3..26cf6d91 100644 --- a/src/NadekoBot/NadekoBot.cs +++ b/src/NadekoBot/NadekoBot.cs @@ -163,7 +163,7 @@ namespace NadekoBot var repeaterService = new MessageRepeaterService(this, Client, AllGuildConfigs); //var converterService = new ConverterService(Db); var commandMapService = new CommandMapService(AllGuildConfigs); - var patreonRewardsService = new PatreonRewardsService(Credentials, Db, Currency); + var patreonRewardsService = new PatreonRewardsService(Credentials, Db, Currency, Client); var verboseErrorsService = new VerboseErrorsService(AllGuildConfigs, Db, CommandHandler, helpService); var pruneService = new PruneService(); #endregion diff --git a/src/NadekoBot/Services/Utility/PatreonRewardsService.cs b/src/NadekoBot/Services/Utility/PatreonRewardsService.cs index f3811c6d..8b463f7e 100644 --- a/src/NadekoBot/Services/Utility/PatreonRewardsService.cs +++ b/src/NadekoBot/Services/Utility/PatreonRewardsService.cs @@ -32,6 +32,8 @@ namespace NadekoBot.Services.Utility private readonly DbService _db; private readonly CurrencyService _currency; + private readonly string cacheFileName = "./patreon-rewards.json"; + public PatreonRewardsService(IBotCredentials creds, DbService db, CurrencyService currency, DiscordSocketClient client) { @@ -41,7 +43,8 @@ namespace NadekoBot.Services.Utility if (string.IsNullOrWhiteSpace(creds.PatreonAccessToken)) return; _log = LogManager.GetCurrentClassLogger(); - Updater = new Timer(async (load) => await RefreshPledges((bool)load), client.ShardId == 0, TimeSpan.Zero, Interval); + Updater = new Timer(async (load) => await RefreshPledges((bool)load), + client.ShardId == 0, client.ShardId == 0 ? TimeSpan.Zero : TimeSpan.FromMinutes(2), Interval); } public async Task RefreshPledges(bool shouldLoad) @@ -93,11 +96,15 @@ namespace NadekoBot.Services.Utility { getPledgesLocker.Release(); } + Console.WriteLine("Pledges loaded from the website"); } else { + if(File.Exists(cacheFileName)) Pledges = JsonConvert.DeserializeObject(File.ReadAllText("./patreon_rewards.json")) .ToImmutableArray(); + + Console.WriteLine("Pledges loaded from the file"); } } From e27e1005eb3443549663cfaed89b69ef2ed35ac1 Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Sat, 24 Jun 2017 07:23:59 +0200 Subject: [PATCH 025/346] poll is now public poll, private poll removed --- .../Modules/Games/Commands/PollCommands.cs | 19 ++---- src/NadekoBot/NadekoBot.cs | 20 +++++- .../Services/Administration/SelfService.cs | 64 +++++++++---------- src/NadekoBot/Services/Games/Poll.cs | 46 +++---------- src/NadekoBot/Services/Games/PollService.cs | 14 +--- .../Services/Utility/ConverterService.cs | 1 - .../Services/Utility/PatreonRewardsService.cs | 4 -- src/NadekoBot/ShardsCoordinator.cs | 2 +- 8 files changed, 64 insertions(+), 106 deletions(-) diff --git a/src/NadekoBot/Modules/Games/Commands/PollCommands.cs b/src/NadekoBot/Modules/Games/Commands/PollCommands.cs index 27dbb03c..0d6b6cb2 100644 --- a/src/NadekoBot/Modules/Games/Commands/PollCommands.cs +++ b/src/NadekoBot/Modules/Games/Commands/PollCommands.cs @@ -26,13 +26,7 @@ namespace NadekoBot.Modules.Games [RequireUserPermission(GuildPermission.ManageMessages)] [RequireContext(ContextType.Guild)] public Task Poll([Remainder] string arg = null) - => InternalStartPoll(arg, false); - - [NadekoCommand, Usage, Description, Aliases] - [RequireUserPermission(GuildPermission.ManageMessages)] - [RequireContext(ContextType.Guild)] - public Task PublicPoll([Remainder] string arg = null) - => InternalStartPoll(arg, true); + => InternalStartPoll(arg); [NadekoCommand, Usage, Description, Aliases] [RequireUserPermission(GuildPermission.ManageMessages)] @@ -44,15 +38,10 @@ namespace NadekoBot.Modules.Games await Context.Channel.EmbedAsync(poll.GetStats(GetText("current_poll_results"))); } - //todo enable private polls, or completely remove them - private async Task InternalStartPoll(string arg, bool isPublic = false) + + private async Task InternalStartPoll(string arg) { - if (isPublic == false) - { - await ReplyErrorLocalized($"Temporarily disabled. Use `{Prefix}ppoll`"); - return; - } - if(await _polls.StartPoll((ITextChannel)Context.Channel, Context.Message, arg, isPublic) == false) + if(await _polls.StartPoll((ITextChannel)Context.Channel, Context.Message, arg) == false) await ReplyErrorLocalized("poll_already_running").ConfigureAwait(false); } diff --git a/src/NadekoBot/NadekoBot.cs b/src/NadekoBot/NadekoBot.cs index 26cf6d91..24033c1f 100644 --- a/src/NadekoBot/NadekoBot.cs +++ b/src/NadekoBot/NadekoBot.cs @@ -290,9 +290,23 @@ namespace NadekoBot Task SetClientReady() { - var _ = Task.Run(() => + var _ = Task.Run(async () => { clientReady.TrySetResult(true); + try + { + await Task.WhenAll((await Client.GetDMChannelsAsync()) + .Select(x => x.CloseAsync())) + .ConfigureAwait(false); + } + catch + { + // ignored + } + finally + { + + } }); return Task.CompletedTask; } @@ -304,8 +318,8 @@ namespace NadekoBot try { - Client.LoginAsync(TokenType.Bot, token).GetAwaiter().GetResult(); - Client.StartAsync().GetAwaiter().GetResult(); + await Client.LoginAsync(TokenType.Bot, token).ConfigureAwait(false); + await Client.StartAsync().ConfigureAwait(false); Client.Ready += SetClientReady; await clientReady.Task.ConfigureAwait(false); Client.Ready -= SetClientReady; diff --git a/src/NadekoBot/Services/Administration/SelfService.cs b/src/NadekoBot/Services/Administration/SelfService.cs index 336f9970..2a12ffca 100644 --- a/src/NadekoBot/Services/Administration/SelfService.cs +++ b/src/NadekoBot/Services/Administration/SelfService.cs @@ -63,44 +63,44 @@ namespace NadekoBot.Services.Administration _client.Guilds.SelectMany(g => g.Users); - //todo load owner channels - //LoadOwnerChannels(); + if(client.ShardId == 0) + LoadOwnerChannels(); }); } - //private void LoadOwnerChannels() - //{ - // var hs = new HashSet(_creds.OwnerIds); - // var channels = new Dictionary>(); + private void LoadOwnerChannels() + { + var hs = new HashSet(_creds.OwnerIds); + var channels = new Dictionary>(); - // if (hs.Count > 0) - // { - // foreach (var g in _client.Guilds) - // { - // if (hs.Count == 0) - // break; + if (hs.Count > 0) + { + foreach (var g in _client.Guilds) + { + if (hs.Count == 0) + break; - // foreach (var u in g.Users) - // { - // if (hs.Remove(u.Id)) - // { - // channels.Add(u.Id, new AsyncLazy(async () => await u.CreateDMChannelAsync())); - // if (hs.Count == 0) - // break; - // } - // } - // } - // } - - // ownerChannels = channels.OrderBy(x => _creds.OwnerIds.IndexOf(x.Key)) - // .Select(x => x.Value) - // .ToImmutableArray(); + foreach (var u in g.Users) + { + if (hs.Remove(u.Id)) + { + channels.Add(u.Id, new AsyncLazy(async () => await u.CreateDMChannelAsync())); + if (hs.Count == 0) + break; + } + } + } + } - // if (!ownerChannels.Any()) - // _log.Warn("No owner channels created! Make sure you've specified correct OwnerId in the credentials.json file."); - // else - // _log.Info($"Created {ownerChannels.Length} out of {_creds.OwnerIds.Length} owner message channels."); - //} + ownerChannels = channels.OrderBy(x => _creds.OwnerIds.IndexOf(x.Key)) + .Select(x => x.Value) + .ToImmutableArray(); + + if (!ownerChannels.Any()) + _log.Warn("No owner channels created! Make sure you've specified correct OwnerId in the credentials.json file."); + else + _log.Info($"Created {ownerChannels.Length} out of {_creds.OwnerIds.Length} owner message channels."); + } // forwards dms public async Task LateExecute(DiscordSocketClient client, IGuild guild, IUserMessage msg) diff --git a/src/NadekoBot/Services/Games/Poll.cs b/src/NadekoBot/Services/Games/Poll.cs index 1a6bbf20..5358e139 100644 --- a/src/NadekoBot/Services/Games/Poll.cs +++ b/src/NadekoBot/Services/Games/Poll.cs @@ -21,13 +21,10 @@ namespace NadekoBot.Services.Games private readonly DiscordSocketClient _client; private readonly NadekoStrings _strings; private bool running = false; - private HashSet _guildUsers; public event Action OnEnded = delegate { }; - public bool IsPublic { get; } - - public Poll(DiscordSocketClient client, NadekoStrings strings, IUserMessage umsg, string question, IEnumerable enumerable, bool isPublic = false) + public Poll(DiscordSocketClient client, NadekoStrings strings, IUserMessage umsg, string question, IEnumerable enumerable) { _client = client; _strings = strings; @@ -36,7 +33,6 @@ namespace NadekoBot.Services.Games _guild = ((ITextChannel)umsg.Channel).Guild; _question = question; answers = enumerable as string[] ?? enumerable.ToArray(); - IsPublic = isPublic; } public EmbedBuilder GetStats(string title) @@ -82,13 +78,7 @@ namespace NadekoBot.Services.Games var msgToSend = GetText("poll_created", Format.Bold(_originalMessage.Author.Username)) + "\n\n" + Format.Bold(_question) + "\n"; var num = 1; msgToSend = answers.Aggregate(msgToSend, (current, answ) => current + $"`{num++}.` **{answ}**\n"); - if (!IsPublic) - msgToSend += "\n" + Format.Bold(GetText("poll_vote_private")); - else - msgToSend += "\n" + Format.Bold(GetText("poll_vote_public")); - - if (!IsPublic) - _guildUsers = new HashSet((await _guild.GetUsersAsync().ConfigureAwait(false)).Select(x => x.Id)); + msgToSend += "\n" + Format.Bold(GetText("poll_vote_public")); await _originalMessage.Channel.SendConfirmAsync(msgToSend).ConfigureAwait(false); running = true; @@ -114,36 +104,16 @@ namespace NadekoBot.Services.Games return false; IMessageChannel ch; - if (IsPublic) - { - //if public, channel must be the same the poll started in - if (_originalMessage.Channel.Id != msg.Channel.Id) - return false; - ch = msg.Channel; - } - else - { - //if private, channel must be dm channel - if ((ch = msg.Channel as IDMChannel) == null) - return false; - - // user must be a member of the guild this poll is in - if (!_guildUsers.Contains(msg.Author.Id)) - return false; - } + //if public, channel must be the same the poll started in + if (_originalMessage.Channel.Id != msg.Channel.Id) + return false; + ch = msg.Channel; //user can vote only once if (_participants.TryAdd(msg.Author.Id, vote)) { - if (!IsPublic) - { - await ch.SendConfirmAsync(GetText("thanks_for_voting", Format.Bold(msg.Author.Username))).ConfigureAwait(false); - } - else - { - var toDelete = await ch.SendConfirmAsync(GetText("poll_voted", Format.Bold(msg.Author.ToString()))).ConfigureAwait(false); - toDelete.DeleteAfter(5); - } + var toDelete = await ch.SendConfirmAsync(GetText("poll_voted", Format.Bold(msg.Author.ToString()))).ConfigureAwait(false); + toDelete.DeleteAfter(5); return true; } return false; diff --git a/src/NadekoBot/Services/Games/PollService.cs b/src/NadekoBot/Services/Games/PollService.cs index ce8852a7..ba6fee19 100644 --- a/src/NadekoBot/Services/Games/PollService.cs +++ b/src/NadekoBot/Services/Games/PollService.cs @@ -23,7 +23,7 @@ namespace NadekoBot.Services.Games _strings = strings; } - public async Task StartPoll(ITextChannel channel, IUserMessage msg, string arg, bool isPublic = false) + public async Task StartPoll(ITextChannel channel, IUserMessage msg, string arg) { if (string.IsNullOrWhiteSpace(arg) || !arg.Contains(";")) return null; @@ -31,7 +31,7 @@ namespace NadekoBot.Services.Games if (data.Length < 3) return null; - var poll = new Poll(_client, _strings, msg, data[0], data.Skip(1), isPublic: isPublic); + var poll = new Poll(_client, _strings, msg, data[0], data.Skip(1)); if (ActivePolls.TryAdd(channel.Guild.Id, poll)) { poll.OnEnded += (gid) => @@ -48,17 +48,7 @@ namespace NadekoBot.Services.Games public async Task TryExecuteEarly(DiscordSocketClient client, IGuild guild, IUserMessage msg) { if (guild == null) - { - foreach (var kvp in ActivePolls) - { - if (!kvp.Value.IsPublic) - { - if (await kvp.Value.TryVote(msg).ConfigureAwait(false)) - return true; - } - } return false; - } if (!ActivePolls.TryGetValue(guild.Id, out var poll)) return false; diff --git a/src/NadekoBot/Services/Utility/ConverterService.cs b/src/NadekoBot/Services/Utility/ConverterService.cs index 1edb470a..40f7fafa 100644 --- a/src/NadekoBot/Services/Utility/ConverterService.cs +++ b/src/NadekoBot/Services/Utility/ConverterService.cs @@ -18,7 +18,6 @@ namespace NadekoBot.Services.Utility public List Units { get; } = new List(); private readonly Logger _log; private readonly Timer _currencyUpdater; - private readonly Timer _currencyLoader; private readonly TimeSpan _updateInterval = new TimeSpan(12, 0, 0); private readonly DbService _db; diff --git a/src/NadekoBot/Services/Utility/PatreonRewardsService.cs b/src/NadekoBot/Services/Utility/PatreonRewardsService.cs index 8b463f7e..3eef3d72 100644 --- a/src/NadekoBot/Services/Utility/PatreonRewardsService.cs +++ b/src/NadekoBot/Services/Utility/PatreonRewardsService.cs @@ -15,7 +15,6 @@ using System.Threading.Tasks; namespace NadekoBot.Services.Utility { - //todo periodically load from the database, update only on shard 0 public class PatreonRewardsService { private readonly SemaphoreSlim getPledgesLocker = new SemaphoreSlim(1, 1); @@ -96,15 +95,12 @@ namespace NadekoBot.Services.Utility { getPledgesLocker.Release(); } - Console.WriteLine("Pledges loaded from the website"); } else { if(File.Exists(cacheFileName)) Pledges = JsonConvert.DeserializeObject(File.ReadAllText("./patreon_rewards.json")) .ToImmutableArray(); - - Console.WriteLine("Pledges loaded from the file"); } } diff --git a/src/NadekoBot/ShardsCoordinator.cs b/src/NadekoBot/ShardsCoordinator.cs index 3fe6da58..fcf80ebe 100644 --- a/src/NadekoBot/ShardsCoordinator.cs +++ b/src/NadekoBot/ShardsCoordinator.cs @@ -53,7 +53,7 @@ namespace NadekoBot FileName = Credentials.ShardRunCommand, Arguments = string.Format(Credentials.ShardRunArguments, i, curProcessId) }); - await Task.Delay(6500); + await Task.Delay(5000); } } From f11429b7146212ac4030b02a8734951e7ea13ba4 Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Sun, 25 Jun 2017 01:45:11 +0200 Subject: [PATCH 026/346] anilist api should work a bit better and is cleaner --- .../Services/Searches/AnimeSearchService.cs | 59 +++---------------- 1 file changed, 9 insertions(+), 50 deletions(-) diff --git a/src/NadekoBot/Services/Searches/AnimeSearchService.cs b/src/NadekoBot/Services/Searches/AnimeSearchService.cs index 2e4ef756..a4187da3 100644 --- a/src/NadekoBot/Services/Searches/AnimeSearchService.cs +++ b/src/NadekoBot/Services/Searches/AnimeSearchService.cs @@ -1,53 +1,18 @@ using Newtonsoft.Json; -using Newtonsoft.Json.Linq; using NLog; using System; -using System.Collections.Generic; -using System.Linq; using System.Net.Http; -using System.Text; -using System.Threading; using System.Threading.Tasks; namespace NadekoBot.Services.Searches { - //todo move to the website public class AnimeSearchService { - private readonly Timer _anilistTokenRefresher; private readonly Logger _log; - private static string anilistToken { get; set; } - public AnimeSearchService() { _log = LogManager.GetCurrentClassLogger(); - _anilistTokenRefresher = new Timer(async (state) => - { - try - { - var headers = new Dictionary - { - {"grant_type", "client_credentials"}, - {"client_id", "kwoth-w0ki9"}, - {"client_secret", "Qd6j4FIAi1ZK6Pc7N7V4Z"}, - }; - - using (var http = new HttpClient()) - { - //http.AddFakeHeaders(); - http.DefaultRequestHeaders.Clear(); - var formContent = new FormUrlEncodedContent(headers); - var response = await http.PostAsync("https://anilist.co/api/auth/access_token", formContent).ConfigureAwait(false); - var stringContent = await response.Content.ReadAsStringAsync().ConfigureAwait(false); - anilistToken = JObject.Parse(stringContent)["access_token"].ToString(); - } - } - catch - { - // ignored - } - }, null, TimeSpan.FromSeconds(0), TimeSpan.FromMinutes(29)); } public async Task GetAnimeData(string query) @@ -57,19 +22,15 @@ namespace NadekoBot.Services.Searches try { - var link = "http://anilist.co/api/anime/search/" + Uri.EscapeUriString(query); + var link = "https://aniapi.nadekobot.me/anime/" + Uri.EscapeDataString(query.Replace("/", " ")); using (var http = new HttpClient()) { - var res = await http.GetStringAsync(link + $"?access_token={anilistToken}").ConfigureAwait(false); - var smallObj = JArray.Parse(res)[0]; - var aniData = await http.GetStringAsync("http://anilist.co/api/anime/" + smallObj["id"] + $"?access_token={anilistToken}").ConfigureAwait(false); - - return await Task.Run(() => { try { return JsonConvert.DeserializeObject(aniData); } catch { return null; } }).ConfigureAwait(false); + var res = await http.GetStringAsync(link).ConfigureAwait(false); + return JsonConvert.DeserializeObject(res); } } - catch (Exception ex) + catch { - _log.Warn(ex, "Failed anime search for {0}", query); return null; } } @@ -80,18 +41,16 @@ namespace NadekoBot.Services.Searches throw new ArgumentNullException(nameof(query)); try { + + var link = "https://aniapi.nadekobot.me/manga/" + Uri.EscapeDataString(query.Replace("/", " ")); using (var http = new HttpClient()) { - var res = await http.GetStringAsync("http://anilist.co/api/manga/search/" + Uri.EscapeUriString(query) + $"?access_token={anilistToken}").ConfigureAwait(false); - var smallObj = JArray.Parse(res)[0]; - var aniData = await http.GetStringAsync("http://anilist.co/api/manga/" + smallObj["id"] + $"?access_token={anilistToken}").ConfigureAwait(false); - - return await Task.Run(() => { try { return JsonConvert.DeserializeObject(aniData); } catch { return null; } }).ConfigureAwait(false); + var res = await http.GetStringAsync(link).ConfigureAwait(false); + return JsonConvert.DeserializeObject(res); } } - catch (Exception ex) + catch { - _log.Warn(ex, "Failed anime search for {0}", query); return null; } } From e1baa3942a2161f77dbc9878c2eb7176b23d3ed9 Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Sun, 25 Jun 2017 02:35:37 +0200 Subject: [PATCH 027/346] .gc and stream follows should properly persist restarts --- .../Modules/Administration/Commands/SelfCommands.cs | 8 ++++---- .../Modules/Games/Commands/PlantAndPickCommands.cs | 2 +- src/NadekoBot/Modules/Utility/Utility.cs | 2 -- src/NadekoBot/NadekoBot.cs | 11 ++++------- src/NadekoBot/Resources/CommandStrings.resx | 10 +++++----- .../Repositories/Impl/GuildConfigRepository.cs | 1 + 6 files changed, 15 insertions(+), 19 deletions(-) diff --git a/src/NadekoBot/Modules/Administration/Commands/SelfCommands.cs b/src/NadekoBot/Modules/Administration/Commands/SelfCommands.cs index 5e5ecb33..449398b4 100644 --- a/src/NadekoBot/Modules/Administration/Commands/SelfCommands.cs +++ b/src/NadekoBot/Modules/Administration/Commands/SelfCommands.cs @@ -14,6 +14,7 @@ using NadekoBot.Services.Database.Models; using Microsoft.EntityFrameworkCore; using NadekoBot.Services.Administration; using System.Diagnostics; +using NadekoBot.DataStructures; namespace NadekoBot.Modules.Administration { @@ -207,12 +208,11 @@ namespace NadekoBot.Modules.Administration //todo 2 shard commands //[NadekoCommand, Usage, Description, Aliases] + //[Shard0Precondition] //[OwnerOnly] - //public async Task ConnectShard(int shardid) + //public async Task RestartShard(int shardid) //{ - // var shard = _client.GetShard(shardid); - - // if (shard == null) + // if (shardid == 0 || shardid > b) // { // await ReplyErrorLocalized("no_shard_id").ConfigureAwait(false); // return; diff --git a/src/NadekoBot/Modules/Games/Commands/PlantAndPickCommands.cs b/src/NadekoBot/Modules/Games/Commands/PlantAndPickCommands.cs index 191fd30d..f055e0bb 100644 --- a/src/NadekoBot/Modules/Games/Commands/PlantAndPickCommands.cs +++ b/src/NadekoBot/Modules/Games/Commands/PlantAndPickCommands.cs @@ -116,7 +116,7 @@ namespace NadekoBot.Modules.Games bool enabled; using (var uow = _db.UnitOfWork) { - var guildConfig = uow.GuildConfigs.For(channel.Id, set => set.Include(gc => gc.GenerateCurrencyChannelIds)); + var guildConfig = uow.GuildConfigs.For(channel.Guild.Id, set => set.Include(gc => gc.GenerateCurrencyChannelIds)); var toAdd = new GCChannelId() { ChannelId = channel.Id }; if (!guildConfig.GenerateCurrencyChannelIds.Contains(toAdd)) diff --git a/src/NadekoBot/Modules/Utility/Utility.cs b/src/NadekoBot/Modules/Utility/Utility.cs index 3ae25163..7a6f9ae1 100644 --- a/src/NadekoBot/Modules/Utility/Utility.cs +++ b/src/NadekoBot/Modules/Utility/Utility.cs @@ -299,8 +299,6 @@ namespace NadekoBot.Modules.Utility Format.Bold(x.ConnectionState.ToString()), Format.Bold(x.Guilds.ToString()))) .ToArray(); - - await Context.Channel.SendPaginatedConfirmAsync(_client, page, (curPage) => { diff --git a/src/NadekoBot/NadekoBot.cs b/src/NadekoBot/NadekoBot.cs index 24033c1f..a180f3b5 100644 --- a/src/NadekoBot/NadekoBot.cs +++ b/src/NadekoBot/NadekoBot.cs @@ -4,8 +4,6 @@ using Discord.WebSocket; using NadekoBot.Services; using NadekoBot.Services.Impl; using NLog; -using NLog.Config; -using NLog.Targets; using System; using System.Linq; using System.Reflection; @@ -28,8 +26,6 @@ using NadekoBot.Services.Help; using System.IO; using NadekoBot.Services.Pokemon; using NadekoBot.DataStructures.ShardCom; -using NadekoBot.DataStructures; -using NadekoBot.Extensions; namespace NadekoBot { @@ -295,9 +291,10 @@ namespace NadekoBot clientReady.TrySetResult(true); try { - await Task.WhenAll((await Client.GetDMChannelsAsync()) - .Select(x => x.CloseAsync())) - .ConfigureAwait(false); + foreach (var chan in (await Client.GetDMChannelsAsync())) + { + await chan.CloseAsync().ConfigureAwait(false); + } } catch { diff --git a/src/NadekoBot/Resources/CommandStrings.resx b/src/NadekoBot/Resources/CommandStrings.resx index 708abfe8..e896535d 100644 --- a/src/NadekoBot/Resources/CommandStrings.resx +++ b/src/NadekoBot/Resources/CommandStrings.resx @@ -3096,14 +3096,14 @@ `{0}shardstats` or `{0}shardstats 2` - - connectshard + + restartshard - + Try (re)connecting a shard with a certain shardid when it dies. No one knows will it work. Keep an eye on the console for errors. - - `{0}connectshard 2` + + `{0}restartshard 2` shardid diff --git a/src/NadekoBot/Services/Database/Repositories/Impl/GuildConfigRepository.cs b/src/NadekoBot/Services/Database/Repositories/Impl/GuildConfigRepository.cs index 37d2af60..3628a4e9 100644 --- a/src/NadekoBot/Services/Database/Repositories/Impl/GuildConfigRepository.cs +++ b/src/NadekoBot/Services/Database/Repositories/Impl/GuildConfigRepository.cs @@ -44,6 +44,7 @@ namespace NadekoBot.Services.Database.Repositories.Impl .Include(gc => gc.SlowmodeIgnoredUsers) .Include(gc => gc.AntiSpamSetting) .ThenInclude(x => x.IgnoredChannels) + .Include(gc => gc.FollowedStreams) .ToList(); /// From 3f2eef5647fabdd05949e5324cd10f5bf6cd09cf Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Sun, 25 Jun 2017 02:39:31 +0200 Subject: [PATCH 028/346] .ve now properly persists restarts --- .../Services/Utility/VerboseErrorsService.cs | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/src/NadekoBot/Services/Utility/VerboseErrorsService.cs b/src/NadekoBot/Services/Utility/VerboseErrorsService.cs index 009de8ee..7658ea1c 100644 --- a/src/NadekoBot/Services/Utility/VerboseErrorsService.cs +++ b/src/NadekoBot/Services/Utility/VerboseErrorsService.cs @@ -50,12 +50,12 @@ namespace NadekoBot.Services.Utility public bool ToggleVerboseErrors(ulong guildId) { - + bool enabled; using (var uow = _db.UnitOfWork) { var gc = uow.GuildConfigs.For(guildId, set => set); - gc.VerboseErrors = !gc.VerboseErrors; + enabled = gc.VerboseErrors = !gc.VerboseErrors; uow.Complete(); @@ -65,15 +65,12 @@ namespace NadekoBot.Services.Utility guildsEnabled.TryRemove(guildId); } - if (guildsEnabled.Add(guildId)) - { - return true; - } + if (enabled) + guildsEnabled.Add(guildId); else - { guildsEnabled.TryRemove(guildId); - return false; - } + + return enabled; } } From ff56af3e736421f2ad018bd1ab454595c4e2af2d Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Sun, 25 Jun 2017 03:56:23 +0200 Subject: [PATCH 029/346] .rar fixed, closes #1306 --- src/NadekoBot/Modules/Administration/Administration.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/NadekoBot/Modules/Administration/Administration.cs b/src/NadekoBot/Modules/Administration/Administration.cs index 15185ca3..56a0affc 100644 --- a/src/NadekoBot/Modules/Administration/Administration.cs +++ b/src/NadekoBot/Modules/Administration/Administration.cs @@ -126,17 +126,17 @@ namespace NadekoBot.Modules.Administration { var guser = (IGuildUser)Context.User; - var userRoles = user.GetRoles(); - if (guser.Id != Context.Guild.OwnerId && - (user.Id == Context.Guild.OwnerId || guser.GetRoles().Max(x => x.Position) <= userRoles.Max(x => x.Position))) + var userRoles = user.GetRoles().Except(new[] { guser.Guild.EveryoneRole }); + if (user.Id == Context.Guild.OwnerId || (Context.User.Id != Context.Guild.OwnerId && guser.GetRoles().Max(x => x.Position) <= userRoles.Max(x => x.Position))) return; try { await user.RemoveRolesAsync(userRoles).ConfigureAwait(false); await ReplyConfirmLocalized("rar", Format.Bold(user.ToString())).ConfigureAwait(false); } - catch + catch (Exception ex) { + Console.WriteLine(ex); await ReplyErrorLocalized("rar_err").ConfigureAwait(false); } } From 902ddc70f609c17349276ccbd10deb2df46ab7d7 Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Sun, 25 Jun 2017 04:58:48 +0200 Subject: [PATCH 030/346] triggered permissions are now translatable --- src/NadekoBot/NadekoBot.cs | 6 +++--- .../Services/CustomReactions/CustomReactionsService.cs | 6 ++++-- src/NadekoBot/Services/Games/ChatterbotService.cs | 7 +++++-- src/NadekoBot/Services/Permissions/PermissionsService.cs | 9 ++++----- src/NadekoBot/_strings/ResponseStrings.en-US.json | 7 ++++--- 5 files changed, 20 insertions(+), 15 deletions(-) diff --git a/src/NadekoBot/NadekoBot.cs b/src/NadekoBot/NadekoBot.cs index a180f3b5..4142aa2f 100644 --- a/src/NadekoBot/NadekoBot.cs +++ b/src/NadekoBot/NadekoBot.cs @@ -165,7 +165,7 @@ namespace NadekoBot #endregion #region permissions - var permissionsService = new PermissionService(Client, Db, BotConfig, CommandHandler); + var permissionsService = new PermissionService(Client, Db, BotConfig, CommandHandler, Strings); var blacklistService = new BlacklistService(BotConfig); var cmdcdsService = new CmdCdService(AllGuildConfigs); var filterService = new FilterService(Client, AllGuildConfigs); @@ -180,11 +180,11 @@ namespace NadekoBot var clashService = new ClashOfClansService(Client, Db, Localization, Strings, uow, startingGuildIdList); var musicService = new MusicService(GoogleApi, Strings, Localization, Db, soundcloudApiService, Credentials, AllGuildConfigs); - var crService = new CustomReactionsService(permissionsService, Db, Client, CommandHandler, BotConfig, uow); + var crService = new CustomReactionsService(permissionsService, Db, Strings, Client, CommandHandler, BotConfig, uow); #region Games var gamesService = new GamesService(Client, BotConfig, AllGuildConfigs, Strings, Images, CommandHandler); - var chatterBotService = new ChatterBotService(Client, permissionsService, AllGuildConfigs, CommandHandler); + var chatterBotService = new ChatterBotService(Client, permissionsService, AllGuildConfigs, CommandHandler, Strings); var pollService = new PollService(Client, Strings); #endregion diff --git a/src/NadekoBot/Services/CustomReactions/CustomReactionsService.cs b/src/NadekoBot/Services/CustomReactions/CustomReactionsService.cs index ecf0dede..c64f5c81 100644 --- a/src/NadekoBot/Services/CustomReactions/CustomReactionsService.cs +++ b/src/NadekoBot/Services/CustomReactions/CustomReactionsService.cs @@ -27,8 +27,9 @@ namespace NadekoBot.Services.CustomReactions private readonly PermissionService _perms; private readonly CommandHandler _cmd; private readonly BotConfig _bc; + private readonly NadekoStrings _strings; - public CustomReactionsService(PermissionService perms, DbService db, + public CustomReactionsService(PermissionService perms, DbService db, NadekoStrings strings, DiscordSocketClient client, CommandHandler cmd, BotConfig bc, IUnitOfWork uow) { _log = LogManager.GetCurrentClassLogger(); @@ -37,6 +38,7 @@ namespace NadekoBot.Services.CustomReactions _perms = perms; _cmd = cmd; _bc = bc; + _strings = strings; var items = uow.CustomReactions.GetAll(); GuildReactions = new ConcurrentDictionary(items.Where(g => g.GuildId != null && g.GuildId != 0).GroupBy(k => k.GuildId.Value).ToDictionary(g => g.Key, g => g.ToArray())); @@ -109,7 +111,7 @@ namespace NadekoBot.Services.CustomReactions { if (pc.Verbose) { - var returnMsg = $"Permission number #{index + 1} **{pc.Permissions[index].GetCommand(_cmd.GetPrefix(guild), sg)}** is preventing this action."; + var returnMsg = _strings.GetText("trigger", guild.Id, "Permissions".ToLowerInvariant(), index + 1, Format.Bold(pc.Permissions[index].GetCommand(_cmd.GetPrefix(guild), (SocketGuild)guild))); try { await msg.Channel.SendErrorAsync(returnMsg).ConfigureAwait(false); } catch { } _log.Info(returnMsg); } diff --git a/src/NadekoBot/Services/Games/ChatterbotService.cs b/src/NadekoBot/Services/Games/ChatterbotService.cs index ad295dec..833eef0b 100644 --- a/src/NadekoBot/Services/Games/ChatterbotService.cs +++ b/src/NadekoBot/Services/Games/ChatterbotService.cs @@ -19,15 +19,18 @@ namespace NadekoBot.Services.Games private readonly Logger _log; private readonly PermissionService _perms; private readonly CommandHandler _cmd; + private readonly NadekoStrings _strings; public ConcurrentDictionary> ChatterBotGuilds { get; } - public ChatterBotService(DiscordSocketClient client, PermissionService perms, IEnumerable gcs, CommandHandler cmd) + public ChatterBotService(DiscordSocketClient client, PermissionService perms, IEnumerable gcs, + CommandHandler cmd, NadekoStrings strings) { _client = client; _log = LogManager.GetCurrentClassLogger(); _perms = perms; _cmd = cmd; + _strings = strings; ChatterBotGuilds = new ConcurrentDictionary>( gcs.Where(gc => gc.CleverbotEnabled) @@ -102,7 +105,7 @@ namespace NadekoBot.Services.Games if (pc.Verbose) { //todo move this to permissions - var returnMsg = $"Permission number #{index + 1} **{pc.Permissions[index].GetCommand(_cmd.GetPrefix(guild), sg)}** is preventing this action."; + var returnMsg = _strings.GetText("trigger", guild.Id, "Permissions".ToLowerInvariant(), index + 1, Format.Bold(pc.Permissions[index].GetCommand(_cmd.GetPrefix(guild), (SocketGuild)guild))); try { await usrMsg.Channel.SendErrorAsync(returnMsg).ConfigureAwait(false); } catch { } _log.Info(returnMsg); } diff --git a/src/NadekoBot/Services/Permissions/PermissionsService.cs b/src/NadekoBot/Services/Permissions/PermissionsService.cs index 00794d2d..c4c586a9 100644 --- a/src/NadekoBot/Services/Permissions/PermissionsService.cs +++ b/src/NadekoBot/Services/Permissions/PermissionsService.cs @@ -20,16 +20,18 @@ namespace NadekoBot.Services.Permissions private readonly DbService _db; private readonly Logger _log; private readonly CommandHandler _cmd; + private readonly NadekoStrings _strings; //guildid, root permission public ConcurrentDictionary Cache { get; } = new ConcurrentDictionary(); - public PermissionService(DiscordSocketClient client, DbService db, BotConfig bc, CommandHandler cmd) + public PermissionService(DiscordSocketClient client, DbService db, BotConfig bc, CommandHandler cmd, NadekoStrings strings) { _log = LogManager.GetCurrentClassLogger(); _db = db; _cmd = cmd; + _strings = strings; var sw = Stopwatch.StartNew(); if (client.ShardId == 0) @@ -205,11 +207,9 @@ WHERE secondaryTargetName LIKE '.%' OR PermissionCache pc = GetCache(guild.Id); if (!resetCommand && !pc.Permissions.CheckPermissions(msg, commandName, moduleName, out int index)) { - var returnMsg = $"Permission number #{index + 1} **{pc.Permissions[index].GetCommand(_cmd.GetPrefix(guild), (SocketGuild)guild)}** is preventing this action."; if (pc.Verbose) - try { await channel.SendErrorAsync(returnMsg).ConfigureAwait(false); } catch { } + try { await channel.SendErrorAsync(_strings.GetText("trigger", guild.Id, "Permissions".ToLowerInvariant(), index + 1, Format.Bold(pc.Permissions[index].GetCommand(_cmd.GetPrefix(guild), (SocketGuild)guild)))).ConfigureAwait(false); } catch { } return true; - //return new ExecuteCommandResult(cmd, pc, SearchResult.FromError(CommandError.Exception, returnMsg)); } @@ -222,7 +222,6 @@ WHERE secondaryTargetName LIKE '.%' OR if (pc.Verbose) try { await channel.SendErrorAsync(returnMsg).ConfigureAwait(false); } catch { } return true; - //return new ExecuteCommandResult(cmd, pc, SearchResult.FromError(CommandError.Exception, $"You need the **{pc.PermRole}** role in order to use permission commands.")); } } } diff --git a/src/NadekoBot/_strings/ResponseStrings.en-US.json b/src/NadekoBot/_strings/ResponseStrings.en-US.json index d1863403..f405c63e 100644 --- a/src/NadekoBot/_strings/ResponseStrings.en-US.json +++ b/src/NadekoBot/_strings/ResponseStrings.en-US.json @@ -154,6 +154,7 @@ "administration_old_topic": "Old topic", "administration_perms": "Error. Most likely I don't have sufficient permissions.", "permissions_perms_reset": "Permissions for this server are reset.", + "permissions_trigger": "Permission number #{0} {1} is preventing this action.", "administration_prot_active": "Active protections", "administration_prot_disable": "{0} has been **disabled** on this server.", "administration_prot_enable": "{0} Enabled", @@ -446,7 +447,7 @@ "music_skipped_to": "Skipped to `{0}:{1}`", "music_songs_shuffled": "Songs shuffled", "music_song_moved": "Song moved", - "music_song_not_found": "No song found.", + "music_song_not_found": "No song found.", "music_time_format": "{0}h {1}m {2}s", "music_to_position": "To position", "music_unlimited": "unlimited", @@ -523,7 +524,7 @@ "searches_cost": "Cost", "searches_date": "Date", "searches_define": "Define:", - "searches_define_unknown": "Can't find the definition for that term.", + "searches_define_unknown": "Can't find the definition for that term.", "searches_dropped": "Dropped", "searches_episodes": "Episodes", "searches_error_occured": "Error occurred.", @@ -534,7 +535,7 @@ "searches_hashtag_error": "Failed finding a definition for that tag.", "searches_height_weight": "Height/Weight", "searches_height_weight_val": "{0}m/{1}kg", - "searches_hex_invalid": "Invalid color specified.", + "searches_hex_invalid": "Invalid color specified.", "searches_humidity": "Humidity", "searches_image_search_for": "Image search for:", "searches_imdb_fail": "Failed to find that movie.", From ee8643bf29aae08840020651ec4a0812734bffdf Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Sun, 25 Jun 2017 06:09:23 +0200 Subject: [PATCH 031/346] You can now choose port where shard communication is happening. JSON explanations with instructions on how to ed it optional shard settings --- NadekoBot.sln | 2 +- docs/JSON Explanations.md | 24 +++++++++++++++---- .../DataStructures/ShardCom/ShardComClient.cs | 9 ++++++- .../DataStructures/ShardCom/ShardComServer.cs | 8 +++++-- src/NadekoBot/NadekoBot.cs | 14 +++++++---- src/NadekoBot/Program.cs | 9 +++++-- src/NadekoBot/Services/Impl/BotCredentials.cs | 13 ++++++++-- src/NadekoBot/Services/Impl/StatsService.cs | 2 +- src/NadekoBot/ShardsCoordinator.cs | 8 ++++--- src/NadekoBot/credentials_example.json | 4 +++- 10 files changed, 71 insertions(+), 22 deletions(-) diff --git a/NadekoBot.sln b/NadekoBot.sln index a80335e2..8c235940 100644 --- a/NadekoBot.sln +++ b/NadekoBot.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 15 -VisualStudioVersion = 15.0.26430.6 +VisualStudioVersion = 15.0.26430.12 MinimumVisualStudioVersion = 10.0.40219.1 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{04929013-5BAB-42B0-B9B2-8F2BB8F16AF2}" EndProject diff --git a/docs/JSON Explanations.md b/docs/JSON Explanations.md index 363c5e8d..bf84432d 100644 --- a/docs/JSON Explanations.md +++ b/docs/JSON Explanations.md @@ -17,7 +17,8 @@ If you do not see `credentials.json` you will need to rename `credentials_exampl "MashapeKey": "4UrKpcWXc2mshS8RKi00000y8Kf5p1Q8kI6jsn32bmd8oVWiY7", "OsuApiKey": "4c8c8fdff8e1234581725db27fd140a7d93320d6", "Db": null, - "TotalShards": 1 + "TotalShards": 1, + "ShardRunCommand": "" } ``` ----- @@ -143,14 +144,29 @@ It should look like: - **TotalShards** - Required if the bot will be connected to more than 1500 servers. - Most likely unnecessary to change until your bot is added to more than 1500 servers. - +- **ShardRunCommand** + - Required if you're sharding your bot on windows using .exe, or in a custom way. + - This internally defaults to `dotnet` + - For example, if you want to shard your NadekoBot which you installed using windows installer, you would want to set it to something like this: `C:\Program Files\NadekoBot\system\NadekoBot.exe` +- **ShardRunArguments** + - Required if you're sharding your bot on windows using .exe, or in a custom way. + - This internally defaults to `run -c Release -- {0} {1} {2}` which will be enough to run linux and other 'from source' setups + - {0} will be replaced by the `shard ID` of the shard being ran, {1} by the shard 0's process id, and {2} by the port shard communication is happening on + - If shard0 (main window) is closed, all other shards will close too + - For example, if you want to shard your NadekoBot which you installed using windows installer, you would want to set it to `{0} {1} {2}` +- **ShardRunPort** + - Bot uses a random UDP port in [5000, 6000) range for communication between shards ----- ## DB files -Nadeko saves all the settings and infomations in `NadekoBot.db` file here: +Nadeko saves all the settings and infomations in `NadekoBot.db` file here: +**On linux** `NadekoBot\src\NadekoBot\bin\Release\netcoreapp1.1\data\NadekoBot.db` (NadekoBot v1.4x) -in order to open the database file you will need [DB Browser for SQLite](http://sqlitebrowser.org/). +**On windows** +`[INSTALL_PATH]\NadekoBot\system\data\NadekoBot.db` + +In order to open the database file you will need [DB Browser for SQLite](http://sqlitebrowser.org/). To make changes diff --git a/src/NadekoBot/DataStructures/ShardCom/ShardComClient.cs b/src/NadekoBot/DataStructures/ShardCom/ShardComClient.cs index a8857f3e..67e1c9f6 100644 --- a/src/NadekoBot/DataStructures/ShardCom/ShardComClient.cs +++ b/src/NadekoBot/DataStructures/ShardCom/ShardComClient.cs @@ -9,13 +9,20 @@ namespace NadekoBot.DataStructures.ShardCom { public class ShardComClient { + private int port; + + public ShardComClient(int port) + { + this.port = port; + } + public async Task Send(ShardComMessage data) { var msg = JsonConvert.SerializeObject(data); using (var client = new UdpClient()) { var bytes = Encoding.UTF8.GetBytes(msg); - await client.SendAsync(bytes, bytes.Length, IPAddress.Loopback.ToString(), ShardComServer.Port).ConfigureAwait(false); + await client.SendAsync(bytes, bytes.Length, IPAddress.Loopback.ToString(), port).ConfigureAwait(false); } } } diff --git a/src/NadekoBot/DataStructures/ShardCom/ShardComServer.cs b/src/NadekoBot/DataStructures/ShardCom/ShardComServer.cs index 61c35a85..d0e1cbf6 100644 --- a/src/NadekoBot/DataStructures/ShardCom/ShardComServer.cs +++ b/src/NadekoBot/DataStructures/ShardCom/ShardComServer.cs @@ -9,8 +9,12 @@ namespace NadekoBot.DataStructures.ShardCom { public class ShardComServer : IDisposable { - public const int Port = 5664; - private readonly UdpClient _client = new UdpClient(Port); + private readonly UdpClient _client; + + public ShardComServer(int port) + { + _client = new UdpClient(port); + } public void Start() { diff --git a/src/NadekoBot/NadekoBot.cs b/src/NadekoBot/NadekoBot.cs index 4142aa2f..04dd0e4b 100644 --- a/src/NadekoBot/NadekoBot.cs +++ b/src/NadekoBot/NadekoBot.cs @@ -65,9 +65,9 @@ namespace NadekoBot public int ShardId { get; } public ShardsCoordinator ShardCoord { get; private set; } - private readonly ShardComClient _comClient = new ShardComClient(); + private readonly ShardComClient _comClient; - public NadekoBot(int shardId, int parentProcessId) + public NadekoBot(int shardId, int parentProcessId, int? port = null) { if (shardId < 0) throw new ArgumentOutOfRangeException(nameof(shardId)); @@ -79,6 +79,10 @@ namespace NadekoBot TerribleElevatedPermissionCheck(); Credentials = new BotCredentials(); + + port = port ?? Credentials.ShardRunPort; + _comClient = new ShardComClient(port.Value); + Db = new DbService(Credentials); using (var uow = Db.UnitOfWork) @@ -108,7 +112,7 @@ namespace NadekoBot Currency = new CurrencyService(BotConfig, Db); GoogleApi = new GoogleApiService(Credentials); - SetupShard(shardId, parentProcessId); + SetupShard(shardId, parentProcessId, port.Value); #if GLOBAL_NADEKO Client.Log += Client_Log; @@ -411,7 +415,7 @@ namespace NadekoBot } } - private void SetupShard(int shardId, int parentProcessId) + private void SetupShard(int shardId, int parentProcessId, int port) { if (shardId != 0) { @@ -432,7 +436,7 @@ namespace NadekoBot } else { - ShardCoord = new ShardsCoordinator(); + ShardCoord = new ShardsCoordinator(port); } } } diff --git a/src/NadekoBot/Program.cs b/src/NadekoBot/Program.cs index a45affcc..09e53142 100644 --- a/src/NadekoBot/Program.cs +++ b/src/NadekoBot/Program.cs @@ -4,8 +4,13 @@ { public static void Main(string[] args) { - if (args.Length == 2 && int.TryParse(args[0], out int shardId) && int.TryParse(args[1], out int parentProcessId)) - new NadekoBot(shardId, parentProcessId).RunAndBlockAsync(args).GetAwaiter().GetResult(); + if (args.Length == 3 && int.TryParse(args[0], out int shardId) && int.TryParse(args[1], out int parentProcessId)) + { + int? port = null; + if (int.TryParse(args[2], out var outPort)) + port = outPort; + new NadekoBot(shardId, parentProcessId, outPort).RunAndBlockAsync(args).GetAwaiter().GetResult(); + } else new NadekoBot(0, 0).RunAndBlockAsync(args).GetAwaiter().GetResult(); } diff --git a/src/NadekoBot/Services/Impl/BotCredentials.cs b/src/NadekoBot/Services/Impl/BotCredentials.cs index b54bed23..3d47a207 100644 --- a/src/NadekoBot/Services/Impl/BotCredentials.cs +++ b/src/NadekoBot/Services/Impl/BotCredentials.cs @@ -34,6 +34,7 @@ namespace NadekoBot.Services.Impl public string PatreonAccessToken { get; } public string ShardRunCommand { get; } public string ShardRunArguments { get; } + public int ShardRunPort { get; } public BotCredentials() { @@ -65,11 +66,16 @@ namespace NadekoBot.Services.Impl PatreonAccessToken = data[nameof(PatreonAccessToken)]; ShardRunCommand = data[nameof(ShardRunCommand)]; ShardRunArguments = data[nameof(ShardRunArguments)]; - if (string.IsNullOrWhiteSpace(ShardRunCommand)) ShardRunCommand = "dotnet"; if (string.IsNullOrWhiteSpace(ShardRunArguments)) - ShardRunArguments = "run -c Release -- {0} {1}"; + ShardRunArguments = "run -c Release -- {0} {1} {2}"; + + var portStr = data[nameof(ShardRunPort)]; + if (string.IsNullOrWhiteSpace(portStr)) + ShardRunPort = new NadekoRandom().Next(5000, 6000); + else + ShardRunPort = int.Parse(portStr); int ts = 1; int.TryParse(data[nameof(TotalShards)], out ts); @@ -115,7 +121,10 @@ namespace NadekoBot.Services.Impl public DBConfig Db { get; set; } = new DBConfig("sqlite", "Filename=./data/NadekoBot.db"); public int TotalShards { get; set; } = 1; public string PatreonAccessToken { get; set; } = ""; + public string ShardRunCommand { get; set; } = ""; + public string ShardRunArguments { get; set; } = ""; + public int? ShardRunPort { get; set; } = null; } private class DbModel diff --git a/src/NadekoBot/Services/Impl/StatsService.cs b/src/NadekoBot/Services/Impl/StatsService.cs index 64507106..07d706e1 100644 --- a/src/NadekoBot/Services/Impl/StatsService.cs +++ b/src/NadekoBot/Services/Impl/StatsService.cs @@ -17,7 +17,7 @@ namespace NadekoBot.Services.Impl private readonly IBotCredentials _creds; private readonly DateTime _started; - public const string BotVersion = "1.43"; + public const string BotVersion = "1.5"; public string Author => "Kwoth#2560"; public string Library => "Discord.Net"; diff --git a/src/NadekoBot/ShardsCoordinator.cs b/src/NadekoBot/ShardsCoordinator.cs index fcf80ebe..18725566 100644 --- a/src/NadekoBot/ShardsCoordinator.cs +++ b/src/NadekoBot/ShardsCoordinator.cs @@ -20,16 +20,18 @@ namespace NadekoBot private readonly Logger _log; private readonly ShardComServer _comServer; + private readonly int _port; - public ShardsCoordinator() + public ShardsCoordinator(int port) { LogSetup.SetupLogger(); Credentials = new BotCredentials(); ShardProcesses = new Process[Credentials.TotalShards]; Statuses = new ShardComMessage[Credentials.TotalShards]; _log = LogManager.GetCurrentClassLogger(); + _port = port; - _comServer = new ShardComServer(); + _comServer = new ShardComServer(port); _comServer.Start(); _comServer.OnDataReceived += _comServer_OnDataReceived; @@ -51,7 +53,7 @@ namespace NadekoBot var p = Process.Start(new ProcessStartInfo() { FileName = Credentials.ShardRunCommand, - Arguments = string.Format(Credentials.ShardRunArguments, i, curProcessId) + Arguments = string.Format(Credentials.ShardRunArguments, i, curProcessId, _port) }); await Task.Delay(5000); } diff --git a/src/NadekoBot/credentials_example.json b/src/NadekoBot/credentials_example.json index 7c7e4095..8f977d87 100644 --- a/src/NadekoBot/credentials_example.json +++ b/src/NadekoBot/credentials_example.json @@ -16,5 +16,7 @@ }, "TotalShards": 1, "PatreonAccessToken": "", - "ShardRunCommand": "" + "ShardRunCommand": "", + "ShardRunArguments": "", + "ShardRunPort": null } \ No newline at end of file From e75f23557bcbab094f9e9ac257990b1c0ab3baa6 Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Sun, 25 Jun 2017 06:18:23 +0200 Subject: [PATCH 032/346] Small addition to docs --- docs/JSON Explanations.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/JSON Explanations.md b/docs/JSON Explanations.md index bf84432d..d83e97b3 100644 --- a/docs/JSON Explanations.md +++ b/docs/JSON Explanations.md @@ -145,10 +145,12 @@ It should look like: - Required if the bot will be connected to more than 1500 servers. - Most likely unnecessary to change until your bot is added to more than 1500 servers. - **ShardRunCommand** - - Required if you're sharding your bot on windows using .exe, or in a custom way. + - Command with which to run shards 1+ + - Required if you're sharding your bot on windows using .exe, or in a custom way. - This internally defaults to `dotnet` - For example, if you want to shard your NadekoBot which you installed using windows installer, you would want to set it to something like this: `C:\Program Files\NadekoBot\system\NadekoBot.exe` - **ShardRunArguments** + - Arguments to the shard run command - Required if you're sharding your bot on windows using .exe, or in a custom way. - This internally defaults to `run -c Release -- {0} {1} {2}` which will be enough to run linux and other 'from source' setups - {0} will be replaced by the `shard ID` of the shard being ran, {1} by the shard 0's process id, and {2} by the port shard communication is happening on From dca4bf39a0b29b5a8ced50c3ba9b61c023e767d5 Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Sun, 25 Jun 2017 07:11:27 +0200 Subject: [PATCH 033/346] Updated credentials_example.json --- src/NadekoBot/credentials_example.json | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/NadekoBot/credentials_example.json b/src/NadekoBot/credentials_example.json index fd39b766..8f977d87 100644 --- a/src/NadekoBot/credentials_example.json +++ b/src/NadekoBot/credentials_example.json @@ -1,15 +1,14 @@ { "ClientId": 123123123, - "BotId": null, "Token": "", "OwnerIds": [ - 105635576866156544 + 0 ], "LoLApiKey": "", "GoogleApiKey": "", "MashapeKey": "", "OsuApiKey": "", - "PatreonAccessToken": "", + "SoundCloudClientId": "", "CarbonKey": "", "Db": { "Type": "sqlite", From 721ad63addbd6e094523bcbf3b28c771f649b926 Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Sun, 25 Jun 2017 08:00:52 +0200 Subject: [PATCH 034/346] Removed semaphore --- src/NadekoBot/NadekoBot.cs | 24 ++++++------------------ 1 file changed, 6 insertions(+), 18 deletions(-) diff --git a/src/NadekoBot/NadekoBot.cs b/src/NadekoBot/NadekoBot.cs index 04dd0e4b..ac63f42a 100644 --- a/src/NadekoBot/NadekoBot.cs +++ b/src/NadekoBot/NadekoBot.cs @@ -60,8 +60,6 @@ namespace NadekoBot public INServiceProvider Services { get; private set; } public BotCredentials Credentials { get; } - private const string _mutexName = @"Global\nadeko_shards_lock"; - private readonly Semaphore sem = new Semaphore(1, 1, _mutexName); public int ShardId { get; } public ShardsCoordinator ShardCoord { get; private set; } @@ -313,23 +311,13 @@ namespace NadekoBot } //connect - try { sem.WaitOne(); } catch (AbandonedMutexException) { } - _log.Info("Shard {0} logging in ...", ShardId); - - try - { - await Client.LoginAsync(TokenType.Bot, token).ConfigureAwait(false); - await Client.StartAsync().ConfigureAwait(false); - Client.Ready += SetClientReady; - await clientReady.Task.ConfigureAwait(false); - Client.Ready -= SetClientReady; - } - finally - { - _log.Info("Shard {0} logged in.", ShardId); - sem.Release(); - } + await Client.LoginAsync(TokenType.Bot, token).ConfigureAwait(false); + await Client.StartAsync().ConfigureAwait(false); + Client.Ready += SetClientReady; + await clientReady.Task.ConfigureAwait(false); + Client.Ready -= SetClientReady; + _log.Info("Shard {0} logged in.", ShardId); } public async Task RunAsync(params string[] args) From 8a4d76b8e6c1ecee4b686430d471a2f900dc678c Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Sun, 25 Jun 2017 09:51:41 +0200 Subject: [PATCH 035/346] removed .net sdk link in windows guide --- docs/guides/Windows Guide.md | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/guides/Windows Guide.md b/docs/guides/Windows Guide.md index 91b4d45b..e0a751f7 100644 --- a/docs/guides/Windows Guide.md +++ b/docs/guides/Windows Guide.md @@ -3,7 +3,6 @@ #### Prerequisites - [Notepad++][Notepad++] (or some other decent text editor) - Windows 8 or later -- [.NET Core SDK (Command line / other)][.NET Core SDK] - [Create Discord Bot application](http://nadekobot.readthedocs.io/en/latest/JSON%20Explanations/#creating-discord-bot-application) and [Invite the bot to your server](http://nadekobot.readthedocs.io/en/latest/JSON%20Explanations/#inviting-your-bot-to-your-server). #### Guide From 273af5ffe45342ec715fb623f2d9ac9a18f2502f Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Sun, 25 Jun 2017 10:58:48 +0200 Subject: [PATCH 036/346] Slight change to the docs --- docs/JSON Explanations.md | 37 +++++++++++++++++++++---------------- 1 file changed, 21 insertions(+), 16 deletions(-) diff --git a/docs/JSON Explanations.md b/docs/JSON Explanations.md index 341cf992..14595dd4 100644 --- a/docs/JSON Explanations.md +++ b/docs/JSON Explanations.md @@ -19,7 +19,9 @@ If you do not see `credentials.json` you will need to rename `credentials_exampl "PatreonAccessToken": "", "Db": null, "TotalShards": 1, - "ShardRunCommand": "" + "ShardRunCommand": "", + "ShardRunArguments": "", + "ShardRunPort": null } ``` ----- @@ -155,21 +157,7 @@ It should look like: - For Patreon creators only. - **TotalShards** - Required if the bot will be connected to more than 1500 servers. - - Most likely unnecessary to change until your bot is added to more than 1500 servers. -- **ShardRunCommand** - - Command with which to run shards 1+ - - Required if you're sharding your bot on windows using .exe, or in a custom way. - - This internally defaults to `dotnet` - - For example, if you want to shard your NadekoBot which you installed using windows installer, you would want to set it to something like this: `C:\Program Files\NadekoBot\system\NadekoBot.exe` -- **ShardRunArguments** - - Arguments to the shard run command - - Required if you're sharding your bot on windows using .exe, or in a custom way. - - This internally defaults to `run -c Release -- {0} {1} {2}` which will be enough to run linux and other 'from source' setups - - {0} will be replaced by the `shard ID` of the shard being ran, {1} by the shard 0's process id, and {2} by the port shard communication is happening on - - If shard0 (main window) is closed, all other shards will close too - - For example, if you want to shard your NadekoBot which you installed using windows installer, you would want to set it to `{0} {1} {2}` -- **ShardRunPort** - - Bot uses a random UDP port in [5000, 6000) range for communication between shards + - Most likely unnecessary to change until your bot is added to more than 1500 servers. ----- ## DB files @@ -196,6 +184,23 @@ in order to open the database file you will need [DB Browser for SQLite](http:// and that will save all the changes. +## Sharding your bot + +- **ShardRunCommand** + - Command with which to run shards 1+ + - Required if you're sharding your bot on windows using .exe, or in a custom way. + - This internally defaults to `dotnet` + - For example, if you want to shard your NadekoBot which you installed using windows installer, you would want to set it to something like this: `C:\Program Files\NadekoBot\system\NadekoBot.exe` +- **ShardRunArguments** + - Arguments to the shard run command + - Required if you're sharding your bot on windows using .exe, or in a custom way. + - This internally defaults to `run -c Release -- {0} {1} {2}` which will be enough to run linux and other 'from source' setups + - {0} will be replaced by the `shard ID` of the shard being ran, {1} by the shard 0's process id, and {2} by the port shard communication is happening on + - If shard0 (main window) is closed, all other shards will close too + - For example, if you want to shard your NadekoBot which you installed using windows installer, you would want to set it to `{0} {1} {2}` +- **ShardRunPort** + - Bot uses a random UDP port in [5000, 6000) range for communication between shards + ![nadekodb](https://cdn.discordapp.com/attachments/251504306010849280/254067055240806400/nadekodb.gif) From 85a07d1cdddc9bdfb532863a2004046fde89f6f7 Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Sun, 25 Jun 2017 11:13:07 +0200 Subject: [PATCH 037/346] fixed string key --- src/NadekoBot/_strings/ResponseStrings.ar.json | 2 +- src/NadekoBot/_strings/ResponseStrings.cs-CZ.json | 2 +- src/NadekoBot/_strings/ResponseStrings.da-DK.json | 2 +- src/NadekoBot/_strings/ResponseStrings.de-DE.json | 2 +- src/NadekoBot/_strings/ResponseStrings.en-US.json | 2 +- src/NadekoBot/_strings/ResponseStrings.es-ES.json | 2 +- src/NadekoBot/_strings/ResponseStrings.fr-FR.json | 2 +- src/NadekoBot/_strings/ResponseStrings.he-IL.json | 2 +- src/NadekoBot/_strings/ResponseStrings.id-ID.json | 2 +- src/NadekoBot/_strings/ResponseStrings.it-IT.json | 2 +- src/NadekoBot/_strings/ResponseStrings.ja-JP.json | 2 +- src/NadekoBot/_strings/ResponseStrings.ko-KR.json | 2 +- src/NadekoBot/_strings/ResponseStrings.nb-NO.json | 2 +- src/NadekoBot/_strings/ResponseStrings.nl-NL.json | 2 +- src/NadekoBot/_strings/ResponseStrings.pl-PL.json | 2 +- src/NadekoBot/_strings/ResponseStrings.pt-BR.json | 2 +- src/NadekoBot/_strings/ResponseStrings.ro-RO.json | 2 +- src/NadekoBot/_strings/ResponseStrings.ru-RU.json | 2 +- src/NadekoBot/_strings/ResponseStrings.sr-cyrl-rs.json | 2 +- src/NadekoBot/_strings/ResponseStrings.sv-SE.json | 2 +- src/NadekoBot/_strings/ResponseStrings.tr-TR.json | 2 +- src/NadekoBot/_strings/ResponseStrings.zh-CN.json | 2 +- src/NadekoBot/_strings/ResponseStrings.zh-TW.json | 2 +- 23 files changed, 23 insertions(+), 23 deletions(-) diff --git a/src/NadekoBot/_strings/ResponseStrings.ar.json b/src/NadekoBot/_strings/ResponseStrings.ar.json index d214ef53..affd4249 100644 --- a/src/NadekoBot/_strings/ResponseStrings.ar.json +++ b/src/NadekoBot/_strings/ResponseStrings.ar.json @@ -243,7 +243,7 @@ "administration_sbdm": "لقد تم حظرك من {0} مزود.\nالسبب: {1}", "administration_user_unbanned": "ازالة الطرد عن المستخدم", "administration_migration_done": "تمت الهجرة !", - "adminsitration_migration_error": "حدث خطأ أثناء الترحيل، راجع وحدة تحكم بوت للحصول على مزيد من المعلومات.", + "administration_migration_error": "حدث خطأ أثناء الترحيل، راجع وحدة تحكم بوت للحصول على مزيد من المعلومات.", "administration_presence_updates": "التحديث الحالي", "administration_sb_user": "المستخدم طرد-مخفض", "gambling_awarded": "تم منح {0} الى {1}", diff --git a/src/NadekoBot/_strings/ResponseStrings.cs-CZ.json b/src/NadekoBot/_strings/ResponseStrings.cs-CZ.json index 594c4728..0e1e0be3 100644 --- a/src/NadekoBot/_strings/ResponseStrings.cs-CZ.json +++ b/src/NadekoBot/_strings/ResponseStrings.cs-CZ.json @@ -243,7 +243,7 @@ "administration_sbdm": "Byl jsi omezen na serveru {0}.\nDůvod: {1}", "administration_user_unbanned": "Uživatel odblokován", "administration_migration_done": "Migrace hotová!", - "adminsitration_migration_error": "Chyba při migrování, zkontroluj konzoli bota pro více informací.", + "administration_migration_error": "Chyba při migrování, zkontroluj konzoli bota pro více informací.", "administration_presence_updates": "Současné aktualizace", "administration_sb_user": "Uživatel omezen", "gambling_awarded": "odměnil uživatele {0} {1}", diff --git a/src/NadekoBot/_strings/ResponseStrings.da-DK.json b/src/NadekoBot/_strings/ResponseStrings.da-DK.json index 18afea3e..1142115d 100644 --- a/src/NadekoBot/_strings/ResponseStrings.da-DK.json +++ b/src/NadekoBot/_strings/ResponseStrings.da-DK.json @@ -243,7 +243,7 @@ "administration_sbdm": "Du er blevet soft-bannet fra {0} serveren.\nBegrundelse: {1}", "administration_user_unbanned": "Brugers udelukkelse ophævet", "administration_migration_done": "Migrasjon gjort!", - "adminsitration_migration_error": "", + "administration_migration_error": "", "administration_presence_updates": "Tilstedeværelses opdateringer", "administration_sb_user": "Brugeren blev soft-bannet", "gambling_awarded": "har givet {0} til {1}", diff --git a/src/NadekoBot/_strings/ResponseStrings.de-DE.json b/src/NadekoBot/_strings/ResponseStrings.de-DE.json index 6ad509b1..396a0f59 100644 --- a/src/NadekoBot/_strings/ResponseStrings.de-DE.json +++ b/src/NadekoBot/_strings/ResponseStrings.de-DE.json @@ -243,7 +243,7 @@ "administration_sbdm": "Sie wurden vom Server {0} gekickt.\nGrund: {1}", "administration_user_unbanned": "Benutzer entbannt", "administration_migration_done": "Migration fertig!", - "adminsitration_migration_error": "Fehler beim migrieren von Daten. Prüfe die Konsole des Bots für mehr Informationen.", + "administration_migration_error": "Fehler beim migrieren von Daten. Prüfe die Konsole des Bots für mehr Informationen.", "administration_presence_updates": "Anwesenheits Änderungen", "administration_sb_user": "Nutzer wurde gekickt", "gambling_awarded": "gab {0} an {1}", diff --git a/src/NadekoBot/_strings/ResponseStrings.en-US.json b/src/NadekoBot/_strings/ResponseStrings.en-US.json index f405c63e..762cccfb 100644 --- a/src/NadekoBot/_strings/ResponseStrings.en-US.json +++ b/src/NadekoBot/_strings/ResponseStrings.en-US.json @@ -249,7 +249,7 @@ "administration_sbdm": "You have been soft-banned from {0} server.\nReason: {1}", "administration_user_unbanned": "User unbanned", "administration_migration_done": "Migration done!", - "adminsitration_migration_error": "Error while migrating, check bot's console for more information.", + "administration_migration_error": "Error while migrating, check bot's console for more information.", "administration_presence_updates": "Presence updates", "administration_sb_user": "User soft-banned", "gambling_awarded": "has awarded {0} to {1}", diff --git a/src/NadekoBot/_strings/ResponseStrings.es-ES.json b/src/NadekoBot/_strings/ResponseStrings.es-ES.json index 448a164a..06f38e5f 100644 --- a/src/NadekoBot/_strings/ResponseStrings.es-ES.json +++ b/src/NadekoBot/_strings/ResponseStrings.es-ES.json @@ -243,7 +243,7 @@ "administration_sbdm": "Has sido advertido en el servidor {0}. \nRazón: {1}", "administration_user_unbanned": "Usuario desbloqueado:", "administration_migration_done": "¡Migración terminada!", - "adminsitration_migration_error": "Error al migrar, revisa la consola para más información.", + "administration_migration_error": "Error al migrar, revisa la consola para más información.", "administration_presence_updates": "Actualizaciones de presencia", "administration_sb_user": "Usuario advertido", "gambling_awarded": "le ha regalado {0} a {1}", diff --git a/src/NadekoBot/_strings/ResponseStrings.fr-FR.json b/src/NadekoBot/_strings/ResponseStrings.fr-FR.json index bf1e517c..9561f554 100644 --- a/src/NadekoBot/_strings/ResponseStrings.fr-FR.json +++ b/src/NadekoBot/_strings/ResponseStrings.fr-FR.json @@ -243,7 +243,7 @@ "administration_sbdm": "Vous avez été expulsé du serveur {0}.\nRaison: {1}", "administration_user_unbanned": "Utilisateur débanni", "administration_migration_done": "Migration effectuée!", - "adminsitration_migration_error": "Erreur lors de la migration, veuillez consulter la console pour plus d'informations.", + "administration_migration_error": "Erreur lors de la migration, veuillez consulter la console pour plus d'informations.", "administration_presence_updates": "Présences mises à jour.", "administration_sb_user": "Utilisateur expulsé.", "gambling_awarded": "a récompensé {0} à {1}", diff --git a/src/NadekoBot/_strings/ResponseStrings.he-IL.json b/src/NadekoBot/_strings/ResponseStrings.he-IL.json index 39fe3af7..7b198d5d 100644 --- a/src/NadekoBot/_strings/ResponseStrings.he-IL.json +++ b/src/NadekoBot/_strings/ResponseStrings.he-IL.json @@ -243,7 +243,7 @@ "administration_sbdm": "אתה קיבלת איסור רך מהסרבר {0}.\nסיבה: {1}", "administration_user_unbanned": "משתמש ונאסר", "administration_migration_done": "נדידה גמורה!", - "adminsitration_migration_error": "שגיעה בזמן הנדידה, תבדוק את ה מסוף בקרה של הבוט בשביל יותר מידע.\n\n ", + "administration_migration_error": "שגיעה בזמן הנדידה, תבדוק את ה מסוף בקרה של הבוט בשביל יותר מידע.\n\n ", "administration_presence_updates": "עדכון נוכחות", "administration_sb_user": "משתמש קיבל איסור רך.", "gambling_awarded": "העניק {0} ל{1}", diff --git a/src/NadekoBot/_strings/ResponseStrings.id-ID.json b/src/NadekoBot/_strings/ResponseStrings.id-ID.json index e8a5ee2c..cdb33708 100644 --- a/src/NadekoBot/_strings/ResponseStrings.id-ID.json +++ b/src/NadekoBot/_strings/ResponseStrings.id-ID.json @@ -243,7 +243,7 @@ "administration_sbdm": "Anda telah diban halus dari server {0}.\nAlasan: {1}", "administration_user_unbanned": "Pengguna telah di unban", "administration_migration_done": "Migrasi selesai!", - "adminsitration_migration_error": "Error ketika migrasi, cek konsol bot untuk informasi lebih lanjut.", + "administration_migration_error": "Error ketika migrasi, cek konsol bot untuk informasi lebih lanjut.", "administration_presence_updates": "Pembaharuan kehadiran", "administration_sb_user": "Pengguna diban halus", "gambling_awarded": "Telah menyerahkan {0} ke {1}", diff --git a/src/NadekoBot/_strings/ResponseStrings.it-IT.json b/src/NadekoBot/_strings/ResponseStrings.it-IT.json index f77d54f0..d0d058f7 100644 --- a/src/NadekoBot/_strings/ResponseStrings.it-IT.json +++ b/src/NadekoBot/_strings/ResponseStrings.it-IT.json @@ -243,7 +243,7 @@ "administration_sbdm": "Sei statto cacciato da {0} server.\nMotivo : {1}", "administration_user_unbanned": "Utente sbannato", "administration_migration_done": "Trasferimento completato!", - "adminsitration_migration_error": "Errore durante il trasferimento, guarda la console del bot per più informazioni.", + "administration_migration_error": "Errore durante il trasferimento, guarda la console del bot per più informazioni.", "administration_presence_updates": "Aggiornamenti sulla presenza", "administration_sb_user": "Utente ammonito", "gambling_awarded": "ha regalato {0} to {1}", diff --git a/src/NadekoBot/_strings/ResponseStrings.ja-JP.json b/src/NadekoBot/_strings/ResponseStrings.ja-JP.json index 78e7d425..4655d076 100644 --- a/src/NadekoBot/_strings/ResponseStrings.ja-JP.json +++ b/src/NadekoBot/_strings/ResponseStrings.ja-JP.json @@ -243,7 +243,7 @@ "administration_sbdm": "あなたは{0}サーバーからソフト禁止されました。\n理由:{1}", "administration_user_unbanned": "Since I'm using \"exiled\" for banned. need to rethink a new word.. so just leave this one alone\n", "administration_migration_done": "移行が完了しました!\n", - "adminsitration_migration_error": "移行中にエラーが発生しました。詳細については、ボットのコンソールを確認してください。\n", + "administration_migration_error": "移行中にエラーが発生しました。詳細については、ボットのコンソールを確認してください。\n", "administration_presence_updates": "存在の更新", "administration_sb_user": "ユーザはソフトバン", "gambling_awarded": "{0}から{1}に授与されました\n", diff --git a/src/NadekoBot/_strings/ResponseStrings.ko-KR.json b/src/NadekoBot/_strings/ResponseStrings.ko-KR.json index e0444f6c..535c2900 100644 --- a/src/NadekoBot/_strings/ResponseStrings.ko-KR.json +++ b/src/NadekoBot/_strings/ResponseStrings.ko-KR.json @@ -243,7 +243,7 @@ "administration_sbdm": "당신은 {0} 서버에서 소프트밴을 당했습니다.\n사유: {1}", "administration_user_unbanned": "사용자 밴 해제", "administration_migration_done": "이전 완료!", - "adminsitration_migration_error": "이전 과정에서 오류가 발생했습니다. 자세한 정보는 봇의 콘솔을 통해서 확인하세요.", + "administration_migration_error": "이전 과정에서 오류가 발생했습니다. 자세한 정보는 봇의 콘솔을 통해서 확인하세요.", "administration_presence_updates": "현재 상태 업데이트", "administration_sb_user": "사용자 소프트 밴", "gambling_awarded": "님이 {1}에게 {0}개를 지급했습니다.", diff --git a/src/NadekoBot/_strings/ResponseStrings.nb-NO.json b/src/NadekoBot/_strings/ResponseStrings.nb-NO.json index 0f98d7d6..4ff837fd 100644 --- a/src/NadekoBot/_strings/ResponseStrings.nb-NO.json +++ b/src/NadekoBot/_strings/ResponseStrings.nb-NO.json @@ -243,7 +243,7 @@ "administration_sbdm": "Du har blitt 'soft-banned' fra {0} server.\nGrunn: {1}", "administration_user_unbanned": "Fjernet bruker fra utestenging", "administration_migration_done": "Migrering fullført!", - "adminsitration_migration_error": "Feil under migrering, sjekk botens konsoll for mer informasjon.", + "administration_migration_error": "Feil under migrering, sjekk botens konsoll for mer informasjon.", "administration_presence_updates": "Oppdatering av tilstedeværelse", "administration_sb_user": "Bruker soft-banned", "gambling_awarded": "har tildelt {0} til {1}", diff --git a/src/NadekoBot/_strings/ResponseStrings.nl-NL.json b/src/NadekoBot/_strings/ResponseStrings.nl-NL.json index d223eb8d..d4975dc7 100644 --- a/src/NadekoBot/_strings/ResponseStrings.nl-NL.json +++ b/src/NadekoBot/_strings/ResponseStrings.nl-NL.json @@ -243,7 +243,7 @@ "administration_sbdm": "Je bent soft-gebant van {0} server.\nReden: {1}", "administration_user_unbanned": "Gebruiker niet meer verbannen", "administration_migration_done": "Migratie klaar!", - "adminsitration_migration_error": "Fout tijdens het migreren, kijk in de bot zijn console voor meer informatie.", + "administration_migration_error": "Fout tijdens het migreren, kijk in de bot zijn console voor meer informatie.", "administration_presence_updates": "Aanwezigheid updates", "administration_sb_user": "Gebruiker soft-gebant", "gambling_awarded": "heeft {0} aan {1} gegeven", diff --git a/src/NadekoBot/_strings/ResponseStrings.pl-PL.json b/src/NadekoBot/_strings/ResponseStrings.pl-PL.json index abfb1764..4cc15df4 100644 --- a/src/NadekoBot/_strings/ResponseStrings.pl-PL.json +++ b/src/NadekoBot/_strings/ResponseStrings.pl-PL.json @@ -243,7 +243,7 @@ "administration_sbdm": "Zostałeś tymczasowo zablokowany na serwerze {0}\nPowód: {1}", "administration_user_unbanned": "Użytkownik został odbanowany", "administration_migration_done": "Migracja zakończona!", - "adminsitration_migration_error": "Błąd podczas migracji, sprawdz konsole bota po więcej informacji.", + "administration_migration_error": "Błąd podczas migracji, sprawdz konsole bota po więcej informacji.", "administration_presence_updates": "Aktualizacje obecności", "administration_sb_user": "Użytkownik tymczasowo zablokowany", "gambling_awarded": "nagrodził {0} {1}", diff --git a/src/NadekoBot/_strings/ResponseStrings.pt-BR.json b/src/NadekoBot/_strings/ResponseStrings.pt-BR.json index 894f5604..4e8d01b7 100644 --- a/src/NadekoBot/_strings/ResponseStrings.pt-BR.json +++ b/src/NadekoBot/_strings/ResponseStrings.pt-BR.json @@ -243,7 +243,7 @@ "administration_sbdm": "Você foi banido temporariamente do servidor {0}.\nMotivo: {1}", "administration_user_unbanned": "Usuário desbanido", "administration_migration_done": "Migração concluída!", - "adminsitration_migration_error": "Erro enquanto migrava, verifique o console do bot para mais informações.", + "administration_migration_error": "Erro enquanto migrava, verifique o console do bot para mais informações.", "administration_presence_updates": "Atualizações de Presença", "administration_sb_user": "Usuário Banido Temporariamente", "gambling_awarded": "concedeu {0} para {1}", diff --git a/src/NadekoBot/_strings/ResponseStrings.ro-RO.json b/src/NadekoBot/_strings/ResponseStrings.ro-RO.json index 27eaa985..864bb8f3 100644 --- a/src/NadekoBot/_strings/ResponseStrings.ro-RO.json +++ b/src/NadekoBot/_strings/ResponseStrings.ro-RO.json @@ -243,7 +243,7 @@ "administration_sbdm": "Ai fost banat-ușor din serverul {0}.\nMotivul: {1}", "administration_user_unbanned": "Utilizator bannat.", "administration_migration_done": "Migrare terminata!", - "adminsitration_migration_error": "Eroare in timpul migrarii, verifica consola bot-ului pentru mai multe informatii.", + "administration_migration_error": "Eroare in timpul migrarii, verifica consola bot-ului pentru mai multe informatii.", "administration_presence_updates": "Actualizări de prezență.", "administration_sb_user": "Utilizator banat-ușor.", "gambling_awarded": "A acordat {0} la {1}.", diff --git a/src/NadekoBot/_strings/ResponseStrings.ru-RU.json b/src/NadekoBot/_strings/ResponseStrings.ru-RU.json index 1955c4fe..e2d15d70 100644 --- a/src/NadekoBot/_strings/ResponseStrings.ru-RU.json +++ b/src/NadekoBot/_strings/ResponseStrings.ru-RU.json @@ -243,7 +243,7 @@ "administration_sbdm": "Вас забанили на сервере {0}. Причина: {1}", "administration_user_unbanned": "Пользователь разбанен.", "administration_migration_done": "Перемещение закончено!", - "adminsitration_migration_error": "Ошибка при переносе файлов, проверьте консоль бота для получения дальнейшей информации.", + "administration_migration_error": "Ошибка при переносе файлов, проверьте консоль бота для получения дальнейшей информации.", "administration_presence_updates": "История присутвия пользователей", "administration_sb_user": "Пользователя выгнали", "gambling_awarded": "наградил {0} пользователю {1}", diff --git a/src/NadekoBot/_strings/ResponseStrings.sr-cyrl-rs.json b/src/NadekoBot/_strings/ResponseStrings.sr-cyrl-rs.json index 703735f1..58f91079 100644 --- a/src/NadekoBot/_strings/ResponseStrings.sr-cyrl-rs.json +++ b/src/NadekoBot/_strings/ResponseStrings.sr-cyrl-rs.json @@ -243,7 +243,7 @@ "administration_sbdm": "You have been soft-banned from {0} server.\nReason: {1}", "administration_user_unbanned": "User Unbanned", "administration_migration_done": "Migration done!", - "adminsitration_migration_error": "Error while migrating, check bot's console for more information.", + "administration_migration_error": "Error while migrating, check bot's console for more information.", "administration_presence_updates": "Presence Updates", "administration_sb_user": "User Soft-Banned", "gambling_awarded": "has awarded {0} to {1}", diff --git a/src/NadekoBot/_strings/ResponseStrings.sv-SE.json b/src/NadekoBot/_strings/ResponseStrings.sv-SE.json index 692215a4..4e170a60 100644 --- a/src/NadekoBot/_strings/ResponseStrings.sv-SE.json +++ b/src/NadekoBot/_strings/ResponseStrings.sv-SE.json @@ -243,7 +243,7 @@ "administration_sbdm": "Du har blivit mjuk-bannad från {0} server.\nAnledning: {1}", "administration_user_unbanned": "Användare inte längre bannad.", "administration_migration_done": "Migration gjort!", - "adminsitration_migration_error": "Fel vid migrering, kontrollera botens konsol för mer information.", + "administration_migration_error": "Fel vid migrering, kontrollera botens konsol för mer information.", "administration_presence_updates": "Närvaro Uppdateringar.", "administration_sb_user": "Användare mjuk-bannad.", "gambling_awarded": "har tilldelat {0} till {1}", diff --git a/src/NadekoBot/_strings/ResponseStrings.tr-TR.json b/src/NadekoBot/_strings/ResponseStrings.tr-TR.json index 245f9b0e..8ed3478a 100644 --- a/src/NadekoBot/_strings/ResponseStrings.tr-TR.json +++ b/src/NadekoBot/_strings/ResponseStrings.tr-TR.json @@ -243,7 +243,7 @@ "administration_sbdm": "{0} sunucusundan hafif bir şekilde yasaklandınız.\nSebep: {1}", "administration_user_unbanned": "Kullanıcı yasağı kaldırıldı", "administration_migration_done": "Eski kayıtlar taşındı!", - "adminsitration_migration_error": "Taşıma işlemi sırasında hata oluştu, daha fazla bilgi için bot konsolunu kontrol edin.", + "administration_migration_error": "Taşıma işlemi sırasında hata oluştu, daha fazla bilgi için bot konsolunu kontrol edin.", "administration_presence_updates": "Durum güncellemeleri", "administration_sb_user": "Kullanıcı hafif bir şekilde yasaklandı.", "gambling_awarded": "{1} 'e {0} ödül vermiş", diff --git a/src/NadekoBot/_strings/ResponseStrings.zh-CN.json b/src/NadekoBot/_strings/ResponseStrings.zh-CN.json index 7c9e5934..7cedbfae 100644 --- a/src/NadekoBot/_strings/ResponseStrings.zh-CN.json +++ b/src/NadekoBot/_strings/ResponseStrings.zh-CN.json @@ -243,7 +243,7 @@ "administration_sbdm": "您已从{0}服务器软禁止。\n原因:{1}", "administration_user_unbanned": "用户已取消禁止", "administration_migration_done": "迁移完成!", - "adminsitration_migration_error": "在迁移时出错,请检查机器人的控制台以获取更多信息。", + "administration_migration_error": "在迁移时出错,请检查机器人的控制台以获取更多信息。", "administration_presence_updates": "在线状态更新", "administration_sb_user": "用户被软禁用", "gambling_awarded": "已将{0}奖励给{1}", diff --git a/src/NadekoBot/_strings/ResponseStrings.zh-TW.json b/src/NadekoBot/_strings/ResponseStrings.zh-TW.json index e2618efd..3c142472 100644 --- a/src/NadekoBot/_strings/ResponseStrings.zh-TW.json +++ b/src/NadekoBot/_strings/ResponseStrings.zh-TW.json @@ -243,7 +243,7 @@ "administration_sbdm": "您已被伺服器 {0} 軟禁。\n理由: {1}", "administration_user_unbanned": "成員已解禁", "administration_migration_done": "合併完成!", - "adminsitration_migration_error": "在合併時發生問題,請查閱命令視窗來取得更多資訊。", + "administration_migration_error": "在合併時發生問題,請查閱命令視窗來取得更多資訊。", "administration_presence_updates": "在線狀態更新", "administration_sb_user": "成員被軟禁", "gambling_awarded": "賜予了 {1} 給 {0}", From aa5b7f96c769cd8f7a8e61a2c65ee233ecf50cc6 Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Tue, 27 Jun 2017 01:01:17 +0200 Subject: [PATCH 038/346] Bot no longer awards itself currency in flower events. closes #1266 --- src/NadekoBot/Modules/Gambling/Commands/CurrencyEvents.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/NadekoBot/Modules/Gambling/Commands/CurrencyEvents.cs b/src/NadekoBot/Modules/Gambling/Commands/CurrencyEvents.cs index 644823bc..a892e594 100644 --- a/src/NadekoBot/Modules/Gambling/Commands/CurrencyEvents.cs +++ b/src/NadekoBot/Modules/Gambling/Commands/CurrencyEvents.cs @@ -153,6 +153,7 @@ namespace NadekoBot.Modules.Gambling private readonly Logger _log; private readonly DiscordSocketClient _client; private readonly CurrencyService _cs; + private readonly SocketSelfUser _botUser; private IUserMessage StartingMessage { get; set; } @@ -164,6 +165,7 @@ namespace NadekoBot.Modules.Gambling _log = LogManager.GetCurrentClassLogger(); _client = client; _cs = cs; + _botUser = client.CurrentUser; Source = new CancellationTokenSource(); CancelToken = Source.Token; } @@ -208,6 +210,9 @@ namespace NadekoBot.Modules.Gambling { try { + if (r.UserId == _botUser.Id) + return; + if (r.Emote.Name == "🌸" && r.User.IsSpecified && ((DateTime.UtcNow - r.User.Value.CreatedAt).TotalDays > 5) && _flowerReactionAwardedUsers.Add(r.User.Value.Id)) { await _cs.AddAsync(r.User.Value, "Flower Reaction Event", amount, false) From 942f15cf0512b8997a24493ae5593ef2e69fd932 Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Wed, 28 Jun 2017 02:44:30 +0200 Subject: [PATCH 039/346] WHEW. Added placeholders in embeds and quotes, added docs about it to explained features. Wrote a placeholder system and fixed some bugs --- docs/Custom Reactions.md | 12 +- docs/Placeholders.md | 24 ++++ docs/index.md | 1 + mkdocs.yml | 3 +- .../Replacements/ReplacementBuilder.cs | 132 ++++++++++++++++++ .../DataStructures/Replacements/Replacer.cs | 54 +++++++ .../Commands/PlayingRotateCommands.cs | 10 +- .../Modules/Utility/Commands/QuoteCommands.cs | 52 +++---- src/NadekoBot/NadekoBot.cs | 2 +- src/NadekoBot/Resources/CommandStrings.resx | 17 +-- .../Administration/PlayingRotateService.cs | 68 ++++----- .../Services/CustomReactions/Extensions.cs | 95 ++++--------- .../Services/GreetSettingsService.cs | 48 ++++--- .../Services/Utility/RemindService.cs | 20 ++- 14 files changed, 328 insertions(+), 210 deletions(-) create mode 100644 docs/Placeholders.md create mode 100644 src/NadekoBot/DataStructures/Replacements/ReplacementBuilder.cs create mode 100644 src/NadekoBot/DataStructures/Replacements/Replacer.cs diff --git a/docs/Custom Reactions.md b/docs/Custom Reactions.md index 8d41afca..7926028d 100644 --- a/docs/Custom Reactions.md +++ b/docs/Custom Reactions.md @@ -36,14 +36,4 @@ For example: Now if you try to trigger `/o/`, it won't print anything. ###Placeholders! -There are currently three different placeholders which we will look at, with more placeholders potentially coming in the future. - -| Placeholder | Description | Example Usage | Usage | -|:-----------:|-------------|---------------|-------| -|`%mention%`|The `%mention%` placeholder is triggered when you type `@BotName` - It's important to note that if you've given the bot a custom nickname, this trigger won't work!|```.acr "Hello %mention%" I, %mention%, also say hello!```|Input: "Hello @BotName" Output: "I, @BotName, also say hello!"| -|`%user%`|The `%user%` placeholder mentions the person who said the command|`.acr "Who am I?" You are %user%!`|Input: "Who am I?" Output: "You are @Username!"| -|`%rng%`|The `%rng%` placeholder generates a random number between 0 and 10. You can also specify a custom range (%rng1-100%) even with negative numbers: `%rng-9--1%` (from -9 to -1) . |`.acr "Random number" %rng%`|Input: "Random number" Output: "2"| -|`%rnduser%`|The `%rnduser%` placeholder mentions a random user from the server. |`.acr "Random user" %rnduser%`|Input: "Random number" Output: @SomeUser| -|`%target%`|The `%target%` placeholder is used to make Nadeko Mention another person or phrase, it is only supported as part of the response|`.acr "Say this: " %target%`|Input: "Say this: I, @BotName, am a parrot!". Output: "I, @BotName, am a parrot!".| - - Thanks to Nekai for being creative. <3 +To learn about placeholders, go [here](Placeholders.md) diff --git a/docs/Placeholders.md b/docs/Placeholders.md new file mode 100644 index 00000000..0039e0a3 --- /dev/null +++ b/docs/Placeholders.md @@ -0,0 +1,24 @@ +Placeholders are used in Quotes, Custom Reactions, Greet/bye messages, playing statuses, and a few other places. + +They can be used to make the message more user friendly, generate random numbers or pictures, etc... + +Some features have their own specific placeholders which are noted in that feature's command help. Some placeholders are not available in certain features because they don't make sense there. + +### Here is a list of the usual placeholders: +- `%mention%` - Mention the bot +- `%shardid%` - Shard id +- `%server%` - Server name +- `%sid%` - Server Id +- `%channel%` - Channel mention +- `%chname%` - Channel mention +- `%cid%` - Channel Id +- `%user%` - User mention +- `%id%` or `%uid%` - User Id +- `%userfull%` - Username#discriminator +- `%userdiscrim%` - discriminator (for example 1234) +- `%rngX-Y%` - Replace X and Y with the range (for example `%rng5-10%` - random between 5 and 10) +- `%time%` - Bot time + +**If you're using placeholders in embeds, don't use %user% and in titles, footers and field names. They will not show properly.** + +![img](http://i.imgur.com/lNNNfs1.png) \ No newline at end of file diff --git a/docs/index.md b/docs/index.md index 5e67f8dc..b7bd5ed4 100644 --- a/docs/index.md +++ b/docs/index.md @@ -28,6 +28,7 @@ If you want to contribute, be sure to PR on the **[1.4][1.4]** branch. - [Permissions System](Permissions System.md) - [JSON Explanations](JSON Explanations.md) - [Custom Reactions](Custom Reactions.md) + - [Placeholders](Placeholders.md) - [Frequently Asked Questions](Frequently Asked Questions.md) - [Contribution Guide](Contribution Guide.md) - [Donate](Donate.md) diff --git a/mkdocs.yml b/mkdocs.yml index e8ccbbad..0ab5e11b 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -14,9 +14,10 @@ pages: - Readme: Readme.md - Commands List: Commands List.md - Features Explained: - - Permissions System: Permissions System.md - JSON Explanations: JSON Explanations.md + - Permissions System: Permissions System.md - Custom Reactions: Custom Reactions.md + - Placeholders: Placeholders.md - Frequently Asked Questions: Frequently Asked Questions.md - Contribution Guide: Contribution Guide.md - ❤ Donate ❤: Donate.md diff --git a/src/NadekoBot/DataStructures/Replacements/ReplacementBuilder.cs b/src/NadekoBot/DataStructures/Replacements/ReplacementBuilder.cs new file mode 100644 index 00000000..b7fabd25 --- /dev/null +++ b/src/NadekoBot/DataStructures/Replacements/ReplacementBuilder.cs @@ -0,0 +1,132 @@ +using Discord; +using Discord.Commands; +using Discord.WebSocket; +using NadekoBot.Extensions; +using NadekoBot.Services; +using NadekoBot.Services.Music; +using System; +using System.Collections.Concurrent; +using System.Linq; +using System.Text.RegularExpressions; + +namespace NadekoBot.DataStructures.Replacements +{ + public class ReplacementBuilder + { + private static readonly Regex rngRegex = new Regex("%rng(?:(?(?:-)?\\d+)-(?(?:-)?\\d+))?%", RegexOptions.Compiled); + private ConcurrentDictionary> _reps = new ConcurrentDictionary>(); + private ConcurrentDictionary> _regex = new ConcurrentDictionary>(); + + public ReplacementBuilder() + { + WithRngRegex(); + } + + public ReplacementBuilder WithDefault(IUser usr, IMessageChannel ch, IGuild g, DiscordSocketClient client) + { + return this.WithUser(usr) + .WithChannel(ch) + .WithServer(g) + .WithClient(client); + } + + public ReplacementBuilder WithDefault(ICommandContext ctx) => + WithDefault(ctx.User, ctx.Channel, ctx.Guild, (DiscordSocketClient)ctx.Client); + + public ReplacementBuilder WithClient(DiscordSocketClient client) + { + _reps.TryAdd("%mention%", () => $"<@{client.CurrentUser.Id}>"); + _reps.TryAdd("%shardid%", () => client.ShardId.ToString()); + _reps.TryAdd("%time%", () => DateTime.Now.ToString("HH:mm " + TimeZoneInfo.Local.StandardName.GetInitials())); + return this; + } + + public ReplacementBuilder WithServer(IGuild g) + { + _reps.TryAdd("%sid%", () => g == null ? "DM" : g.Id.ToString()); + _reps.TryAdd("%server%", () => g == null ? "DM" : g.Name); + return this; + } + + public ReplacementBuilder WithChannel(IMessageChannel ch) + { + _reps.TryAdd("%channel%", () => (ch as ITextChannel)?.Mention ?? "#" + ch.Name); + _reps.TryAdd("%chname%", () => ch.Name); + _reps.TryAdd("%cid%", () => ch?.Id.ToString()); + return this; + } + + public ReplacementBuilder WithUser(IUser user) + { + _reps.TryAdd("%user%", () => user.Mention); + _reps.TryAdd("%userfull%", () => user.ToString()); + _reps.TryAdd("%username%", () => user.Username); + _reps.TryAdd("%userdiscrim%", () => user.Discriminator); + _reps.TryAdd("%id%", () => user.Id.ToString()); + _reps.TryAdd("%uid%", () => user.Id.ToString()); + return this; + } + + public ReplacementBuilder WithStats(DiscordSocketClient c) + { + _reps.TryAdd("%servers%", () => c.Guilds.Count.ToString()); + _reps.TryAdd("%users%", () => c.Guilds.Sum(s => s.Users.Count).ToString()); + return this; + } + + public ReplacementBuilder WithMusic(MusicService ms) + { + _reps.TryAdd("%playing%", () => + { + var cnt = ms.MusicPlayers.Count(kvp => kvp.Value.CurrentSong != null); + if (cnt != 1) return cnt.ToString(); + try + { + var mp = ms.MusicPlayers.FirstOrDefault(); + return mp.Value.CurrentSong.SongInfo.Title; + } + catch + { + return "No songs"; + } + }); + _reps.TryAdd("%queued%", () => ms.MusicPlayers.Sum(kvp => kvp.Value.Playlist.Count).ToString()); + return this; + } + + public ReplacementBuilder WithRngRegex() + { + var rng = new NadekoRandom(); + _regex.TryAdd(rngRegex, (match) => + { + int from = 0; + int.TryParse(match.Groups["from"].ToString(), out from); + + int to = 0; + int.TryParse(match.Groups["to"].ToString(), out to); + + if (from == 0 && to == 0) + { + return rng.Next(0, 11).ToString(); + } + + if (from >= to) + return string.Empty; + + return rng.Next(from, to + 1).ToString(); + }); + return this; + } + + public ReplacementBuilder WithOverride(string key, Func output) + { + _reps.AddOrUpdate(key, output, delegate { return output; }); + return this; + } + + public Replacer Build() + { + return new Replacer(_reps.Select(x => (x.Key, x.Value)).ToArray(), _regex.Select(x => (x.Key, x.Value)).ToArray()); + } + } +} diff --git a/src/NadekoBot/DataStructures/Replacements/Replacer.cs b/src/NadekoBot/DataStructures/Replacements/Replacer.cs new file mode 100644 index 00000000..ec1bbe7e --- /dev/null +++ b/src/NadekoBot/DataStructures/Replacements/Replacer.cs @@ -0,0 +1,54 @@ +using System; +using System.Collections.Generic; +using System.Text.RegularExpressions; + +namespace NadekoBot.DataStructures.Replacements +{ + public class Replacer + { + private readonly IEnumerable<(string Key, Func Text)> _replacements; + private readonly IEnumerable<(Regex Regex, Func Replacement)> _regex; + + public Replacer(IEnumerable<(string, Func)> replacements, IEnumerable<(Regex, Func)> regex) + { + _replacements = replacements; + _regex = regex; + } + + public string Replace(string input) + { + if (string.IsNullOrWhiteSpace(input)) + return input; + + foreach (var item in _replacements) + { + if (input.Contains(item.Key)) + input = input.Replace(item.Key, item.Text()); + } + + foreach (var item in _regex) + { + input = item.Regex.Replace(input, (m) => item.Replacement(m)); + } + + return input; + } + + public void Replace(CREmbed embedData) + { + embedData.PlainText = Replace(embedData.PlainText); + embedData.Description = Replace(embedData.Description); + embedData.Title = Replace(embedData.Title); + + if (embedData.Fields != null) + foreach (var f in embedData.Fields) + { + f.Name = Replace(f.Name); + f.Value = Replace(f.Value); + } + + if (embedData.Footer != null) + embedData.Footer.Text = Replace(embedData.Footer.Text); + } + } +} diff --git a/src/NadekoBot/Modules/Administration/Commands/PlayingRotateCommands.cs b/src/NadekoBot/Modules/Administration/Commands/PlayingRotateCommands.cs index 47c39849..867ff91a 100644 --- a/src/NadekoBot/Modules/Administration/Commands/PlayingRotateCommands.cs +++ b/src/NadekoBot/Modules/Administration/Commands/PlayingRotateCommands.cs @@ -33,11 +33,11 @@ namespace NadekoBot.Modules.Administration { var config = uow.BotConfig.GetOrCreate(); - _service.RotatingStatuses = config.RotatingStatuses = !config.RotatingStatuses; + config.RotatingStatuses = !config.RotatingStatuses; uow.Complete(); } } - if (_service.RotatingStatuses) + if (_service.BotConfig.RotatingStatuses) await ReplyConfirmLocalized("ropl_enabled").ConfigureAwait(false); else await ReplyConfirmLocalized("ropl_disabled").ConfigureAwait(false); @@ -52,7 +52,6 @@ namespace NadekoBot.Modules.Administration var config = uow.BotConfig.GetOrCreate(); var toAdd = new PlayingStatus { Status = status }; config.RotatingStatusMessages.Add(toAdd); - _service.RotatingStatusMessages.Add(toAdd); await uow.CompleteAsync(); } @@ -63,13 +62,13 @@ namespace NadekoBot.Modules.Administration [OwnerOnly] public async Task ListPlaying() { - if (!_service.RotatingStatusMessages.Any()) + if (!_service.BotConfig.RotatingStatusMessages.Any()) await ReplyErrorLocalized("ropl_not_set").ConfigureAwait(false); else { var i = 1; await ReplyConfirmLocalized("ropl_list", - string.Join("\n\t", _service.RotatingStatusMessages.Select(rs => $"`{i++}.` {rs.Status}"))) + string.Join("\n\t", _service.BotConfig.RotatingStatusMessages.Select(rs => $"`{i++}.` {rs.Status}"))) .ConfigureAwait(false); } @@ -90,7 +89,6 @@ namespace NadekoBot.Modules.Administration return; msg = config.RotatingStatusMessages[index].Status; config.RotatingStatusMessages.RemoveAt(index); - _service.RotatingStatusMessages.RemoveAt(index); await uow.CompleteAsync(); } await ReplyConfirmLocalized("reprm", msg).ConfigureAwait(false); diff --git a/src/NadekoBot/Modules/Utility/Commands/QuoteCommands.cs b/src/NadekoBot/Modules/Utility/Commands/QuoteCommands.cs index 5ac2e69a..dcdc035a 100644 --- a/src/NadekoBot/Modules/Utility/Commands/QuoteCommands.cs +++ b/src/NadekoBot/Modules/Utility/Commands/QuoteCommands.cs @@ -9,6 +9,7 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using NadekoBot.DataStructures; +using NadekoBot.DataStructures.Replacements; namespace NadekoBot.Modules.Utility { @@ -66,19 +67,14 @@ namespace NadekoBot.Modules.Utility if (quote == null) return; - CREmbed crembed; - if (CREmbed.TryParse(quote.Text, out crembed)) + if (CREmbed.TryParse(quote.Text, out var crembed)) { - try - { - await Context.Channel.EmbedAsync(crembed.ToEmbed(), crembed.PlainText ?? "") - .ConfigureAwait(false); - } - catch (Exception ex) - { - _log.Warn("Sending CREmbed failed"); - _log.Warn(ex); - } + new ReplacementBuilder() + .WithDefault(Context) + .Build() + .Replace(crembed); + await Context.Channel.EmbedAsync(crembed.ToEmbed(), crembed.PlainText?.SanitizeMentions() ?? "") + .ConfigureAwait(false); return; } await Context.Channel.SendMessageAsync($"`#{quote.Id}` 📣 " + quote.Text.SanitizeMentions()); @@ -118,29 +114,27 @@ namespace NadekoBot.Modules.Utility using (var uow = _db.UnitOfWork) { var qfromid = uow.Quotes.Get(id); - CREmbed crembed; - + if (qfromid == null) { await Context.Channel.SendErrorAsync(GetText("quotes_notfound")); } - else if (CREmbed.TryParse(qfromid.Text, out crembed)) + else if (CREmbed.TryParse(qfromid.Text, out var crembed)) { - try - { - await Context.Channel.EmbedAsync(crembed.ToEmbed(), crembed.PlainText ?? "") - .ConfigureAwait(false); - } - catch (Exception ex) - { - _log.Warn("Sending CREmbed failed"); - _log.Warn(ex); - } - return; + new ReplacementBuilder() + .WithDefault(Context) + .Build() + .Replace(crembed); + + await Context.Channel.EmbedAsync(crembed.ToEmbed(), crembed.PlainText?.SanitizeMentions() ?? "") + .ConfigureAwait(false); } - - else { await Context.Channel.SendMessageAsync($"`#{qfromid.Id}` 🗯️ " + qfromid.Keyword.ToLowerInvariant().SanitizeMentions() + ": " + - qfromid.Text.SanitizeMentions()); } + else + { + await Context.Channel.SendMessageAsync($"`#{qfromid.Id}` 🗯️ " + qfromid.Keyword.ToLowerInvariant().SanitizeMentions() + ": " + + qfromid.Text.SanitizeMentions()); + } + } } diff --git a/src/NadekoBot/NadekoBot.cs b/src/NadekoBot/NadekoBot.cs index ac63f42a..c75878b3 100644 --- a/src/NadekoBot/NadekoBot.cs +++ b/src/NadekoBot/NadekoBot.cs @@ -199,7 +199,7 @@ namespace NadekoBot var muteService = new MuteService(Client, AllGuildConfigs, Db); var ratelimitService = new SlowmodeService(Client, AllGuildConfigs); var protectionService = new ProtectionService(Client, AllGuildConfigs, muteService); - var playingRotateService = new PlayingRotateService(Client, BotConfig, musicService); + var playingRotateService = new PlayingRotateService(Client, BotConfig, musicService, Db); var gameVcService = new GameVoiceChannelService(Client, Db, AllGuildConfigs); var autoAssignRoleService = new AutoAssignRoleService(Client, AllGuildConfigs); var logCommandService = new LogCommandService(Client, Strings, AllGuildConfigs, Db, muteService, protectionService); diff --git a/src/NadekoBot/Resources/CommandStrings.resx b/src/NadekoBot/Resources/CommandStrings.resx index e896535d..27d37eb7 100644 --- a/src/NadekoBot/Resources/CommandStrings.resx +++ b/src/NadekoBot/Resources/CommandStrings.resx @@ -1377,15 +1377,6 @@ `{0}typeadd wordswords` - - poll - - - Creates a poll which requires users to send the number of the voting option to the bot. - - - `{0}poll Question?;Answer1;Answ 2;A_3` - pollend @@ -2583,13 +2574,13 @@ `{0}totube` - - publicpoll ppoll + + poll ppoll - + Creates a public poll which requires users to type a number of the voting option in the channel command is ran in. - + `{0}ppoll Question?;Answer1;Answ 2;A_3` diff --git a/src/NadekoBot/Services/Administration/PlayingRotateService.cs b/src/NadekoBot/Services/Administration/PlayingRotateService.cs index c7a1bf41..9e33aef7 100644 --- a/src/NadekoBot/Services/Administration/PlayingRotateService.cs +++ b/src/NadekoBot/Services/Administration/PlayingRotateService.cs @@ -1,59 +1,65 @@ using Discord.WebSocket; -using NadekoBot.Extensions; +using NadekoBot.DataStructures.Replacements; +using NadekoBot.Services; using NadekoBot.Services.Database.Models; using NadekoBot.Services.Music; using NLog; using System; -using System.Collections.Generic; using System.Linq; using System.Threading; namespace NadekoBot.Services.Administration { - //todo 99 - Could make a placeholder service, which can work for any module - //and have replacements which are dependent on the types provided in the constructor public class PlayingRotateService { - public List RotatingStatusMessages { get; } - public volatile bool RotatingStatuses; private readonly Timer _t; private readonly DiscordSocketClient _client; - private readonly BotConfig _bc; private readonly MusicService _music; private readonly Logger _log; + private readonly Replacer _rep; + private readonly DbService _db; + public BotConfig BotConfig { get; private set; } //todo load whole botconifg, not just for this service when you have the time private class TimerState { public int Index { get; set; } } - public PlayingRotateService(DiscordSocketClient client, BotConfig bc, MusicService music) + public PlayingRotateService(DiscordSocketClient client, BotConfig bc, MusicService music, DbService db) { _client = client; - _bc = bc; + BotConfig = bc; _music = music; + _db = db; _log = LogManager.GetCurrentClassLogger(); + _rep = new ReplacementBuilder() + .WithClient(client) + .WithStats(client) + .WithMusic(music) + .Build(); - RotatingStatusMessages = _bc.RotatingStatusMessages; - RotatingStatuses = _bc.RotatingStatuses; - _t = new Timer(async (objState) => { try { + using (var uow = _db.UnitOfWork) + { + BotConfig = uow.BotConfig.GetOrCreate(); + } var state = (TimerState)objState; - if (!RotatingStatuses) + if (!BotConfig.RotatingStatuses) return; - if (state.Index >= RotatingStatusMessages.Count) + if (state.Index >= BotConfig.RotatingStatusMessages.Count) state.Index = 0; - if (!RotatingStatusMessages.Any()) + if (!BotConfig.RotatingStatusMessages.Any()) return; - var status = RotatingStatusMessages[state.Index++].Status; + var status = BotConfig.RotatingStatusMessages[state.Index++].Status; if (string.IsNullOrWhiteSpace(status)) return; - PlayingPlaceholders.ForEach(e => status = status.Replace(e.Key, e.Value(_client, _music))); - ShardSpecificPlaceholders.ForEach(e => status = status.Replace(e.Key, e.Value(client))); + + status = _rep.Replace(status); + try { await client.SetGameAsync(status).ConfigureAwait(false); } catch (Exception ex) { @@ -66,31 +72,5 @@ namespace NadekoBot.Services.Administration } }, new TimerState(), TimeSpan.FromMinutes(1), TimeSpan.FromMinutes(1)); } - - public Dictionary> PlayingPlaceholders { get; } = - new Dictionary> { - { "%servers%", (c, ms) => c.Guilds.Count.ToString()}, - { "%users%", (c, ms) => c.Guilds.Sum(s => s.Users.Count).ToString()}, - { "%playing%", (c, ms) => { - var cnt = ms.MusicPlayers.Count(kvp => kvp.Value.CurrentSong != null); - if (cnt != 1) return cnt.ToString(); - try { - var mp = ms.MusicPlayers.FirstOrDefault(); - return mp.Value.CurrentSong.SongInfo.Title; - } - catch { - return "No songs"; - } - } - }, - { "%queued%", (c, ms) => ms.MusicPlayers.Sum(kvp => kvp.Value.Playlist.Count).ToString()}, - { "%time%", (c, ms) => DateTime.Now.ToString("HH:mm " + TimeZoneInfo.Local.StandardName.GetInitials()) }, - }; - - public Dictionary> ShardSpecificPlaceholders { get; } = - new Dictionary> { - { "%shardid%", (client) => client.ShardId.ToString()}, - { "%shardguilds%", (client) => client.Guilds.Count.ToString()}, - }; } } diff --git a/src/NadekoBot/Services/CustomReactions/Extensions.cs b/src/NadekoBot/Services/CustomReactions/Extensions.cs index 4d6d4d06..6a70241d 100644 --- a/src/NadekoBot/Services/CustomReactions/Extensions.cs +++ b/src/NadekoBot/Services/CustomReactions/Extensions.cs @@ -3,6 +3,7 @@ using AngleSharp.Dom.Html; using Discord; using Discord.WebSocket; using NadekoBot.DataStructures; +using NadekoBot.DataStructures.Replacements; using NadekoBot.Extensions; using NadekoBot.Services.Database.Models; using System; @@ -15,62 +16,12 @@ namespace NadekoBot.Services.CustomReactions { public static class Extensions { - public static Dictionary> responsePlaceholders = new Dictionary>() - { - {"%target%", (ctx, trigger) => { return ctx.Content.Substring(trigger.Length).Trim().SanitizeMentions(); } }, - {"%rnduser%", (ctx, client) => { - //var ch = ctx.Channel as ITextChannel; - //if(ch == null) - // return ""; - - //var g = ch.Guild as SocketGuild; - //if(g == null) - // return ""; - //try { - // var usr = g.Users.Skip(new NadekoRandom().Next(0, g.Users.Count)).FirstOrDefault(); - // return usr.Mention; - //} - //catch { - return "[%rnduser% is temp. disabled]"; - //} - - //var users = g.Users.ToArray(); - - //return users[new NadekoRandom().Next(0, users.Length-1)].Mention; - } }, - }; - - public static Dictionary> placeholders = new Dictionary>() - { - {"%mention%", (ctx, client) => { return $"<@{client.CurrentUser.Id}>"; } }, - {"%user%", (ctx, client) => { return ctx.Author.Mention; } }, - //{"%rng%", (ctx) => { return new NadekoRandom().Next(0,10).ToString(); } } - }; - - private static readonly Regex rngRegex = new Regex("%rng(?:(?(?:-)?\\d+)-(?(?:-)?\\d+))?%", RegexOptions.Compiled); private static readonly Regex imgRegex = new Regex("%(img|image):(?.*?)%", RegexOptions.Compiled); private static readonly NadekoRandom rng = new NadekoRandom(); public static Dictionary>> regexPlaceholders = new Dictionary>>() { - { rngRegex, (match) => { - int from = 0; - int.TryParse(match.Groups["from"].ToString(), out from); - - int to = 0; - int.TryParse(match.Groups["to"].ToString(), out to); - - if(from == 0 && to == 0) - { - return Task.FromResult(rng.Next(0, 11).ToString()); - } - - if(from >= to) - return Task.FromResult(string.Empty); - - return Task.FromResult(rng.Next(from,to+1).ToString()); - } }, { imgRegex, async (match) => { var tag = match.Groups["tag"].ToString(); if(string.IsNullOrWhiteSpace(tag)) @@ -96,29 +47,24 @@ namespace NadekoBot.Services.CustomReactions private static string ResolveTriggerString(this string str, IUserMessage ctx, DiscordSocketClient client) { - foreach (var ph in placeholders) - { - if (str.Contains(ph.Key)) - str = str.ToLowerInvariant().Replace(ph.Key, ph.Value(ctx, client)); - } + var rep = new ReplacementBuilder() + .WithUser(ctx.Author) + .WithClient(client) + .Build(); + + str = rep.Replace(str.ToLowerInvariant()); + return str; } private static async Task ResolveResponseStringAsync(this string str, IUserMessage ctx, DiscordSocketClient client, string resolvedTrigger) { - foreach (var ph in placeholders) - { - var lowerKey = ph.Key.ToLowerInvariant(); - if (str.Contains(lowerKey)) - str = str.Replace(lowerKey, ph.Value(ctx, client)); - } + var rep = new ReplacementBuilder() + .WithDefault(ctx.Author, ctx.Channel, (ctx.Channel as ITextChannel)?.Guild, client) + .WithOverride("%target%", () => ctx.Content.Substring(resolvedTrigger.Length).Trim()) + .Build(); - foreach (var ph in responsePlaceholders) - { - var lowerKey = ph.Key.ToLowerInvariant(); - if (str.Contains(lowerKey)) - str = str.Replace(lowerKey, ph.Value(ctx, resolvedTrigger)); - } + str = rep.Replace(str.ToLowerInvariant()); foreach (var ph in regexPlaceholders) { @@ -133,17 +79,24 @@ namespace NadekoBot.Services.CustomReactions public static Task ResponseWithContextAsync(this CustomReaction cr, IUserMessage ctx, DiscordSocketClient client) => cr.Response.ResolveResponseStringAsync(ctx, client, cr.Trigger.ResolveTriggerString(ctx, client)); - public static async Task Send(this CustomReaction cr, IUserMessage context, DiscordSocketClient client, CustomReactionsService crs) + public static async Task Send(this CustomReaction cr, IUserMessage ctx, DiscordSocketClient client, CustomReactionsService crs) { - var channel = cr.DmResponse ? await context.Author.CreateDMChannelAsync() : context.Channel; + var channel = cr.DmResponse ? await ctx.Author.CreateDMChannelAsync() : ctx.Channel; crs.ReactionStats.AddOrUpdate(cr.Trigger, 1, (k, old) => ++old); if (CREmbed.TryParse(cr.Response, out CREmbed crembed)) { - return await channel.EmbedAsync(crembed.ToEmbed(), crembed.PlainText ?? ""); + var rep = new ReplacementBuilder() + .WithDefault(ctx.Author, ctx.Channel, (ctx.Channel as ITextChannel)?.Guild, client) + .WithOverride("%target%", () => ctx.Content.Substring(cr.Trigger.ResolveTriggerString(ctx, client).Length).Trim()) + .Build(); + + rep.Replace(crembed); + + return await channel.EmbedAsync(crembed.ToEmbed(), crembed.PlainText?.SanitizeMentions() ?? ""); } - return await channel.SendMessageAsync((await cr.ResponseWithContextAsync(context, client)).SanitizeMentions()); + return await channel.SendMessageAsync((await cr.ResponseWithContextAsync(ctx, client)).SanitizeMentions()); } } } diff --git a/src/NadekoBot/Services/GreetSettingsService.cs b/src/NadekoBot/Services/GreetSettingsService.cs index 3296c602..b38ac5bd 100644 --- a/src/NadekoBot/Services/GreetSettingsService.cs +++ b/src/NadekoBot/Services/GreetSettingsService.cs @@ -1,6 +1,7 @@ using Discord; using Discord.WebSocket; using NadekoBot.DataStructures; +using NadekoBot.DataStructures.Replacements; using NadekoBot.Extensions; using NadekoBot.Services.Database.Models; using NLog; @@ -45,15 +46,17 @@ namespace NadekoBot.Services if (channel == null) //maybe warn the server owner that the channel is missing return; - CREmbed embedData; - if (CREmbed.TryParse(conf.ChannelByeMessageText, out embedData)) + + var rep = new ReplacementBuilder() + .WithDefault(user, channel, user.Guild, _client) + .Build(); + + if (CREmbed.TryParse(conf.ChannelByeMessageText, out var embedData)) { - embedData.PlainText = embedData.PlainText?.Replace("%user%", user.Username).Replace("%id%", user.Id.ToString()).Replace("%server%", user.Guild.Name); - embedData.Description = embedData.Description?.Replace("%user%", user.Username).Replace("%id%", user.Id.ToString()).Replace("%server%", user.Guild.Name); - embedData.Title = embedData.Title?.Replace("%user%", user.Username).Replace("%id%", user.Id.ToString()).Replace("%server%", user.Guild.Name); + rep.Replace(embedData); try { - var toDelete = await channel.EmbedAsync(embedData.ToEmbed(), embedData.PlainText ?? "").ConfigureAwait(false); + var toDelete = await channel.EmbedAsync(embedData.ToEmbed(), embedData.PlainText?.SanitizeMentions() ?? "").ConfigureAwait(false); if (conf.AutoDeleteByeMessagesTimer > 0) { toDelete.DeleteAfter(conf.AutoDeleteByeMessagesTimer); @@ -63,7 +66,7 @@ namespace NadekoBot.Services } else { - var msg = conf.ChannelByeMessageText.Replace("%user%", user.Username).Replace("%id%", user.Id.ToString()).Replace("%server%", user.Guild.Name); + var msg = rep.Replace(conf.ChannelByeMessageText); if (string.IsNullOrWhiteSpace(msg)) return; try @@ -98,16 +101,16 @@ namespace NadekoBot.Services var channel = (await user.Guild.GetTextChannelsAsync()).SingleOrDefault(c => c.Id == conf.GreetMessageChannelId); if (channel != null) //maybe warn the server owner that the channel is missing { + var rep = new ReplacementBuilder() + .WithDefault(user, channel, user.Guild, _client) + .Build(); - CREmbed embedData; - if (CREmbed.TryParse(conf.ChannelGreetMessageText, out embedData)) + if (CREmbed.TryParse(conf.ChannelGreetMessageText, out var embedData)) { - embedData.PlainText = embedData.PlainText?.Replace("%user%", user.Mention).Replace("%id%", user.Id.ToString()).Replace("%server%", user.Guild.Name); - embedData.Description = embedData.Description?.Replace("%user%", user.Mention).Replace("%id%", user.Id.ToString()).Replace("%server%", user.Guild.Name); - embedData.Title = embedData.Title?.Replace("%user%", user.ToString()).Replace("%id%", user.Id.ToString()).Replace("%server%", user.Guild.Name); + rep.Replace(embedData); try { - var toDelete = await channel.EmbedAsync(embedData.ToEmbed(), embedData.PlainText ?? "").ConfigureAwait(false); + var toDelete = await channel.EmbedAsync(embedData.ToEmbed(), embedData.PlainText?.SanitizeMentions() ?? "").ConfigureAwait(false); if (conf.AutoDeleteGreetMessagesTimer > 0) { toDelete.DeleteAfter(conf.AutoDeleteGreetMessagesTimer); @@ -117,7 +120,7 @@ namespace NadekoBot.Services } else { - var msg = conf.ChannelGreetMessageText.Replace("%user%", user.Mention).Replace("%id%", user.Id.ToString()).Replace("%server%", user.Guild.Name); + var msg = rep.Replace(conf.ChannelGreetMessageText); if (!string.IsNullOrWhiteSpace(msg)) { try @@ -140,21 +143,22 @@ namespace NadekoBot.Services if (channel != null) { - CREmbed embedData; - if (CREmbed.TryParse(conf.DmGreetMessageText, out embedData)) + var rep = new ReplacementBuilder() + .WithDefault(user, channel, user.Guild, _client) + .Build(); + if (CREmbed.TryParse(conf.DmGreetMessageText, out var embedData)) { - embedData.PlainText = embedData.PlainText?.Replace("%user%", user.ToString()).Replace("%id%", user.Id.ToString()).Replace("%server%", user.Guild.Name); - embedData.Description = embedData.Description?.Replace("%user%", user.ToString()).Replace("%id%", user.Id.ToString()).Replace("%server%", user.Guild.Name); - embedData.Title = embedData.Title?.Replace("%user%", user.ToString()).Replace("%id%", user.Id.ToString()).Replace("%server%", user.Guild.Name); + + rep.Replace(embedData); try { - await channel.EmbedAsync(embedData.ToEmbed(), embedData.PlainText ?? "").ConfigureAwait(false); + await channel.EmbedAsync(embedData.ToEmbed(), embedData.PlainText?.SanitizeMentions() ?? "").ConfigureAwait(false); } catch (Exception ex) { _log.Warn(ex); } } else { - var msg = conf.DmGreetMessageText.Replace("%user%", user.ToString()).Replace("%id%", user.Id.ToString()).Replace("%server%", user.Guild.Name); + var msg = rep.Replace(conf.DmGreetMessageText); if (!string.IsNullOrWhiteSpace(msg)) { await channel.SendConfirmAsync(msg).ConfigureAwait(false); @@ -409,4 +413,4 @@ namespace NadekoBot.Services ChannelByeMessageText = g.ChannelByeMessageText, }; } -} +} \ No newline at end of file diff --git a/src/NadekoBot/Services/Utility/RemindService.cs b/src/NadekoBot/Services/Utility/RemindService.cs index 8629712d..592564c3 100644 --- a/src/NadekoBot/Services/Utility/RemindService.cs +++ b/src/NadekoBot/Services/Utility/RemindService.cs @@ -1,5 +1,6 @@ using Discord; using Discord.WebSocket; +using NadekoBot.DataStructures.Replacements; using NadekoBot.Extensions; using NadekoBot.Services.Database; using NadekoBot.Services.Database.Models; @@ -20,13 +21,6 @@ namespace NadekoBot.Services.Utility public string RemindMessageFormat { get; } - public readonly IDictionary> _replacements = new Dictionary> - { - { "%message%" , (r) => r.Message }, - { "%user%", (r) => $"<@!{r.UserId}>" }, - { "%target%", (r) => r.IsPrivate ? "Direct Message" : $"<#{r.ChannelId}>"} - }; - private readonly Logger _log; private readonly CancellationTokenSource cancelSource; private readonly CancellationToken cancelAllToken; @@ -82,11 +76,13 @@ namespace NadekoBot.Services.Utility if (ch == null) return; - await ch.SendMessageAsync( - _replacements.Aggregate(RemindMessageFormat, - (cur, replace) => cur.Replace(replace.Key, replace.Value(r))) - .SanitizeMentions() - ).ConfigureAwait(false); //it works trust me + var rep = new ReplacementBuilder() + .WithOverride("%user%", () => $"<@!{r.UserId}>") + .WithOverride("%message%", () => r.Message) + .WithOverride("%target%", () => r.IsPrivate ? "Direct Message" : $"<#{r.ChannelId}>") + .Build(); + + await ch.SendMessageAsync(rep.Replace(RemindMessageFormat).SanitizeMentions()).ConfigureAwait(false); //it works trust me } catch (Exception ex) { _log.Warn(ex); } finally From ed3ccbf6d91de7f4e1ee6e5448d89bcf4eab553e Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Wed, 28 Jun 2017 02:46:26 +0200 Subject: [PATCH 040/346] version upped to 1.51 --- src/NadekoBot/Services/Impl/StatsService.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/NadekoBot/Services/Impl/StatsService.cs b/src/NadekoBot/Services/Impl/StatsService.cs index 07d706e1..338c656b 100644 --- a/src/NadekoBot/Services/Impl/StatsService.cs +++ b/src/NadekoBot/Services/Impl/StatsService.cs @@ -17,7 +17,7 @@ namespace NadekoBot.Services.Impl private readonly IBotCredentials _creds; private readonly DateTime _started; - public const string BotVersion = "1.5"; + public const string BotVersion = "1.51"; public string Author => "Kwoth#2560"; public string Library => "Discord.Net"; From cedf08b12882c5b8de6282824ba9c9e5f1a64f66 Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Wed, 28 Jun 2017 03:19:16 +0200 Subject: [PATCH 041/346] normal quote responses now have replacements too, not only embeds --- .../Modules/Utility/Commands/QuoteCommands.cs | 22 ++++++++++--------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/src/NadekoBot/Modules/Utility/Commands/QuoteCommands.cs b/src/NadekoBot/Modules/Utility/Commands/QuoteCommands.cs index dcdc035a..d8ea1e84 100644 --- a/src/NadekoBot/Modules/Utility/Commands/QuoteCommands.cs +++ b/src/NadekoBot/Modules/Utility/Commands/QuoteCommands.cs @@ -67,17 +67,18 @@ namespace NadekoBot.Modules.Utility if (quote == null) return; + var rep = new ReplacementBuilder() + .WithDefault(Context) + .Build(); + if (CREmbed.TryParse(quote.Text, out var crembed)) { - new ReplacementBuilder() - .WithDefault(Context) - .Build() - .Replace(crembed); + rep.Replace(crembed); await Context.Channel.EmbedAsync(crembed.ToEmbed(), crembed.PlainText?.SanitizeMentions() ?? "") .ConfigureAwait(false); return; } - await Context.Channel.SendMessageAsync($"`#{quote.Id}` 📣 " + quote.Text.SanitizeMentions()); + await Context.Channel.SendMessageAsync($"`#{quote.Id}` 📣 " + rep.Replace(quote.Text)?.SanitizeMentions()); } [NadekoCommand, Usage, Description, Aliases] @@ -115,16 +116,17 @@ namespace NadekoBot.Modules.Utility { var qfromid = uow.Quotes.Get(id); + var rep = new ReplacementBuilder() + .WithDefault(Context) + .Build(); + if (qfromid == null) { await Context.Channel.SendErrorAsync(GetText("quotes_notfound")); } else if (CREmbed.TryParse(qfromid.Text, out var crembed)) { - new ReplacementBuilder() - .WithDefault(Context) - .Build() - .Replace(crembed); + rep.Replace(crembed); await Context.Channel.EmbedAsync(crembed.ToEmbed(), crembed.PlainText?.SanitizeMentions() ?? "") .ConfigureAwait(false); @@ -132,7 +134,7 @@ namespace NadekoBot.Modules.Utility else { await Context.Channel.SendMessageAsync($"`#{qfromid.Id}` 🗯️ " + qfromid.Keyword.ToLowerInvariant().SanitizeMentions() + ": " + - qfromid.Text.SanitizeMentions()); + rep.Replace(qfromid.Text)?.SanitizeMentions()); } } From 36069d75525951ac7054663d2cf48eed8f7bae11 Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Wed, 28 Jun 2017 03:29:33 +0200 Subject: [PATCH 042/346] remove user's message when they vote in the polls. close #1313 --- src/NadekoBot/Services/Games/Poll.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/NadekoBot/Services/Games/Poll.cs b/src/NadekoBot/Services/Games/Poll.cs index 5358e139..ea06e3b6 100644 --- a/src/NadekoBot/Services/Games/Poll.cs +++ b/src/NadekoBot/Services/Games/Poll.cs @@ -114,6 +114,7 @@ namespace NadekoBot.Services.Games { var toDelete = await ch.SendConfirmAsync(GetText("poll_voted", Format.Bold(msg.Author.ToString()))).ConfigureAwait(false); toDelete.DeleteAfter(5); + try { await msg.DeleteAsync().ConfigureAwait(false); } catch { } return true; } return false; From cbd2de284f59e4086ac4ded2e644d01a8fdf3ee6 Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Wed, 28 Jun 2017 03:42:02 +0200 Subject: [PATCH 043/346] .hangmanstop command added, updated commandlist --- docs/Commands List.md | 11 +++-------- .../Modules/Games/Commands/HangmanCommands.cs | 13 +++++++++++++ src/NadekoBot/Resources/CommandStrings.resx | 9 +++++++++ src/NadekoBot/_strings/ResponseStrings.en-US.json | 1 + 4 files changed, 26 insertions(+), 8 deletions(-) diff --git a/docs/Commands List.md b/docs/Commands List.md index d90dd900..23cef108 100644 --- a/docs/Commands List.md +++ b/docs/Commands List.md @@ -34,7 +34,6 @@ Commands and aliases | Description | Usage `.creatxtchanl` `.ctch` | Creates a new text channel with a given name. **Requires ManageChannels server permission.** | `.ctch TextChannelName` `.settopic` `.st` | Sets a topic on the current channel. **Requires ManageChannels server permission.** | `.st My new topic` `.setchanlname` `.schn` | Changes the name of the current channel. **Requires ManageChannels server permission.** | `.schn NewName` -`.prune` `.clear` | `.prune` removes all Nadeko's messages in the last 100 messages. `.prune X` removes last `X` number of messages from the channel (up to 100). `.prune @Someone` removes all Someone's messages in the last 100 messages. `.prune @Someone X` removes last `X` number of 'Someone's' messages in the channel. | `.prune` or `.prune 5` or `.prune @Someone` or `.prune @Someone X` `.mentionrole` `.menro` | Mentions every person from the provided role or roles (separated by a ',') on this server. **Requires MentionEveryone server permission.** | `.menro RoleName` `.donators` | List of the lovely people who donated to keep this project alive. | `.donators` `.donadd` | Add a donator to the database. **Bot owner only** | `.donadd Donate Amount` @@ -65,6 +64,7 @@ Commands and aliases | Description | Usage `.antispam` | Stops people from repeating same message X times in a row. You can specify to either mute, kick or ban the offenders. Max message count is 10. **Requires Administrator server permission.** | `.antispam 3 Mute` or `.antispam 4 Kick` or `.antispam 6 Ban` `.antispamignore` | Toggles whether antispam ignores current channel. Antispam must be enabled. | `.antispamignore` `.antilist` `.antilst` | Shows currently enabled protection features. | `.antilist` +`.prune` `.clear` | `.prune` removes all Nadeko's messages in the last 100 messages. `.prune X` removes last `X` number of messages from the channel (up to 100). `.prune @Someone` removes all Someone's messages in the last 100 messages. `.prune @Someone X` removes last `X` number of 'Someone's' messages in the channel. | `.prune` or `.prune 5` or `.prune @Someone` or `.prune @Someone X` `.slowmode` | Toggles slowmode. Disable by specifying no parameters. To enable, specify a number of messages each user can send, and an interval in seconds. For example 1 message every 5 seconds. **Requires ManageMessages server permission.** | `.slowmode 1 5` or `.slowmode` `.slowmodewl` | Ignores a role or a user from the slowmode feature. **Requires ManageMessages server permission.** | `.slowmodewl SomeRole` or `.slowmodewl AdminDude` `.adsarm` | Toggles the automatic deletion of confirmations for `.iam` and `.iamn` commands. **Requires ManageMessages server permission.** | `.adsarm` @@ -81,7 +81,6 @@ Commands and aliases | Description | Usage `.scclr` | Removes all startup commands. **Bot owner only** | `.scclr` `.fwmsgs` | Toggles forwarding of non-command messages sent to bot's DM to the bot owners **Bot owner only** | `.fwmsgs` `.fwtoall` | Toggles whether messages will be forwarded to all bot owners or only to the first one specified in the credentials.json file **Bot owner only** | `.fwtoall` -`.connectshard` | Try (re)connecting a shard with a certain shardid when it dies. No one knows will it work. Keep an eye on the console for errors. **Bot owner only** | `.connectshard 2` `.leave` | Makes Nadeko leave the server. Either server name or server ID is required. **Bot owner only** | `.leave 123123123331` `.die` | Shuts the bot down. **Bot owner only** | `.die` `.setname` `.newnm` | Gives the bot a new name. **Bot owner only** | `.newnm BotName` @@ -199,11 +198,11 @@ Commands and aliases | Description | Usage `.cleverbot` | Toggles cleverbot session. When enabled, the bot will reply to messages starting with bot mention in the server. Custom reactions starting with %mention% won't work if cleverbot is enabled. **Requires ManageMessages server permission.** | `.cleverbot` `.hangmanlist` | Shows a list of hangman term types. | `.hangmanlist` `.hangman` | Starts a game of hangman in the channel. Use `.hangmanlist` to see a list of available term types. Defaults to 'all'. | `.hangman` or `.hangman movies` +`.hangmanstop` | Stops the active hangman game on this channel if it exists. | `.hangmanstop` `.pick` | Picks the currency planted in this channel. 60 seconds cooldown. | `.pick` `.plant` | Spend an amount of currency to plant it in this channel. Default is 1. (If bot is restarted or crashes, the currency will be lost) | `.plant` or `.plant 5` `.gencurrency` `.gc` | Toggles currency generation on this channel. Every posted message will have chance to spawn currency. Chance is specified by the Bot Owner. (default is 2%) **Requires ManageMessages server permission.** | `.gc` -`.poll` | Creates a poll which requires users to send the number of the voting option to the bot. **Requires ManageMessages server permission.** | `.poll Question?;Answer1;Answ 2;A_3` -`.publicpoll` `.ppoll` | Creates a public poll which requires users to type a number of the voting option in the channel command is ran in. **Requires ManageMessages server permission.** | `.ppoll Question?;Answer1;Answ 2;A_3` +`.poll` `.ppoll` | Creates a public poll which requires users to type a number of the voting option in the channel command is ran in. **Requires ManageMessages server permission.** | `.ppoll Question?;Answer1;Answ 2;A_3` `.pollstats` | Shows the poll results without stopping the poll on this server. **Requires ManageMessages server permission.** | `.pollstats` `.pollend` | Stops active poll on this server and prints the results in this channel. **Requires ManageMessages server permission.** | `.pollend` `.typestart` | Starts a typing contest. | `.typestart` @@ -411,7 +410,6 @@ Commands and aliases | Description | Usage `.channeltopic` `.ct` | Sends current channel's topic as a message. | `.ct` `.createinvite` `.crinv` | Creates a new invite which has infinite max uses and never expires. **Requires CreateInstantInvite channel permission.** | `.crinv` `.shardstats` | Stats for shards. Paginated with 25 shards per page. | `.shardstats` or `.shardstats 2` -`.shardid` | Shows which shard is a certain guild on, by guildid. | `.shardid 117523346618318850` `.stats` | Shows some basic stats for Nadeko. | `.stats` `.showemojis` `.se` | Shows a name and a link to every SPECIAL emoji in the message. | `.se A message full of SPECIAL emojis` `.listservers` | Lists servers the bot is on with some basic info. 15 per page. **Bot owner only** | `.listservers 3` @@ -421,9 +419,6 @@ Commands and aliases | Description | Usage `.calcops` | Shows all available operations in the `.calc` command | `.calcops` `.alias` `.cmdmap` | Create a custom alias for a certain Nadeko command. Provide no alias to remove the existing one. **Requires Administrator server permission.** | `.alias allin $bf 100 h` or `.alias "linux thingy" >loonix Spyware Windows` `.aliaslist` `.cmdmaplist` `.aliases` | Shows the list of currently set aliases. Paginated. | `.aliaslist` or `.aliaslist 3` -`.scsc` | Starts an instance of cross server channel. You will get a token as a DM that other people will use to tune in to the same instance. **Bot owner only** | `.scsc` -`.jcsc` | Joins current channel to an instance of cross server channel using the token. **Requires ManageServer server permission.** | `.jcsc TokenHere` -`.lcsc` | Leaves a cross server channel instance from this channel. **Requires ManageServer server permission.** | `.lcsc` `.serverinfo` `.sinfo` | Shows info about the server the bot is on. If no channel is supplied, it defaults to current one. | `.sinfo Some Server` `.channelinfo` `.cinfo` | Shows info about the channel. If no channel is supplied, it defaults to current one. | `.cinfo #some-channel` `.userinfo` `.uinfo` | Shows info about the user. If no user is supplied, it defaults a user running the command. | `.uinfo @SomeUser` diff --git a/src/NadekoBot/Modules/Games/Commands/HangmanCommands.cs b/src/NadekoBot/Modules/Games/Commands/HangmanCommands.cs index c515c0b8..e74b74f5 100644 --- a/src/NadekoBot/Modules/Games/Commands/HangmanCommands.cs +++ b/src/NadekoBot/Modules/Games/Commands/HangmanCommands.cs @@ -25,12 +25,14 @@ namespace NadekoBot.Modules.Games //channelId, game public static ConcurrentDictionary HangmanGames { get; } = new ConcurrentDictionary(); [NadekoCommand, Usage, Description, Aliases] + [RequireContext(ContextType.Guild)] public async Task Hangmanlist() { await Context.Channel.SendConfirmAsync(Format.Code(GetText("hangman_types", Prefix)) + "\n" + string.Join(", ", HangmanTermPool.data.Keys)); } [NadekoCommand, Usage, Description, Aliases] + [RequireContext(ContextType.Guild)] public async Task Hangman([Remainder]string type = "All") { var hm = new HangmanGame(_client, Context.Channel, type); @@ -59,6 +61,17 @@ namespace NadekoBot.Modules.Games await Context.Channel.SendConfirmAsync(GetText("hangman_game_started"), hm.ScrambledWord + "\n" + hm.GetHangman()); } + + [NadekoCommand, Usage, Description, Aliases] + [RequireContext(ContextType.Guild)] + public async Task HangmanStop() + { + if (HangmanGames.TryRemove(Context.Channel.Id, out HangmanGame throwaway)) + { + throwaway.Dispose(); + await ReplyConfirmLocalized("hangman_stopped").ConfigureAwait(false); + } + } } } } diff --git a/src/NadekoBot/Resources/CommandStrings.resx b/src/NadekoBot/Resources/CommandStrings.resx index 27d37eb7..064a19ba 100644 --- a/src/NadekoBot/Resources/CommandStrings.resx +++ b/src/NadekoBot/Resources/CommandStrings.resx @@ -2781,6 +2781,15 @@ `{0}hangman` or `{0}hangman movies` + + hangmanstop + + + Stops the active hangman game on this channel if it exists. + + + `{0}hangmanstop` + crstatsclear diff --git a/src/NadekoBot/_strings/ResponseStrings.en-US.json b/src/NadekoBot/_strings/ResponseStrings.en-US.json index 762cccfb..92135277 100644 --- a/src/NadekoBot/_strings/ResponseStrings.en-US.json +++ b/src/NadekoBot/_strings/ResponseStrings.en-US.json @@ -375,6 +375,7 @@ "games_hangman_game_started": "Hangman game started", "games_hangman_running": "Hangman game already running on this channel.", "games_hangman_start_errored": "Starting hangman errored.", + "games_hangman_stopped": "Hangman game stopped.", "games_hangman_types": "List of \"{0}hangman\" term types:", "games_leaderboard": "Leaderboard", "games_not_enough": "You don't have enough {0}", From e9365b7753641e1a7f3a0c4de5df60e4316fd3db Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Wed, 28 Jun 2017 03:48:55 +0200 Subject: [PATCH 044/346] Added module name under the command help when using .h \ q --- src/NadekoBot/Services/Help/HelpService.cs | 1 + src/NadekoBot/_strings/ResponseStrings.en-US.json | 1 + 2 files changed, 2 insertions(+) diff --git a/src/NadekoBot/Services/Help/HelpService.cs b/src/NadekoBot/Services/Help/HelpService.cs index 83b64927..cdc6adbe 100644 --- a/src/NadekoBot/Services/Help/HelpService.cs +++ b/src/NadekoBot/Services/Help/HelpService.cs @@ -48,6 +48,7 @@ namespace NadekoBot.Services.Help return new EmbedBuilder() .AddField(fb => fb.WithName(str).WithValue($"{com.RealSummary(prefix)} {GetCommandRequirements(com, guild)}").WithIsInline(true)) .AddField(fb => fb.WithName(GetText("usage", guild)).WithValue(com.RealRemarks(prefix)).WithIsInline(false)) + .WithFooter(efb => efb.WithText(GetText("module", guild, com.Module.GetTopLevelModule().Name))) .WithColor(NadekoBot.OkColor); } diff --git a/src/NadekoBot/_strings/ResponseStrings.en-US.json b/src/NadekoBot/_strings/ResponseStrings.en-US.json index 92135277..872008cb 100644 --- a/src/NadekoBot/_strings/ResponseStrings.en-US.json +++ b/src/NadekoBot/_strings/ResponseStrings.en-US.json @@ -303,6 +303,7 @@ "help_server_permission": "Requires {0} server permission.", "help_table_of_contents": "Table of contents", "help_usage": "Usage", + "help_module": "Module: {0}", "nsfw_autohentai_started": "Autohentai started. Reposting every {0}s with one of the following tags:\n{1}", "nsfw_tag": "Tag", "gambling_animal_race": "Animal race", From b08ad7cb77d17746bc24d04be7b1f2d7e56934fb Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Wed, 28 Jun 2017 04:59:32 +0200 Subject: [PATCH 045/346] .warnlogall command added --- .../Commands/UserPunishCommands.cs | 33 +++++++++++++++++++ src/NadekoBot/Resources/CommandStrings.resx | 9 +++++ src/NadekoBot/_Extensions/Extensions.cs | 21 ++++++------ .../_strings/ResponseStrings.en-US.json | 3 +- 4 files changed, 55 insertions(+), 11 deletions(-) diff --git a/src/NadekoBot/Modules/Administration/Commands/UserPunishCommands.cs b/src/NadekoBot/Modules/Administration/Commands/UserPunishCommands.cs index 3a110f79..ae76811f 100644 --- a/src/NadekoBot/Modules/Administration/Commands/UserPunishCommands.cs +++ b/src/NadekoBot/Modules/Administration/Commands/UserPunishCommands.cs @@ -191,6 +191,39 @@ namespace NadekoBot.Modules.Administration await Context.Channel.EmbedAsync(embed); } + [NadekoCommand, Usage, Description, Aliases] + [RequireContext(ContextType.Guild)] + [RequireUserPermission(GuildPermission.BanMembers)] + public async Task WarnlogAll(int page = 1) + { + if (--page < 0) + return; + IGrouping[] warnings; + using (var uow = _db.UnitOfWork) + { + warnings = uow.Warnings.GetAll().GroupBy(x => x.UserId).ToArray(); + } + + await Context.Channel.SendPaginatedConfirmAsync((DiscordSocketClient)Context.Client, page, async (curPage) => + { + var ws = await Task.WhenAll(warnings.Skip(curPage * 15) + .Take(15) + .ToArray() + .Select(async x => + { + var all = x.Count(); + var forgiven = x.Count(y => y.Forgiven); + var total = all - forgiven; + return ((await Context.Guild.GetUserAsync(x.Key))?.ToString() ?? x.Key.ToString()) + $" | {total} ({all} - {forgiven})"; + })); + + return new EmbedBuilder() + .WithTitle(GetText("warnings_list")) + .WithDescription(string.Join("\n", ws)); + + }, warnings.Length / 15); + } + [NadekoCommand, Usage, Description, Aliases] [RequireContext(ContextType.Guild)] [RequireUserPermission(GuildPermission.BanMembers)] diff --git a/src/NadekoBot/Resources/CommandStrings.resx b/src/NadekoBot/Resources/CommandStrings.resx index 064a19ba..8ddcf04d 100644 --- a/src/NadekoBot/Resources/CommandStrings.resx +++ b/src/NadekoBot/Resources/CommandStrings.resx @@ -3258,6 +3258,15 @@ `{0}warnlog @b1nzy` + + warnlogall + + + See a list of all warnings on the server. 15 users per page. + + + `{0}warnlogall` or `{0}warnlogall 2` + warn diff --git a/src/NadekoBot/_Extensions/Extensions.cs b/src/NadekoBot/_Extensions/Extensions.cs index 10e97e14..8488071b 100644 --- a/src/NadekoBot/_Extensions/Extensions.cs +++ b/src/NadekoBot/_Extensions/Extensions.cs @@ -66,15 +66,16 @@ namespace NadekoBot.Extensions ms.Seek(0, SeekOrigin.Begin); return ms; } - + public static Task SendPaginatedConfirmAsync(this IMessageChannel channel, DiscordSocketClient client, int currentPage, Func pageFunc, int? lastPage = null, bool addPaginatedFooter = true) => + channel.SendPaginatedConfirmAsync(client, currentPage, (x) => Task.FromResult(pageFunc(x)), lastPage, addPaginatedFooter); /// /// danny kamisama /// - public static async Task SendPaginatedConfirmAsync(this IMessageChannel channel, DiscordSocketClient client, int currentPage, Func pageFunc, int? lastPage = null, bool addPaginatedFooter = true) + public static async Task SendPaginatedConfirmAsync(this IMessageChannel channel, DiscordSocketClient client, int currentPage, Func> pageFunc, int? lastPage = null, bool addPaginatedFooter = true) { - var embed = pageFunc(currentPage); + var embed = await pageFunc(currentPage).ConfigureAwait(false); - if(addPaginatedFooter) + if (addPaginatedFooter) embed.AddPaginatedFooter(currentPage, lastPage); var msg = await channel.EmbedAsync(embed) as IUserMessage; @@ -82,8 +83,8 @@ namespace NadekoBot.Extensions if (lastPage == 0) return; - - await msg.AddReactionAsync( arrow_left).ConfigureAwait(false); + + await msg.AddReactionAsync(arrow_left).ConfigureAwait(false); await msg.AddReactionAsync(arrow_right).ConfigureAwait(false); await Task.Delay(2000).ConfigureAwait(false); @@ -96,7 +97,7 @@ namespace NadekoBot.Extensions { if (currentPage == 0) return; - var toSend = pageFunc(--currentPage); + var toSend = await pageFunc(--currentPage).ConfigureAwait(false); if (addPaginatedFooter) toSend.AddPaginatedFooter(currentPage, lastPage); await msg.ModifyAsync(x => x.Embed = toSend.Build()).ConfigureAwait(false); @@ -105,7 +106,7 @@ namespace NadekoBot.Extensions { if (lastPage == null || lastPage > currentPage) { - var toSend = pageFunc(++currentPage); + var toSend = await pageFunc(++currentPage).ConfigureAwait(false); if (addPaginatedFooter) toSend.AddPaginatedFooter(currentPage, lastPage); await msg.ModifyAsync(x => x.Embed = toSend.Build()).ConfigureAwait(false); @@ -315,7 +316,7 @@ namespace NadekoBot.Extensions return list; } } - + /// /// Easy use of fast, efficient case-insensitive Contains check with StringComparison Member Types /// CurrentCulture, CurrentCultureIgnoreCase, InvariantCulture, InvariantCultureIgnoreCase, Ordinal, OrdinalIgnoreCase @@ -482,4 +483,4 @@ namespace NadekoBot.Extensions : usr.GetAvatarUrl(ImageFormat.Auto); } } -} +} \ No newline at end of file diff --git a/src/NadekoBot/_strings/ResponseStrings.en-US.json b/src/NadekoBot/_strings/ResponseStrings.en-US.json index 872008cb..9edb025c 100644 --- a/src/NadekoBot/_strings/ResponseStrings.en-US.json +++ b/src/NadekoBot/_strings/ResponseStrings.en-US.json @@ -303,7 +303,7 @@ "help_server_permission": "Requires {0} server permission.", "help_table_of_contents": "Table of contents", "help_usage": "Usage", - "help_module": "Module: {0}", + "help_module": "Module: {0}", "nsfw_autohentai_started": "Autohentai started. Reposting every {0}s with one of the following tags:\n{1}", "nsfw_tag": "Tag", "gambling_animal_race": "Animal race", @@ -735,6 +735,7 @@ "administration_warned_on_by": "On {0} at {1} by {2}", "administration_warnings_cleared": "All warnings have been cleared for {0}.", "administration_warnings_none": "No warning on this page.", + "administration_warnings_list": "List of all warned users on the server.", "administration_warnlog_for": "Warnlog for {0}", "administration_warnpl_none": "No punishments set.", "administration_warn_cleared_by": "cleared by {0}", From 5fc9c18d9898009bdebf8f8ca3df34ea5a23692b Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Wed, 28 Jun 2017 07:05:08 +0200 Subject: [PATCH 046/346] You can now use .warnlog on yourself --- .../Modules/Administration/Commands/UserPunishCommands.cs | 7 +++++-- src/NadekoBot/_strings/ResponseStrings.en-US.json | 2 +- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/NadekoBot/Modules/Administration/Commands/UserPunishCommands.cs b/src/NadekoBot/Modules/Administration/Commands/UserPunishCommands.cs index ae76811f..7e2a453a 100644 --- a/src/NadekoBot/Modules/Administration/Commands/UserPunishCommands.cs +++ b/src/NadekoBot/Modules/Administration/Commands/UserPunishCommands.cs @@ -131,24 +131,27 @@ namespace NadekoBot.Modules.Administration [NadekoCommand, Usage, Description, Aliases] [RequireContext(ContextType.Guild)] [RequireUserPermission(GuildPermission.BanMembers)] + [Priority(1)] public Task Warnlog(int page, IGuildUser user) => Warnlog(page, user.Id); [NadekoCommand, Usage, Description, Aliases] [RequireContext(ContextType.Guild)] - [RequireUserPermission(GuildPermission.BanMembers)] + [Priority(0)] public Task Warnlog(IGuildUser user) - => Warnlog(user.Id); + => Context.User.Id == user.Id || ((IGuildUser)Context.User).GuildPermissions.BanMembers ? Warnlog(user.Id) : Task.CompletedTask; [NadekoCommand, Usage, Description, Aliases] [RequireContext(ContextType.Guild)] [RequireUserPermission(GuildPermission.BanMembers)] + [Priority(3)] public Task Warnlog(int page, ulong userId) => InternalWarnlog(userId, page - 1); [NadekoCommand, Usage, Description, Aliases] [RequireContext(ContextType.Guild)] [RequireUserPermission(GuildPermission.BanMembers)] + [Priority(2)] public Task Warnlog(ulong userId) => InternalWarnlog(userId, 0); diff --git a/src/NadekoBot/_strings/ResponseStrings.en-US.json b/src/NadekoBot/_strings/ResponseStrings.en-US.json index 9edb025c..e7a3b885 100644 --- a/src/NadekoBot/_strings/ResponseStrings.en-US.json +++ b/src/NadekoBot/_strings/ResponseStrings.en-US.json @@ -735,7 +735,7 @@ "administration_warned_on_by": "On {0} at {1} by {2}", "administration_warnings_cleared": "All warnings have been cleared for {0}.", "administration_warnings_none": "No warning on this page.", - "administration_warnings_list": "List of all warned users on the server.", + "administration_warnings_list": "List of all warned users on the server", "administration_warnlog_for": "Warnlog for {0}", "administration_warnpl_none": "No punishments set.", "administration_warn_cleared_by": "cleared by {0}", From d7a0168fc526a19b9c094c704be42a58689cc6c5 Mon Sep 17 00:00:00 2001 From: Poag Date: Wed, 28 Jun 2017 10:13:10 +0100 Subject: [PATCH 047/346] Update Docker Guide.md --- docs/guides/Docker Guide.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/guides/Docker Guide.md b/docs/guides/Docker Guide.md index 8969e373..7023f39f 100644 --- a/docs/guides/Docker Guide.md +++ b/docs/guides/Docker Guide.md @@ -8,7 +8,7 @@ Follow the respective guide for your operating system found here [Docker Engine For this guide we will be using the folder /nadeko as our config root folder. ```bash -docker create --name=nadeko -v /nadeko/:/root/nadeko uirel/nadeko:1.4 +docker create --name=nadeko -v /nadeko/conf/:/root/nadeko -v /nadeko/data:/opt/NadekoBot/src/NadekoBot/bin/Release/netcoreapp1.1/data uirel/nadeko:1.4 ``` -If you are coming from a previous version of nadeko (the old docker) make sure your crednetials.json has been copied into this directory and is the only thing in this folder. @@ -37,7 +37,9 @@ The following commands are required for the default options `docker stop nadeko; docker rm nadeko` -`docker create --name=nadeko -v /nadeko/:/root/nadeko uirel/nadeko:1.4` +``` +docker create --name=nadeko -v /nadeko/conf/:/root/nadeko -v /nadeko/data:/opt/NadekoBot/src/NadekoBot/bin/Release/netcoreapp1.1/data uirel/nadeko:1.4 +``` `docker start nadeko` From 1df7e4d3d070f689131f5f933bb8c7c311bef646 Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Thu, 29 Jun 2017 05:07:48 +0200 Subject: [PATCH 048/346] custom reaction responses are no longer lowercase? --- src/NadekoBot/Services/CustomReactions/Extensions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/NadekoBot/Services/CustomReactions/Extensions.cs b/src/NadekoBot/Services/CustomReactions/Extensions.cs index 6a70241d..4e659e9e 100644 --- a/src/NadekoBot/Services/CustomReactions/Extensions.cs +++ b/src/NadekoBot/Services/CustomReactions/Extensions.cs @@ -64,7 +64,7 @@ namespace NadekoBot.Services.CustomReactions .WithOverride("%target%", () => ctx.Content.Substring(resolvedTrigger.Length).Trim()) .Build(); - str = rep.Replace(str.ToLowerInvariant()); + str = rep.Replace(str); foreach (var ph in regexPlaceholders) { From db1581f67d0fba65c7cfca4d8eb1c20db3188438 Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Thu, 29 Jun 2017 06:10:56 +0200 Subject: [PATCH 049/346] fixed public bot compilation issue for good --- src/NadekoBot/NadekoBot.cs | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/src/NadekoBot/NadekoBot.cs b/src/NadekoBot/NadekoBot.cs index c75878b3..200487b2 100644 --- a/src/NadekoBot/NadekoBot.cs +++ b/src/NadekoBot/NadekoBot.cs @@ -26,6 +26,8 @@ using NadekoBot.Services.Help; using System.IO; using NadekoBot.Services.Pokemon; using NadekoBot.DataStructures.ShardCom; +using NadekoBot.DataStructures; +using NadekoBot.Extensions; namespace NadekoBot { @@ -346,6 +348,10 @@ namespace NadekoBot var _ = await CommandService.AddModulesAsync(this.GetType().GetTypeInfo().Assembly); + bool isPublicNadeko = false; +#if GLOBAL_NADEKO + isPublicNadeko = true; +#endif //Console.WriteLine(string.Join(", ", CommandService.Commands // .Distinct(x => x.Name + x.Module.Name) // .SelectMany(x => x.Aliases) @@ -353,14 +359,14 @@ namespace NadekoBot // .Where(x => x.Count() > 1) // .Select(x => x.Key + $"({x.Count()})"))); -//unload modules which are not available on the public bot -#if GLOBAL_NADEKO - CommandService - .Modules - .ToArray() - .Where(x => x.Preconditions.Any(y => y.GetType() == typeof(NoPublicBot))) - .ForEach(x => CommandService.RemoveModuleAsync(x)); -#endif + //unload modules which are not available on the public bot + + if(isPublicNadeko) + CommandService + .Modules + .ToArray() + .Where(x => x.Preconditions.Any(y => y.GetType() == typeof(NoPublicBot))) + .ForEach(x => CommandService.RemoveModuleAsync(x)); Ready = true; _log.Info($"Shard {ShardId} ready."); From 728539528f42e33c775c8305cec80ac1afdd8e4b Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Thu, 29 Jun 2017 17:58:16 +0200 Subject: [PATCH 050/346] drastic nsfw and safebooru improvements --- .../DataStructures/SearchImageCacher.cs | 213 ++++++++++++++++++ src/NadekoBot/Modules/NSFW/NSFW.cs | 172 +++----------- src/NadekoBot/Modules/Searches/Searches.cs | 9 +- .../Services/CustomReactions/Extensions.cs | 2 +- src/NadekoBot/Services/Impl/StatsService.cs | 2 +- .../Services/Searches/SearchesService.cs | 64 +----- 6 files changed, 262 insertions(+), 200 deletions(-) create mode 100644 src/NadekoBot/DataStructures/SearchImageCacher.cs diff --git a/src/NadekoBot/DataStructures/SearchImageCacher.cs b/src/NadekoBot/DataStructures/SearchImageCacher.cs new file mode 100644 index 00000000..1296c311 --- /dev/null +++ b/src/NadekoBot/DataStructures/SearchImageCacher.cs @@ -0,0 +1,213 @@ +using NadekoBot.Extensions; +using NadekoBot.Services; +using Newtonsoft.Json; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using System.Xml; +using System.Xml.Linq; + +namespace NadekoBot.DataStructures +{ + public class SearchImageCacher + { + private readonly NadekoRandom _rng; + private readonly ConcurrentDictionary _locks = new ConcurrentDictionary(); + + private readonly SortedSet _cache; + + public SearchImageCacher() + { + _rng = new NadekoRandom(); + _cache = new SortedSet(); + } + + public async Task GetImage(string tag, bool forceExplicit, DapiSearchType type) + { + tag = tag?.ToLowerInvariant(); + + if (type == DapiSearchType.E621) + tag = tag?.Replace("yuri", "female/female"); + + var _lock = GetLock(type); + await _lock.WaitAsync(); + try + { + ImageCacherObject[] imgs; + if (!string.IsNullOrWhiteSpace(tag)) + { + imgs = _cache.Where(x => x.Tags.IsSupersetOf(tag.Split('+')) && x.SearchType == type && (!forceExplicit || x.Rating == "e")).ToArray(); + } + else + { + tag = null; + imgs = _cache.Where(x => x.SearchType == type).ToArray(); + } + ImageCacherObject img; + if (imgs.Length == 0) + img = null; + else + img = imgs[_rng.Next(imgs.Length)]; + + if (img != null) + { + _cache.Remove(img); + return img; + } + else + { + var images = await DownloadImages(tag, forceExplicit, type).ConfigureAwait(false); + if (images.Length == 0) + return null; + var toReturn = images[_rng.Next(images.Length)]; + foreach (var dledImg in images) + { + if(dledImg != toReturn) + _cache.Add(dledImg); + } + return toReturn; + } + } + finally + { + _lock.Release(); + } + } + + private SemaphoreSlim GetLock(DapiSearchType type) + { + return _locks.GetOrAdd(type, _ => new SemaphoreSlim(1, 1)); + } + + public async Task DownloadImages(string tag, bool isExplicit, DapiSearchType type) + { + Console.WriteLine($"Loading extra images from {type}"); + tag = tag?.Replace(" ", "_").ToLowerInvariant(); + if (isExplicit) + tag = "rating%3Aexplicit+" + tag; + var website = ""; + switch (type) + { + case DapiSearchType.Safebooru: + website = $"https://safebooru.org/index.php?page=dapi&s=post&q=index&limit=1000&tags={tag}"; + break; + case DapiSearchType.E621: + website = $"https://e621.net/post/index.json?limit=1000&tags={tag}"; + break; + case DapiSearchType.Danbooru: + website = $"https://danbooru.donmai.us/posts.json?limit=200&tags={tag}"; + break; + case DapiSearchType.Gelbooru: + website = $"http://gelbooru.com/index.php?page=dapi&s=post&q=index&limit=1000&tags={tag}"; + break; + case DapiSearchType.Rule34: + website = $"https://rule34.xxx/index.php?page=dapi&s=post&q=index&limit=100&tags={tag}"; + break; + case DapiSearchType.Konachan: + website = $"https://konachan.com/post.json?s=post&q=index&limit=1000&tags={tag}"; + break; + case DapiSearchType.Yandere: + website = $"https://yande.re/post.json?limit=1000&tags={tag}"; + break; + } + + using (var http = new HttpClient()) + { + http.AddFakeHeaders(); + + if (type == DapiSearchType.Konachan || type == DapiSearchType.Yandere || + type == DapiSearchType.E621 || type == DapiSearchType.Danbooru) + { + var data = await http.GetStringAsync(website).ConfigureAwait(false); + return JsonConvert.DeserializeObject(data) + .Where(x => x.File_Url != null) + .Select(x => new ImageCacherObject(x, type)) + .ToArray(); + } + + return (await LoadXmlAsync(website, type)).ToArray(); + } + } + + private async Task LoadXmlAsync(string website, DapiSearchType type) + { + var list = new List(1000); + using (var http = new HttpClient()) + { + using (var reader = XmlReader.Create(await http.GetStreamAsync(website), new XmlReaderSettings() + { + Async = true, + })) + { + while (await reader.ReadAsync()) + { + if (reader.NodeType == XmlNodeType.Element && + reader.Name == "post") + { + list.Add(new ImageCacherObject(new DapiImageObject() + { + File_Url = reader["file_url"], + Tags = reader["tags"], + Rating = reader["rating"] ?? "e" + + }, type)); + } + } + } + } + return list.ToArray(); + } + } + + public class ImageCacherObject : IComparable + { + public DapiSearchType SearchType { get; } + public string FileUrl { get; } + public HashSet Tags { get; } + public string Rating { get; } + + public ImageCacherObject(DapiImageObject obj, DapiSearchType type) + { + if (type == DapiSearchType.Danbooru) + this.FileUrl = "https://danbooru.donmai.us" + obj.File_Url; + else + this.FileUrl = obj.File_Url.StartsWith("http") ? obj.File_Url : "https:" + obj.File_Url; + this.SearchType = type; + this.Rating = obj.Rating; + this.Tags = new HashSet((obj.Tags ?? obj.Tag_String).Split(' ')); + } + + public override string ToString() + { + return FileUrl; + } + + public int CompareTo(ImageCacherObject other) + { + return FileUrl.CompareTo(other.FileUrl); + } + } + + public class DapiImageObject + { + public string File_Url { get; set; } + public string Tags { get; set; } + public string Tag_String { get; set; } + public string Rating { get; set; } + } + + public enum DapiSearchType + { + Safebooru, + E621, + Gelbooru, + Konachan, + Rule34, + Yandere, + Danbooru + } +} diff --git a/src/NadekoBot/Modules/NSFW/NSFW.cs b/src/NadekoBot/Modules/NSFW/NSFW.cs index d09467dd..7ce29178 100644 --- a/src/NadekoBot/Modules/NSFW/NSFW.cs +++ b/src/NadekoBot/Modules/NSFW/NSFW.cs @@ -12,6 +12,7 @@ using System.Xml; using System.Threading; using System.Collections.Concurrent; using NadekoBot.Services.Searches; +using NadekoBot.DataStructures; namespace NadekoBot.Modules.NSFW { @@ -28,29 +29,12 @@ namespace NadekoBot.Modules.NSFW private async Task InternalHentai(IMessageChannel channel, string tag, bool noError) { - tag = tag?.Trim() ?? ""; - - tag = "rating%3Aexplicit+" + tag; - var rng = new NadekoRandom(); - var provider = Task.FromResult(""); - switch (rng.Next(0, 4)) - { - case 0: - provider = GetDanbooruImageLink(tag); - break; - case 1: - provider = GetGelbooruImageLink(tag); - break; - case 2: - provider = GetKonachanImageLink(tag); - break; - case 3: - provider = GetYandereImageLink(tag); - break; - } - var link = await provider.ConfigureAwait(false); - if (string.IsNullOrWhiteSpace(link)) + var arr = Enum.GetValues(typeof(DapiSearchType)); + var type = (DapiSearchType)arr.GetValue(new NadekoRandom().Next(2, arr.Length)); + var img = await _service.DapiSearch(tag, type, Context.Guild?.Id, true).ConfigureAwait(false); + + if (img == null) { if (!noError) await ReplyErrorLocalized("not_found").ConfigureAwait(false); @@ -58,8 +42,8 @@ namespace NadekoBot.Modules.NSFW } await channel.EmbedAsync(new EmbedBuilder().WithOkColor() - .WithImageUrl(link) - .WithDescription($"[{GetText("tag")}: {tag}]({link})")) + .WithImageUrl(img.FileUrl) + .WithDescription($"[{GetText("tag")}: {tag}]({img})")) .ConfigureAwait(false); } @@ -108,114 +92,62 @@ namespace NadekoBot.Modules.NSFW return t; }); - await ReplyConfirmLocalized("autohentai_started", - interval, + await ReplyConfirmLocalized("autohentai_started", + interval, string.Join(", ", tagsArr)).ConfigureAwait(false); } - +#endif [NadekoCommand, Usage, Description, Aliases] public async Task HentaiBomb([Remainder] string tag = null) { - if (!_hentaiBombBlacklist.Add(Context.User.Id)) + if (!_hentaiBombBlacklist.Add(Context.Guild?.Id ?? Context.User.Id)) return; try { - tag = tag?.Trim() ?? ""; - tag = "rating%3Aexplicit+" + tag; + var images = await Task.WhenAll(_service.DapiSearch(tag, DapiSearchType.Gelbooru, Context.Guild?.Id, true), + _service.DapiSearch(tag, DapiSearchType.Danbooru, Context.Guild?.Id, true), + _service.DapiSearch(tag, DapiSearchType.Konachan, Context.Guild?.Id, true), + _service.DapiSearch(tag, DapiSearchType.Yandere, Context.Guild?.Id, true)).ConfigureAwait(false); - var links = await Task.WhenAll(GetGelbooruImageLink(tag), - GetDanbooruImageLink(tag), - GetKonachanImageLink(tag), - GetYandereImageLink(tag)).ConfigureAwait(false); - - var linksEnum = links?.Where(l => l != null).ToArray(); - if (links == null || !linksEnum.Any()) + var linksEnum = images?.Where(l => l != null).ToArray(); + if (images == null || !linksEnum.Any()) { await ReplyErrorLocalized("not_found").ConfigureAwait(false); return; } - await Context.Channel.SendMessageAsync(string.Join("\n\n", linksEnum)).ConfigureAwait(false); + await Context.Channel.SendMessageAsync(string.Join("\n\n", linksEnum.Select(x => x.FileUrl))).ConfigureAwait(false); } finally { - await Task.Delay(5000).ConfigureAwait(false); - _hentaiBombBlacklist.TryRemove(Context.User.Id); + _hentaiBombBlacklist.TryRemove(Context.Guild?.Id ?? Context.User.Id); } } -#endif + [NadekoCommand, Usage, Description, Aliases] public Task Yandere([Remainder] string tag = null) - => InternalDapiCommand(tag, DapiSearchType.Yandere); + => InternalDapiCommand(tag, DapiSearchType.Yandere, false); [NadekoCommand, Usage, Description, Aliases] public Task Konachan([Remainder] string tag = null) - => InternalDapiCommand(tag, DapiSearchType.Konachan); + => InternalDapiCommand(tag, DapiSearchType.Konachan, false); [NadekoCommand, Usage, Description, Aliases] - public async Task E621([Remainder] string tag = null) - { - tag = tag?.Trim() ?? ""; - - var url = await GetE621ImageLink(tag).ConfigureAwait(false); - - if (url == null) - await ReplyErrorLocalized("not_found").ConfigureAwait(false); - else - await Context.Channel.EmbedAsync(new EmbedBuilder().WithOkColor() - .WithDescription(Context.User.Mention + " " + tag) - .WithImageUrl(url) - .WithFooter(efb => efb.WithText("e621"))) - .ConfigureAwait(false); - } + public Task E621([Remainder] string tag = null) + => InternalDapiCommand(tag, DapiSearchType.E621, false); [NadekoCommand, Usage, Description, Aliases] public Task Rule34([Remainder] string tag = null) - => InternalDapiCommand(tag, DapiSearchType.Rule34); + => InternalDapiCommand(tag, DapiSearchType.Rule34, false); [NadekoCommand, Usage, Description, Aliases] - public async Task Danbooru([Remainder] string tag = null) - { - tag = tag?.Trim() ?? ""; - - var url = await GetDanbooruImageLink(tag).ConfigureAwait(false); - - if (url == null) - await ReplyErrorLocalized("not_found").ConfigureAwait(false); - else - await Context.Channel.EmbedAsync(new EmbedBuilder().WithOkColor() - .WithDescription(Context.User.Mention + " " + tag) - .WithImageUrl(url) - .WithFooter(efb => efb.WithText("Danbooru"))) - .ConfigureAwait(false); - } - - public static Task GetDanbooruImageLink(string tag) => Task.Run(async () => - { - try - { - using (var http = new HttpClient()) - { - http.AddFakeHeaders(); - var data = await http.GetStreamAsync("https://danbooru.donmai.us/posts.xml?limit=100&tags=" + tag).ConfigureAwait(false); - var doc = new XmlDocument(); - doc.Load(data); - var nodes = doc.GetElementsByTagName("file-url"); - - var node = nodes[new NadekoRandom().Next(0, nodes.Count)]; - return "https://danbooru.donmai.us" + node.InnerText; - } - } - catch - { - return null; - } - }); + public Task Danbooru([Remainder] string tag = null) + => InternalDapiCommand(tag, DapiSearchType.Danbooru, false); [NadekoCommand, Usage, Description, Aliases] public Task Gelbooru([Remainder] string tag = null) - => InternalDapiCommand(tag, DapiSearchType.Gelbooru); + => InternalDapiCommand(tag, DapiSearchType.Gelbooru, false); [NadekoCommand, Usage, Description, Aliases] public async Task Boobs() @@ -253,52 +185,16 @@ namespace NadekoBot.Modules.NSFW } } - public static Task GetE621ImageLink(string tag) => Task.Run(async () => + public async Task InternalDapiCommand(string tag, DapiSearchType type, bool forceExplicit) { - try - { - using (var http = new HttpClient()) - { - http.AddFakeHeaders(); - var data = await http.GetStreamAsync("http://e621.net/post/index.xml?tags=" + tag).ConfigureAwait(false); - var doc = new XmlDocument(); - doc.Load(data); - var nodes = doc.GetElementsByTagName("file_url"); + var imgObj = await _service.DapiSearch(tag, type, Context.Guild?.Id, forceExplicit).ConfigureAwait(false); - var node = nodes[new NadekoRandom().Next(0, nodes.Count)]; - return node.InnerText; - } - } - catch - { - return null; - } - }); - - public Task GetRule34ImageLink(string tag) => - _service.DapiSearch(tag, DapiSearchType.Rule34); - - public Task GetYandereImageLink(string tag) => - _service.DapiSearch(tag, DapiSearchType.Yandere); - - public Task GetKonachanImageLink(string tag) => - _service.DapiSearch(tag, DapiSearchType.Konachan); - - public Task GetGelbooruImageLink(string tag) => - _service.DapiSearch(tag, DapiSearchType.Gelbooru); - - public async Task InternalDapiCommand(string tag, DapiSearchType type) - { - tag = tag?.Trim() ?? ""; - - var url = await _service.DapiSearch(tag, type).ConfigureAwait(false); - - if (url == null) + if (imgObj == null) await ReplyErrorLocalized("not_found").ConfigureAwait(false); else await Context.Channel.EmbedAsync(new EmbedBuilder().WithOkColor() - .WithDescription($"{Context.User} [{tag}]({url}) ") - .WithImageUrl(url) + .WithDescription($"{Context.User} [{tag ?? "url"}]({imgObj}) ") + .WithImageUrl(imgObj.FileUrl) .WithFooter(efb => efb.WithText(type.ToString()))).ConfigureAwait(false); } } diff --git a/src/NadekoBot/Modules/Searches/Searches.cs b/src/NadekoBot/Modules/Searches/Searches.cs index b7886f44..0a8eec54 100644 --- a/src/NadekoBot/Modules/Searches/Searches.cs +++ b/src/NadekoBot/Modules/Searches/Searches.cs @@ -21,6 +21,7 @@ using NadekoBot.Attributes; using Discord.Commands; using ImageSharp; using NadekoBot.Services.Searches; +using NadekoBot.DataStructures; namespace NadekoBot.Modules.Searches { @@ -791,14 +792,14 @@ namespace NadekoBot.Modules.Searches tag = tag?.Trim() ?? ""; - var url = await _searches.DapiSearch(tag, type).ConfigureAwait(false); + var imgObj = await _searches.DapiSearch(tag, type, Context.Guild?.Id).ConfigureAwait(false); - if (url == null) + if (imgObj == null) await channel.SendErrorAsync(umsg.Author.Mention + " " + GetText("no_results")); else await channel.EmbedAsync(new EmbedBuilder().WithOkColor() - .WithDescription($"{umsg.Author.Mention} [{tag}]({url})") - .WithImageUrl(url) + .WithDescription($"{umsg.Author.Mention} [{tag ?? "url"}]({imgObj.FileUrl})") + .WithImageUrl(imgObj.FileUrl) .WithFooter(efb => efb.WithText(type.ToString()))).ConfigureAwait(false); } diff --git a/src/NadekoBot/Services/CustomReactions/Extensions.cs b/src/NadekoBot/Services/CustomReactions/Extensions.cs index 4e659e9e..af968192 100644 --- a/src/NadekoBot/Services/CustomReactions/Extensions.cs +++ b/src/NadekoBot/Services/CustomReactions/Extensions.cs @@ -41,7 +41,7 @@ namespace NadekoBot.Services.CustomReactions if (img?.Source == null) return ""; - return " "+img.Source.Replace("b.", ".") + " "; + return " " + img.Source.Replace("b.", ".") + " "; } } }; diff --git a/src/NadekoBot/Services/Impl/StatsService.cs b/src/NadekoBot/Services/Impl/StatsService.cs index 338c656b..4b9b6bac 100644 --- a/src/NadekoBot/Services/Impl/StatsService.cs +++ b/src/NadekoBot/Services/Impl/StatsService.cs @@ -17,7 +17,7 @@ namespace NadekoBot.Services.Impl private readonly IBotCredentials _creds; private readonly DateTime _started; - public const string BotVersion = "1.51"; + public const string BotVersion = "1.52"; public string Author => "Kwoth#2560"; public string Library => "Discord.Net"; diff --git a/src/NadekoBot/Services/Searches/SearchesService.cs b/src/NadekoBot/Services/Searches/SearchesService.cs index 8c98a22e..54a268c8 100644 --- a/src/NadekoBot/Services/Searches/SearchesService.cs +++ b/src/NadekoBot/Services/Searches/SearchesService.cs @@ -1,11 +1,13 @@ using Discord; using Discord.WebSocket; +using NadekoBot.DataStructures; using NadekoBot.Extensions; using Newtonsoft.Json; using NLog; using System; using System.Collections.Concurrent; using System.Collections.Generic; +using System.Diagnostics; using System.IO; using System.Net.Http; using System.Threading.Tasks; @@ -31,6 +33,8 @@ namespace NadekoBot.Services.Searches public List WowJokes { get; } = new List(); public List MagicItems { get; } = new List(); + private readonly ConcurrentDictionary _imageCacher = new ConcurrentDictionary(); + public SearchesService(DiscordSocketClient client, IGoogleApiService google, DbService db) { _client = client; @@ -113,64 +117,13 @@ namespace NadekoBot.Services.Searches return (await _google.Translate(text, from, to).ConfigureAwait(false)).SanitizeMentions(); } - public async Task DapiSearch(string tag, DapiSearchType type) + public Task DapiSearch(string tag, DapiSearchType type, ulong? guild, bool isExplicit = false) { - tag = tag?.Replace(" ", "_"); - var website = ""; - switch (type) - { - case DapiSearchType.Safebooru: - website = $"https://safebooru.org/index.php?page=dapi&s=post&q=index&limit=100&tags={tag}"; - break; - case DapiSearchType.Gelbooru: - website = $"http://gelbooru.com/index.php?page=dapi&s=post&q=index&limit=100&tags={tag}"; - break; - case DapiSearchType.Rule34: - website = $"https://rule34.xxx/index.php?page=dapi&s=post&q=index&limit=100&tags={tag}"; - break; - case DapiSearchType.Konachan: - website = $"https://konachan.com/post.xml?s=post&q=index&limit=100&tags={tag}"; - break; - case DapiSearchType.Yandere: - website = $"https://yande.re/post.xml?limit=100&tags={tag}"; - break; - } - try - { - var toReturn = await Task.Run(async () => - { - using (var http = new HttpClient()) - { - http.AddFakeHeaders(); - var data = await http.GetStreamAsync(website).ConfigureAwait(false); - var doc = new XmlDocument(); - doc.Load(data); - - var node = doc.LastChild.ChildNodes[new NadekoRandom().Next(0, doc.LastChild.ChildNodes.Count)]; - - var url = node.Attributes["file_url"].Value; - if (!url.StartsWith("http")) - url = "https:" + url; - return url; - } - }).ConfigureAwait(false); - return toReturn; - } - catch - { - return null; - } + var cacher = _imageCacher.GetOrAdd(guild, (key) => new SearchImageCacher()); + + return cacher.GetImage(tag, isExplicit, type); } } - - public enum DapiSearchType - { - Safebooru, - Gelbooru, - Konachan, - Rule34, - Yandere - } public struct UserChannelPair { @@ -178,7 +131,6 @@ namespace NadekoBot.Services.Searches public ulong ChannelId { get; set; } } - public class StreamStatus { public bool IsLive { get; set; } From cb5fdde6d06f777e7afe92e5832c28130b46e3dc Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Thu, 29 Jun 2017 18:40:13 +0200 Subject: [PATCH 051/346] Fixed .guide links --- src/NadekoBot/Modules/Help/Help.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/NadekoBot/Modules/Help/Help.cs b/src/NadekoBot/Modules/Help/Help.cs index 37377713..18ddf039 100644 --- a/src/NadekoBot/Modules/Help/Help.cs +++ b/src/NadekoBot/Modules/Help/Help.cs @@ -155,8 +155,8 @@ namespace NadekoBot.Modules.Help public async Task Guide() { await ConfirmLocalized("guide", - "http://nadekobot.readthedocs.io/en/1.3x/Commands%20List/", - "http://nadekobot.readthedocs.io/en/1.3x/").ConfigureAwait(false); + "http://nadekobot.readthedocs.io/en/latest/Commands%20List/", + "http://nadekobot.readthedocs.io/en/latest/").ConfigureAwait(false); } [NadekoCommand, Usage, Description, Aliases] From 8f5c63a057f1208fbe0c2929c2341109b6de16d7 Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Fri, 30 Jun 2017 05:40:17 +0200 Subject: [PATCH 052/346] fixed .ropl toggle response --- .../Commands/PlayingRotateCommands.cs | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/src/NadekoBot/Modules/Administration/Commands/PlayingRotateCommands.cs b/src/NadekoBot/Modules/Administration/Commands/PlayingRotateCommands.cs index 867ff91a..03957dfc 100644 --- a/src/NadekoBot/Modules/Administration/Commands/PlayingRotateCommands.cs +++ b/src/NadekoBot/Modules/Administration/Commands/PlayingRotateCommands.cs @@ -27,17 +27,15 @@ namespace NadekoBot.Modules.Administration [OwnerOnly] public async Task RotatePlaying() { - lock (_locker) + bool enabled; + using (var uow = _db.UnitOfWork) { - using (var uow = _db.UnitOfWork) - { - var config = uow.BotConfig.GetOrCreate(); + var config = uow.BotConfig.GetOrCreate(); - config.RotatingStatuses = !config.RotatingStatuses; - uow.Complete(); - } + enabled = config.RotatingStatuses = !config.RotatingStatuses; + uow.Complete(); } - if (_service.BotConfig.RotatingStatuses) + if (enabled) await ReplyConfirmLocalized("ropl_enabled").ConfigureAwait(false); else await ReplyConfirmLocalized("ropl_disabled").ConfigureAwait(false); From d242952d4a84e1e0186c930e178250bdf3062eef Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Sat, 1 Jul 2017 08:15:58 +0200 Subject: [PATCH 053/346] Huge amount of work on the music rework. Around 60% done. Fixed bot getting stuck when server region is changed. --- .../Replacements/ReplacementBuilder.cs | 38 +- .../DataStructures/SyncPrecondition.cs | 23 + src/NadekoBot/Modules/Music/Music.cs | 1229 +++++++++-------- .../Administration/PlayingRotateService.cs | 2 +- src/NadekoBot/Services/Impl/StatsService.cs | 2 +- .../Services/Impl/SyncPreconditionService.cs | 13 + src/NadekoBot/Services/Music/Exceptions.cs | 12 +- src/NadekoBot/Services/Music/MusicControls.cs | 381 ----- src/NadekoBot/Services/Music/MusicPlayer.cs | 657 +++++++++ src/NadekoBot/Services/Music/MusicQueue.cs | 119 ++ src/NadekoBot/Services/Music/MusicService.cs | 541 ++++---- src/NadekoBot/Services/Music/Song.cs | 466 +++---- src/NadekoBot/Services/Music/SongBuffer.cs | 378 ++--- src/NadekoBot/Services/Music/SongInfo.cs | 85 ++ src/NadekoBot/Services/Music/SongResolver.cs | 111 ++ 15 files changed, 2304 insertions(+), 1753 deletions(-) create mode 100644 src/NadekoBot/DataStructures/SyncPrecondition.cs create mode 100644 src/NadekoBot/Services/Impl/SyncPreconditionService.cs delete mode 100644 src/NadekoBot/Services/Music/MusicControls.cs create mode 100644 src/NadekoBot/Services/Music/MusicPlayer.cs create mode 100644 src/NadekoBot/Services/Music/MusicQueue.cs create mode 100644 src/NadekoBot/Services/Music/SongInfo.cs create mode 100644 src/NadekoBot/Services/Music/SongResolver.cs diff --git a/src/NadekoBot/DataStructures/Replacements/ReplacementBuilder.cs b/src/NadekoBot/DataStructures/Replacements/ReplacementBuilder.cs index b7fabd25..2a7ac6bf 100644 --- a/src/NadekoBot/DataStructures/Replacements/ReplacementBuilder.cs +++ b/src/NadekoBot/DataStructures/Replacements/ReplacementBuilder.cs @@ -74,25 +74,25 @@ namespace NadekoBot.DataStructures.Replacements return this; } - public ReplacementBuilder WithMusic(MusicService ms) - { - _reps.TryAdd("%playing%", () => - { - var cnt = ms.MusicPlayers.Count(kvp => kvp.Value.CurrentSong != null); - if (cnt != 1) return cnt.ToString(); - try - { - var mp = ms.MusicPlayers.FirstOrDefault(); - return mp.Value.CurrentSong.SongInfo.Title; - } - catch - { - return "No songs"; - } - }); - _reps.TryAdd("%queued%", () => ms.MusicPlayers.Sum(kvp => kvp.Value.Playlist.Count).ToString()); - return this; - } + //public ReplacementBuilder WithMusic(MusicService ms) + //{ + // _reps.TryAdd("%playing%", () => + // { + // var cnt = ms.MusicPlayers.Count(kvp => kvp.Value.CurrentSong != null); + // if (cnt != 1) return cnt.ToString(); + // try + // { + // var mp = ms.MusicPlayers.FirstOrDefault(); + // return mp.Value.CurrentSong.SongInfo.Title; + // } + // catch + // { + // return "No songs"; + // } + // }); + // _reps.TryAdd("%queued%", () => ms.MusicPlayers.Sum(kvp => kvp.Value.Playlist.Count).ToString()); + // return this; + //} public ReplacementBuilder WithRngRegex() { diff --git a/src/NadekoBot/DataStructures/SyncPrecondition.cs b/src/NadekoBot/DataStructures/SyncPrecondition.cs new file mode 100644 index 00000000..6dd675e5 --- /dev/null +++ b/src/NadekoBot/DataStructures/SyncPrecondition.cs @@ -0,0 +1,23 @@ +using Discord.Commands; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace NadekoBot.DataStructures +{ + //public class SyncPrecondition : PreconditionAttribute + //{ + // public override Task CheckPermissions(ICommandContext context, + // CommandInfo command, + // IServiceProvider services) + // { + + // } + //} + //public enum SyncType + //{ + // Guild + //} +} diff --git a/src/NadekoBot/Modules/Music/Music.cs b/src/NadekoBot/Modules/Music/Music.cs index ebec33c5..472acd17 100644 --- a/src/NadekoBot/Modules/Music/Music.cs +++ b/src/NadekoBot/Modules/Music/Music.cs @@ -15,6 +15,7 @@ using NadekoBot.Services.Database.Models; using System.Threading; using NadekoBot.Services.Music; using NadekoBot.DataStructures; +using System.Collections.Concurrent; namespace NadekoBot.Modules.Music { @@ -35,139 +36,79 @@ namespace NadekoBot.Modules.Music _google = google; _db = db; _music = music; - //it can fail if its currenctly opened or doesn't exist. Either way i don't care - _client.UserVoiceStateUpdated += Client_UserVoiceStateUpdated; + //_client.UserVoiceStateUpdated += Client_UserVoiceStateUpdated; } - private Task Client_UserVoiceStateUpdated(SocketUser iusr, SocketVoiceState oldState, SocketVoiceState newState) + //private Task Client_UserVoiceStateUpdated(SocketUser iusr, SocketVoiceState oldState, SocketVoiceState newState) + //{ + // var usr = iusr as SocketGuildUser; + // if (usr == null || + // oldState.VoiceChannel == newState.VoiceChannel) + // return Task.CompletedTask; + + // MusicPlayer player; + // if ((player = _music.GetPlayer(usr.Guild.Id)) == null) + // return Task.CompletedTask; + + // try + // { + // //if bot moved + // if ((player.PlaybackVoiceChannel == oldState.VoiceChannel) && + // usr.Id == _client.CurrentUser.Id) + // { + // if (player.Paused && newState.VoiceChannel.Users.Count > 1) //unpause if there are people in the new channel + // player.TogglePause(); + // else if (!player.Paused && newState.VoiceChannel.Users.Count <= 1) // pause if there are no users in the new channel + // player.TogglePause(); + + // return Task.CompletedTask; + // } + + + // //if some other user moved + // if ((player.PlaybackVoiceChannel == newState.VoiceChannel && //if joined first, and player paused, unpause + // player.Paused && + // newState.VoiceChannel.Users.Count == 2) || // keep in mind bot is in the channel (+1) + // (player.PlaybackVoiceChannel == oldState.VoiceChannel && // if left last, and player unpaused, pause + // !player.Paused && + // oldState.VoiceChannel.Users.Count == 1)) + // { + // player.TogglePause(); + // return Task.CompletedTask; + // } + + // } + // catch + // { + // // ignored + // } + // return Task.CompletedTask; + //} + + private async Task InternalQueue(MusicPlayer mp, SongInfo songInfo, bool silent) { - var usr = iusr as SocketGuildUser; - if (usr == null || - oldState.VoiceChannel == newState.VoiceChannel) - return Task.CompletedTask; - - MusicPlayer player; - if ((player = _music.GetPlayer(usr.Guild.Id)) == null) - return Task.CompletedTask; - - try + var qData = mp.Enqueue(songInfo); + if (qData.Success) { - //if bot moved - if ((player.PlaybackVoiceChannel == oldState.VoiceChannel) && - usr.Id == _client.CurrentUser.Id) + if (!silent) { - if (player.Paused && newState.VoiceChannel.Users.Count > 1) //unpause if there are people in the new channel - player.TogglePause(); - else if (!player.Paused && newState.VoiceChannel.Users.Count <= 1) // pause if there are no users in the new channel - player.TogglePause(); - - return Task.CompletedTask; + try + { + //var queuedMessage = await textCh.SendConfirmAsync($"🎵 Queued **{resolvedSong.SongInfo.Title}** at `#{musicPlayer.Playlist.Count + 1}`").ConfigureAwait(false); + var queuedMessage = await mp.OutputTextChannel.EmbedAsync(new EmbedBuilder().WithOkColor() + .WithAuthor(eab => eab.WithName(GetText("queued_song") + " #" + (qData.Index)).WithMusicIcon()) + .WithDescription($"{songInfo.PrettyName}\n{GetText("queue")} ") + .WithThumbnailUrl(songInfo.Thumbnail) + .WithFooter(ef => ef.WithText(songInfo.PrettyProvider))) + .ConfigureAwait(false); + queuedMessage?.DeleteAfter(10); + } + catch + { + // ignored + } // if queued message sending fails, don't attempt to delete it } - - - //if some other user moved - if ((player.PlaybackVoiceChannel == newState.VoiceChannel && //if joined first, and player paused, unpause - player.Paused && - newState.VoiceChannel.Users.Count == 2) || // keep in mind bot is in the channel (+1) - (player.PlaybackVoiceChannel == oldState.VoiceChannel && // if left last, and player unpaused, pause - !player.Paused && - oldState.VoiceChannel.Users.Count == 1)) - { - player.TogglePause(); - return Task.CompletedTask; - } - - } - catch - { - // ignored - } - return Task.CompletedTask; - } - - [NadekoCommand, Usage, Description, Aliases] - [RequireContext(ContextType.Guild)] - public Task Next(int skipCount = 1) - { - if (skipCount < 1) - return Task.CompletedTask; - - MusicPlayer musicPlayer; - if ((musicPlayer = _music.GetPlayer(Context.Guild.Id)) == null) - return Task.CompletedTask; - if (musicPlayer.PlaybackVoiceChannel == ((IGuildUser)Context.User).VoiceChannel) - { - while (--skipCount > 0) - { - musicPlayer.RemoveSongAt(0); - } - musicPlayer.Next(); - } - return Task.CompletedTask; - } - - [NadekoCommand, Usage, Description, Aliases] - [RequireContext(ContextType.Guild)] - public Task Stop() - { - MusicPlayer musicPlayer; - if ((musicPlayer = _music.GetPlayer(Context.Guild.Id)) == null) - return Task.CompletedTask; - if (((IGuildUser)Context.User).VoiceChannel == musicPlayer.PlaybackVoiceChannel) - { - musicPlayer.Autoplay = false; - musicPlayer.Stop(); - } - return Task.CompletedTask; - } - - [NadekoCommand, Usage, Description, Aliases] - [RequireContext(ContextType.Guild)] - public Task Destroy() - { - MusicPlayer musicPlayer; - if ((musicPlayer = _music.GetPlayer(Context.Guild.Id)) == null) - return Task.CompletedTask; - if (((IGuildUser)Context.User).VoiceChannel == musicPlayer.PlaybackVoiceChannel) - _music.DestroyPlayer(Context.Guild.Id); - - return Task.CompletedTask; - - } - - [NadekoCommand, Usage, Description, Aliases] - [RequireContext(ContextType.Guild)] - public Task Pause() - { - MusicPlayer musicPlayer; - if ((musicPlayer = _music.GetPlayer(Context.Guild.Id)) == null) - return Task.CompletedTask; - if (((IGuildUser)Context.User).VoiceChannel != musicPlayer.PlaybackVoiceChannel) - return Task.CompletedTask; - musicPlayer.TogglePause(); - return Task.CompletedTask; - } - - [NadekoCommand, Usage, Description, Aliases] - [RequireContext(ContextType.Guild)] - public async Task Fairplay() - { - var channel = (ITextChannel)Context.Channel; - MusicPlayer musicPlayer; - if ((musicPlayer = _music.GetPlayer(Context.Guild.Id)) == null) - return; - if (((IGuildUser)Context.User).VoiceChannel != musicPlayer.PlaybackVoiceChannel) - return; - var val = musicPlayer.FairPlay = !musicPlayer.FairPlay; - - if (val) - { - await ReplyConfirmLocalized("fp_enabled").ConfigureAwait(false); - } - else - { - await ReplyConfirmLocalized("fp_disabled").ConfigureAwait(false); } } @@ -175,7 +116,10 @@ namespace NadekoBot.Modules.Music [RequireContext(ContextType.Guild)] public async Task Queue([Remainder] string query) { - await _music.QueueSong(((IGuildUser)Context.User), (ITextChannel)Context.Channel, ((IGuildUser)Context.User).VoiceChannel, query).ConfigureAwait(false); + var mp = await _music.GetOrCreatePlayer(Context); + var songInfo = await _music.ResolveSong(query, Context.User.ToString()); + await InternalQueue(mp, songInfo, false); + if ((await Context.Guild.GetCurrentUserAsync()).GetPermissions((IGuildChannel)Context.Channel).ManageMessages) { Context.Message.DeleteAfter(10); @@ -217,29 +161,16 @@ namespace NadekoBot.Modules.Music { try { await msg.DeleteAsync().ConfigureAwait(false); } catch { } } - - } - - [NadekoCommand, Usage, Description, Aliases] - [RequireContext(ContextType.Guild)] - public async Task SoundCloudQueue([Remainder] string query) - { - await _music.QueueSong(((IGuildUser)Context.User), (ITextChannel)Context.Channel, ((IGuildUser)Context.User).VoiceChannel, query, musicType: MusicType.Soundcloud).ConfigureAwait(false); - if ((await Context.Guild.GetCurrentUserAsync()).GetPermissions((IGuildChannel)Context.Channel).ManageMessages) - { - Context.Message.DeleteAfter(10); - } } [NadekoCommand, Usage, Description, Aliases] [RequireContext(ContextType.Guild)] public async Task ListQueue(int page = 1) { - Song currentSong; - MusicPlayer musicPlayer; - if ((musicPlayer = _music.GetPlayer(Context.Guild.Id)) == null) - return; - if ((currentSong = musicPlayer?.CurrentSong) == null) + var mp = await _music.GetOrCreatePlayer(Context); + var (current, songs) = mp.QueueArray(); + + if (!songs.Any()) { await ReplyErrorLocalized("no_player").ConfigureAwait(false); return; @@ -247,33 +178,39 @@ namespace NadekoBot.Modules.Music if (--page < 0) return; - - try { await musicPlayer.UpdateSongDurationsAsync().ConfigureAwait(false); } catch { } + //todo say whether music player is stopped + //try { await musicPlayer.UpdateSongDurationsAsync().ConfigureAwait(false); } catch { } const int itemsPerPage = 10; - var total = musicPlayer.TotalPlaytime; - var totalStr = total == TimeSpan.MaxValue ? "∞" : GetText("time_format", - (int) total.TotalHours, - total.Minutes, - total.Seconds); - var maxPlaytime = musicPlayer.MaxPlaytimeSeconds; - var lastPage = musicPlayer.Playlist.Count / itemsPerPage; + //var total = musicPlayer.TotalPlaytime; + //var totalStr = total == TimeSpan.MaxValue ? "∞" : GetText("time_format", + // (int)total.TotalHours, + // total.Minutes, + // total.Seconds); + //var maxPlaytime = musicPlayer.MaxPlaytimeSeconds; + var lastPage = songs.Length / itemsPerPage; Func printAction = curPage => { var startAt = itemsPerPage * curPage; var number = 0 + startAt; - var desc = string.Join("\n", musicPlayer.Playlist + var desc = string.Join("\n", songs .Skip(startAt) .Take(itemsPerPage) - .Select(v => $"`{++number}.` {v.PrettyFullName}")); - - desc = $"`🔊` {currentSong.PrettyFullName}\n\n" + desc; + .Select(v => + { + if(number++ == current) + return $"**⇒**`{number}.` {v.PrettyFullName}"; + else + return $"`{number}.` {v.PrettyFullName}"; + })); //todo v.prettyfullname instead of title - if (musicPlayer.RepeatSong) - desc = "🔂 " + GetText("repeating_cur_song") +"\n\n" + desc; - else if (musicPlayer.RepeatPlaylist) - desc = "🔁 " + GetText("repeating_playlist")+"\n\n" + desc; + desc = $"`🔊` {songs[current].PrettyFullName}\n\n" + desc; + + if (mp.RepeatCurrentSong) + desc = "🔂 " + GetText("repeating_cur_song") + "\n\n" + desc; + //else if (musicPlayer.RepeatPlaylist) + // desc = "🔁 " + GetText("repeating_playlist") + "\n\n" + desc; @@ -281,12 +218,12 @@ namespace NadekoBot.Modules.Music .WithAuthor(eab => eab.WithName(GetText("player_queue", curPage + 1, lastPage + 1)) .WithMusicIcon()) .WithDescription(desc) - .WithFooter(ef => ef.WithText($"{musicPlayer.PrettyVolume} | {musicPlayer.Playlist.Count} " + - $"{("tracks".SnPl(musicPlayer.Playlist.Count))} | {totalStr} | " + - (musicPlayer.FairPlay - ? "✔️" + GetText("fairplay") - : "✖️" + GetText("fairplay")) + " | " + - (maxPlaytime == 0 ? "unlimited" : GetText("play_limit", maxPlaytime)))) + //.WithFooter(ef => ef.WithText($"{musicPlayer.PrettyVolume} | {musicPlayer.Playlist.Count} " + + // $"{("tracks".SnPl(musicPlayer.Playlist.Count))} | {totalStr} | " + + // (musicPlayer.FairPlay + // ? "✔️" + GetText("fairplay") + // : "✖️" + GetText("fairplay")) + " | " + + // (maxPlaytime == 0 ? "unlimited" : GetText("play_limit", maxPlaytime)))) .WithOkColor(); return embed; @@ -296,41 +233,52 @@ namespace NadekoBot.Modules.Music [NadekoCommand, Usage, Description, Aliases] [RequireContext(ContextType.Guild)] - public async Task NowPlaying() + public async Task Next(int skipCount = 1) { - MusicPlayer musicPlayer; - if ((musicPlayer = _music.GetPlayer(Context.Guild.Id)) == null) + if (skipCount < 1) return; - var currentSong = musicPlayer.CurrentSong; - if (currentSong == null) - return; - try { await musicPlayer.UpdateSongDurationsAsync().ConfigureAwait(false); } catch { } + + var mp = await _music.GetOrCreatePlayer(Context); - var embed = new EmbedBuilder().WithOkColor() - .WithAuthor(eab => eab.WithName(GetText("now_playing")).WithMusicIcon()) - .WithDescription(currentSong.PrettyName) - .WithThumbnailUrl(currentSong.Thumbnail) - .WithFooter(ef => ef.WithText(musicPlayer.PrettyVolume + " | " + currentSong.PrettyFullTime + $" | {currentSong.PrettyProvider} | {currentSong.QueuerName}")); + mp.Next(); + } - await Context.Channel.EmbedAsync(embed).ConfigureAwait(false); + [NadekoCommand, Usage, Description, Aliases] + [RequireContext(ContextType.Guild)] + public async Task Stop() + { + var mp = await _music.GetOrCreatePlayer(Context); + mp.Stop(); + } + + [NadekoCommand, Usage, Description, Aliases] + [RequireContext(ContextType.Guild)] + public Task Destroy() + { + _music.DestroyPlayer(Context.Guild.Id); + return Task.CompletedTask; + } + + [NadekoCommand, Usage, Description, Aliases] + [RequireContext(ContextType.Guild)] + public async Task Pause() + { + var mp = await _music.GetOrCreatePlayer(Context); + mp.TogglePause(); } [NadekoCommand, Usage, Description, Aliases] [RequireContext(ContextType.Guild)] public async Task Volume(int val) { - MusicPlayer musicPlayer; - if ((musicPlayer = _music.GetPlayer(Context.Guild.Id)) == null) - return; - if (((IGuildUser)Context.User).VoiceChannel != musicPlayer.PlaybackVoiceChannel) - return; + var mp = await _music.GetOrCreatePlayer(Context); if (val < 0 || val > 100) { await ReplyErrorLocalized("volume_input_invalid").ConfigureAwait(false); return; } - var volume = musicPlayer.SetVolume(val); - await ReplyConfirmLocalized("volume_set", volume).ConfigureAwait(false); + mp.SetVolume(val); + await ReplyConfirmLocalized("volume_set", val).ConfigureAwait(false); } [NadekoCommand, Usage, Description, Aliases] @@ -350,401 +298,40 @@ namespace NadekoBot.Modules.Music await ReplyConfirmLocalized("defvol_set", val).ConfigureAwait(false); } - [NadekoCommand, Usage, Description, Aliases] - [RequireContext(ContextType.Guild)] - public async Task ShufflePlaylist() - { - MusicPlayer musicPlayer; - if ((musicPlayer = _music.GetPlayer(Context.Guild.Id)) == null) - return; - if (((IGuildUser)Context.User).VoiceChannel != musicPlayer.PlaybackVoiceChannel) - return; - if (musicPlayer.Playlist.Count < 2) - return; - - musicPlayer.Shuffle(); - await ReplyConfirmLocalized("songs_shuffled").ConfigureAwait(false); - } - - [NadekoCommand, Usage, Description, Aliases] - [RequireContext(ContextType.Guild)] - public async Task Playlist([Remainder] string playlist) - { - - var arg = playlist; - if (string.IsNullOrWhiteSpace(arg)) - return; - if (((IGuildUser)Context.User).VoiceChannel?.Guild != Context.Guild) - { - await ReplyErrorLocalized("must_be_in_voice").ConfigureAwait(false); - return; - } - var plId = (await _google.GetPlaylistIdsByKeywordsAsync(arg).ConfigureAwait(false)).FirstOrDefault(); - if (plId == null) - { - await ReplyErrorLocalized("no_search_results").ConfigureAwait(false); - return; - } - var ids = await _google.GetPlaylistTracksAsync(plId, 500).ConfigureAwait(false); - if (!ids.Any()) - { - await ReplyErrorLocalized("no_search_results").ConfigureAwait(false); - return; - } - var count = ids.Count(); - var msg = await Context.Channel.SendMessageAsync("🎵 " + GetText("attempting_to_queue", - Format.Bold(count.ToString()))).ConfigureAwait(false); - - var cancelSource = new CancellationTokenSource(); - - var gusr = (IGuildUser)Context.User; - while (ids.Any() && !cancelSource.IsCancellationRequested) - { - var tasks = Task.WhenAll(ids.Take(5).Select(async id => - { - if (cancelSource.Token.IsCancellationRequested) - return; - try - { - await _music.QueueSong(gusr, (ITextChannel)Context.Channel, gusr.VoiceChannel, id, true).ConfigureAwait(false); - } - catch (SongNotFoundException) { } - catch { try { cancelSource.Cancel(); } catch { } } - })); - - await Task.WhenAny(tasks, Task.Delay(Timeout.Infinite, cancelSource.Token)); - ids = ids.Skip(5); - } - - await msg.ModifyAsync(m => m.Content = "✅ " + Format.Bold(GetText("playlist_queue_complete"))).ConfigureAwait(false); - } - - [NadekoCommand, Usage, Description, Aliases] - [RequireContext(ContextType.Guild)] - public async Task SoundCloudPl([Remainder] string pl) - { - - pl = pl?.Trim(); - - if (string.IsNullOrWhiteSpace(pl)) - return; - - using (var http = new HttpClient()) - { - var scvids = JObject.Parse(await http.GetStringAsync($"https://scapi.nadekobot.me/resolve?url={pl}").ConfigureAwait(false))["tracks"].ToObject(); - await _music.QueueSong(((IGuildUser)Context.User), (ITextChannel)Context.Channel, ((IGuildUser)Context.User).VoiceChannel, scvids[0].TrackLink).ConfigureAwait(false); - - MusicPlayer musicPlayer; - if ((musicPlayer = _music.GetPlayer(Context.Guild.Id)) == null) - return; - - foreach (var svideo in scvids.Skip(1)) - { - try - { - musicPlayer.AddSong(new Song(new SongInfo - { - Title = svideo.FullName, - Provider = "SoundCloud", - Uri = await svideo.StreamLink(), - ProviderType = MusicType.Normal, - Query = svideo.TrackLink, - }), ((IGuildUser)Context.User).Username); - } - catch (PlaylistFullException) { break; } - } - } - } - - [NadekoCommand, Usage, Description, Aliases] - [RequireContext(ContextType.Guild)] - [OwnerOnly] - public async Task LocalPl([Remainder] string directory) - { - - var arg = directory; - if (string.IsNullOrWhiteSpace(arg)) - return; - var dir = new DirectoryInfo(arg); - var fileEnum = dir.GetFiles("*", SearchOption.AllDirectories) - .Where(x => !x.Attributes.HasFlag(FileAttributes.Hidden | FileAttributes.System)); - var gusr = (IGuildUser)Context.User; - foreach (var file in fileEnum) - { - try - { - await _music.QueueSong(gusr, (ITextChannel)Context.Channel, gusr.VoiceChannel, file.FullName, true, MusicType.Local).ConfigureAwait(false); - } - catch (PlaylistFullException) - { - break; - } - catch - { - // ignored - } - } - await ReplyConfirmLocalized("dir_queue_complete").ConfigureAwait(false); - } - - [NadekoCommand, Usage, Description, Aliases] - [RequireContext(ContextType.Guild)] - public async Task Radio(string radioLink) - { - - if (((IGuildUser)Context.User).VoiceChannel?.Guild != Context.Guild) - { - await ReplyErrorLocalized("must_be_in_voice").ConfigureAwait(false); - return; - } - await _music.QueueSong(((IGuildUser)Context.User), (ITextChannel)Context.Channel, ((IGuildUser)Context.User).VoiceChannel, radioLink, musicType: MusicType.Radio).ConfigureAwait(false); - if ((await Context.Guild.GetCurrentUserAsync()).GetPermissions((IGuildChannel)Context.Channel).ManageMessages) - { - Context.Message.DeleteAfter(10); - } - } - - [NadekoCommand, Usage, Description, Aliases] - [RequireContext(ContextType.Guild)] - [OwnerOnly] - public async Task Local([Remainder] string path) - { - - var arg = path; - if (string.IsNullOrWhiteSpace(arg)) - return; - await _music.QueueSong(((IGuildUser)Context.User), (ITextChannel)Context.Channel, ((IGuildUser)Context.User).VoiceChannel, path, musicType: MusicType.Local).ConfigureAwait(false); - - } - - //[NadekoCommand, Usage, Description, Aliases] - //[RequireContext(ContextType.Guild)] - //public async Task Move() - //{ - - // MusicPlayer musicPlayer; - // var voiceChannel = ((IGuildUser)Context.User).VoiceChannel; - // if (voiceChannel == null || voiceChannel.Guild != Context.Guild || !MusicPlayers.TryGetValue(Context.Guild.Id, out musicPlayer)) - // return; - // await musicPlayer.MoveToVoiceChannel(voiceChannel); - //} - [NadekoCommand, Usage, Description, Aliases] [RequireContext(ContextType.Guild)] [Priority(0)] - public Task SongRemove(int num) + public async Task SongRemove(int index) { - MusicPlayer musicPlayer; - if ((musicPlayer = _music.GetPlayer(Context.Guild.Id)) == null) - return Task.CompletedTask; - if (((IGuildUser)Context.User).VoiceChannel != musicPlayer.PlaybackVoiceChannel) - return Task.CompletedTask; + var mp = await _music.GetOrCreatePlayer(Context); + try + { + var song = mp.RemoveAt(index - 1); + var embed = new EmbedBuilder() + .WithAuthor(eab => eab.WithName(GetText("removed_song") + " #" + (index + 1)).WithMusicIcon()) + .WithDescription(song.PrettyName) + .WithFooter(ef => ef.WithText(song.PrettyInfo)) + .WithErrorColor(); - musicPlayer.RemoveSongAt(num - 1); - return Task.CompletedTask; + await mp.OutputTextChannel.EmbedAsync(embed).ConfigureAwait(false); + } + catch (ArgumentOutOfRangeException) + { + //todo error message + } } + public enum All { All } [NadekoCommand, Usage, Description, Aliases] [RequireContext(ContextType.Guild)] [Priority(1)] - public async Task SongRemove(string all) + public async Task SongRemove(All all) { - if (all.Trim().ToUpperInvariant() != "ALL") - return; - MusicPlayer musicPlayer; - if ((musicPlayer = _music.GetPlayer(Context.Guild.Id)) == null) - return; - musicPlayer.ClearQueue(); + var mp = await _music.GetOrCreatePlayer(Context); + mp.Stop(true); await ReplyConfirmLocalized("queue_cleared").ConfigureAwait(false); } - [NadekoCommand, Usage, Description, Aliases] - [RequireContext(ContextType.Guild)] - public async Task MoveSong([Remainder] string fromto) - { - if (string.IsNullOrWhiteSpace(fromto)) - return; - - MusicPlayer musicPlayer; - if ((musicPlayer = _music.GetPlayer(Context.Guild.Id)) == null) - return; - - fromto = fromto?.Trim(); - var fromtoArr = fromto.Split('>'); - - int n1; - int n2; - - var playlist = musicPlayer.Playlist as List ?? musicPlayer.Playlist.ToList(); - - if (fromtoArr.Length != 2 || !int.TryParse(fromtoArr[0], out n1) || - !int.TryParse(fromtoArr[1], out n2) || n1 < 1 || n2 < 1 || n1 == n2 || - n1 > playlist.Count || n2 > playlist.Count) - { - await ReplyConfirmLocalized("invalid_input").ConfigureAwait(false); - return; - } - - var s = playlist[n1 - 1]; - playlist.Insert(n2 - 1, s); - var nn1 = n2 < n1 ? n1 : n1 - 1; - playlist.RemoveAt(nn1); - - var embed = new EmbedBuilder() - .WithTitle($"{s.SongInfo.Title.TrimTo(70)}") - .WithUrl(s.SongUrl) - .WithAuthor(eab => eab.WithName(GetText("song_moved")).WithIconUrl("https://cdn.discordapp.com/attachments/155726317222887425/258605269972549642/music1.png")) - .AddField(fb => fb.WithName(GetText("from_position")).WithValue($"#{n1}").WithIsInline(true)) - .AddField(fb => fb.WithName(GetText("to_position")).WithValue($"#{n2}").WithIsInline(true)) - .WithColor(NadekoBot.OkColor); - await Context.Channel.EmbedAsync(embed).ConfigureAwait(false); - - //await channel.SendConfirmAsync($"🎵Moved {s.PrettyName} `from #{n1} to #{n2}`").ConfigureAwait(false); - - - } - - [NadekoCommand, Usage, Description, Aliases] - [RequireContext(ContextType.Guild)] - public async Task SetMaxQueue(uint size = 0) - { - MusicPlayer musicPlayer; - if ((musicPlayer = _music.GetPlayer(Context.Guild.Id)) == null) - return; - - musicPlayer.MaxQueueSize = size; - - if(size == 0) - await ReplyConfirmLocalized("max_queue_unlimited").ConfigureAwait(false); - else - await ReplyConfirmLocalized("max_queue_x", size).ConfigureAwait(false); - } - - [NadekoCommand, Usage, Description, Aliases] - [RequireContext(ContextType.Guild)] - public async Task SetMaxPlaytime(uint seconds) - { - if (seconds < 15 && seconds != 0) - return; - - var channel = (ITextChannel)Context.Channel; - MusicPlayer musicPlayer; - if ((musicPlayer = _music.GetPlayer(Context.Guild.Id)) == null) - return; - musicPlayer.MaxPlaytimeSeconds = seconds; - if (seconds == 0) - await ReplyConfirmLocalized("max_playtime_none").ConfigureAwait(false); - else - await ReplyConfirmLocalized("max_playtime_set", seconds).ConfigureAwait(false); - } - - [NadekoCommand, Usage, Description, Aliases] - [RequireContext(ContextType.Guild)] - public async Task ReptCurSong() - { - MusicPlayer musicPlayer; - if ((musicPlayer = _music.GetPlayer(Context.Guild.Id)) == null) - return; - var currentSong = musicPlayer.CurrentSong; - if (currentSong == null) - return; - var currentValue = musicPlayer.ToggleRepeatSong(); - - if (currentValue) - await Context.Channel.EmbedAsync(new EmbedBuilder() - .WithOkColor() - .WithAuthor(eab => eab.WithMusicIcon().WithName("🔂 " + GetText("repeating_track"))) - .WithDescription(currentSong.PrettyName) - .WithFooter(ef => ef.WithText(currentSong.PrettyInfo))).ConfigureAwait(false); - else - await Context.Channel.SendConfirmAsync("🔂 " + GetText("repeating_track_stopped")) - .ConfigureAwait(false); - } - - [NadekoCommand, Usage, Description, Aliases] - [RequireContext(ContextType.Guild)] - public async Task RepeatPl() - { - MusicPlayer musicPlayer; - if ((musicPlayer = _music.GetPlayer(Context.Guild.Id)) == null) - return; - var currentValue = musicPlayer.ToggleRepeatPlaylist(); - if(currentValue) - await ReplyConfirmLocalized("rpl_enabled").ConfigureAwait(false); - else - await ReplyConfirmLocalized("rpl_disabled").ConfigureAwait(false); - } - - [NadekoCommand, Usage, Description, Aliases] - [RequireContext(ContextType.Guild)] - public async Task Save([Remainder] string name) - { - MusicPlayer musicPlayer; - if ((musicPlayer = _music.GetPlayer(Context.Guild.Id)) == null) - return; - - var curSong = musicPlayer.CurrentSong; - var songs = musicPlayer.Playlist.Append(curSong) - .Select(s => new PlaylistSong() - { - Provider = s.SongInfo.Provider, - ProviderType = s.SongInfo.ProviderType, - Title = s.SongInfo.Title, - Uri = s.SongInfo.Uri, - Query = s.SongInfo.Query, - }).ToList(); - - MusicPlaylist playlist; - using (var uow = _db.UnitOfWork) - { - playlist = new MusicPlaylist - { - Name = name, - Author = Context.User.Username, - AuthorId = Context.User.Id, - Songs = songs, - }; - uow.MusicPlaylists.Add(playlist); - await uow.CompleteAsync().ConfigureAwait(false); - } - - await Context.Channel.EmbedAsync(new EmbedBuilder().WithOkColor() - .WithTitle(GetText("playlist_saved")) - .AddField(efb => efb.WithName(GetText("name")).WithValue(name)) - .AddField(efb => efb.WithName(GetText("id")).WithValue(playlist.Id.ToString()))); - } - - [NadekoCommand, Usage, Description, Aliases] - [RequireContext(ContextType.Guild)] - public async Task Load([Remainder] int id) - { - MusicPlaylist mpl; - using (var uow = _db.UnitOfWork) - { - mpl = uow.MusicPlaylists.GetWithSongs(id); - } - - if (mpl == null) - { - await ReplyErrorLocalized("playlist_id_not_found").ConfigureAwait(false); - return; - } - IUserMessage msg = null; - try { msg = await Context.Channel.SendMessageAsync(GetText("attempting_to_queue", Format.Bold(mpl.Songs.Count.ToString()))).ConfigureAwait(false); } catch (Exception ex) { _log.Warn(ex); } - foreach (var item in mpl.Songs) - { - var usr = (IGuildUser)Context.User; - try - { - await _music.QueueSong(usr, (ITextChannel)Context.Channel, usr.VoiceChannel, item.Query, true, item.ProviderType).ConfigureAwait(false); - } - catch (SongNotFoundException) { } - catch { break; } - } - if (msg != null) - await msg.ModifyAsync(m => m.Content = GetText("playlist_queue_complete")).ConfigureAwait(false); - } - [NadekoCommand, Usage, Description, Aliases] [RequireContext(ContextType.Guild)] public async Task Playlists([Remainder] int num = 1) @@ -767,7 +354,7 @@ namespace NadekoBot.Modules.Music await Context.Channel.EmbedAsync(embed).ConfigureAwait(false); } - + [NadekoCommand, Usage, Description, Aliases] [RequireContext(ContextType.Guild)] public async Task DeletePlaylist([Remainder] int id) @@ -803,69 +390,489 @@ namespace NadekoBot.Modules.Music [NadekoCommand, Usage, Description, Aliases] [RequireContext(ContextType.Guild)] - public async Task Goto(int time) + public async Task Save([Remainder] string name) { - MusicPlayer musicPlayer; - if ((musicPlayer = _music.GetPlayer(Context.Guild.Id)) == null) - return; - if (((IGuildUser)Context.User).VoiceChannel != musicPlayer.PlaybackVoiceChannel) - return; + var mp = await _music.GetOrCreatePlayer(Context); - if (time < 0) - return; + var songs = mp.QueueArray().Songs + .Select(s => new PlaylistSong() + { + Provider = s.Provider, + ProviderType = s.ProviderType, + Title = s.Title, + Uri = s.Uri, + Query = s.Query, + }).ToList(); - var currentSong = musicPlayer.CurrentSong; + MusicPlaylist playlist; + using (var uow = _db.UnitOfWork) + { + playlist = new MusicPlaylist + { + Name = name, + Author = Context.User.Username, + AuthorId = Context.User.Id, + Songs = songs, + }; + uow.MusicPlaylists.Add(playlist); + await uow.CompleteAsync().ConfigureAwait(false); + } - if (currentSong == null) - return; - - //currentSong.PrintStatusMessage = false; - var gotoSong = currentSong.Clone(); - gotoSong.SkipTo = time; - musicPlayer.AddSong(gotoSong, 0); - musicPlayer.Next(); - - var minutes = (time / 60).ToString(); - var seconds = (time % 60).ToString(); - - if (minutes.Length == 1) - minutes = "0" + minutes; - if (seconds.Length == 1) - seconds = "0" + seconds; - - await ReplyConfirmLocalized("skipped_to", minutes, seconds).ConfigureAwait(false); + await Context.Channel.EmbedAsync(new EmbedBuilder().WithOkColor() + .WithTitle(GetText("playlist_saved")) + .AddField(efb => efb.WithName(GetText("name")).WithValue(name)) + .AddField(efb => efb.WithName(GetText("id")).WithValue(playlist.Id.ToString()))); } + private readonly ConcurrentHashSet PlaylistLoadBlacklist = new ConcurrentHashSet(); + [NadekoCommand, Usage, Description, Aliases] [RequireContext(ContextType.Guild)] - public async Task Autoplay() + public async Task Load([Remainder] int id) { - MusicPlayer musicPlayer; - if ((musicPlayer = _music.GetPlayer(Context.Guild.Id)) == null) + if (!PlaylistLoadBlacklist.Add(Context.Guild.Id)) return; + try + { + var mp = await _music.GetOrCreatePlayer(Context); + MusicPlaylist mpl; + using (var uow = _db.UnitOfWork) + { + mpl = uow.MusicPlaylists.GetWithSongs(id); + } - if (!musicPlayer.ToggleAutoplay()) - await ReplyConfirmLocalized("autoplay_disabled").ConfigureAwait(false); - else - await ReplyConfirmLocalized("autoplay_enabled").ConfigureAwait(false); + if (mpl == null) + { + await ReplyErrorLocalized("playlist_id_not_found").ConfigureAwait(false); + return; + } + IUserMessage msg = null; + try { msg = await Context.Channel.SendMessageAsync(GetText("attempting_to_queue", Format.Bold(mpl.Songs.Count.ToString()))).ConfigureAwait(false); } catch (Exception ex) { _log.Warn(ex); } + foreach (var item in mpl.Songs) + { + try + { + //todo fix for all + if (item.ProviderType == MusicType.Normal) + await Task.WhenAll(Task.Delay(1000), InternalQueue(mp, await _music.ResolveSong(item.Query, Context.User.ToString(), item.ProviderType), true)).ConfigureAwait(false); + } + catch (SongNotFoundException) { } + catch { break; } + } + if (msg != null) + await msg.ModifyAsync(m => m.Content = GetText("playlist_queue_complete")).ConfigureAwait(false); + } + finally + { + PlaylistLoadBlacklist.TryRemove(Context.Guild.Id); + } } + //[NadekoCommand, Usage, Description, Aliases] + //[RequireContext(ContextType.Guild)] + //public async Task Fairplay() + //{ + // var channel = (ITextChannel)Context.Channel; + // MusicPlayer musicPlayer; + // if ((musicPlayer = _music.GetPlayer(Context.Guild.Id)) == null) + // return; + // if (((IGuildUser)Context.User).VoiceChannel != musicPlayer.PlaybackVoiceChannel) + // return; + // var val = musicPlayer.FairPlay = !musicPlayer.FairPlay; + + // if (val) + // { + // await ReplyConfirmLocalized("fp_enabled").ConfigureAwait(false); + // } + // else + // { + // await ReplyConfirmLocalized("fp_disabled").ConfigureAwait(false); + // } + //} + + //[NadekoCommand, Usage, Description, Aliases] + //[RequireContext(ContextType.Guild)] + //public async Task SoundCloudQueue([Remainder] string query) + //{ + // await _music.QueueSong(((IGuildUser)Context.User), (ITextChannel)Context.Channel, ((IGuildUser)Context.User).VoiceChannel, query, musicType: MusicType.Soundcloud).ConfigureAwait(false); + // if ((await Context.Guild.GetCurrentUserAsync()).GetPermissions((IGuildChannel)Context.Channel).ManageMessages) + // { + // Context.Message.DeleteAfter(10); + // } + //} + + [NadekoCommand, Usage, Description, Aliases] + [RequireContext(ContextType.Guild)] + public async Task NowPlaying() + { + var mp = await _music.GetOrCreatePlayer(Context); + var (_, currentSong) = mp.Current; + if (currentSong == null) + return; + //try { await mp.UpdateSongDurationsAsync().ConfigureAwait(false); } catch { } + + var embed = new EmbedBuilder().WithOkColor() + .WithAuthor(eab => eab.WithName(GetText("now_playing")).WithMusicIcon()) + .WithDescription(currentSong.PrettyName) + .WithThumbnailUrl(currentSong.Thumbnail) + .WithFooter(ef => ef.WithText(mp.PrettyVolume + " | " + /*currentSong.PrettyFullTime +*/ $" | {currentSong.PrettyProvider} | {currentSong.QueuerName}")); + + await Context.Channel.EmbedAsync(embed).ConfigureAwait(false); + } + + //[NadekoCommand, Usage, Description, Aliases] + //[RequireContext(ContextType.Guild)] + //public async Task ShufflePlaylist() + //{ + // MusicPlayer musicPlayer; + // if ((musicPlayer = _music.GetPlayer(Context.Guild.Id)) == null) + // return; + // if (((IGuildUser)Context.User).VoiceChannel != musicPlayer.PlaybackVoiceChannel) + // return; + // if (musicPlayer.Playlist.Count < 2) + // return; + + // musicPlayer.Shuffle(); + // await ReplyConfirmLocalized("songs_shuffled").ConfigureAwait(false); + //} + + //[NadekoCommand, Usage, Description, Aliases] + //[RequireContext(ContextType.Guild)] + //public async Task Playlist([Remainder] string playlist) + //{ + + // var arg = playlist; + // if (string.IsNullOrWhiteSpace(arg)) + // return; + // if (((IGuildUser)Context.User).VoiceChannel?.Guild != Context.Guild) + // { + // await ReplyErrorLocalized("must_be_in_voice").ConfigureAwait(false); + // return; + // } + // var plId = (await _google.GetPlaylistIdsByKeywordsAsync(arg).ConfigureAwait(false)).FirstOrDefault(); + // if (plId == null) + // { + // await ReplyErrorLocalized("no_search_results").ConfigureAwait(false); + // return; + // } + // var ids = await _google.GetPlaylistTracksAsync(plId, 500).ConfigureAwait(false); + // if (!ids.Any()) + // { + // await ReplyErrorLocalized("no_search_results").ConfigureAwait(false); + // return; + // } + // var count = ids.Count(); + // var msg = await Context.Channel.SendMessageAsync("🎵 " + GetText("attempting_to_queue", + // Format.Bold(count.ToString()))).ConfigureAwait(false); + + // var cancelSource = new CancellationTokenSource(); + + // var gusr = (IGuildUser)Context.User; + // while (ids.Any() && !cancelSource.IsCancellationRequested) + // { + // var tasks = Task.WhenAll(ids.Take(5).Select(async id => + // { + // if (cancelSource.Token.IsCancellationRequested) + // return; + // try + // { + // await _music.QueueSong(gusr, (ITextChannel)Context.Channel, gusr.VoiceChannel, id, true).ConfigureAwait(false); + // } + // catch (SongNotFoundException) { } + // catch { try { cancelSource.Cancel(); } catch { } } + // })); + + // await Task.WhenAny(tasks, Task.Delay(Timeout.Infinite, cancelSource.Token)); + // ids = ids.Skip(5); + // } + + // await msg.ModifyAsync(m => m.Content = "✅ " + Format.Bold(GetText("playlist_queue_complete"))).ConfigureAwait(false); + //} + + //[NadekoCommand, Usage, Description, Aliases] + //[RequireContext(ContextType.Guild)] + //public async Task SoundCloudPl([Remainder] string pl) + //{ + + // pl = pl?.Trim(); + + // if (string.IsNullOrWhiteSpace(pl)) + // return; + + // using (var http = new HttpClient()) + // { + // var scvids = JObject.Parse(await http.GetStringAsync($"https://scapi.nadekobot.me/resolve?url={pl}").ConfigureAwait(false))["tracks"].ToObject(); + // await _music.QueueSong(((IGuildUser)Context.User), (ITextChannel)Context.Channel, ((IGuildUser)Context.User).VoiceChannel, scvids[0].TrackLink).ConfigureAwait(false); + + // MusicPlayer musicPlayer; + // if ((musicPlayer = _music.GetPlayer(Context.Guild.Id)) == null) + // return; + + // foreach (var svideo in scvids.Skip(1)) + // { + // try + // { + // musicPlayer.AddSong(new Song(new SongInfo + // { + // Title = svideo.FullName, + // Provider = "SoundCloud", + // Uri = await svideo.StreamLink(), + // ProviderType = MusicType.Normal, + // Query = svideo.TrackLink, + // }), ((IGuildUser)Context.User).Username); + // } + // catch (PlaylistFullException) { break; } + // } + // } + //} + + //[NadekoCommand, Usage, Description, Aliases] + //[RequireContext(ContextType.Guild)] + //[OwnerOnly] + //public async Task LocalPl([Remainder] string directory) + //{ + + // var arg = directory; + // if (string.IsNullOrWhiteSpace(arg)) + // return; + // var dir = new DirectoryInfo(arg); + // var fileEnum = dir.GetFiles("*", SearchOption.AllDirectories) + // .Where(x => !x.Attributes.HasFlag(FileAttributes.Hidden | FileAttributes.System)); + // var gusr = (IGuildUser)Context.User; + // foreach (var file in fileEnum) + // { + // try + // { + // await _music.QueueSong(gusr, (ITextChannel)Context.Channel, gusr.VoiceChannel, file.FullName, true, MusicType.Local).ConfigureAwait(false); + // } + // catch (PlaylistFullException) + // { + // break; + // } + // catch + // { + // // ignored + // } + // } + // await ReplyConfirmLocalized("dir_queue_complete").ConfigureAwait(false); + //} + + //[NadekoCommand, Usage, Description, Aliases] + //[RequireContext(ContextType.Guild)] + //public async Task Radio(string radioLink) + //{ + + // if (((IGuildUser)Context.User).VoiceChannel?.Guild != Context.Guild) + // { + // await ReplyErrorLocalized("must_be_in_voice").ConfigureAwait(false); + // return; + // } + // await _music.QueueSong(((IGuildUser)Context.User), (ITextChannel)Context.Channel, ((IGuildUser)Context.User).VoiceChannel, radioLink, musicType: MusicType.Radio).ConfigureAwait(false); + // if ((await Context.Guild.GetCurrentUserAsync()).GetPermissions((IGuildChannel)Context.Channel).ManageMessages) + // { + // Context.Message.DeleteAfter(10); + // } + //} + + //[NadekoCommand, Usage, Description, Aliases] + //[RequireContext(ContextType.Guild)] + //[OwnerOnly] + //public async Task Local([Remainder] string path) + //{ + + // var arg = path; + // if (string.IsNullOrWhiteSpace(arg)) + // return; + // await _music.QueueSong(((IGuildUser)Context.User), (ITextChannel)Context.Channel, ((IGuildUser)Context.User).VoiceChannel, path, musicType: MusicType.Local).ConfigureAwait(false); + + //} + + //[NadekoCommand, Usage, Description, Aliases] + //[RequireContext(ContextType.Guild)] + //public async Task Move() + //{ + + // MusicPlayer musicPlayer; + // var voiceChannel = ((IGuildUser)Context.User).VoiceChannel; + // if (voiceChannel == null || voiceChannel.Guild != Context.Guild || !MusicPlayers.TryGetValue(Context.Guild.Id, out musicPlayer)) + // return; + // await musicPlayer.MoveToVoiceChannel(voiceChannel); + //} + + //[NadekoCommand, Usage, Description, Aliases] + //[RequireContext(ContextType.Guild)] + //public async Task MoveSong([Remainder] string fromto) + //{ + // if (string.IsNullOrWhiteSpace(fromto)) + // return; + + // MusicPlayer musicPlayer; + // if ((musicPlayer = _music.GetPlayer(Context.Guild.Id)) == null) + // return; + + // fromto = fromto?.Trim(); + // var fromtoArr = fromto.Split('>'); + + // int n1; + // int n2; + + // var playlist = musicPlayer.Playlist as List ?? musicPlayer.Playlist.ToList(); + + // if (fromtoArr.Length != 2 || !int.TryParse(fromtoArr[0], out n1) || + // !int.TryParse(fromtoArr[1], out n2) || n1 < 1 || n2 < 1 || n1 == n2 || + // n1 > playlist.Count || n2 > playlist.Count) + // { + // await ReplyConfirmLocalized("invalid_input").ConfigureAwait(false); + // return; + // } + + // var s = playlist[n1 - 1]; + // playlist.Insert(n2 - 1, s); + // var nn1 = n2 < n1 ? n1 : n1 - 1; + // playlist.RemoveAt(nn1); + + // var embed = new EmbedBuilder() + // .WithTitle($"{s.SongInfo.Title.TrimTo(70)}") + // .WithUrl(s.SongUrl) + // .WithAuthor(eab => eab.WithName(GetText("song_moved")).WithIconUrl("https://cdn.discordapp.com/attachments/155726317222887425/258605269972549642/music1.png")) + // .AddField(fb => fb.WithName(GetText("from_position")).WithValue($"#{n1}").WithIsInline(true)) + // .AddField(fb => fb.WithName(GetText("to_position")).WithValue($"#{n2}").WithIsInline(true)) + // .WithColor(NadekoBot.OkColor); + // await Context.Channel.EmbedAsync(embed).ConfigureAwait(false); + + // //await channel.SendConfirmAsync($"🎵Moved {s.PrettyName} `from #{n1} to #{n2}`").ConfigureAwait(false); + + + //} + + //[NadekoCommand, Usage, Description, Aliases] + //[RequireContext(ContextType.Guild)] + //public async Task SetMaxQueue(uint size = 0) + //{ + // MusicPlayer musicPlayer; + // if ((musicPlayer = _music.GetPlayer(Context.Guild.Id)) == null) + // return; + + // musicPlayer.MaxQueueSize = size; + + // if(size == 0) + // await ReplyConfirmLocalized("max_queue_unlimited").ConfigureAwait(false); + // else + // await ReplyConfirmLocalized("max_queue_x", size).ConfigureAwait(false); + //} + + //[NadekoCommand, Usage, Description, Aliases] + //[RequireContext(ContextType.Guild)] + //public async Task SetMaxPlaytime(uint seconds) + //{ + // if (seconds < 15 && seconds != 0) + // return; + + // var channel = (ITextChannel)Context.Channel; + // MusicPlayer musicPlayer; + // if ((musicPlayer = _music.GetPlayer(Context.Guild.Id)) == null) + // return; + // musicPlayer.MaxPlaytimeSeconds = seconds; + // if (seconds == 0) + // await ReplyConfirmLocalized("max_playtime_none").ConfigureAwait(false); + // else + // await ReplyConfirmLocalized("max_playtime_set", seconds).ConfigureAwait(false); + //} + + [NadekoCommand, Usage, Description, Aliases] + [RequireContext(ContextType.Guild)] + public async Task ReptCurSong() + { + var mp = await _music.GetOrCreatePlayer(Context); + var (_, currentSong) = mp.Current; + if (currentSong == null) + return; + var currentValue = mp.ToggleRepeatSong(); + + if (currentValue) + await Context.Channel.EmbedAsync(new EmbedBuilder() + .WithOkColor() + .WithAuthor(eab => eab.WithMusicIcon().WithName("🔂 " + GetText("repeating_track"))) + .WithDescription(currentSong.PrettyName) + .WithFooter(ef => ef.WithText(currentSong.PrettyInfo))).ConfigureAwait(false); + else + await Context.Channel.SendConfirmAsync("🔂 " + GetText("repeating_track_stopped")) + .ConfigureAwait(false); + } + + //[NadekoCommand, Usage, Description, Aliases] + //[RequireContext(ContextType.Guild)] + //public async Task RepeatPl() + //{ + // MusicPlayer musicPlayer; + // if ((musicPlayer = _music.GetPlayer(Context.Guild.Id)) == null) + // return; + // var currentValue = musicPlayer.ToggleRepeatPlaylist(); + // if(currentValue) + // await ReplyConfirmLocalized("rpl_enabled").ConfigureAwait(false); + // else + // await ReplyConfirmLocalized("rpl_disabled").ConfigureAwait(false); + //} + + //[NadekoCommand, Usage, Description, Aliases] + //[RequireContext(ContextType.Guild)] + //public async Task Goto(int time) + //{ + // MusicPlayer musicPlayer; + // if ((musicPlayer = _music.GetPlayer(Context.Guild.Id)) == null) + // return; + // if (((IGuildUser)Context.User).VoiceChannel != musicPlayer.PlaybackVoiceChannel) + // return; + + // if (time < 0) + // return; + + // var currentSong = musicPlayer.CurrentSong; + + // if (currentSong == null) + // return; + + // //currentSong.PrintStatusMessage = false; + // var gotoSong = currentSong.Clone(); + // gotoSong.SkipTo = time; + // musicPlayer.AddSong(gotoSong, 0); + // musicPlayer.Next(); + + // var minutes = (time / 60).ToString(); + // var seconds = (time % 60).ToString(); + + // if (minutes.Length == 1) + // minutes = "0" + minutes; + // if (seconds.Length == 1) + // seconds = "0" + seconds; + + // await ReplyConfirmLocalized("skipped_to", minutes, seconds).ConfigureAwait(false); + //} + + //[NadekoCommand, Usage, Description, Aliases] + //[RequireContext(ContextType.Guild)] + //public async Task Autoplay() + //{ + // MusicPlayer musicPlayer; + // if ((musicPlayer = _music.GetPlayer(Context.Guild.Id)) == null) + // return; + + // if (!musicPlayer.ToggleAutoplay()) + // await ReplyConfirmLocalized("autoplay_disabled").ConfigureAwait(false); + // else + // await ReplyConfirmLocalized("autoplay_enabled").ConfigureAwait(false); + //} + [NadekoCommand, Usage, Description, Aliases] [RequireContext(ContextType.Guild)] [RequireUserPermission(GuildPermission.ManageMessages)] public async Task SetMusicChannel() { - MusicPlayer musicPlayer; - if ((musicPlayer = _music.GetPlayer(Context.Guild.Id)) == null) - { - await ReplyErrorLocalized("no_player").ConfigureAwait(false); - return; - } + var mp = await _music.GetOrCreatePlayer(Context); - musicPlayer.OutputTextChannel = (ITextChannel)Context.Channel; + mp.OutputTextChannel = (ITextChannel)Context.Channel; await ReplyConfirmLocalized("set_music_channel").ConfigureAwait(false); } - } } \ No newline at end of file diff --git a/src/NadekoBot/Services/Administration/PlayingRotateService.cs b/src/NadekoBot/Services/Administration/PlayingRotateService.cs index 9e33aef7..d4e09ad1 100644 --- a/src/NadekoBot/Services/Administration/PlayingRotateService.cs +++ b/src/NadekoBot/Services/Administration/PlayingRotateService.cs @@ -35,7 +35,7 @@ namespace NadekoBot.Services.Administration _rep = new ReplacementBuilder() .WithClient(client) .WithStats(client) - .WithMusic(music) + //.WithMusic(music) .Build(); _t = new Timer(async (objState) => diff --git a/src/NadekoBot/Services/Impl/StatsService.cs b/src/NadekoBot/Services/Impl/StatsService.cs index 4b9b6bac..ed7ec815 100644 --- a/src/NadekoBot/Services/Impl/StatsService.cs +++ b/src/NadekoBot/Services/Impl/StatsService.cs @@ -17,7 +17,7 @@ namespace NadekoBot.Services.Impl private readonly IBotCredentials _creds; private readonly DateTime _started; - public const string BotVersion = "1.52"; + public const string BotVersion = "1.53"; public string Author => "Kwoth#2560"; public string Library => "Discord.Net"; diff --git a/src/NadekoBot/Services/Impl/SyncPreconditionService.cs b/src/NadekoBot/Services/Impl/SyncPreconditionService.cs new file mode 100644 index 00000000..30d5b48a --- /dev/null +++ b/src/NadekoBot/Services/Impl/SyncPreconditionService.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace NadekoBot.Services.Impl +{ + public class SyncPreconditionService + { + + } +} diff --git a/src/NadekoBot/Services/Music/Exceptions.cs b/src/NadekoBot/Services/Music/Exceptions.cs index 1dbe8ad7..af195c15 100644 --- a/src/NadekoBot/Services/Music/Exceptions.cs +++ b/src/NadekoBot/Services/Music/Exceptions.cs @@ -2,7 +2,7 @@ namespace NadekoBot.Services.Music { - class PlaylistFullException : Exception + public class PlaylistFullException : Exception { public PlaylistFullException(string message) : base(message) { @@ -10,11 +10,19 @@ namespace NadekoBot.Services.Music public PlaylistFullException() : base("Queue is full.") { } } - class SongNotFoundException : Exception + public class SongNotFoundException : Exception { public SongNotFoundException(string message) : base(message) { } public SongNotFoundException() : base("Song is not found.") { } } + public class NotInVoiceChannelException : Exception + { + public NotInVoiceChannelException(string message) : base(message) + { + } + + public NotInVoiceChannelException() : base("You're not in the voice channel on this server.") { } + } } diff --git a/src/NadekoBot/Services/Music/MusicControls.cs b/src/NadekoBot/Services/Music/MusicControls.cs deleted file mode 100644 index 4e688351..00000000 --- a/src/NadekoBot/Services/Music/MusicControls.cs +++ /dev/null @@ -1,381 +0,0 @@ -using Discord; -using Discord.Audio; -using NadekoBot.Extensions; -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using NLog; -using NadekoBot.Services.Database.Models; - -namespace NadekoBot.Services.Music -{ - public enum StreamState - { - Resolving, - Queued, - Playing, - Completed - } - - public class MusicPlayer - { - private IAudioClient AudioClient { get; set; } - - /// - /// Player will prioritize different queuer name - /// over the song position in the playlist - /// - public bool FairPlay { get; set; } = false; - - /// - /// Song will stop playing after this amount of time. - /// To prevent people queueing radio or looped songs - /// while other people want to listen to other songs too. - /// - public uint MaxPlaytimeSeconds { get; set; } = 0; - - - // this should be written better - public TimeSpan TotalPlaytime => - _playlist.Any(s => s.TotalTime == TimeSpan.MaxValue) ? - TimeSpan.MaxValue : - new TimeSpan(_playlist.Sum(s => s.TotalTime.Ticks)); - - /// - /// Users who recently got their music wish - /// - private ConcurrentHashSet RecentlyPlayedUsers { get; } = new ConcurrentHashSet(); - - private readonly List _playlist = new List(); - private readonly Logger _log; - private readonly IGoogleApiService _google; - - public IReadOnlyCollection Playlist => _playlist; - - public Song CurrentSong { get; private set; } - public CancellationTokenSource SongCancelSource { get; private set; } - private CancellationToken CancelToken { get; set; } - - public bool Paused { get; set; } - - public float Volume { get; private set; } - - public event Action OnCompleted = delegate { }; - public event Action OnStarted = delegate { }; - public event Action OnPauseChanged = delegate { }; - - public IVoiceChannel PlaybackVoiceChannel { get; private set; } - public ITextChannel OutputTextChannel { get; set; } - - private bool Destroyed { get; set; } - public bool RepeatSong { get; private set; } - public bool RepeatPlaylist { get; private set; } - public bool Autoplay { get; set; } - public uint MaxQueueSize { get; set; } = 0; - - private ConcurrentQueue ActionQueue { get; } = new ConcurrentQueue(); - - public string PrettyVolume => $"🔉 {(int)(Volume * 100)}%"; - - public event Action SongRemoved = delegate { }; - - public MusicPlayer(IVoiceChannel startingVoiceChannel, ITextChannel outputChannel, float? defaultVolume, IGoogleApiService google) - { - _log = LogManager.GetCurrentClassLogger(); - _google = google; - - OutputTextChannel = outputChannel; - Volume = defaultVolume ?? 1.0f; - - PlaybackVoiceChannel = startingVoiceChannel ?? throw new ArgumentNullException(nameof(startingVoiceChannel)); - SongCancelSource = new CancellationTokenSource(); - CancelToken = SongCancelSource.Token; - - Task.Run(async () => - { - try - { - while (!Destroyed) - { - try - { - if (ActionQueue.TryDequeue(out Action action)) - { - action(); - } - } - finally - { - await Task.Delay(100).ConfigureAwait(false); - } - } - } - catch (Exception ex) - { - _log.Warn("Action queue crashed"); - _log.Warn(ex); - } - }).ConfigureAwait(false); - - var t = new Thread(async () => - { - while (!Destroyed) - { - try - { - CurrentSong = GetNextSong(); - - if (CurrentSong == null) - continue; - - while (AudioClient?.ConnectionState == ConnectionState.Disconnecting || - AudioClient?.ConnectionState == ConnectionState.Connecting) - { - _log.Info("Waiting for Audio client"); - await Task.Delay(200).ConfigureAwait(false); - } - - if (AudioClient == null || AudioClient.ConnectionState == ConnectionState.Disconnected) - AudioClient = await PlaybackVoiceChannel.ConnectAsync().ConfigureAwait(false); - - var index = _playlist.IndexOf(CurrentSong); - if (index != -1) - RemoveSongAt(index, true); - - OnStarted(this, CurrentSong); - try - { - await CurrentSong.Play(AudioClient, CancelToken); - } - catch (OperationCanceledException) - { - } - finally - { - OnCompleted(this, CurrentSong); - } - - - if (RepeatPlaylist & !RepeatSong) - AddSong(CurrentSong, CurrentSong.QueuerName); - - if (RepeatSong) - AddSong(CurrentSong, 0); - - } - catch (Exception ex) - { - _log.Warn("Music thread almost crashed."); - _log.Warn(ex); - await Task.Delay(3000).ConfigureAwait(false); - } - finally - { - if (!CancelToken.IsCancellationRequested) - { - SongCancelSource.Cancel(); - } - SongCancelSource = new CancellationTokenSource(); - CancelToken = SongCancelSource.Token; - CurrentSong = null; - await Task.Delay(300).ConfigureAwait(false); - } - } - }); - - t.Start(); - } - - public void Next() - { - ActionQueue.Enqueue(() => - { - Paused = false; - SongCancelSource.Cancel(); - }); - } - - public void Stop() - { - ActionQueue.Enqueue(() => - { - RepeatPlaylist = false; - RepeatSong = false; - Autoplay = false; - _playlist.Clear(); - if (!SongCancelSource.IsCancellationRequested) - SongCancelSource.Cancel(); - }); - } - - public void TogglePause() => OnPauseChanged(Paused = !Paused); - - public int SetVolume(int volume) - { - if (volume < 0) - volume = 0; - if (volume > 100) - volume = 100; - - Volume = volume / 100.0f; - return volume; - } - - private Song GetNextSong() - { - if (!FairPlay) - { - return _playlist.FirstOrDefault(); - } - var song = _playlist.FirstOrDefault(c => !RecentlyPlayedUsers.Contains(c.QueuerName)) - ?? _playlist.FirstOrDefault(); - - if (song == null) - return null; - - if (RecentlyPlayedUsers.Contains(song.QueuerName)) - { - RecentlyPlayedUsers.Clear(); - } - - RecentlyPlayedUsers.Add(song.QueuerName); - return song; - } - - public void Shuffle() - { - ActionQueue.Enqueue(() => - { - var oldPlaylist = _playlist.ToArray(); - _playlist.Clear(); - _playlist.AddRange(oldPlaylist.Shuffle()); - }); - } - - public void AddSong(Song s, string username) - { - if (s == null) - throw new ArgumentNullException(nameof(s)); - ThrowIfQueueFull(); - ActionQueue.Enqueue(() => - { - s.MusicPlayer = this; - s.QueuerName = username.TrimTo(10); - _playlist.Add(s); - }); - } - - public void AddSong(Song s, int index) - { - if (s == null) - throw new ArgumentNullException(nameof(s)); - ActionQueue.Enqueue(() => - { - _playlist.Insert(index, s); - }); - } - - public void RemoveSong(Song s) - { - if (s == null) - throw new ArgumentNullException(nameof(s)); - ActionQueue.Enqueue(() => - { - _playlist.Remove(s); - }); - } - - public void RemoveSongAt(int index, bool silent = false) - { - ActionQueue.Enqueue(() => - { - if (index < 0 || index >= _playlist.Count) - return; - var song = _playlist.ElementAtOrDefault(index); - if (_playlist.Remove(song) && !silent) - { - SongRemoved(song, index); - } - - }); - } - - public void ClearQueue() - { - ActionQueue.Enqueue(() => - { - _playlist.Clear(); - }); - } - - public async Task UpdateSongDurationsAsync() - { - var curSong = CurrentSong; - var toUpdate = _playlist.Where(s => s.SongInfo.ProviderType == MusicType.Normal && - s.TotalTime == TimeSpan.Zero) - .ToArray(); - if (curSong != null) - { - Array.Resize(ref toUpdate, toUpdate.Length + 1); - toUpdate[toUpdate.Length - 1] = curSong; - } - var ids = toUpdate.Select(s => s.SongInfo.Query.Substring(s.SongInfo.Query.LastIndexOf("?v=") + 3)) - .Distinct(); - - var durations = await _google.GetVideoDurationsAsync(ids); - - toUpdate.ForEach(s => - { - foreach (var kvp in durations) - { - if (s.SongInfo.Query.EndsWith(kvp.Key)) - { - s.TotalTime = kvp.Value; - return; - } - } - }); - } - - public void Destroy() - { - ActionQueue.Enqueue(async () => - { - RepeatPlaylist = false; - RepeatSong = false; - Autoplay = false; - Destroyed = true; - _playlist.Clear(); - - try { await AudioClient.StopAsync(); } catch { } - if (!SongCancelSource.IsCancellationRequested) - SongCancelSource.Cancel(); - }); - } - - //public async Task MoveToVoiceChannel(IVoiceChannel voiceChannel) - //{ - // if (audioClient?.ConnectionState != ConnectionState.Connected) - // throw new InvalidOperationException("Can't move while bot is not connected to voice channel."); - // PlaybackVoiceChannel = voiceChannel; - // audioClient = await voiceChannel.ConnectAsync().ConfigureAwait(false); - //} - - public bool ToggleRepeatSong() => RepeatSong = !RepeatSong; - - public bool ToggleRepeatPlaylist() => RepeatPlaylist = !RepeatPlaylist; - - public bool ToggleAutoplay() => Autoplay = !Autoplay; - - public void ThrowIfQueueFull() - { - if (MaxQueueSize == 0) - return; - if (_playlist.Count >= MaxQueueSize) - throw new PlaylistFullException(); - } - } -} diff --git a/src/NadekoBot/Services/Music/MusicPlayer.cs b/src/NadekoBot/Services/Music/MusicPlayer.cs new file mode 100644 index 00000000..64c3aa22 --- /dev/null +++ b/src/NadekoBot/Services/Music/MusicPlayer.cs @@ -0,0 +1,657 @@ +using Discord; +using Discord.Audio; +using System; +using System.Threading; +using System.Threading.Tasks; +using NLog; +using System.Diagnostics; + +namespace NadekoBot.Services.Music +{ + public enum StreamState + { + Resolving, + Queued, + Playing, + Completed + } + public class MusicPlayer : IDisposable + { + private readonly Task _player; + private readonly IVoiceChannel VoiceChannel; + private readonly Logger _log; + + private MusicQueue Queue { get; } = new MusicQueue(); + + public bool Exited { get; set; } = false; + public bool Stopped { get; private set; } = false; + public float Volume { get; private set; } = 1.0f; + public string PrettyVolume => $"🔉 {(int)(Volume * 100)}%"; + private TaskCompletionSource pauseTaskSource { get; set; } = null; + + private CancellationTokenSource SongCancelSource { get; set; } + public ITextChannel OutputTextChannel { get; set; } + public (int Index, SongInfo Current) Current + { + get + { + if (Stopped) + return (0, null); + return Queue.Current; + } + } + + public bool RepeatCurrentSong { get; private set; } + + private IAudioClient _audioClient; + private readonly object locker = new object(); + + #region events + public event Action OnStarted; + public event Action OnCompleted; + public event Action OnPauseChanged; + #endregion + + public MusicPlayer(MusicService musicService, IVoiceChannel vch, ITextChannel output, float volume) + { + _log = LogManager.GetCurrentClassLogger(); + this.Volume = volume; + this.VoiceChannel = vch; + this.SongCancelSource = new CancellationTokenSource(); + this.OutputTextChannel = output; + + _player = Task.Run(async () => + { + while (!Exited) + { + CancellationToken cancelToken; + (int Index, SongInfo Song) data; + lock (locker) + { + data = Queue.Current; + cancelToken = SongCancelSource.Token; + } + try + { + _log.Info("Checking for songs"); + if (data.Song == null) + continue; + + _log.Info("Connecting"); + + + _log.Info("Starting"); + var p = Process.Start(new ProcessStartInfo + { + FileName = "ffmpeg", + Arguments = $"-i {data.Song.Uri} -f s16le -ar 48000 -vn -ac 2 pipe:1 -loglevel quiet", + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = false, + CreateNoWindow = true, + }); + var ac = await GetAudioClient(); + if (ac == null) + continue; + var pcm = ac.CreatePCMStream(AudioApplication.Music); + + OnStarted?.Invoke(this, data.Song); + + byte[] buffer = new byte[3840]; + int bytesRead = 0; + try + { + while ((bytesRead = await p.StandardOutput.BaseStream.ReadAsync(buffer, 0, buffer.Length, cancelToken).ConfigureAwait(false)) > 0) + { + var vol = Volume; + if (vol != 1) + AdjustVolume(buffer, vol); + await pcm.WriteAsync(buffer, 0, bytesRead, cancelToken); + + await (pauseTaskSource?.Task ?? Task.CompletedTask); + } + } + catch (OperationCanceledException) + { + _log.Info("Song Canceled"); + } + catch (Exception ex) + { + _log.Warn(ex); + } + finally + { + //flush is known to get stuck from time to time, just cancel it if it takes more than 1 second + var flushCancel = new CancellationTokenSource(); + var flushToken = flushCancel.Token; + var flushDelay = Task.Delay(1000, flushToken); + await Task.WhenAny(flushDelay, pcm.FlushAsync(flushToken)); + flushCancel.Cancel(); + + OnCompleted?.Invoke(this, data.Song); + } + } + finally + { + _log.Info("Next song"); + do + { + await Task.Delay(100); + } + while (Stopped && !Exited); + if(!RepeatCurrentSong) + Queue.Next(); + } + } + }, SongCancelSource.Token); + } + + private async Task GetAudioClient(bool reconnect = false) + { + if (_audioClient == null || + _audioClient.ConnectionState != ConnectionState.Connected || + reconnect) + try + { + _audioClient = await VoiceChannel.ConnectAsync(); + } + catch + { + return null; + } + return _audioClient; + } + + public (bool Success, int Index) Enqueue(SongInfo song) + { + _log.Info("Adding song"); + Queue.Add(song); + return (true, Queue.Count); + } + + public void Next() + { + lock (locker) + { + Stopped = false; + Unpause(); + } + CancelCurrentSong(); + } + + public void Stop(bool clearQueue = false) + { + lock (locker) + { + Stopped = true; + Queue.ResetCurrent(); + if (clearQueue) + Queue.Clear(); + Unpause(); + } + CancelCurrentSong(); + } + + private void Unpause() + { + if (pauseTaskSource != null) + { + pauseTaskSource.TrySetResult(true); + pauseTaskSource = null; + } + } + + public void TogglePause() + { + lock (locker) + { + if (pauseTaskSource == null) + pauseTaskSource = new TaskCompletionSource(); + else + { + Unpause(); + } + } + OnPauseChanged?.Invoke(this, pauseTaskSource != null); + } + + public void SetVolume(int volume) + { + if (volume < 0 || volume > 100) + throw new ArgumentOutOfRangeException(nameof(volume)); + Volume = ((float)volume) / 100; + } + + public SongInfo RemoveAt(int index) + { + lock (locker) + { + var cur = Queue.Current; + if (cur.Index == index) + Next(); + return Queue.RemoveAt(index); + } + } + + private void CancelCurrentSong() + { + lock (locker) + { + var cs = SongCancelSource; + SongCancelSource = new CancellationTokenSource(); + cs.Cancel(); + } + } + + public void ClearQueue() + { + lock (locker) + { + Queue.Clear(); + } + } + + public (int CurrentIndex, SongInfo[] Songs) QueueArray() + => Queue.ToArray(); + + //aidiakapi ftw + public static unsafe byte[] AdjustVolume(byte[] audioSamples, float volume) + { + if (Math.Abs(volume - 1f) < 0.0001f) return audioSamples; + + // 16-bit precision for the multiplication + var volumeFixed = (int)Math.Round(volume * 65536d); + + var count = audioSamples.Length / 2; + + fixed (byte* srcBytes = audioSamples) + { + var src = (short*)srcBytes; + + for (var i = count; i != 0; i--, src++) + *src = (short)(((*src) * volumeFixed) >> 16); + } + + return audioSamples; + } + + public bool ToggleRepeatSong() + { + lock (locker) + { + return RepeatCurrentSong = !RepeatCurrentSong; + } + } + + public void Dispose() + { + _log.Info("Disposing"); + lock (locker) + { + Exited = true; + Unpause(); + } + CancelCurrentSong(); + OnCompleted = null; + OnPauseChanged = null; + OnStarted = null; + } + + + //private IAudioClient AudioClient { get; set; } + + ///// + ///// Player will prioritize different queuer name + ///// over the song position in the playlist + ///// + //public bool FairPlay { get; set; } = false; + + ///// + ///// Song will stop playing after this amount of time. + ///// To prevent people queueing radio or looped songs + ///// while other people want to listen to other songs too. + ///// + //public uint MaxPlaytimeSeconds { get; set; } = 0; + + + //// this should be written better + //public TimeSpan TotalPlaytime => + // _playlist.Any(s => s.TotalTime == TimeSpan.MaxValue) ? + // TimeSpan.MaxValue : + // new TimeSpan(_playlist.Sum(s => s.TotalTime.Ticks)); + + ///// + ///// Users who recently got their music wish + ///// + //private ConcurrentHashSet RecentlyPlayedUsers { get; } = new ConcurrentHashSet(); + + //private readonly List _playlist = new List(); + //private readonly Logger _log; + //private readonly IGoogleApiService _google; + + //public IReadOnlyCollection Playlist => _playlist; + + //public Song CurrentSong { get; private set; } + //public CancellationTokenSource SongCancelSource { get; private set; } + //private CancellationToken CancelToken { get; set; } + + //public bool Paused { get; set; } + + //public float Volume { get; private set; } + + //public event Action OnCompleted = delegate { }; + //public event Action OnStarted = delegate { }; + //public event Action OnPauseChanged = delegate { }; + + //public IVoiceChannel PlaybackVoiceChannel { get; private set; } + //public ITextChannel OutputTextChannel { get; set; } + + //private bool Destroyed { get; set; } + //public bool RepeatSong { get; private set; } + //public bool RepeatPlaylist { get; private set; } + //public bool Autoplay { get; set; } + //public uint MaxQueueSize { get; set; } = 0; + + //private ConcurrentQueue ActionQueue { get; } = new ConcurrentQueue(); + + //public string PrettyVolume => $"🔉 {(int)(Volume * 100)}%"; + + //public event Action SongRemoved = delegate { }; + + //public MusicPlayer(IVoiceChannel startingVoiceChannel, ITextChannel outputChannel, float? defaultVolume, IGoogleApiService google) + //{ + // _log = LogManager.GetCurrentClassLogger(); + // _google = google; + + // OutputTextChannel = outputChannel; + // Volume = defaultVolume ?? 1.0f; + + // PlaybackVoiceChannel = startingVoiceChannel ?? throw new ArgumentNullException(nameof(startingVoiceChannel)); + // SongCancelSource = new CancellationTokenSource(); + // CancelToken = SongCancelSource.Token; + + // Task.Run(async () => + // { + // try + // { + // while (!Destroyed) + // { + // try + // { + // if (ActionQueue.TryDequeue(out Action action)) + // { + // action(); + // } + // } + // finally + // { + // await Task.Delay(100).ConfigureAwait(false); + // } + // } + // } + // catch (Exception ex) + // { + // _log.Warn("Action queue crashed"); + // _log.Warn(ex); + // } + // }).ConfigureAwait(false); + + // var t = new Thread(async () => + // { + // while (!Destroyed) + // { + // try + // { + // CurrentSong = GetNextSong(); + + // if (CurrentSong == null) + // continue; + + // while (AudioClient?.ConnectionState == ConnectionState.Disconnecting || + // AudioClient?.ConnectionState == ConnectionState.Connecting) + // { + // _log.Info("Waiting for Audio client"); + // await Task.Delay(200).ConfigureAwait(false); + // } + + // if (AudioClient == null || AudioClient.ConnectionState == ConnectionState.Disconnected) + // AudioClient = await PlaybackVoiceChannel.ConnectAsync().ConfigureAwait(false); + + // var index = _playlist.IndexOf(CurrentSong); + // if (index != -1) + // RemoveSongAt(index, true); + + // OnStarted(this, CurrentSong); + // try + // { + // await CurrentSong.Play(AudioClient, CancelToken); + // } + // catch (OperationCanceledException) + // { + // } + // finally + // { + // OnCompleted(this, CurrentSong); + // } + + + // if (RepeatPlaylist & !RepeatSong) + // AddSong(CurrentSong, CurrentSong.QueuerName); + + // if (RepeatSong) + // AddSong(CurrentSong, 0); + + // } + // catch (Exception ex) + // { + // _log.Warn("Music thread almost crashed."); + // _log.Warn(ex); + // await Task.Delay(3000).ConfigureAwait(false); + // } + // finally + // { + // if (!CancelToken.IsCancellationRequested) + // { + // SongCancelSource.Cancel(); + // } + // SongCancelSource = new CancellationTokenSource(); + // CancelToken = SongCancelSource.Token; + // CurrentSong = null; + // await Task.Delay(300).ConfigureAwait(false); + // } + // } + // }); + + // t.Start(); + //} + + //public void Next() + //{ + // ActionQueue.Enqueue(() => + // { + // Paused = false; + // SongCancelSource.Cancel(); + // }); + //} + + //public void Stop() + //{ + // ActionQueue.Enqueue(() => + // { + // RepeatPlaylist = false; + // RepeatSong = false; + // Autoplay = false; + // _playlist.Clear(); + // if (!SongCancelSource.IsCancellationRequested) + // SongCancelSource.Cancel(); + // }); + //} + + //public void TogglePause() => OnPauseChanged(Paused = !Paused); + + //public int SetVolume(int volume) + //{ + // if (volume < 0) + // volume = 0; + // if (volume > 100) + // volume = 100; + + // Volume = volume / 100.0f; + // return volume; + //} + + //private Song GetNextSong() + //{ + // if (!FairPlay) + // { + // return _playlist.FirstOrDefault(); + // } + // var song = _playlist.FirstOrDefault(c => !RecentlyPlayedUsers.Contains(c.QueuerName)) + // ?? _playlist.FirstOrDefault(); + + // if (song == null) + // return null; + + // if (RecentlyPlayedUsers.Contains(song.QueuerName)) + // { + // RecentlyPlayedUsers.Clear(); + // } + + // RecentlyPlayedUsers.Add(song.QueuerName); + // return song; + //} + + //public void Shuffle() + //{ + // ActionQueue.Enqueue(() => + // { + // var oldPlaylist = _playlist.ToArray(); + // _playlist.Clear(); + // _playlist.AddRange(oldPlaylist.Shuffle()); + // }); + //} + + //public void AddSong(Song s, string username) + //{ + // if (s == null) + // throw new ArgumentNullException(nameof(s)); + // ThrowIfQueueFull(); + // ActionQueue.Enqueue(() => + // { + // s.MusicPlayer = this; + // s.QueuerName = username.TrimTo(10); + // _playlist.Add(s); + // }); + //} + + //public void AddSong(Song s, int index) + //{ + // if (s == null) + // throw new ArgumentNullException(nameof(s)); + // ActionQueue.Enqueue(() => + // { + // _playlist.Insert(index, s); + // }); + //} + + //public void RemoveSong(Song s) + //{ + // if (s == null) + // throw new ArgumentNullException(nameof(s)); + // ActionQueue.Enqueue(() => + // { + // _playlist.Remove(s); + // }); + //} + + //public void RemoveSongAt(int index, bool silent = false) + //{ + // ActionQueue.Enqueue(() => + // { + // if (index < 0 || index >= _playlist.Count) + // return; + // var song = _playlist.ElementAtOrDefault(index); + // if (_playlist.Remove(song) && !silent) + // { + // SongRemoved(song, index); + // } + + // }); + //} + + //public void ClearQueue() + //{ + // ActionQueue.Enqueue(() => + // { + // _playlist.Clear(); + // }); + //} + + //public async Task UpdateSongDurationsAsync() + //{ + // var curSong = CurrentSong; + // var toUpdate = _playlist.Where(s => s.SongInfo.ProviderType == MusicType.Normal && + // s.TotalTime == TimeSpan.Zero) + // .ToArray(); + // if (curSong != null) + // { + // Array.Resize(ref toUpdate, toUpdate.Length + 1); + // toUpdate[toUpdate.Length - 1] = curSong; + // } + // var ids = toUpdate.Select(s => s.SongInfo.Query.Substring(s.SongInfo.Query.LastIndexOf("?v=") + 3)) + // .Distinct(); + + // var durations = await _google.GetVideoDurationsAsync(ids); + + // toUpdate.ForEach(s => + // { + // foreach (var kvp in durations) + // { + // if (s.SongInfo.Query.EndsWith(kvp.Key)) + // { + // s.TotalTime = kvp.Value; + // return; + // } + // } + // }); + //} + + //public void Destroy() + //{ + // ActionQueue.Enqueue(async () => + // { + // RepeatPlaylist = false; + // RepeatSong = false; + // Autoplay = false; + // Destroyed = true; + // _playlist.Clear(); + + // try { await AudioClient.StopAsync(); } catch { } + // if (!SongCancelSource.IsCancellationRequested) + // SongCancelSource.Cancel(); + // }); + //} + + ////public async Task MoveToVoiceChannel(IVoiceChannel voiceChannel) + ////{ + //// if (audioClient?.ConnectionState != ConnectionState.Connected) + //// throw new InvalidOperationException("Can't move while bot is not connected to voice channel."); + //// PlaybackVoiceChannel = voiceChannel; + //// audioClient = await voiceChannel.ConnectAsync().ConfigureAwait(false); + ////} + + //public bool ToggleRepeatSong() => RepeatSong = !RepeatSong; + + //public bool ToggleRepeatPlaylist() => RepeatPlaylist = !RepeatPlaylist; + + //public bool ToggleAutoplay() => Autoplay = !Autoplay; + + //public void ThrowIfQueueFull() + //{ + // if (MaxQueueSize == 0) + // return; + // if (_playlist.Count >= MaxQueueSize) + // throw new PlaylistFullException(); + //} + } +} diff --git a/src/NadekoBot/Services/Music/MusicQueue.cs b/src/NadekoBot/Services/Music/MusicQueue.cs new file mode 100644 index 00000000..50219dc8 --- /dev/null +++ b/src/NadekoBot/Services/Music/MusicQueue.cs @@ -0,0 +1,119 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace NadekoBot.Services.Music +{ + public class MusicQueue : IDisposable + { + private LinkedList Songs { get; } = new LinkedList(); + private int _currentIndex = 0; + private int CurrentIndex + { + get + { + return _currentIndex; + } + set + { + lock (locker) + { + if (Songs.Count == 0) + _currentIndex = 0; + else + _currentIndex = value %= Songs.Count; + } + } + } + public (int Index, SongInfo Song) Current + { + get + { + var cur = CurrentIndex; + return (cur, Songs.ElementAtOrDefault(cur)); + } + } + + private readonly object locker = new object(); + private TaskCompletionSource nextSource { get; } = new TaskCompletionSource(); + public int Count + { + get + { + lock (locker) + { + return Songs.Count; + } + } + } + + public void Add(SongInfo song) + { + lock (locker) + { + Songs.AddLast(song); + } + } + + public void Next() + { + CurrentIndex++; + } + + public void Dispose() + { + Clear(); + } + + public SongInfo RemoveAt(int index) + { + lock (locker) + { + if (index < 0 || index >= Songs.Count) + throw new ArgumentOutOfRangeException(nameof(index)); + + var current = Songs.First; + for (int i = 0; i < Songs.Count; i++) + { + if (i == index) + { + Songs.Remove(current); + if (CurrentIndex != 0) + { + if (CurrentIndex >= index) + { + --CurrentIndex; + } + } + break; + } + } + return current.Value; + } + } + + public void Clear() + { + lock (locker) + { + Songs.Clear(); + CurrentIndex = 0; + } + } + + public (int, SongInfo[]) ToArray() + { + lock (locker) + { + return (CurrentIndex, Songs.ToArray()); + } + } + + public void ResetCurrent() + { + CurrentIndex = 0; + } + } +} diff --git a/src/NadekoBot/Services/Music/MusicService.cs b/src/NadekoBot/Services/Music/MusicService.cs index 9ea7a958..eb2a5016 100644 --- a/src/NadekoBot/Services/Music/MusicService.cs +++ b/src/NadekoBot/Services/Music/MusicService.cs @@ -11,6 +11,7 @@ using System.IO; using VideoLibrary; using System.Net.Http; using System.Collections.Generic; +using Discord.Commands; namespace NadekoBot.Services.Music { @@ -29,8 +30,8 @@ namespace NadekoBot.Services.Music public ConcurrentDictionary MusicPlayers { get; } = new ConcurrentDictionary(); - public MusicService(IGoogleApiService google, - NadekoStrings strings, ILocalization localization, DbService db, + public MusicService(IGoogleApiService google, + NadekoStrings strings, ILocalization localization, DbService db, SoundCloudApiService sc, IBotCredentials creds, IEnumerable gcs) { _google = google; @@ -48,28 +49,49 @@ namespace NadekoBot.Services.Music Directory.CreateDirectory(MusicDataPath); } - public MusicPlayer GetPlayer(ulong guildId) + // public MusicPlayer GetPlayer(ulong guildId) + // { + // MusicPlayers.TryGetValue(guildId, out var player); + // return player; + // } + public float GetDefaultVolume(ulong guildId) { - MusicPlayers.TryGetValue(guildId, out var player); - return player; + return _defaultVolumes.GetOrAdd(guildId, (id) => + { + using (var uow = _db.UnitOfWork) + { + return uow.GuildConfigs.For(guildId, set => set).DefaultMusicVolume; + } + }); } - public MusicPlayer GetOrCreatePlayer(ulong guildId, IVoiceChannel voiceCh, ITextChannel textCh) + public Task GetOrCreatePlayer(ICommandContext context) + { + var gUsr = (IGuildUser)context.User; + var txtCh = (ITextChannel)context.Channel; + var vCh = gUsr.VoiceChannel; + return GetOrCreatePlayer(context.Guild.Id, vCh, txtCh); + } + + public async Task GetOrCreatePlayer(ulong guildId, IVoiceChannel voiceCh, ITextChannel textCh) { string GetText(string text, params object[] replacements) => _strings.GetText(text, _localization.GetCultureInfo(textCh.Guild), "Music".ToLowerInvariant(), replacements); - return MusicPlayers.GetOrAdd(guildId, server => + if (voiceCh == null || voiceCh.Guild != textCh.Guild) { - var vol = _defaultVolumes.GetOrAdd(guildId, (id) => + if (textCh != null) { - using (var uow = _db.UnitOfWork) - { - return uow.GuildConfigs.For(guildId, set => set).DefaultMusicVolume; - } - }); - - var mp = new MusicPlayer(voiceCh, textCh, vol, _google); + await textCh.SendErrorAsync(GetText("must_be_in_voice")).ConfigureAwait(false); + } + throw new ArgumentException(nameof(voiceCh)); + } + + return MusicPlayers.GetOrAdd(guildId, _ => + { + var vol = GetDefaultVolume(guildId); + var mp = new MusicPlayer(this, voiceCh, textCh, vol); + IUserMessage playingMessage = null; IUserMessage lastFinishedMessage = null; mp.OnCompleted += async (s, song) => @@ -91,30 +113,30 @@ namespace NadekoBot.Services.Music // ignored } - if (mp.Autoplay && mp.Playlist.Count == 0 && song.SongInfo.ProviderType == MusicType.Normal) - { - var relatedVideos = (await _google.GetRelatedVideosAsync(song.SongInfo.Query, 4)).ToList(); - if (relatedVideos.Count > 0) - await QueueSong(await textCh.Guild.GetCurrentUserAsync(), - textCh, - voiceCh, - relatedVideos[new NadekoRandom().Next(0, relatedVideos.Count)], - true).ConfigureAwait(false); - } + //todo autoplay should be independent from event handlers + //if (mp.Autoplay && mp.Playlist.Count == 0 && song.SongInfo.ProviderType == MusicType.Normal) + //{ + // var relatedVideos = (await _google.GetRelatedVideosAsync(song.SongInfo.Query, 4)).ToList(); + // if (relatedVideos.Count > 0) + // await QueueSong(await textCh.Guild.GetCurrentUserAsync(), + // textCh, + // voiceCh, + // relatedVideos[new NadekoRandom().Next(0, relatedVideos.Count)], + // true).ConfigureAwait(false); + //} } catch { // ignored } }; - mp.OnStarted += async (player, song) => { - try { await mp.UpdateSongDurationsAsync().ConfigureAwait(false); } - catch - { - // ignored - } + //try { await mp.UpdateSongDurationsAsync().ConfigureAwait(false); } + //catch + //{ + // // ignored + //} var sender = player; if (sender == null) return; @@ -125,7 +147,7 @@ namespace NadekoBot.Services.Music playingMessage = await mp.OutputTextChannel.EmbedAsync(new EmbedBuilder().WithOkColor() .WithAuthor(eab => eab.WithName(GetText("playing_song")).WithMusicIcon()) .WithDescription(song.PrettyName) - .WithFooter(ef => ef.WithText(song.PrettyInfo))) + .WithFooter(ef => ef.WithText(mp.PrettyVolume + " | " + song.PrettyInfo))) .ConfigureAwait(false); } catch @@ -133,7 +155,7 @@ namespace NadekoBot.Services.Music // ignored } }; - mp.OnPauseChanged += async (paused) => + mp.OnPauseChanged += async (player, paused) => { try { @@ -150,291 +172,228 @@ namespace NadekoBot.Services.Music // ignored } }; + //mp.SongRemoved += async (song, index) => + //{ + // try + // { + // var embed = new EmbedBuilder() + // .WithAuthor(eab => eab.WithName(GetText("removed_song") + " #" + (index + 1)).WithMusicIcon()) + // .WithDescription(song.PrettyName) + // .WithFooter(ef => ef.WithText(song.PrettyInfo)) + // .WithErrorColor(); - mp.SongRemoved += async (song, index) => - { - try - { - var embed = new EmbedBuilder() - .WithAuthor(eab => eab.WithName(GetText("removed_song") + " #" + (index + 1)).WithMusicIcon()) - .WithDescription(song.PrettyName) - .WithFooter(ef => ef.WithText(song.PrettyInfo)) - .WithErrorColor(); + // await mp.OutputTextChannel.EmbedAsync(embed).ConfigureAwait(false); - await mp.OutputTextChannel.EmbedAsync(embed).ConfigureAwait(false); - - } - catch - { - // ignored - } - }; + // } + // catch + // { + // // ignored + // } + //}; return mp; }); } - - public async Task QueueSong(IGuildUser queuer, ITextChannel textCh, IVoiceChannel voiceCh, string query, bool silent = false, MusicType musicType = MusicType.Normal) + public async Task ResolveSong(string query, string queuerName, MusicType musicType = MusicType.Normal) { - string GetText(string text, params object[] replacements) => - _strings.GetText(text, _localization.GetCultureInfo(textCh.Guild), "Music".ToLowerInvariant(), replacements); + query.ThrowIfNull(nameof(query)); - if (voiceCh == null || voiceCh.Guild != textCh.Guild) - { - if (!silent) - await textCh.SendErrorAsync(GetText("must_be_in_voice")).ConfigureAwait(false); - throw new ArgumentNullException(nameof(voiceCh)); - } - if (string.IsNullOrWhiteSpace(query) || query.Length < 3) - throw new ArgumentException("Invalid song query.", nameof(query)); + SongInfo sinfo; - var musicPlayer = GetOrCreatePlayer(textCh.Guild.Id, voiceCh, textCh); - Song resolvedSong; - try - { - musicPlayer.ThrowIfQueueFull(); - resolvedSong = await ResolveSong(query, musicType).ConfigureAwait(false); + sinfo = await ResolveYoutubeSong(query, queuerName).ConfigureAwait(false); - if (resolvedSong == null) - throw new SongNotFoundException(); + return sinfo; + } - musicPlayer.AddSong(resolvedSong, queuer.Username); - } - catch (PlaylistFullException) + public async Task ResolveYoutubeSong(string query, string queuerName) + { + var link = (await _google.GetVideoLinksByKeywordAsync(query).ConfigureAwait(false)).FirstOrDefault(); + if (string.IsNullOrWhiteSpace(link)) + throw new OperationCanceledException("Not a valid youtube query."); + var allVideos = await Task.Run(async () => { try { return await YouTube.Default.GetAllVideosAsync(link).ConfigureAwait(false); } catch { return Enumerable.Empty(); } }).ConfigureAwait(false); + var videos = allVideos.Where(v => v.AdaptiveKind == AdaptiveKind.Audio); + var video = videos + .Where(v => v.AudioBitrate < 256) + .OrderByDescending(v => v.AudioBitrate) + .FirstOrDefault(); + + if (video == null) // do something with this error + throw new Exception("Could not load any video elements based on the query."); + //var m = Regex.Match(query, @"\?t=(?\d*)"); + //int gotoTime = 0; + //if (m.Captures.Count > 0) + // int.TryParse(m.Groups["t"].ToString(), out gotoTime); + var song = new SongInfo { - try - { - await textCh.SendConfirmAsync(GetText("queue_full", musicPlayer.MaxQueueSize)); - } - catch - { - // ignored - } - throw; - } - if (!silent) - { - try - { - //var queuedMessage = await textCh.SendConfirmAsync($"🎵 Queued **{resolvedSong.SongInfo.Title}** at `#{musicPlayer.Playlist.Count + 1}`").ConfigureAwait(false); - var queuedMessage = await textCh.EmbedAsync(new EmbedBuilder().WithOkColor() - .WithAuthor(eab => eab.WithName(GetText("queued_song") + " #" + (musicPlayer.Playlist.Count + 1)).WithMusicIcon()) - .WithDescription($"{resolvedSong.PrettyName}\n{GetText("queue")} ") - .WithThumbnailUrl(resolvedSong.Thumbnail) - .WithFooter(ef => ef.WithText(resolvedSong.PrettyProvider))) - .ConfigureAwait(false); - queuedMessage?.DeleteAfter(10); - } - catch - { - // ignored - } // if queued message sending fails, don't attempt to delete it - } + Title = video.Title.Substring(0, video.Title.Length - 10), // removing trailing "- You Tube" + Provider = "YouTube", + Uri = await video.GetUriAsync().ConfigureAwait(false), + Query = link, + ProviderType = MusicType.Normal, + QueuerName = queuerName + }; + return song; } public void DestroyPlayer(ulong id) { if (MusicPlayers.TryRemove(id, out var mp)) - mp.Destroy(); + mp.Dispose(); } + // public async Task QueueSong(IGuildUser queuer, ITextChannel textCh, IVoiceChannel voiceCh, string query, bool silent = false, MusicType musicType = MusicType.Normal) + // { + // string GetText(string text, params object[] replacements) => + // _strings.GetText(text, _localization.GetCultureInfo(textCh.Guild), "Music".ToLowerInvariant(), replacements); - public async Task ResolveSong(string query, MusicType musicType = MusicType.Normal) - { - if (string.IsNullOrWhiteSpace(query)) - throw new ArgumentNullException(nameof(query)); + //if (string.IsNullOrWhiteSpace(query) || query.Length< 3) + // throw new ArgumentException("Invalid song query.", nameof(query)); - if (musicType != MusicType.Local && IsRadioLink(query)) - { - musicType = MusicType.Radio; - query = await HandleStreamContainers(query).ConfigureAwait(false) ?? query; - } + // var musicPlayer = GetOrCreatePlayer(textCh.Guild.Id, voiceCh, textCh); + // Song resolvedSong; + // try + // { + // musicPlayer.ThrowIfQueueFull(); + // resolvedSong = await ResolveSong(query, musicType).ConfigureAwait(false); - try - { - switch (musicType) - { - case MusicType.Local: - return new Song(new SongInfo - { - Uri = "\"" + Path.GetFullPath(query) + "\"", - Title = Path.GetFileNameWithoutExtension(query), - Provider = "Local File", - ProviderType = musicType, - Query = query, - }); - case MusicType.Radio: - return new Song(new SongInfo - { - Uri = query, - Title = $"{query}", - Provider = "Radio Stream", - ProviderType = musicType, - Query = query - }) - { TotalTime = TimeSpan.MaxValue }; - } - if (_sc.IsSoundCloudLink(query)) - { - var svideo = await _sc.ResolveVideoAsync(query).ConfigureAwait(false); - return new Song(new SongInfo - { - Title = svideo.FullName, - Provider = "SoundCloud", - Uri = await svideo.StreamLink(), - ProviderType = musicType, - Query = svideo.TrackLink, - AlbumArt = svideo.artwork_url, - }) - { TotalTime = TimeSpan.FromMilliseconds(svideo.Duration) }; - } + // if (resolvedSong == null) + // throw new SongNotFoundException(); - if (musicType == MusicType.Soundcloud) - { - var svideo = await _sc.GetVideoByQueryAsync(query).ConfigureAwait(false); - return new Song(new SongInfo - { - Title = svideo.FullName, - Provider = "SoundCloud", - Uri = await svideo.StreamLink(), - ProviderType = MusicType.Soundcloud, - Query = svideo.TrackLink, - AlbumArt = svideo.artwork_url, - }) - { TotalTime = TimeSpan.FromMilliseconds(svideo.Duration) }; - } + // musicPlayer.AddSong(resolvedSong, queuer.Username); + // } + // catch (PlaylistFullException) + // { + // try + // { + // await textCh.SendConfirmAsync(GetText("queue_full", musicPlayer.MaxQueueSize)); + // } + // catch + // { + // // ignored + // } + // throw; + // } + // if (!silent) + // { + // try + // { + // //var queuedMessage = await textCh.SendConfirmAsync($"🎵 Queued **{resolvedSong.SongInfo.Title}** at `#{musicPlayer.Playlist.Count + 1}`").ConfigureAwait(false); + // var queuedMessage = await textCh.EmbedAsync(new EmbedBuilder().WithOkColor() + // .WithAuthor(eab => eab.WithName(GetText("queued_song") + " #" + (musicPlayer.Playlist.Count + 1)).WithMusicIcon()) + // .WithDescription($"{resolvedSong.PrettyName}\n{GetText("queue")} ") + // .WithThumbnailUrl(resolvedSong.Thumbnail) + // .WithFooter(ef => ef.WithText(resolvedSong.PrettyProvider))) + // .ConfigureAwait(false); + // queuedMessage?.DeleteAfter(10); + // } + // catch + // { + // // ignored + // } // if queued message sending fails, don't attempt to delete it + // } + // } - var link = (await _google.GetVideoLinksByKeywordAsync(query).ConfigureAwait(false)).FirstOrDefault(); - if (string.IsNullOrWhiteSpace(link)) - throw new OperationCanceledException("Not a valid youtube query."); - var allVideos = await Task.Run(async () => { try { return await YouTube.Default.GetAllVideosAsync(link).ConfigureAwait(false); } catch { return Enumerable.Empty(); } }).ConfigureAwait(false); - var videos = allVideos.Where(v => v.AdaptiveKind == AdaptiveKind.Audio); - var video = videos - .Where(v => v.AudioBitrate < 256) - .OrderByDescending(v => v.AudioBitrate) - .FirstOrDefault(); - if (video == null) // do something with this error - throw new Exception("Could not load any video elements based on the query."); - var m = Regex.Match(query, @"\?t=(?\d*)"); - int gotoTime = 0; - if (m.Captures.Count > 0) - int.TryParse(m.Groups["t"].ToString(), out gotoTime); - var song = new Song(new SongInfo - { - Title = video.Title.Substring(0, video.Title.Length - 10), // removing trailing "- You Tube" - Provider = "YouTube", - Uri = await video.GetUriAsync().ConfigureAwait(false), - Query = link, - ProviderType = musicType, - }); - song.SkipTo = gotoTime; - return song; - } - catch (Exception ex) - { - _log.Warn($"Failed resolving the link.{ex.Message}"); - _log.Warn(ex); - return null; - } - } - private async Task HandleStreamContainers(string query) - { - string file = null; - try - { - using (var http = new HttpClient()) - { - file = await http.GetStringAsync(query).ConfigureAwait(false); - } - } - catch - { - return query; - } - if (query.Contains(".pls")) - { - //File1=http://armitunes.com:8000/ - //Regex.Match(query) - try - { - var m = Regex.Match(file, "File1=(?.*?)\\n"); - var res = m.Groups["url"]?.ToString(); - return res?.Trim(); - } - catch - { - _log.Warn($"Failed reading .pls:\n{file}"); - return null; - } - } - if (query.Contains(".m3u")) - { - /* -# This is a comment - C:\xxx4xx\xxxxxx3x\xx2xxxx\xx.mp3 - C:\xxx5xx\x6xxxxxx\x7xxxxx\xx.mp3 - */ - try - { - var m = Regex.Match(file, "(?^[^#].*)", RegexOptions.Multiline); - var res = m.Groups["url"]?.ToString(); - return res?.Trim(); - } - catch - { - _log.Warn($"Failed reading .m3u:\n{file}"); - return null; - } - } - if (query.Contains(".asx")) - { - // - try - { - var m = Regex.Match(file, ".*?)\""); - var res = m.Groups["url"]?.ToString(); - return res?.Trim(); - } - catch - { - _log.Warn($"Failed reading .asx:\n{file}"); - return null; - } - } - if (query.Contains(".xspf")) - { - /* - - - - file:///mp3s/song_1.mp3 - */ - try - { - var m = Regex.Match(file, "(?.*?)"); - var res = m.Groups["url"]?.ToString(); - return res?.Trim(); - } - catch - { - _log.Warn($"Failed reading .xspf:\n{file}"); - return null; - } - } - return query; - } - private bool IsRadioLink(string query) => - (query.StartsWith("http") || - query.StartsWith("ww")) - && - (query.Contains(".pls") || - query.Contains(".m3u") || - query.Contains(".asx") || - query.Contains(".xspf")); + // private async Task HandleStreamContainers(string query) + // { + // string file = null; + // try + // { + // using (var http = new HttpClient()) + // { + // file = await http.GetStringAsync(query).ConfigureAwait(false); + // } + // } + // catch + // { + // return query; + // } + // if (query.Contains(".pls")) + // { + // //File1=http://armitunes.com:8000/ + // //Regex.Match(query) + // try + // { + // var m = Regex.Match(file, "File1=(?.*?)\\n"); + // var res = m.Groups["url"]?.ToString(); + // return res?.Trim(); + // } + // catch + // { + // _log.Warn($"Failed reading .pls:\n{file}"); + // return null; + // } + // } + // if (query.Contains(".m3u")) + // { + // /* + //# This is a comment + // C:\xxx4xx\xxxxxx3x\xx2xxxx\xx.mp3 + // C:\xxx5xx\x6xxxxxx\x7xxxxx\xx.mp3 + // */ + // try + // { + // var m = Regex.Match(file, "(?^[^#].*)", RegexOptions.Multiline); + // var res = m.Groups["url"]?.ToString(); + // return res?.Trim(); + // } + // catch + // { + // _log.Warn($"Failed reading .m3u:\n{file}"); + // return null; + // } + + // } + // if (query.Contains(".asx")) + // { + // // + // try + // { + // var m = Regex.Match(file, ".*?)\""); + // var res = m.Groups["url"]?.ToString(); + // return res?.Trim(); + // } + // catch + // { + // _log.Warn($"Failed reading .asx:\n{file}"); + // return null; + // } + // } + // if (query.Contains(".xspf")) + // { + // /* + // + // + // + // file:///mp3s/song_1.mp3 + // */ + // try + // { + // var m = Regex.Match(file, "(?.*?)"); + // var res = m.Groups["url"]?.ToString(); + // return res?.Trim(); + // } + // catch + // { + // _log.Warn($"Failed reading .xspf:\n{file}"); + // return null; + // } + // } + + // return query; + // } + + // private bool IsRadioLink(string query) => + // (query.StartsWith("http") || + // query.StartsWith("ww")) + // && + // (query.Contains(".pls") || + // query.Contains(".m3u") || + // query.Contains(".asx") || + // query.Contains(".xspf")); } -} +} \ No newline at end of file diff --git a/src/NadekoBot/Services/Music/Song.cs b/src/NadekoBot/Services/Music/Song.cs index 187b3b6e..074238d6 100644 --- a/src/NadekoBot/Services/Music/Song.cs +++ b/src/NadekoBot/Services/Music/Song.cs @@ -1,296 +1,246 @@ -using Discord.Audio; -using NadekoBot.Extensions; -using NLog; -using System; -using System.Diagnostics; -using System.IO; -using System.Text.RegularExpressions; -using System.Threading; -using System.Threading.Tasks; +using NadekoBot.Extensions; using System.Net; using Discord; using NadekoBot.Services.Database.Models; +using System; namespace NadekoBot.Services.Music { - public class SongInfo - { - public string Provider { get; set; } - public MusicType ProviderType { get; set; } - public string Query { get; set; } - public string Title { get; set; } - public string Uri { get; set; } - public string AlbumArt { get; set; } - } + //public class Song + //{ + // public SongInfo SongInfo { get; } + // public MusicPlayer MusicPlayer { get; set; } - public class Song - { - public SongInfo SongInfo { get; } - public MusicPlayer MusicPlayer { get; set; } + // private string _queuerName; + // public string QueuerName { get{ + // return Format.Sanitize(_queuerName); + // } set { _queuerName = value; } } - private string _queuerName; - public string QueuerName { get{ - return Format.Sanitize(_queuerName); - } set { _queuerName = value; } } + // public TimeSpan TotalTime { get; set; } = TimeSpan.Zero; + // public TimeSpan CurrentTime => TimeSpan.FromSeconds(BytesSent / (float)_frameBytes / (1000 / (float)_milliseconds)); - public TimeSpan TotalTime { get; set; } = TimeSpan.Zero; - public TimeSpan CurrentTime => TimeSpan.FromSeconds(BytesSent / (float)_frameBytes / (1000 / (float)_milliseconds)); + // private const int _milliseconds = 20; + // private const int _samplesPerFrame = (48000 / 1000) * _milliseconds; + // private const int _frameBytes = 3840; //16-bit, 2 channels - private const int _milliseconds = 20; - private const int _samplesPerFrame = (48000 / 1000) * _milliseconds; - private const int _frameBytes = 3840; //16-bit, 2 channels + // private ulong BytesSent { get; set; } - private ulong BytesSent { get; set; } + // //pwetty - //pwetty + // public string PrettyProvider => + // $"{(SongInfo.Provider ?? "???")}"; - public string PrettyProvider => - $"{(SongInfo.Provider ?? "???")}"; + // public string PrettyFullTime => PrettyCurrentTime + " / " + PrettyTotalTime; - public string PrettyFullTime => PrettyCurrentTime + " / " + PrettyTotalTime; + // public string PrettyName => $"**[{SongInfo.Title.TrimTo(65)}]({SongUrl})**"; - public string PrettyName => $"**[{SongInfo.Title.TrimTo(65)}]({SongUrl})**"; + // public string PrettyInfo => $"{MusicPlayer.PrettyVolume} | {PrettyTotalTime} | {PrettyProvider} | {QueuerName}"; - public string PrettyInfo => $"{MusicPlayer.PrettyVolume} | {PrettyTotalTime} | {PrettyProvider} | {QueuerName}"; + // public string PrettyFullName => $"{PrettyName}\n\t\t`{PrettyTotalTime} | {PrettyProvider} | {QueuerName}`"; - public string PrettyFullName => $"{PrettyName}\n\t\t`{PrettyTotalTime} | {PrettyProvider} | {QueuerName}`"; + // public string PrettyCurrentTime { + // get { + // var time = CurrentTime.ToString(@"mm\:ss"); + // var hrs = (int)CurrentTime.TotalHours; - public string PrettyCurrentTime { - get { - var time = CurrentTime.ToString(@"mm\:ss"); - var hrs = (int)CurrentTime.TotalHours; + // if (hrs > 0) + // return hrs + ":" + time; + // else + // return time; + // } + // } - if (hrs > 0) - return hrs + ":" + time; - else - return time; - } - } + // public string PrettyTotalTime { + // get + // { + // if (TotalTime == TimeSpan.Zero) + // return "(?)"; + // if (TotalTime == TimeSpan.MaxValue) + // return "∞"; + // var time = TotalTime.ToString(@"mm\:ss"); + // var hrs = (int)TotalTime.TotalHours; - public string PrettyTotalTime { - get - { - if (TotalTime == TimeSpan.Zero) - return "(?)"; - if (TotalTime == TimeSpan.MaxValue) - return "∞"; - var time = TotalTime.ToString(@"mm\:ss"); - var hrs = (int)TotalTime.TotalHours; + // if (hrs > 0) + // return hrs + ":" + time; + // return time; + // } + // } - if (hrs > 0) - return hrs + ":" + time; - return time; - } - } + // public string Thumbnail { + // get { + // switch (SongInfo.ProviderType) + // { + // case MusicType.Radio: + // return "https://cdn.discordapp.com/attachments/155726317222887425/261850925063340032/1482522097_radio.png"; //test links + // case MusicType.Normal: + // //todo 50 have videoid in songinfo from the start + // var videoId = Regex.Match(SongInfo.Query, "<=v=[a-zA-Z0-9-]+(?=&)|(?<=[0-9])[^&\n]+|(?<=v=)[^&\n]+"); + // return $"https://img.youtube.com/vi/{ videoId }/0.jpg"; + // case MusicType.Local: + // return "https://cdn.discordapp.com/attachments/155726317222887425/261850914783100928/1482522077_music.png"; //test links + // case MusicType.Soundcloud: + // return SongInfo.AlbumArt; + // default: + // return ""; + // } + // } + // } - public string Thumbnail { - get { - switch (SongInfo.ProviderType) - { - case MusicType.Radio: - return "https://cdn.discordapp.com/attachments/155726317222887425/261850925063340032/1482522097_radio.png"; //test links - case MusicType.Normal: - //todo 50 have videoid in songinfo from the start - var videoId = Regex.Match(SongInfo.Query, "<=v=[a-zA-Z0-9-]+(?=&)|(?<=[0-9])[^&\n]+|(?<=v=)[^&\n]+"); - return $"https://img.youtube.com/vi/{ videoId }/0.jpg"; - case MusicType.Local: - return "https://cdn.discordapp.com/attachments/155726317222887425/261850914783100928/1482522077_music.png"; //test links - case MusicType.Soundcloud: - return SongInfo.AlbumArt; - default: - return ""; - } - } - } + // public string SongUrl { + // get { + // switch (SongInfo.ProviderType) + // { + // case MusicType.Normal: + // return SongInfo.Query; + // case MusicType.Soundcloud: + // return SongInfo.Query; + // case MusicType.Local: + // return $"https://google.com/search?q={ WebUtility.UrlEncode(SongInfo.Title).Replace(' ', '+') }"; + // case MusicType.Radio: + // return $"https://google.com/search?q={SongInfo.Title}"; + // default: + // return ""; + // } + // } + // } - public string SongUrl { - get { - switch (SongInfo.ProviderType) - { - case MusicType.Normal: - return SongInfo.Query; - case MusicType.Soundcloud: - return SongInfo.Query; - case MusicType.Local: - return $"https://google.com/search?q={ WebUtility.UrlEncode(SongInfo.Title).Replace(' ', '+') }"; - case MusicType.Radio: - return $"https://google.com/search?q={SongInfo.Title}"; - default: - return ""; - } - } - } + // private readonly Logger _log; - public int SkipTo { get; set; } + // public Song(SongInfo songInfo) + // { + // SongInfo = songInfo; + // _log = LogManager.GetCurrentClassLogger(); + // } - private readonly Logger _log; + // public async Task Play(IAudioClient voiceClient, CancellationToken cancelToken) + // { + // BytesSent = (ulong) SkipTo * 3840 * 50; + // var filename = Path.Combine(MusicService.MusicDataPath, DateTime.UtcNow.UnixTimestamp().ToString()); - public Song(SongInfo songInfo) - { - SongInfo = songInfo; - _log = LogManager.GetCurrentClassLogger(); - } + // var inStream = new SongBuffer(MusicPlayer, filename, SongInfo, SkipTo, _frameBytes * 100); + // var bufferTask = inStream.BufferSong(cancelToken).ConfigureAwait(false); - public Song Clone() - { - var s = new Song(SongInfo) - { - MusicPlayer = MusicPlayer, - QueuerName = QueuerName - }; - return s; - } + // try + // { + // var attempt = 0; - public async Task Play(IAudioClient voiceClient, CancellationToken cancelToken) - { - BytesSent = (ulong) SkipTo * 3840 * 50; - var filename = Path.Combine(MusicService.MusicDataPath, DateTime.UtcNow.UnixTimestamp().ToString()); + // var prebufferingTask = CheckPrebufferingAsync(inStream, cancelToken, 1.MiB()); //Fast connection can do this easy + // var finished = false; + // var count = 0; + // var sw = new Stopwatch(); + // var slowconnection = false; + // sw.Start(); + // while (!finished) + // { + // var t = await Task.WhenAny(prebufferingTask, Task.Delay(2000, cancelToken)); + // if (t != prebufferingTask) + // { + // count++; + // if (count == 10) + // { + // slowconnection = true; + // prebufferingTask = CheckPrebufferingAsync(inStream, cancelToken, 20.MiB()); + // _log.Warn("Slow connection buffering more to ensure no disruption, consider hosting in cloud"); + // continue; + // } - var inStream = new SongBuffer(MusicPlayer, filename, SongInfo, SkipTo, _frameBytes * 100); - var bufferTask = inStream.BufferSong(cancelToken).ConfigureAwait(false); + // if (inStream.BufferingCompleted && count == 1) + // { + // _log.Debug("Prebuffering canceled. Cannot get any data from the stream."); + // return; + // } + // else + // { + // continue; + // } + // } + // else if (prebufferingTask.IsCanceled) + // { + // _log.Debug("Prebuffering canceled. Cannot get any data from the stream."); + // return; + // } + // finished = true; + // } + // sw.Stop(); + // _log.Debug("Prebuffering successfully completed in " + sw.Elapsed); - try - { - var attempt = 0; + // var outStream = voiceClient.CreatePCMStream(AudioApplication.Music); - var prebufferingTask = CheckPrebufferingAsync(inStream, cancelToken, 1.MiB()); //Fast connection can do this easy - var finished = false; - var count = 0; - var sw = new Stopwatch(); - var slowconnection = false; - sw.Start(); - while (!finished) - { - var t = await Task.WhenAny(prebufferingTask, Task.Delay(2000, cancelToken)); - if (t != prebufferingTask) - { - count++; - if (count == 10) - { - slowconnection = true; - prebufferingTask = CheckPrebufferingAsync(inStream, cancelToken, 20.MiB()); - _log.Warn("Slow connection buffering more to ensure no disruption, consider hosting in cloud"); - continue; - } + // int nextTime = Environment.TickCount + _milliseconds; - if (inStream.BufferingCompleted && count == 1) - { - _log.Debug("Prebuffering canceled. Cannot get any data from the stream."); - return; - } - else - { - continue; - } - } - else if (prebufferingTask.IsCanceled) - { - _log.Debug("Prebuffering canceled. Cannot get any data from the stream."); - return; - } - finished = true; - } - sw.Stop(); - _log.Debug("Prebuffering successfully completed in " + sw.Elapsed); + // byte[] buffer = new byte[_frameBytes]; + // while (!cancelToken.IsCancellationRequested && //song canceled for whatever reason + // !(MusicPlayer.MaxPlaytimeSeconds != 0 && CurrentTime.TotalSeconds >= MusicPlayer.MaxPlaytimeSeconds)) // or exceedded max playtime + // { + // //Console.WriteLine($"Read: {songBuffer.ReadPosition}\nWrite: {songBuffer.WritePosition}\nContentLength:{songBuffer.ContentLength}\n---------"); + // var read = await inStream.ReadAsync(buffer, 0, buffer.Length).ConfigureAwait(false); + // //await inStream.CopyToAsync(voiceClient.OutputStream); + // if (read < _frameBytes) + // _log.Debug("read {0}", read); + // unchecked + // { + // BytesSent += (ulong)read; + // } + // if (read < _frameBytes) + // { + // if (read == 0) + // { + // if (inStream.BufferingCompleted) + // break; + // if (attempt++ == 20) + // { + // MusicPlayer.SongCancelSource.Cancel(); + // break; + // } + // if (slowconnection) + // { + // _log.Warn("Slow connection has disrupted music, waiting a bit for buffer"); - var outStream = voiceClient.CreatePCMStream(AudioApplication.Music); + // await Task.Delay(1000, cancelToken).ConfigureAwait(false); + // nextTime = Environment.TickCount + _milliseconds; + // } + // else + // { + // await Task.Delay(100, cancelToken).ConfigureAwait(false); + // nextTime = Environment.TickCount + _milliseconds; + // } + // } + // else + // attempt = 0; + // } + // else + // attempt = 0; - int nextTime = Environment.TickCount + _milliseconds; - - byte[] buffer = new byte[_frameBytes]; - while (!cancelToken.IsCancellationRequested && //song canceled for whatever reason - !(MusicPlayer.MaxPlaytimeSeconds != 0 && CurrentTime.TotalSeconds >= MusicPlayer.MaxPlaytimeSeconds)) // or exceedded max playtime - { - //Console.WriteLine($"Read: {songBuffer.ReadPosition}\nWrite: {songBuffer.WritePosition}\nContentLength:{songBuffer.ContentLength}\n---------"); - var read = await inStream.ReadAsync(buffer, 0, buffer.Length).ConfigureAwait(false); - //await inStream.CopyToAsync(voiceClient.OutputStream); - if (read < _frameBytes) - _log.Debug("read {0}", read); - unchecked - { - BytesSent += (ulong)read; - } - if (read < _frameBytes) - { - if (read == 0) - { - if (inStream.BufferingCompleted) - break; - if (attempt++ == 20) - { - MusicPlayer.SongCancelSource.Cancel(); - break; - } - if (slowconnection) - { - _log.Warn("Slow connection has disrupted music, waiting a bit for buffer"); - - await Task.Delay(1000, cancelToken).ConfigureAwait(false); - nextTime = Environment.TickCount + _milliseconds; - } - else - { - await Task.Delay(100, cancelToken).ConfigureAwait(false); - nextTime = Environment.TickCount + _milliseconds; - } - } - else - attempt = 0; - } - else - attempt = 0; - - while (MusicPlayer.Paused) - { - await Task.Delay(200, cancelToken).ConfigureAwait(false); - nextTime = Environment.TickCount + _milliseconds; - } + // while (MusicPlayer.Paused) + // { + // await Task.Delay(200, cancelToken).ConfigureAwait(false); + // nextTime = Environment.TickCount + _milliseconds; + // } - buffer = AdjustVolume(buffer, MusicPlayer.Volume); - if (read != _frameBytes) continue; - nextTime = unchecked(nextTime + _milliseconds); - int delayMillis = unchecked(nextTime - Environment.TickCount); - if (delayMillis > 0) - await Task.Delay(delayMillis, cancelToken).ConfigureAwait(false); - await outStream.WriteAsync(buffer, 0, read).ConfigureAwait(false); - } - } - finally - { - await bufferTask; - inStream.Dispose(); - } - } + // buffer = AdjustVolume(buffer, MusicPlayer.Volume); + // if (read != _frameBytes) continue; + // nextTime = unchecked(nextTime + _milliseconds); + // int delayMillis = unchecked(nextTime - Environment.TickCount); + // if (delayMillis > 0) + // await Task.Delay(delayMillis, cancelToken).ConfigureAwait(false); + // await outStream.WriteAsync(buffer, 0, read).ConfigureAwait(false); + // } + // } + // finally + // { + // await bufferTask; + // inStream.Dispose(); + // } + // } - private async Task CheckPrebufferingAsync(SongBuffer inStream, CancellationToken cancelToken, long size) - { - while (!inStream.BufferingCompleted && inStream.Length < size) - { - await Task.Delay(100, cancelToken); - } - _log.Debug("Buffering successfull"); - } - - //aidiakapi ftw - public static unsafe byte[] AdjustVolume(byte[] audioSamples, float volume) - { - if (Math.Abs(volume - 1f) < 0.0001f) return audioSamples; - - // 16-bit precision for the multiplication - var volumeFixed = (int)Math.Round(volume * 65536d); - - var count = audioSamples.Length / 2; - - fixed (byte* srcBytes = audioSamples) - { - var src = (short*)srcBytes; - - for (var i = count; i != 0; i--, src++) - *src = (short)(((*src) * volumeFixed) >> 16); - } - - return audioSamples; - } - } + // private async Task CheckPrebufferingAsync(SongBuffer inStream, CancellationToken cancelToken, long size) + // { + // while (!inStream.BufferingCompleted && inStream.Length < size) + // { + // await Task.Delay(100, cancelToken); + // } + // _log.Debug("Buffering successfull"); + // } + //} } \ No newline at end of file diff --git a/src/NadekoBot/Services/Music/SongBuffer.cs b/src/NadekoBot/Services/Music/SongBuffer.cs index ff3a0ed2..f11c2cce 100644 --- a/src/NadekoBot/Services/Music/SongBuffer.cs +++ b/src/NadekoBot/Services/Music/SongBuffer.cs @@ -1,219 +1,219 @@ -using NadekoBot.Extensions; -using NLog; -using System; -using System.Diagnostics; -using System.IO; -using System.Threading; -using System.Threading.Tasks; +//using NadekoBot.Extensions; +//using NLog; +//using System; +//using System.Diagnostics; +//using System.IO; +//using System.Threading; +//using System.Threading.Tasks; -namespace NadekoBot.Services.Music -{ - /// - /// Create a buffer for a song file. It will create multiples files to ensure, that radio don't fill up disk space. - /// It also help for large music by deleting files that are already seen. - /// - class SongBuffer : Stream - { - public SongBuffer(MusicPlayer musicPlayer, string basename, SongInfo songInfo, int skipTo, int maxFileSize) - { - MusicPlayer = musicPlayer; - Basename = basename; - SongInfo = songInfo; - SkipTo = skipTo; - MaxFileSize = maxFileSize; - CurrentFileStream = new FileStream(this.GetNextFile(), FileMode.OpenOrCreate, FileAccess.Read, FileShare.Write); - _log = LogManager.GetCurrentClassLogger(); - } +//namespace NadekoBot.Services.Music +//{ +// /// +// /// Create a buffer for a song file. It will create multiples files to ensure, that radio don't fill up disk space. +// /// It also help for large music by deleting files that are already seen. +// /// +// class SongBuffer : Stream +// { +// public SongBuffer(MusicPlayer musicPlayer, string basename, SongInfo songInfo, int skipTo, int maxFileSize) +// { +// MusicPlayer = musicPlayer; +// Basename = basename; +// SongInfo = songInfo; +// SkipTo = skipTo; +// MaxFileSize = maxFileSize; +// CurrentFileStream = new FileStream(this.GetNextFile(), FileMode.OpenOrCreate, FileAccess.Read, FileShare.Write); +// _log = LogManager.GetCurrentClassLogger(); +// } - MusicPlayer MusicPlayer { get; } +// MusicPlayer MusicPlayer { get; } - private string Basename { get; } +// private string Basename { get; } - private SongInfo SongInfo { get; } +// private SongInfo SongInfo { get; } - private int SkipTo { get; } +// private int SkipTo { get; } - private int MaxFileSize { get; } = 2.MiB(); +// private int MaxFileSize { get; } = 2.MiB(); - private long FileNumber = -1; +// private long FileNumber = -1; - private long NextFileToRead = 0; +// private long NextFileToRead = 0; - public bool BufferingCompleted { get; private set; } = false; +// public bool BufferingCompleted { get; private set; } = false; - private ulong CurrentBufferSize = 0; +// private ulong CurrentBufferSize = 0; - private FileStream CurrentFileStream; - private Logger _log; +// private FileStream CurrentFileStream; +// private Logger _log; - public Task BufferSong(CancellationToken cancelToken) => - Task.Run(async () => - { - Process p = null; - FileStream outStream = null; - try - { - p = Process.Start(new ProcessStartInfo - { - FileName = "ffmpeg", - Arguments = $"-ss {SkipTo} -i {SongInfo.Uri} -f s16le -ar 48000 -vn -ac 2 pipe:1 -loglevel quiet", - UseShellExecute = false, - RedirectStandardOutput = true, - RedirectStandardError = false, - CreateNoWindow = true, - }); +// public Task BufferSong(CancellationToken cancelToken) => +// Task.Run(async () => +// { +// Process p = null; +// FileStream outStream = null; +// try +// { +// p = Process.Start(new ProcessStartInfo +// { +// FileName = "ffmpeg", +// Arguments = $"-ss {SkipTo} -i {SongInfo.Uri} -f s16le -ar 48000 -vn -ac 2 pipe:1 -loglevel quiet", +// UseShellExecute = false, +// RedirectStandardOutput = true, +// RedirectStandardError = false, +// CreateNoWindow = true, +// }); - byte[] buffer = new byte[81920]; - int currentFileSize = 0; - ulong prebufferSize = 100ul.MiB(); +// byte[] buffer = new byte[81920]; +// int currentFileSize = 0; +// ulong prebufferSize = 100ul.MiB(); - outStream = new FileStream(Basename + "-" + ++FileNumber, FileMode.Append, FileAccess.Write, FileShare.Read); - while (!p.HasExited) //Also fix low bandwidth - { - int bytesRead = await p.StandardOutput.BaseStream.ReadAsync(buffer, 0, buffer.Length, cancelToken).ConfigureAwait(false); - if (currentFileSize >= MaxFileSize) - { - try - { - outStream.Dispose(); - } - catch { } - outStream = new FileStream(Basename + "-" + ++FileNumber, FileMode.Append, FileAccess.Write, FileShare.Read); - currentFileSize = bytesRead; - } - else - { - currentFileSize += bytesRead; - } - CurrentBufferSize += Convert.ToUInt64(bytesRead); - await outStream.WriteAsync(buffer, 0, bytesRead, cancelToken).ConfigureAwait(false); - while (CurrentBufferSize > prebufferSize) - await Task.Delay(100, cancelToken); - } - BufferingCompleted = true; - } - catch (System.ComponentModel.Win32Exception) - { - var oldclr = Console.ForegroundColor; - Console.ForegroundColor = ConsoleColor.Red; - Console.WriteLine(@"You have not properly installed or configured FFMPEG. -Please install and configure FFMPEG to play music. -Check the guides for your platform on how to setup ffmpeg correctly: - Windows Guide: https://goo.gl/OjKk8F - Linux Guide: https://goo.gl/ShjCUo"); - Console.ForegroundColor = oldclr; - } - catch (Exception ex) - { - Console.WriteLine($"Buffering stopped: {ex.Message}"); - } - finally - { - if (outStream != null) - outStream.Dispose(); - Console.WriteLine($"Buffering done."); - if (p != null) - { - try - { - p.Kill(); - } - catch { } - p.Dispose(); - } - } - }); +// outStream = new FileStream(Basename + "-" + ++FileNumber, FileMode.Append, FileAccess.Write, FileShare.Read); +// while (!p.HasExited) //Also fix low bandwidth +// { +// int bytesRead = await p.StandardOutput.BaseStream.ReadAsync(buffer, 0, buffer.Length, cancelToken).ConfigureAwait(false); +// if (currentFileSize >= MaxFileSize) +// { +// try +// { +// outStream.Dispose(); +// } +// catch { } +// outStream = new FileStream(Basename + "-" + ++FileNumber, FileMode.Append, FileAccess.Write, FileShare.Read); +// currentFileSize = bytesRead; +// } +// else +// { +// currentFileSize += bytesRead; +// } +// CurrentBufferSize += Convert.ToUInt64(bytesRead); +// await outStream.WriteAsync(buffer, 0, bytesRead, cancelToken).ConfigureAwait(false); +// while (CurrentBufferSize > prebufferSize) +// await Task.Delay(100, cancelToken); +// } +// BufferingCompleted = true; +// } +// catch (System.ComponentModel.Win32Exception) +// { +// var oldclr = Console.ForegroundColor; +// Console.ForegroundColor = ConsoleColor.Red; +// Console.WriteLine(@"You have not properly installed or configured FFMPEG. +//Please install and configure FFMPEG to play music. +//Check the guides for your platform on how to setup ffmpeg correctly: +// Windows Guide: https://goo.gl/OjKk8F +// Linux Guide: https://goo.gl/ShjCUo"); +// Console.ForegroundColor = oldclr; +// } +// catch (Exception ex) +// { +// Console.WriteLine($"Buffering stopped: {ex.Message}"); +// } +// finally +// { +// if (outStream != null) +// outStream.Dispose(); +// Console.WriteLine($"Buffering done."); +// if (p != null) +// { +// try +// { +// p.Kill(); +// } +// catch { } +// p.Dispose(); +// } +// } +// }); - /// - /// Return the next file to read, and delete the old one - /// - /// Name of the file to read - private string GetNextFile() - { - string filename = Basename + "-" + NextFileToRead; +// /// +// /// Return the next file to read, and delete the old one +// /// +// /// Name of the file to read +// private string GetNextFile() +// { +// string filename = Basename + "-" + NextFileToRead; - if (NextFileToRead != 0) - { - try - { - CurrentBufferSize -= Convert.ToUInt64(new FileInfo(Basename + "-" + (NextFileToRead - 1)).Length); - File.Delete(Basename + "-" + (NextFileToRead - 1)); - } - catch { } - } - NextFileToRead++; - return filename; - } +// if (NextFileToRead != 0) +// { +// try +// { +// CurrentBufferSize -= Convert.ToUInt64(new FileInfo(Basename + "-" + (NextFileToRead - 1)).Length); +// File.Delete(Basename + "-" + (NextFileToRead - 1)); +// } +// catch { } +// } +// NextFileToRead++; +// return filename; +// } - private bool IsNextFileReady() - { - return NextFileToRead <= FileNumber; - } +// private bool IsNextFileReady() +// { +// return NextFileToRead <= FileNumber; +// } - private void CleanFiles() - { - for (long i = NextFileToRead - 1; i <= FileNumber; i++) - { - try - { - File.Delete(Basename + "-" + i); - } - catch { } - } - } +// private void CleanFiles() +// { +// for (long i = NextFileToRead - 1; i <= FileNumber; i++) +// { +// try +// { +// File.Delete(Basename + "-" + i); +// } +// catch { } +// } +// } - //Stream part +// //Stream part - public override bool CanRead => true; +// public override bool CanRead => true; - public override bool CanSeek => false; +// public override bool CanSeek => false; - public override bool CanWrite => false; +// public override bool CanWrite => false; - public override long Length => (long)CurrentBufferSize; +// public override long Length => (long)CurrentBufferSize; - public override long Position { get; set; } = 0; +// public override long Position { get; set; } = 0; - public override void Flush() { } +// public override void Flush() { } - public override int Read(byte[] buffer, int offset, int count) - { - int read = CurrentFileStream.Read(buffer, offset, count); - if (read < count) - { - if (!BufferingCompleted || IsNextFileReady()) - { - CurrentFileStream.Dispose(); - CurrentFileStream = new FileStream(GetNextFile(), FileMode.OpenOrCreate, FileAccess.Read, FileShare.Write); - read += CurrentFileStream.Read(buffer, read + offset, count - read); - } - if (read < count) - Array.Clear(buffer, read, count - read); - } - return read; - } +// public override int Read(byte[] buffer, int offset, int count) +// { +// int read = CurrentFileStream.Read(buffer, offset, count); +// if (read < count) +// { +// if (!BufferingCompleted || IsNextFileReady()) +// { +// CurrentFileStream.Dispose(); +// CurrentFileStream = new FileStream(GetNextFile(), FileMode.OpenOrCreate, FileAccess.Read, FileShare.Write); +// read += CurrentFileStream.Read(buffer, read + offset, count - read); +// } +// if (read < count) +// Array.Clear(buffer, read, count - read); +// } +// return read; +// } - public override long Seek(long offset, SeekOrigin origin) - { - throw new NotImplementedException(); - } +// public override long Seek(long offset, SeekOrigin origin) +// { +// throw new NotImplementedException(); +// } - public override void SetLength(long value) - { - throw new NotImplementedException(); - } +// public override void SetLength(long value) +// { +// throw new NotImplementedException(); +// } - public override void Write(byte[] buffer, int offset, int count) - { - throw new NotImplementedException(); - } +// public override void Write(byte[] buffer, int offset, int count) +// { +// throw new NotImplementedException(); +// } - public new void Dispose() - { - CurrentFileStream.Dispose(); - MusicPlayer.SongCancelSource.Cancel(); - CleanFiles(); - base.Dispose(); - } - } -} \ No newline at end of file +// public new void Dispose() +// { +// CurrentFileStream.Dispose(); +// MusicPlayer.SongCancelSource.Cancel(); +// CleanFiles(); +// base.Dispose(); +// } +// } +//} \ No newline at end of file diff --git a/src/NadekoBot/Services/Music/SongInfo.cs b/src/NadekoBot/Services/Music/SongInfo.cs new file mode 100644 index 00000000..4ad63b4a --- /dev/null +++ b/src/NadekoBot/Services/Music/SongInfo.cs @@ -0,0 +1,85 @@ +using Discord; +using NadekoBot.Extensions; +using NadekoBot.Services.Database.Models; +using System; +using System.Net; +using System.Text.RegularExpressions; + +namespace NadekoBot.Services.Music +{ + public class SongInfo + { + public string Provider { get; set; } + public MusicType ProviderType { get; set; } + public string Query { get; set; } + public string Title { get; set; } + public string Uri { get; set; } + public string AlbumArt { get; set; } + public string QueuerName { get; set; } + public TimeSpan TotalTime = TimeSpan.Zero; + + public string PrettyProvider => (Provider ?? "???"); + //public string PrettyFullTime => PrettyCurrentTime + " / " + PrettyTotalTime; + public string PrettyName => $"**[{Title.TrimTo(65)}]({SongUrl})**"; + public string PrettyInfo => $"{PrettyTotalTime} | {PrettyProvider} | {QueuerName}"; + public string PrettyFullName => $"{PrettyName}\n\t\t`{PrettyTotalTime} | {PrettyProvider} | {Format.Sanitize(QueuerName.TrimTo(15))}`"; + public string PrettyTotalTime + { + get + { + if (TotalTime == TimeSpan.Zero) + return "(?)"; + if (TotalTime == TimeSpan.MaxValue) + return "∞"; + var time = TotalTime.ToString(@"mm\:ss"); + var hrs = (int)TotalTime.TotalHours; + + if (hrs > 0) + return hrs + ":" + time; + return time; + } + } + + public string SongUrl + { + get + { + switch (ProviderType) + { + case MusicType.Normal: + return Query; + case MusicType.Soundcloud: + return Query; + case MusicType.Local: + return $"https://google.com/search?q={ WebUtility.UrlEncode(Title).Replace(' ', '+') }"; + case MusicType.Radio: + return $"https://google.com/search?q={Title}"; + default: + return ""; + } + } + } + private readonly Regex videoIdRegex = new Regex("<=v=[a-zA-Z0-9-]+(?=&)|(?<=[0-9])[^&\n]+|(?<=v=)[^&\n]+", RegexOptions.Compiled); + public string Thumbnail + { + get + { + switch (ProviderType) + { + case MusicType.Radio: + return "https://cdn.discordapp.com/attachments/155726317222887425/261850925063340032/1482522097_radio.png"; //test links + case MusicType.Normal: + //todo have videoid in songinfo from the start + var videoId = videoIdRegex.Match(Query); + return $"https://img.youtube.com/vi/{ videoId }/0.jpg"; + case MusicType.Local: + return "https://cdn.discordapp.com/attachments/155726317222887425/261850914783100928/1482522077_music.png"; //test links + case MusicType.Soundcloud: + return AlbumArt; + default: + return ""; + } + } + } + } +} diff --git a/src/NadekoBot/Services/Music/SongResolver.cs b/src/NadekoBot/Services/Music/SongResolver.cs new file mode 100644 index 00000000..06fe5194 --- /dev/null +++ b/src/NadekoBot/Services/Music/SongResolver.cs @@ -0,0 +1,111 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace NadekoBot.Services.Music +{ + public class SongResolver + { + // public async Task ResolveSong(string query, MusicType musicType = MusicType.Normal) + // { + // if (string.IsNullOrWhiteSpace(query)) + // throw new ArgumentNullException(nameof(query)); + + // if (musicType != MusicType.Local && IsRadioLink(query)) + // { + // musicType = MusicType.Radio; + // query = await HandleStreamContainers(query).ConfigureAwait(false) ?? query; + // } + + // try + // { + // switch (musicType) + // { + // case MusicType.Local: + // return new Song(new SongInfo + // { + // Uri = "\"" + Path.GetFullPath(query) + "\"", + // Title = Path.GetFileNameWithoutExtension(query), + // Provider = "Local File", + // ProviderType = musicType, + // Query = query, + // }); + // case MusicType.Radio: + // return new Song(new SongInfo + // { + // Uri = query, + // Title = $"{query}", + // Provider = "Radio Stream", + // ProviderType = musicType, + // Query = query + // }) + // { TotalTime = TimeSpan.MaxValue }; + // } + // if (_sc.IsSoundCloudLink(query)) + // { + // var svideo = await _sc.ResolveVideoAsync(query).ConfigureAwait(false); + // return new Song(new SongInfo + // { + // Title = svideo.FullName, + // Provider = "SoundCloud", + // Uri = await svideo.StreamLink(), + // ProviderType = musicType, + // Query = svideo.TrackLink, + // AlbumArt = svideo.artwork_url, + // }) + // { TotalTime = TimeSpan.FromMilliseconds(svideo.Duration) }; + // } + + // if (musicType == MusicType.Soundcloud) + // { + // var svideo = await _sc.GetVideoByQueryAsync(query).ConfigureAwait(false); + // return new Song(new SongInfo + // { + // Title = svideo.FullName, + // Provider = "SoundCloud", + // Uri = await svideo.StreamLink(), + // ProviderType = MusicType.Soundcloud, + // Query = svideo.TrackLink, + // AlbumArt = svideo.artwork_url, + // }) + // { TotalTime = TimeSpan.FromMilliseconds(svideo.Duration) }; + // } + + // var link = (await _google.GetVideoLinksByKeywordAsync(query).ConfigureAwait(false)).FirstOrDefault(); + // if (string.IsNullOrWhiteSpace(link)) + // throw new OperationCanceledException("Not a valid youtube query."); + // var allVideos = await Task.Run(async () => { try { return await YouTube.Default.GetAllVideosAsync(link).ConfigureAwait(false); } catch { return Enumerable.Empty(); } }).ConfigureAwait(false); + // var videos = allVideos.Where(v => v.AdaptiveKind == AdaptiveKind.Audio); + // var video = videos + // .Where(v => v.AudioBitrate < 256) + // .OrderByDescending(v => v.AudioBitrate) + // .FirstOrDefault(); + + // if (video == null) // do something with this error + // throw new Exception("Could not load any video elements based on the query."); + // var m = Regex.Match(query, @"\?t=(?\d*)"); + // int gotoTime = 0; + // if (m.Captures.Count > 0) + // int.TryParse(m.Groups["t"].ToString(), out gotoTime); + // var song = new Song(new SongInfo + // { + // Title = video.Title.Substring(0, video.Title.Length - 10), // removing trailing "- You Tube" + // Provider = "YouTube", + // Uri = await video.GetUriAsync().ConfigureAwait(false), + // Query = link, + // ProviderType = musicType, + // }); + // song.SkipTo = gotoTime; + // return song; + // } + // catch (Exception ex) + // { + // _log.Warn($"Failed resolving the link.{ex.Message}"); + // _log.Warn(ex); + // return null; + // } + // } + } +} From f8ad6dda507ea0b7d173a218ec226707d450521c Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Sat, 1 Jul 2017 08:16:06 +0200 Subject: [PATCH 054/346] small changes --- src/NadekoBot/Modules/Music/Music.cs | 5 ++-- src/NadekoBot/Services/Music/MusicPlayer.cs | 27 +++++++++++++------- src/NadekoBot/Services/Music/MusicQueue.cs | 5 +++- src/NadekoBot/Services/Music/MusicService.cs | 6 ++--- 4 files changed, 26 insertions(+), 17 deletions(-) diff --git a/src/NadekoBot/Modules/Music/Music.cs b/src/NadekoBot/Modules/Music/Music.cs index 472acd17..a8b7ae0a 100644 --- a/src/NadekoBot/Modules/Music/Music.cs +++ b/src/NadekoBot/Modules/Music/Music.cs @@ -253,10 +253,9 @@ namespace NadekoBot.Modules.Music [NadekoCommand, Usage, Description, Aliases] [RequireContext(ContextType.Guild)] - public Task Destroy() + public async Task Destroy() { - _music.DestroyPlayer(Context.Guild.Id); - return Task.CompletedTask; + await _music.DestroyPlayer(Context.Guild.Id); } [NadekoCommand, Usage, Description, Aliases] diff --git a/src/NadekoBot/Services/Music/MusicPlayer.cs b/src/NadekoBot/Services/Music/MusicPlayer.cs index 64c3aa22..b4bc4009 100644 --- a/src/NadekoBot/Services/Music/MusicPlayer.cs +++ b/src/NadekoBot/Services/Music/MusicPlayer.cs @@ -15,7 +15,7 @@ namespace NadekoBot.Services.Music Playing, Completed } - public class MusicPlayer : IDisposable + public class MusicPlayer { private readonly Task _player; private readonly IVoiceChannel VoiceChannel; @@ -92,7 +92,12 @@ namespace NadekoBot.Services.Music }); var ac = await GetAudioClient(); if (ac == null) + { + await Task.Delay(900); + // just wait some time, maybe bot doesn't even have perms to join that voice channel, + // i don't want to spam connection attempts continue; + } var pcm = ac.CreatePCMStream(AudioApplication.Music); OnStarted?.Invoke(this, data.Song); @@ -175,8 +180,8 @@ namespace NadekoBot.Services.Music { Stopped = false; Unpause(); + CancelCurrentSong(); } - CancelCurrentSong(); } public void Stop(bool clearQueue = false) @@ -188,8 +193,8 @@ namespace NadekoBot.Services.Music if (clearQueue) Queue.Clear(); Unpause(); + CancelCurrentSong(); } - CancelCurrentSong(); } private void Unpause() @@ -283,18 +288,22 @@ namespace NadekoBot.Services.Music } } - public void Dispose() + public async Task Destroy() { - _log.Info("Disposing"); + _log.Info("Destroying"); lock (locker) { + Stop(); Exited = true; Unpause(); + + OnCompleted = null; + OnPauseChanged = null; + OnStarted = null; } - CancelCurrentSong(); - OnCompleted = null; - OnPauseChanged = null; - OnStarted = null; + var ac = _audioClient; + if (ac != null) + await ac.StopAsync(); } diff --git a/src/NadekoBot/Services/Music/MusicQueue.cs b/src/NadekoBot/Services/Music/MusicQueue.cs index 50219dc8..cf3a0818 100644 --- a/src/NadekoBot/Services/Music/MusicQueue.cs +++ b/src/NadekoBot/Services/Music/MusicQueue.cs @@ -113,7 +113,10 @@ namespace NadekoBot.Services.Music public void ResetCurrent() { - CurrentIndex = 0; + lock (locker) + { + CurrentIndex = 0; + } } } } diff --git a/src/NadekoBot/Services/Music/MusicService.cs b/src/NadekoBot/Services/Music/MusicService.cs index eb2a5016..79d787fc 100644 --- a/src/NadekoBot/Services/Music/MusicService.cs +++ b/src/NadekoBot/Services/Music/MusicService.cs @@ -5,11 +5,9 @@ using System.Threading.Tasks; using Discord; using NadekoBot.Extensions; using NadekoBot.Services.Database.Models; -using System.Text.RegularExpressions; using NLog; using System.IO; using VideoLibrary; -using System.Net.Http; using System.Collections.Generic; using Discord.Commands; @@ -235,10 +233,10 @@ namespace NadekoBot.Services.Music return song; } - public void DestroyPlayer(ulong id) + public async Task DestroyPlayer(ulong id) { if (MusicPlayers.TryRemove(id, out var mp)) - mp.Dispose(); + await mp.Destroy(); } // public async Task QueueSong(IGuildUser queuer, ITextChannel textCh, IVoiceChannel voiceCh, string query, bool silent = false, MusicType musicType = MusicType.Normal) From 9889baf8bd977f9b7c4f8b34a505ba86750075e6 Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Sat, 1 Jul 2017 17:16:03 +0200 Subject: [PATCH 055/346] Poopy buffer is back ^_^ Music lag fixes... --- .../DataStructures/PoopyRingBuffer.cs | 145 ++++++++++++++++++ src/NadekoBot/Modules/Music/Music.cs | 1 + src/NadekoBot/Services/Music/MusicPlayer.cs | 103 +++++++------ src/NadekoBot/Services/Music/MusicService.cs | 5 - src/NadekoBot/Services/Music/SongBuffer.cs | 83 +++++++++- 5 files changed, 277 insertions(+), 60 deletions(-) create mode 100644 src/NadekoBot/DataStructures/PoopyRingBuffer.cs diff --git a/src/NadekoBot/DataStructures/PoopyRingBuffer.cs b/src/NadekoBot/DataStructures/PoopyRingBuffer.cs new file mode 100644 index 00000000..4f9db9be --- /dev/null +++ b/src/NadekoBot/DataStructures/PoopyRingBuffer.cs @@ -0,0 +1,145 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace NadekoBot.DataStructures +{ + public class PoopyRingBuffer : IDisposable + { + // readpos == writepos means empty + // writepos == readpos - 1 means full + + private readonly byte[] buffer; + private readonly object posLock = new object(); + public int Capacity { get; } + + private volatile int _readPos = 0; + private int ReadPos + { + get => _readPos; + set { lock (posLock) _readPos = value; } + } + private volatile int _writePos = 0; + private int WritePos + { + get => _writePos; + set { lock (posLock) _writePos = value; } + } + private int Length + { + get + { + lock (posLock) + { + return ReadPos <= WritePos ? + WritePos - ReadPos : + Capacity - (ReadPos - WritePos); + } + } + } + + public int RemainingCapacity + { + get { lock (posLock) return Capacity - Length - 1; } + } + + private readonly SemaphoreSlim _locker = new SemaphoreSlim(1, 1); + + public PoopyRingBuffer(int capacity = 38400) + { + this.Capacity = capacity + 1; + this.buffer = new byte[this.Capacity]; + } + + public Task ReadAsync(byte[] b, int offset, int toRead, CancellationToken cancelToken) => Task.Run(async () => + { + await _locker.WaitAsync(cancelToken); + try + { + Console.WriteLine("Reading {0}", toRead); + if (WritePos == ReadPos) + return 0; + + if (toRead > Length) + toRead = Length; + + if (WritePos > ReadPos) + { + Buffer.BlockCopy(buffer, ReadPos, b, offset, toRead); + ReadPos += toRead; + } + else + { + var toEnd = Capacity - ReadPos; + var firstRead = toRead > toEnd ? + toEnd : + toRead; + Buffer.BlockCopy(buffer, ReadPos, b, offset, firstRead); + ReadPos += firstRead; + var secondRead = toRead - firstRead; + if (secondRead > 0) + { + Buffer.BlockCopy(buffer, 0, b, offset + firstRead, secondRead); + ReadPos = secondRead; + } + } + Console.WriteLine("Readpos: {0} WritePos: {1}", ReadPos, WritePos); + return toRead; + } + finally + { + _locker.Release(); + } + }); + + public Task WriteAsync(byte[] b, int offset, int toWrite, CancellationToken cancelToken) => Task.Run(async () => + { + while (toWrite > RemainingCapacity) + await Task.Delay(100, cancelToken); + + await _locker.WaitAsync(cancelToken); + try + { + Console.WriteLine("Writing {0}", toWrite); + if (WritePos < ReadPos) + { + Buffer.BlockCopy(b, offset, buffer, WritePos, toWrite); + WritePos += toWrite; + } + else + { + var toEnd = Capacity - WritePos; + var firstWrite = toWrite > toEnd ? + toEnd : + toWrite; + Buffer.BlockCopy(b, offset, buffer, WritePos, firstWrite); + var secondWrite = toWrite - firstWrite; + if (secondWrite > 0) + { + Buffer.BlockCopy(b, offset + firstWrite, buffer, 0, secondWrite); + WritePos = secondWrite; + } + else + { + WritePos += firstWrite; + if (WritePos == Capacity) + WritePos = 0; + } + } + Console.WriteLine("Readpos: {0} WritePos: {1}", ReadPos, WritePos); + return toWrite; + } + finally + { + _locker.Release(); + } + }); + + public void Dispose() + { + } + } +} diff --git a/src/NadekoBot/Modules/Music/Music.cs b/src/NadekoBot/Modules/Music/Music.cs index a8b7ae0a..3681d631 100644 --- a/src/NadekoBot/Modules/Music/Music.cs +++ b/src/NadekoBot/Modules/Music/Music.cs @@ -451,6 +451,7 @@ namespace NadekoBot.Modules.Music { try { + await Task.Yield(); //todo fix for all if (item.ProviderType == MusicType.Normal) await Task.WhenAll(Task.Delay(1000), InternalQueue(mp, await _music.ResolveSong(item.Query, Context.User.ToString(), item.ProviderType), true)).ConfigureAwait(false); diff --git a/src/NadekoBot/Services/Music/MusicPlayer.cs b/src/NadekoBot/Services/Music/MusicPlayer.cs index b4bc4009..97581b2f 100644 --- a/src/NadekoBot/Services/Music/MusicPlayer.cs +++ b/src/NadekoBot/Services/Music/MusicPlayer.cs @@ -81,59 +81,58 @@ namespace NadekoBot.Services.Music _log.Info("Starting"); - var p = Process.Start(new ProcessStartInfo + using (var b = new SongBuffer(data.Song.Uri, "")) { - FileName = "ffmpeg", - Arguments = $"-i {data.Song.Uri} -f s16le -ar 48000 -vn -ac 2 pipe:1 -loglevel quiet", - UseShellExecute = false, - RedirectStandardOutput = true, - RedirectStandardError = false, - CreateNoWindow = true, - }); - var ac = await GetAudioClient(); - if (ac == null) - { - await Task.Delay(900); - // just wait some time, maybe bot doesn't even have perms to join that voice channel, - // i don't want to spam connection attempts - continue; - } - var pcm = ac.CreatePCMStream(AudioApplication.Music); + var bufferSuccess = await b.StartBuffering(cancelToken); - OnStarted?.Invoke(this, data.Song); + if (bufferSuccess == false) + continue; - byte[] buffer = new byte[3840]; - int bytesRead = 0; - try - { - while ((bytesRead = await p.StandardOutput.BaseStream.ReadAsync(buffer, 0, buffer.Length, cancelToken).ConfigureAwait(false)) > 0) + var ac = await GetAudioClient(); + if (ac == null) { - var vol = Volume; - if (vol != 1) - AdjustVolume(buffer, vol); - await pcm.WriteAsync(buffer, 0, bytesRead, cancelToken); - - await (pauseTaskSource?.Task ?? Task.CompletedTask); + await Task.Delay(900); + // just wait some time, maybe bot doesn't even have perms to join that voice channel, + // i don't want to spam connection attempts + continue; } - } - catch (OperationCanceledException) - { - _log.Info("Song Canceled"); - } - catch (Exception ex) - { - _log.Warn(ex); - } - finally - { - //flush is known to get stuck from time to time, just cancel it if it takes more than 1 second - var flushCancel = new CancellationTokenSource(); - var flushToken = flushCancel.Token; - var flushDelay = Task.Delay(1000, flushToken); - await Task.WhenAny(flushDelay, pcm.FlushAsync(flushToken)); - flushCancel.Cancel(); + var pcm = ac.CreatePCMStream(AudioApplication.Music); - OnCompleted?.Invoke(this, data.Song); + OnStarted?.Invoke(this, data.Song); + + byte[] buffer = new byte[3840]; + int bytesRead = 0; + try + { + while ((bytesRead = await b.ReadAsync(buffer, 0, buffer.Length, cancelToken).ConfigureAwait(false)) > 0) + { + var vol = Volume; + if (vol != 1) + AdjustVolume(buffer, vol); + await Task.WhenAll(Task.Delay(10), pcm.WriteAsync(buffer, 0, bytesRead, cancelToken)).ConfigureAwait(false); + + await (pauseTaskSource?.Task ?? Task.CompletedTask); + } + } + catch (OperationCanceledException) + { + _log.Info("Song Canceled"); + } + catch (Exception ex) + { + _log.Warn(ex); + } + finally + { + //flush is known to get stuck from time to time, just cancel it if it takes more than 1 second + var flushCancel = new CancellationTokenSource(); + var flushToken = flushCancel.Token; + var flushDelay = Task.Delay(1000, flushToken); + await Task.WhenAny(flushDelay, pcm.FlushAsync(flushToken)); + flushCancel.Cancel(); + + OnCompleted?.Invoke(this, data.Song); + } } } finally @@ -141,7 +140,7 @@ namespace NadekoBot.Services.Music _log.Info("Next song"); do { - await Task.Delay(100); + await Task.Delay(500); } while (Stopped && !Exited); if(!RepeatCurrentSong) @@ -158,6 +157,14 @@ namespace NadekoBot.Services.Music reconnect) try { + try + { + await _audioClient?.StopAsync(); + _audioClient?.Dispose(); + } + catch + { + } _audioClient = await VoiceChannel.ConnectAsync(); } catch diff --git a/src/NadekoBot/Services/Music/MusicService.cs b/src/NadekoBot/Services/Music/MusicService.cs index 79d787fc..986fd0fd 100644 --- a/src/NadekoBot/Services/Music/MusicService.cs +++ b/src/NadekoBot/Services/Music/MusicService.cs @@ -47,11 +47,6 @@ namespace NadekoBot.Services.Music Directory.CreateDirectory(MusicDataPath); } - // public MusicPlayer GetPlayer(ulong guildId) - // { - // MusicPlayers.TryGetValue(guildId, out var player); - // return player; - // } public float GetDefaultVolume(ulong guildId) { return _defaultVolumes.GetOrAdd(guildId, (id) => diff --git a/src/NadekoBot/Services/Music/SongBuffer.cs b/src/NadekoBot/Services/Music/SongBuffer.cs index f11c2cce..c5c08175 100644 --- a/src/NadekoBot/Services/Music/SongBuffer.cs +++ b/src/NadekoBot/Services/Music/SongBuffer.cs @@ -1,10 +1,79 @@ -//using NadekoBot.Extensions; -//using NLog; -//using System; -//using System.Diagnostics; -//using System.IO; -//using System.Threading; -//using System.Threading.Tasks; +using NadekoBot.DataStructures; +using System; +using System.Diagnostics; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace NadekoBot.Services.Music +{ + public class SongBuffer : IDisposable + { + const int maxReadSize = 3840; + private Process p; + private PoopyRingBuffer _outStream = new PoopyRingBuffer(); + + private readonly SemaphoreSlim _lock = new SemaphoreSlim(1, 1); + + public string SongUri { get; private set; } + + public SongBuffer(string songUri, string skipTo) + { + this.SongUri = songUri; + + this.p = Process.Start(new ProcessStartInfo + { + FileName = "ffmpeg", + Arguments = $"-i {songUri} -f s16le -ar 48000 -vn -ac 2 pipe:1 -loglevel quiet", + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = false, + CreateNoWindow = true, + }); + } + + public Task StartBuffering(CancellationToken cancelToken) + { + var toReturn = new TaskCompletionSource(); + var _ = Task.Run(async () => + { + try + { + byte[] buffer = new byte[3840]; + while (!this.p.HasExited || cancelToken.IsCancellationRequested) + { + int bytesRead = await p.StandardOutput.BaseStream.ReadAsync(buffer, 0, buffer.Length, cancelToken).ConfigureAwait(false); + + await _outStream.WriteAsync(buffer, 0, bytesRead, cancelToken); + + if (_outStream.RemainingCapacity < _outStream.Capacity * 0.9f) + toReturn.TrySetResult(true); + } + } + catch + { + toReturn.TrySetResult(false); + //ignored + } + }, cancelToken); + + return toReturn.Task; + } + + public Task ReadAsync(byte[] b, int offset, int toRead, CancellationToken cancelToken) + { + return _outStream.ReadAsync(b, offset, toRead, cancelToken); + } + + public void Dispose() + { + try { this.p.Kill(); } + catch { } + _outStream.Dispose(); + this.p.Dispose(); + } + } +} //namespace NadekoBot.Services.Music //{ From 3731994061391e4caf134e427e5b5da20813b2d2 Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Sat, 1 Jul 2017 21:22:11 +0200 Subject: [PATCH 056/346] Lot more work, fixes, addition, untested new implementations... --- .../DataStructures/PoopyRingBuffer.cs | 2 +- src/NadekoBot/Modules/Music/Music.cs | 139 +++++++++--------- src/NadekoBot/NadekoBot.cs | 2 +- src/NadekoBot/Services/Music/MusicPlayer.cs | 136 ++++++++++++----- src/NadekoBot/Services/Music/MusicQueue.cs | 30 +++- src/NadekoBot/Services/Music/MusicService.cs | 24 ++- src/NadekoBot/Services/Music/SongBuffer.cs | 2 +- src/NadekoBot/Services/Music/SongInfo.cs | 1 + .../_strings/ResponseStrings.en-US.json | 4 +- 9 files changed, 229 insertions(+), 111 deletions(-) diff --git a/src/NadekoBot/DataStructures/PoopyRingBuffer.cs b/src/NadekoBot/DataStructures/PoopyRingBuffer.cs index 4f9db9be..4c8764db 100644 --- a/src/NadekoBot/DataStructures/PoopyRingBuffer.cs +++ b/src/NadekoBot/DataStructures/PoopyRingBuffer.cs @@ -48,7 +48,7 @@ namespace NadekoBot.DataStructures private readonly SemaphoreSlim _locker = new SemaphoreSlim(1, 1); - public PoopyRingBuffer(int capacity = 38400) + public PoopyRingBuffer(int capacity = 3640 * 200) { this.Capacity = capacity + 1; this.buffer = new byte[this.Capacity]; diff --git a/src/NadekoBot/Modules/Music/Music.cs b/src/NadekoBot/Modules/Music/Music.cs index 3681d631..c82e59cc 100644 --- a/src/NadekoBot/Modules/Music/Music.cs +++ b/src/NadekoBot/Modules/Music/Music.cs @@ -111,11 +111,15 @@ namespace NadekoBot.Modules.Music } } } + //todo add play command. .play = .n, .play whatever = .q whatever + + [NadekoCommand, Usage, Description, Aliases] [RequireContext(ContextType.Guild)] public async Task Queue([Remainder] string query) { + //todo add a notice that player is stopped if user queues a song while it is var mp = await _music.GetOrCreatePlayer(Context); var songInfo = await _music.ResolveSong(query, Context.User.ToString()); await InternalQueue(mp, songInfo, false); @@ -209,11 +213,9 @@ namespace NadekoBot.Modules.Music if (mp.RepeatCurrentSong) desc = "🔂 " + GetText("repeating_cur_song") + "\n\n" + desc; - //else if (musicPlayer.RepeatPlaylist) - // desc = "🔁 " + GetText("repeating_playlist") + "\n\n" + desc; - - - + else if (mp.Shuffle) + desc = "🔀 " + GetText("shuffling_playlist") + "\n\n" + desc; + var embed = new EmbedBuilder() .WithAuthor(eab => eab.WithName(GetText("player_queue", curPage + 1, lastPage + 1)) .WithMusicIcon()) @@ -307,7 +309,7 @@ namespace NadekoBot.Modules.Music { var song = mp.RemoveAt(index - 1); var embed = new EmbedBuilder() - .WithAuthor(eab => eab.WithName(GetText("removed_song") + " #" + (index + 1)).WithMusicIcon()) + .WithAuthor(eab => eab.WithName(GetText("removed_song") + " #" + (index)).WithMusicIcon()) .WithDescription(song.PrettyName) .WithFooter(ef => ef.WithText(song.PrettyInfo)) .WithErrorColor(); @@ -519,22 +521,18 @@ namespace NadekoBot.Modules.Music await Context.Channel.EmbedAsync(embed).ConfigureAwait(false); } - - //[NadekoCommand, Usage, Description, Aliases] - //[RequireContext(ContextType.Guild)] - //public async Task ShufflePlaylist() - //{ - // MusicPlayer musicPlayer; - // if ((musicPlayer = _music.GetPlayer(Context.Guild.Id)) == null) - // return; - // if (((IGuildUser)Context.User).VoiceChannel != musicPlayer.PlaybackVoiceChannel) - // return; - // if (musicPlayer.Playlist.Count < 2) - // return; - - // musicPlayer.Shuffle(); - // await ReplyConfirmLocalized("songs_shuffled").ConfigureAwait(false); - //} + //todo test shuffle + [NadekoCommand, Usage, Description, Aliases] + [RequireContext(ContextType.Guild)] + public async Task ShufflePlaylist() + { + var mp = await _music.GetOrCreatePlayer(Context); + var val = mp.ToggleShuffle(); + if(val) + await ReplyConfirmLocalized("songs_shuffle_enable").ConfigureAwait(false); + else + await ReplyConfirmLocalized("songs_shuffle_disable").ConfigureAwait(false); + } //[NadekoCommand, Usage, Description, Aliases] //[RequireContext(ContextType.Guild)] @@ -687,17 +685,22 @@ namespace NadekoBot.Modules.Music //} - //[NadekoCommand, Usage, Description, Aliases] - //[RequireContext(ContextType.Guild)] - //public async Task Move() - //{ + [NadekoCommand, Usage, Description, Aliases] + [RequireContext(ContextType.Guild)] + public async Task Move() + { + var vch = ((IGuildUser)Context.User).VoiceChannel; - // MusicPlayer musicPlayer; - // var voiceChannel = ((IGuildUser)Context.User).VoiceChannel; - // if (voiceChannel == null || voiceChannel.Guild != Context.Guild || !MusicPlayers.TryGetValue(Context.Guild.Id, out musicPlayer)) - // return; - // await musicPlayer.MoveToVoiceChannel(voiceChannel); - //} + if (vch == null) + return; + + var mp = _music.GetPlayerOrDefault(Context.Guild.Id); + + if (mp == null) + return; + //todo test move + mp.SetVoiceChannel(vch); + } //[NadekoCommand, Usage, Description, Aliases] //[RequireContext(ContextType.Guild)] @@ -745,21 +748,21 @@ namespace NadekoBot.Modules.Music //} - //[NadekoCommand, Usage, Description, Aliases] - //[RequireContext(ContextType.Guild)] - //public async Task SetMaxQueue(uint size = 0) - //{ - // MusicPlayer musicPlayer; - // if ((musicPlayer = _music.GetPlayer(Context.Guild.Id)) == null) - // return; + [NadekoCommand, Usage, Description, Aliases] + [RequireContext(ContextType.Guild)] + public async Task SetMaxQueue(uint size = 0) + { + if (size < 0) + return; + var mp = await _music.GetOrCreatePlayer(Context); - // musicPlayer.MaxQueueSize = size; + mp.SetMaxQueueSize(size); - // if(size == 0) - // await ReplyConfirmLocalized("max_queue_unlimited").ConfigureAwait(false); - // else - // await ReplyConfirmLocalized("max_queue_x", size).ConfigureAwait(false); - //} + if (size == 0) + await ReplyConfirmLocalized("max_queue_unlimited").ConfigureAwait(false); + else + await ReplyConfirmLocalized("max_queue_x", size).ConfigureAwait(false); + } //[NadekoCommand, Usage, Description, Aliases] //[RequireContext(ContextType.Guild)] @@ -800,19 +803,17 @@ namespace NadekoBot.Modules.Music .ConfigureAwait(false); } - //[NadekoCommand, Usage, Description, Aliases] - //[RequireContext(ContextType.Guild)] - //public async Task RepeatPl() - //{ - // MusicPlayer musicPlayer; - // if ((musicPlayer = _music.GetPlayer(Context.Guild.Id)) == null) - // return; - // var currentValue = musicPlayer.ToggleRepeatPlaylist(); - // if(currentValue) - // await ReplyConfirmLocalized("rpl_enabled").ConfigureAwait(false); - // else - // await ReplyConfirmLocalized("rpl_disabled").ConfigureAwait(false); - //} + [NadekoCommand, Usage, Description, Aliases] + [RequireContext(ContextType.Guild)] + public async Task RepeatPl() + { + var mp = await _music.GetOrCreatePlayer(Context); + var currentValue = mp.ToggleRepeatPlaylist(); + if (currentValue) + await ReplyConfirmLocalized("rpl_enabled").ConfigureAwait(false); + else + await ReplyConfirmLocalized("rpl_disabled").ConfigureAwait(false); + } //[NadekoCommand, Usage, Description, Aliases] //[RequireContext(ContextType.Guild)] @@ -849,19 +850,17 @@ namespace NadekoBot.Modules.Music // await ReplyConfirmLocalized("skipped_to", minutes, seconds).ConfigureAwait(false); //} - //[NadekoCommand, Usage, Description, Aliases] - //[RequireContext(ContextType.Guild)] - //public async Task Autoplay() - //{ - // MusicPlayer musicPlayer; - // if ((musicPlayer = _music.GetPlayer(Context.Guild.Id)) == null) - // return; + [NadekoCommand, Usage, Description, Aliases] + [RequireContext(ContextType.Guild)] + public async Task Autoplay() + { + var mp = await _music.GetOrCreatePlayer(Context); - // if (!musicPlayer.ToggleAutoplay()) - // await ReplyConfirmLocalized("autoplay_disabled").ConfigureAwait(false); - // else - // await ReplyConfirmLocalized("autoplay_enabled").ConfigureAwait(false); - //} + if (!mp.ToggleAutoplay()) + await ReplyConfirmLocalized("autoplay_disabled").ConfigureAwait(false); + else + await ReplyConfirmLocalized("autoplay_enabled").ConfigureAwait(false); + } [NadekoCommand, Usage, Description, Aliases] [RequireContext(ContextType.Guild)] diff --git a/src/NadekoBot/NadekoBot.cs b/src/NadekoBot/NadekoBot.cs index 200487b2..d75cd963 100644 --- a/src/NadekoBot/NadekoBot.cs +++ b/src/NadekoBot/NadekoBot.cs @@ -183,7 +183,7 @@ namespace NadekoBot #endregion var clashService = new ClashOfClansService(Client, Db, Localization, Strings, uow, startingGuildIdList); - var musicService = new MusicService(GoogleApi, Strings, Localization, Db, soundcloudApiService, Credentials, AllGuildConfigs); + var musicService = new MusicService(Client, GoogleApi, Strings, Localization, Db, soundcloudApiService, Credentials, AllGuildConfigs); var crService = new CustomReactionsService(permissionsService, Db, Strings, Client, CommandHandler, BotConfig, uow); #region Games diff --git a/src/NadekoBot/Services/Music/MusicPlayer.cs b/src/NadekoBot/Services/Music/MusicPlayer.cs index 97581b2f..d6cac926 100644 --- a/src/NadekoBot/Services/Music/MusicPlayer.cs +++ b/src/NadekoBot/Services/Music/MusicPlayer.cs @@ -18,7 +18,7 @@ namespace NadekoBot.Services.Music public class MusicPlayer { private readonly Task _player; - private readonly IVoiceChannel VoiceChannel; + private IVoiceChannel VoiceChannel { get; set; } private readonly Logger _log; private MusicQueue Queue { get; } = new MusicQueue(); @@ -42,9 +42,13 @@ namespace NadekoBot.Services.Music } public bool RepeatCurrentSong { get; private set; } + public bool Shuffle { get; private set; } + public bool Autoplay { get; private set; } + public bool RepeatPlaylist { get; private set; } = true; private IAudioClient _audioClient; private readonly object locker = new object(); + private MusicService _musicService; #region events public event Action OnStarted; @@ -59,6 +63,7 @@ namespace NadekoBot.Services.Music this.VoiceChannel = vch; this.SongCancelSource = new CancellationTokenSource(); this.OutputTextChannel = output; + this._musicService = musicService; _player = Task.Run(async () => { @@ -96,42 +101,43 @@ namespace NadekoBot.Services.Music // i don't want to spam connection attempts continue; } - var pcm = ac.CreatePCMStream(AudioApplication.Music); - - OnStarted?.Invoke(this, data.Song); - - byte[] buffer = new byte[3840]; - int bytesRead = 0; - try + using (var pcm = ac.CreatePCMStream(AudioApplication.Music)) { - while ((bytesRead = await b.ReadAsync(buffer, 0, buffer.Length, cancelToken).ConfigureAwait(false)) > 0) + OnStarted?.Invoke(this, data.Song); + + byte[] buffer = new byte[3840]; + int bytesRead = 0; + try { - var vol = Volume; - if (vol != 1) - AdjustVolume(buffer, vol); - await Task.WhenAll(Task.Delay(10), pcm.WriteAsync(buffer, 0, bytesRead, cancelToken)).ConfigureAwait(false); + while ((bytesRead = await b.ReadAsync(buffer, 0, buffer.Length, cancelToken).ConfigureAwait(false)) > 0) + { + var vol = Volume; + if (vol != 1) + AdjustVolume(buffer, vol); + await Task.WhenAll(Task.Delay(10), pcm.WriteAsync(buffer, 0, bytesRead, cancelToken)).ConfigureAwait(false); - await (pauseTaskSource?.Task ?? Task.CompletedTask); + await (pauseTaskSource?.Task ?? Task.CompletedTask); + } } - } - catch (OperationCanceledException) - { - _log.Info("Song Canceled"); - } - catch (Exception ex) - { - _log.Warn(ex); - } - finally - { - //flush is known to get stuck from time to time, just cancel it if it takes more than 1 second - var flushCancel = new CancellationTokenSource(); - var flushToken = flushCancel.Token; - var flushDelay = Task.Delay(1000, flushToken); - await Task.WhenAny(flushDelay, pcm.FlushAsync(flushToken)); - flushCancel.Cancel(); + catch (OperationCanceledException) + { + _log.Info("Song Canceled"); + } + catch (Exception ex) + { + _log.Warn(ex); + } + finally + { + //flush is known to get stuck from time to time, just cancel it if it takes more than 1 second + var flushCancel = new CancellationTokenSource(); + var flushToken = flushCancel.Token; + var flushDelay = Task.Delay(1000, flushToken); + await Task.WhenAny(flushDelay, pcm.FlushAsync(flushToken)); + flushCancel.Cancel(); - OnCompleted?.Invoke(this, data.Song); + OnCompleted?.Invoke(this, data.Song); + } } } } @@ -143,8 +149,35 @@ namespace NadekoBot.Services.Music await Task.Delay(500); } while (Stopped && !Exited); - if(!RepeatCurrentSong) - Queue.Next(); + if (!RepeatCurrentSong) //if repeating current song, just ignore other settings, and play this song again (don't change the index) + { + if (Shuffle) + Queue.Random(); //if shuffle is set, set current song index to a random number + else + { + //if last song, and autoplay is enabled, and if it's a youtube song + // do autplay magix + if (Queue.Count == data.Index && Autoplay && data.Song?.Provider == "YouTube") + { + try + { + //todo test autoplay + await _musicService.TryQueueRelatedSongAsync(data.Song.Query, OutputTextChannel, VoiceChannel); + Queue.Next(); + } + catch { } + } + else if (Queue.Count == data.Index && !RepeatPlaylist) + { + //todo test repeatplaylist + Stop(); + } + else + { + Queue.Next(); + } + } + } } } }, SongCancelSource.Token); @@ -313,6 +346,41 @@ namespace NadekoBot.Services.Music await ac.StopAsync(); } + public bool ToggleShuffle() + { + lock (locker) + { + return Shuffle = !Shuffle; + } + } + + public bool ToggleAutoplay() + { + lock (locker) + { + return Autoplay = !Autoplay; + } + } + + public bool ToggleRepeatPlaylist() + { + lock (locker) + { + return RepeatPlaylist = !RepeatPlaylist; + } + } + + public void SetMaxQueueSize(uint size) + { + Queue.SetMaxQueueSize(size); + } + + public void SetVoiceChannel(IVoiceChannel vch) + { + VoiceChannel = vch; + Next(); + } + //private IAudioClient AudioClient { get; set; } diff --git a/src/NadekoBot/Services/Music/MusicQueue.cs b/src/NadekoBot/Services/Music/MusicQueue.cs index cf3a0818..dd5986aa 100644 --- a/src/NadekoBot/Services/Music/MusicQueue.cs +++ b/src/NadekoBot/Services/Music/MusicQueue.cs @@ -1,4 +1,5 @@ -using System; +using NadekoBot.Extensions; +using System; using System.Collections.Generic; using System.Linq; using System.Text; @@ -49,17 +50,23 @@ namespace NadekoBot.Services.Music } } + public uint maxQueueSize { get; private set; } + public void Add(SongInfo song) { + song.ThrowIfNull(nameof(song)); lock (locker) { + if(CurrentIndex >= maxQueueSize) + throw new PlaylistFullException(); Songs.AddLast(song); } } public void Next() { - CurrentIndex++; + lock(locker) + CurrentIndex++; } public void Dispose() @@ -118,5 +125,24 @@ namespace NadekoBot.Services.Music CurrentIndex = 0; } } + + public void Random() + { + lock (locker) + { + CurrentIndex = new NadekoRandom().Next(Songs.Count); + } + } + + public void SetMaxQueueSize(uint size) + { + if (size < 0) + throw new ArgumentOutOfRangeException(nameof(size)); + + lock (locker) + { + maxQueueSize = size; + } + } } } diff --git a/src/NadekoBot/Services/Music/MusicService.cs b/src/NadekoBot/Services/Music/MusicService.cs index 986fd0fd..ccc70dbc 100644 --- a/src/NadekoBot/Services/Music/MusicService.cs +++ b/src/NadekoBot/Services/Music/MusicService.cs @@ -10,6 +10,7 @@ using System.IO; using VideoLibrary; using System.Collections.Generic; using Discord.Commands; +using Discord.WebSocket; namespace NadekoBot.Services.Music { @@ -25,13 +26,15 @@ namespace NadekoBot.Services.Music private readonly SoundCloudApiService _sc; private readonly IBotCredentials _creds; private readonly ConcurrentDictionary _defaultVolumes; + private readonly DiscordSocketClient _client; public ConcurrentDictionary MusicPlayers { get; } = new ConcurrentDictionary(); - public MusicService(IGoogleApiService google, + public MusicService(DiscordSocketClient client, IGoogleApiService google, NadekoStrings strings, ILocalization localization, DbService db, SoundCloudApiService sc, IBotCredentials creds, IEnumerable gcs) { + _client = client; _google = google; _strings = strings; _localization = localization; @@ -187,6 +190,25 @@ namespace NadekoBot.Services.Music }); } + public MusicPlayer GetPlayerOrDefault(ulong guildId) + { + if (MusicPlayers.TryGetValue(guildId, out var mp)) + return mp; + else + return null; + } + + public async Task TryQueueRelatedSongAsync(string query, ITextChannel txtCh, IVoiceChannel vch) + { + var related = (await _google.GetRelatedVideosAsync(query, 4)).ToArray(); + if (!related.Any()) + return; + + var si = await ResolveSong(related[new NadekoRandom().Next(related.Length)], _client.CurrentUser.ToString(), MusicType.Normal); + var mp = await GetOrCreatePlayer(txtCh.GuildId, vch, txtCh); + mp.Enqueue(si); + } + public async Task ResolveSong(string query, string queuerName, MusicType musicType = MusicType.Normal) { query.ThrowIfNull(nameof(query)); diff --git a/src/NadekoBot/Services/Music/SongBuffer.cs b/src/NadekoBot/Services/Music/SongBuffer.cs index c5c08175..5f4c8c91 100644 --- a/src/NadekoBot/Services/Music/SongBuffer.cs +++ b/src/NadekoBot/Services/Music/SongBuffer.cs @@ -24,7 +24,7 @@ namespace NadekoBot.Services.Music this.p = Process.Start(new ProcessStartInfo { FileName = "ffmpeg", - Arguments = $"-i {songUri} -f s16le -ar 48000 -vn -ac 2 pipe:1 -loglevel quiet", + Arguments = $"-i {songUri} -f s16le -ar 48000 -vn -b:a 128k -ac 2 pipe:1 -loglevel quiet -nostdin", UseShellExecute = false, RedirectStandardOutput = true, RedirectStandardError = false, diff --git a/src/NadekoBot/Services/Music/SongInfo.cs b/src/NadekoBot/Services/Music/SongInfo.cs index 4ad63b4a..28d407ee 100644 --- a/src/NadekoBot/Services/Music/SongInfo.cs +++ b/src/NadekoBot/Services/Music/SongInfo.cs @@ -4,6 +4,7 @@ using NadekoBot.Services.Database.Models; using System; using System.Net; using System.Text.RegularExpressions; +using System.Threading.Tasks; namespace NadekoBot.Services.Music { diff --git a/src/NadekoBot/_strings/ResponseStrings.en-US.json b/src/NadekoBot/_strings/ResponseStrings.en-US.json index e7a3b885..378ac8a3 100644 --- a/src/NadekoBot/_strings/ResponseStrings.en-US.json +++ b/src/NadekoBot/_strings/ResponseStrings.en-US.json @@ -442,12 +442,14 @@ "music_repeating_playlist": "Repeating playlist", "music_repeating_track": "Repeating track", "music_repeating_track_stopped": "Current track repeat stopped.", + "music_shuffling_playlist": "Shuffling songs", "music_resumed": "Music playback resumed.", "music_rpl_disabled": "Repeat playlist disabled.", "music_rpl_enabled": "Repeat playlist enabled.", "music_set_music_channel": "I will now output playing, finished, paused and removed songs in this channel.", "music_skipped_to": "Skipped to `{0}:{1}`", - "music_songs_shuffled": "Songs shuffled", + "music_songs_shuffle_enable": "Songs will shuffle from now on.", + "music_songs_shuffle_disable": "Songs will no longer shuffle.", "music_song_moved": "Song moved", "music_song_not_found": "No song found.", "music_time_format": "{0}h {1}m {2}s", From 9bb72d99769419b9e42467bc99c4bb7d75c97c28 Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Sun, 2 Jul 2017 08:03:24 +0200 Subject: [PATCH 057/346] Shuffle will now show in .lq at the top, and instead of shuffling playlist, it will randomly jump to a song in the playlist. " --- src/NadekoBot/Modules/Music/Music.cs | 22 +++--- src/NadekoBot/Services/Music/MusicPlayer.cs | 68 +++++++++---------- src/NadekoBot/Services/Music/MusicQueue.cs | 2 +- .../_strings/ResponseStrings.en-US.json | 1 + 4 files changed, 47 insertions(+), 46 deletions(-) diff --git a/src/NadekoBot/Modules/Music/Music.cs b/src/NadekoBot/Modules/Music/Music.cs index c82e59cc..11448b60 100644 --- a/src/NadekoBot/Modules/Music/Music.cs +++ b/src/NadekoBot/Modules/Music/Music.cs @@ -1,18 +1,14 @@ using Discord.Commands; using Discord.WebSocket; using NadekoBot.Services; -using System.IO; using Discord; using System.Threading.Tasks; using NadekoBot.Attributes; using System; using System.Linq; using NadekoBot.Extensions; -using System.Net.Http; -using Newtonsoft.Json.Linq; using System.Collections.Generic; using NadekoBot.Services.Database.Models; -using System.Threading; using NadekoBot.Services.Music; using NadekoBot.DataStructures; using System.Collections.Concurrent; @@ -102,24 +98,26 @@ namespace NadekoBot.Modules.Music .WithThumbnailUrl(songInfo.Thumbnail) .WithFooter(ef => ef.WithText(songInfo.PrettyProvider))) .ConfigureAwait(false); + if (mp.Stopped) + { + (await ReplyErrorLocalized("music_queue_stopped", Format.Code(Prefix + "play")).ConfigureAwait(false)).DeleteAfter(10); + } queuedMessage?.DeleteAfter(10); } catch { // ignored - } // if queued message sending fails, don't attempt to delete it + } } } } + //todo add play command. .play = .n, .play whatever = .q whatever - - [NadekoCommand, Usage, Description, Aliases] [RequireContext(ContextType.Guild)] public async Task Queue([Remainder] string query) { - //todo add a notice that player is stopped if user queues a song while it is var mp = await _music.GetOrCreatePlayer(Context); var songInfo = await _music.ResolveSong(query, Context.User.ToString()); await InternalQueue(mp, songInfo, false); @@ -207,7 +205,7 @@ namespace NadekoBot.Modules.Music return $"**⇒**`{number}.` {v.PrettyFullName}"; else return $"`{number}.` {v.PrettyFullName}"; - })); //todo v.prettyfullname instead of title + })); desc = $"`🔊` {songs[current].PrettyFullName}\n\n" + desc; @@ -687,7 +685,7 @@ namespace NadekoBot.Modules.Music [NadekoCommand, Usage, Description, Aliases] [RequireContext(ContextType.Guild)] - public async Task Move() + public void Move() { var vch = ((IGuildUser)Context.User).VoiceChannel; @@ -748,6 +746,7 @@ namespace NadekoBot.Modules.Music //} + //todo test smq [NadekoCommand, Usage, Description, Aliases] [RequireContext(ContextType.Guild)] public async Task SetMaxQueue(uint size = 0) @@ -782,6 +781,7 @@ namespace NadekoBot.Modules.Music // await ReplyConfirmLocalized("max_playtime_set", seconds).ConfigureAwait(false); //} + //todo test rcs [NadekoCommand, Usage, Description, Aliases] [RequireContext(ContextType.Guild)] public async Task ReptCurSong() @@ -803,6 +803,7 @@ namespace NadekoBot.Modules.Music .ConfigureAwait(false); } + //todo test rpl [NadekoCommand, Usage, Description, Aliases] [RequireContext(ContextType.Guild)] public async Task RepeatPl() @@ -862,6 +863,7 @@ namespace NadekoBot.Modules.Music await ReplyConfirmLocalized("autoplay_enabled").ConfigureAwait(false); } + //todo test output text channel [NadekoCommand, Usage, Description, Aliases] [RequireContext(ContextType.Guild)] [RequireUserPermission(GuildPermission.ManageMessages)] diff --git a/src/NadekoBot/Services/Music/MusicPlayer.cs b/src/NadekoBot/Services/Music/MusicPlayer.cs index d6cac926..24da1242 100644 --- a/src/NadekoBot/Services/Music/MusicPlayer.cs +++ b/src/NadekoBot/Services/Music/MusicPlayer.cs @@ -96,48 +96,46 @@ namespace NadekoBot.Services.Music var ac = await GetAudioClient(); if (ac == null) { - await Task.Delay(900); + await Task.Delay(900, cancelToken); // just wait some time, maybe bot doesn't even have perms to join that voice channel, // i don't want to spam connection attempts continue; } - using (var pcm = ac.CreatePCMStream(AudioApplication.Music)) + var pcm = ac.CreatePCMStream(AudioApplication.Music, bufferMillis: 250); + OnStarted?.Invoke(this, data.Song); + + byte[] buffer = new byte[3840]; + int bytesRead = 0; + try { - OnStarted?.Invoke(this, data.Song); + while ((bytesRead = await b.ReadAsync(buffer, 0, buffer.Length, cancelToken).ConfigureAwait(false)) > 0) + { + var vol = Volume; + if (vol != 1) + AdjustVolume(buffer, vol); + await Task.WhenAll(Task.Delay(10), pcm.WriteAsync(buffer, 0, bytesRead, cancelToken)).ConfigureAwait(false); - byte[] buffer = new byte[3840]; - int bytesRead = 0; - try - { - while ((bytesRead = await b.ReadAsync(buffer, 0, buffer.Length, cancelToken).ConfigureAwait(false)) > 0) - { - var vol = Volume; - if (vol != 1) - AdjustVolume(buffer, vol); - await Task.WhenAll(Task.Delay(10), pcm.WriteAsync(buffer, 0, bytesRead, cancelToken)).ConfigureAwait(false); + await (pauseTaskSource?.Task ?? Task.CompletedTask); + } + } + catch (OperationCanceledException) + { + _log.Info("Song Canceled"); + } + catch (Exception ex) + { + _log.Warn(ex); + } + finally + { + //flush is known to get stuck from time to time, just cancel it if it takes more than 1 second + var flushCancel = new CancellationTokenSource(); + var flushToken = flushCancel.Token; + var flushDelay = Task.Delay(1000, flushToken); + await Task.WhenAny(flushDelay, pcm.FlushAsync(flushToken)); + flushCancel.Cancel(); - await (pauseTaskSource?.Task ?? Task.CompletedTask); - } - } - catch (OperationCanceledException) - { - _log.Info("Song Canceled"); - } - catch (Exception ex) - { - _log.Warn(ex); - } - finally - { - //flush is known to get stuck from time to time, just cancel it if it takes more than 1 second - var flushCancel = new CancellationTokenSource(); - var flushToken = flushCancel.Token; - var flushDelay = Task.Delay(1000, flushToken); - await Task.WhenAny(flushDelay, pcm.FlushAsync(flushToken)); - flushCancel.Cancel(); - - OnCompleted?.Invoke(this, data.Song); - } + OnCompleted?.Invoke(this, data.Song); } } } diff --git a/src/NadekoBot/Services/Music/MusicQueue.cs b/src/NadekoBot/Services/Music/MusicQueue.cs index dd5986aa..0715ef2f 100644 --- a/src/NadekoBot/Services/Music/MusicQueue.cs +++ b/src/NadekoBot/Services/Music/MusicQueue.cs @@ -57,7 +57,7 @@ namespace NadekoBot.Services.Music song.ThrowIfNull(nameof(song)); lock (locker) { - if(CurrentIndex >= maxQueueSize) + if(maxQueueSize !=0 && CurrentIndex >= maxQueueSize) throw new PlaylistFullException(); Songs.AddLast(song); } diff --git a/src/NadekoBot/_strings/ResponseStrings.en-US.json b/src/NadekoBot/_strings/ResponseStrings.en-US.json index 378ac8a3..ab8aea41 100644 --- a/src/NadekoBot/_strings/ResponseStrings.en-US.json +++ b/src/NadekoBot/_strings/ResponseStrings.en-US.json @@ -437,6 +437,7 @@ "music_queued_song": "Queued song", "music_queue_cleared": "Music queue cleared.", "music_queue_full": "Queue is full at {0}/{0}.", + "music_queue_stopped": "Player is stopped. Use {0} command to start playing.", "music_removed_song": "Removed song", "music_repeating_cur_song": "Repeating current song", "music_repeating_playlist": "Repeating playlist", From 42658355b1f122c09889f83af936d16b2cc3ce7c Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Sun, 2 Jul 2017 09:55:26 +0200 Subject: [PATCH 058/346] music debug stuff --- .../DataStructures/PoopyRingBuffer.cs | 5 ++-- src/NadekoBot/Modules/Music/Music.cs | 1 + src/NadekoBot/Services/Music/MusicPlayer.cs | 1 + src/NadekoBot/Services/Music/SongBuffer.cs | 27 ++++++++++++++++--- 4 files changed, 27 insertions(+), 7 deletions(-) diff --git a/src/NadekoBot/DataStructures/PoopyRingBuffer.cs b/src/NadekoBot/DataStructures/PoopyRingBuffer.cs index 4c8764db..0a2ca39f 100644 --- a/src/NadekoBot/DataStructures/PoopyRingBuffer.cs +++ b/src/NadekoBot/DataStructures/PoopyRingBuffer.cs @@ -48,7 +48,7 @@ namespace NadekoBot.DataStructures private readonly SemaphoreSlim _locker = new SemaphoreSlim(1, 1); - public PoopyRingBuffer(int capacity = 3640 * 200) + public PoopyRingBuffer(int capacity = 3840 * 300) { this.Capacity = capacity + 1; this.buffer = new byte[this.Capacity]; @@ -59,7 +59,6 @@ namespace NadekoBot.DataStructures await _locker.WaitAsync(cancelToken); try { - Console.WriteLine("Reading {0}", toRead); if (WritePos == ReadPos) return 0; @@ -129,7 +128,7 @@ namespace NadekoBot.DataStructures WritePos = 0; } } - Console.WriteLine("Readpos: {0} WritePos: {1}", ReadPos, WritePos); + Console.WriteLine("Readpos: {0} WritePos: {1} ({2})", ReadPos, WritePos, ReadPos - WritePos); return toWrite; } finally diff --git a/src/NadekoBot/Modules/Music/Music.cs b/src/NadekoBot/Modules/Music/Music.cs index 11448b60..a8906e62 100644 --- a/src/NadekoBot/Modules/Music/Music.cs +++ b/src/NadekoBot/Modules/Music/Music.cs @@ -165,6 +165,7 @@ namespace NadekoBot.Modules.Music } } + //todo, page should default to the page the current song is on [NadekoCommand, Usage, Description, Aliases] [RequireContext(ContextType.Guild)] public async Task ListQueue(int page = 1) diff --git a/src/NadekoBot/Services/Music/MusicPlayer.cs b/src/NadekoBot/Services/Music/MusicPlayer.cs index 24da1242..535cfe34 100644 --- a/src/NadekoBot/Services/Music/MusicPlayer.cs +++ b/src/NadekoBot/Services/Music/MusicPlayer.cs @@ -117,6 +117,7 @@ namespace NadekoBot.Services.Music await (pauseTaskSource?.Task ?? Task.CompletedTask); } + _log.Info(">>>>>>>>>READ 0<<<<<<<<<<"); } catch (OperationCanceledException) { diff --git a/src/NadekoBot/Services/Music/SongBuffer.cs b/src/NadekoBot/Services/Music/SongBuffer.cs index 5f4c8c91..1c73da10 100644 --- a/src/NadekoBot/Services/Music/SongBuffer.cs +++ b/src/NadekoBot/Services/Music/SongBuffer.cs @@ -42,17 +42,36 @@ namespace NadekoBot.Services.Music byte[] buffer = new byte[3840]; while (!this.p.HasExited || cancelToken.IsCancellationRequested) { - int bytesRead = await p.StandardOutput.BaseStream.ReadAsync(buffer, 0, buffer.Length, cancelToken).ConfigureAwait(false); - + var toRead = buffer.Length; + var remCap = _outStream.RemainingCapacity; + if (remCap < 3840) + { + if (_outStream.RemainingCapacity == 0) + { + Console.WriteLine("Buffer full, not gonnna read from ffmpeg"); + await Task.Delay(10); + continue; + } + toRead = remCap; + } + int bytesRead = await p.StandardOutput.BaseStream.ReadAsync(buffer, 0, toRead, cancelToken).ConfigureAwait(false); + if (bytesRead == 0) + { + Console.WriteLine("I'm not reading anything from ffmpeg"); + await Task.Delay(50); + } await _outStream.WriteAsync(buffer, 0, bytesRead, cancelToken); if (_outStream.RemainingCapacity < _outStream.Capacity * 0.9f) - toReturn.TrySetResult(true); + if(toReturn.TrySetResult(true)) + Console.WriteLine("Prebuffering finished"); } + Console.WriteLine("FFMPEG killed or song canceled"); } catch { - toReturn.TrySetResult(false); + if(toReturn.TrySetResult(false)) + Console.WriteLine("Prebuffering failed"); //ignored } }, cancelToken); From e792e7b39e2256e3df28c97279a7e035028176a1 Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Sun, 2 Jul 2017 10:22:47 +0200 Subject: [PATCH 059/346] removed -nostdin ffmpeg argument --- src/NadekoBot/Services/Music/SongBuffer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/NadekoBot/Services/Music/SongBuffer.cs b/src/NadekoBot/Services/Music/SongBuffer.cs index 1c73da10..5fe27a52 100644 --- a/src/NadekoBot/Services/Music/SongBuffer.cs +++ b/src/NadekoBot/Services/Music/SongBuffer.cs @@ -24,7 +24,7 @@ namespace NadekoBot.Services.Music this.p = Process.Start(new ProcessStartInfo { FileName = "ffmpeg", - Arguments = $"-i {songUri} -f s16le -ar 48000 -vn -b:a 128k -ac 2 pipe:1 -loglevel quiet -nostdin", + Arguments = $"-i {songUri} -f s16le -ar 48000 -vn -ac 2 pipe:1 -loglevel quiet", UseShellExecute = false, RedirectStandardOutput = true, RedirectStandardError = false, From f2d1b821d04173126c4d74c89a6b73aa68f19330 Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Sun, 2 Jul 2017 10:33:47 +0200 Subject: [PATCH 060/346] Trying to pinpoint song skipping --- src/NadekoBot/Services/Music/SongBuffer.cs | 23 ++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/src/NadekoBot/Services/Music/SongBuffer.cs b/src/NadekoBot/Services/Music/SongBuffer.cs index 5fe27a52..e657d6a4 100644 --- a/src/NadekoBot/Services/Music/SongBuffer.cs +++ b/src/NadekoBot/Services/Music/SongBuffer.cs @@ -24,7 +24,7 @@ namespace NadekoBot.Services.Music this.p = Process.Start(new ProcessStartInfo { FileName = "ffmpeg", - Arguments = $"-i {songUri} -f s16le -ar 48000 -vn -ac 2 pipe:1 -loglevel quiet", + Arguments = $"-i {songUri} -f s16le -ar 48000 -vn -ac 2 pipe:1 -loglevel quiet -nostdin", UseShellExecute = false, RedirectStandardOutput = true, RedirectStandardError = false, @@ -40,7 +40,7 @@ namespace NadekoBot.Services.Music try { byte[] buffer = new byte[3840]; - while (!this.p.HasExited || cancelToken.IsCancellationRequested) + while (cancelToken.IsCancellationRequested) { var toRead = buffer.Length; var remCap = _outStream.RemainingCapacity; @@ -54,13 +54,20 @@ namespace NadekoBot.Services.Music } toRead = remCap; } - int bytesRead = await p.StandardOutput.BaseStream.ReadAsync(buffer, 0, toRead, cancelToken).ConfigureAwait(false); - if (bytesRead == 0) + do { - Console.WriteLine("I'm not reading anything from ffmpeg"); - await Task.Delay(50); - } - await _outStream.WriteAsync(buffer, 0, bytesRead, cancelToken); + if(p.HasExited) + Console.WriteLine("FFMPEG has exited, I'm in the read/write loop"); + + int bytesRead = await p.StandardOutput.BaseStream.ReadAsync(buffer, 0, toRead, cancelToken).ConfigureAwait(false); + if (bytesRead == 0) + { + Console.WriteLine("I'm not reading anything from ffmpeg"); + await Task.Delay(20); + } + await _outStream.WriteAsync(buffer, 0, bytesRead, cancelToken); + + } while (p.HasExited && !cancelToken.IsCancellationRequested); if (_outStream.RemainingCapacity < _outStream.Capacity * 0.9f) if(toReturn.TrySetResult(true)) From 4d52566250d912bcf4ba3fccad970bb54afcc433 Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Sun, 2 Jul 2017 10:35:46 +0200 Subject: [PATCH 061/346] woops --- src/NadekoBot/Services/Music/SongBuffer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/NadekoBot/Services/Music/SongBuffer.cs b/src/NadekoBot/Services/Music/SongBuffer.cs index e657d6a4..65d29111 100644 --- a/src/NadekoBot/Services/Music/SongBuffer.cs +++ b/src/NadekoBot/Services/Music/SongBuffer.cs @@ -40,7 +40,7 @@ namespace NadekoBot.Services.Music try { byte[] buffer = new byte[3840]; - while (cancelToken.IsCancellationRequested) + while (!cancelToken.IsCancellationRequested) { var toRead = buffer.Length; var remCap = _outStream.RemainingCapacity; From bbe3ac66e3a60aadb998d739da5ed3b4c91222b1 Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Sun, 2 Jul 2017 10:44:30 +0200 Subject: [PATCH 062/346] Try reading more from ffmpeg --- src/NadekoBot/Services/Music/SongBuffer.cs | 31 +++++++++------------- 1 file changed, 13 insertions(+), 18 deletions(-) diff --git a/src/NadekoBot/Services/Music/SongBuffer.cs b/src/NadekoBot/Services/Music/SongBuffer.cs index 65d29111..7c2a68ff 100644 --- a/src/NadekoBot/Services/Music/SongBuffer.cs +++ b/src/NadekoBot/Services/Music/SongBuffer.cs @@ -9,7 +9,7 @@ namespace NadekoBot.Services.Music { public class SongBuffer : IDisposable { - const int maxReadSize = 3840; + const int readSize = 38400; private Process p; private PoopyRingBuffer _outStream = new PoopyRingBuffer(); @@ -39,38 +39,33 @@ namespace NadekoBot.Services.Music { try { - byte[] buffer = new byte[3840]; + byte[] buffer = new byte[readSize]; while (!cancelToken.IsCancellationRequested) { var toRead = buffer.Length; var remCap = _outStream.RemainingCapacity; - if (remCap < 3840) + if (remCap < readSize) { if (_outStream.RemainingCapacity == 0) { Console.WriteLine("Buffer full, not gonnna read from ffmpeg"); - await Task.Delay(10); + await Task.Delay(20); continue; } toRead = remCap; } - do + + int bytesRead = await p.StandardOutput.BaseStream.ReadAsync(buffer, 0, toRead, cancelToken).ConfigureAwait(false); + if (bytesRead == 0) { - if(p.HasExited) - Console.WriteLine("FFMPEG has exited, I'm in the read/write loop"); - - int bytesRead = await p.StandardOutput.BaseStream.ReadAsync(buffer, 0, toRead, cancelToken).ConfigureAwait(false); - if (bytesRead == 0) - { - Console.WriteLine("I'm not reading anything from ffmpeg"); - await Task.Delay(20); - } - await _outStream.WriteAsync(buffer, 0, bytesRead, cancelToken); - - } while (p.HasExited && !cancelToken.IsCancellationRequested); + Console.WriteLine("I'm not reading anything from ffmpeg"); + await Task.Delay(20); + continue; + } + await _outStream.WriteAsync(buffer, 0, bytesRead, cancelToken); if (_outStream.RemainingCapacity < _outStream.Capacity * 0.9f) - if(toReturn.TrySetResult(true)) + if (toReturn.TrySetResult(true)) Console.WriteLine("Prebuffering finished"); } Console.WriteLine("FFMPEG killed or song canceled"); From 7de15bf444a73b0fda2f2eed86b2ac6eecbfc30e Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Sun, 2 Jul 2017 10:58:04 +0200 Subject: [PATCH 063/346] magic numbers --- src/NadekoBot/Services/Music/MusicPlayer.cs | 4 ++-- src/NadekoBot/Services/Music/SongBuffer.cs | 23 +++++++++++---------- 2 files changed, 14 insertions(+), 13 deletions(-) diff --git a/src/NadekoBot/Services/Music/MusicPlayer.cs b/src/NadekoBot/Services/Music/MusicPlayer.cs index 535cfe34..1ac0cbad 100644 --- a/src/NadekoBot/Services/Music/MusicPlayer.cs +++ b/src/NadekoBot/Services/Music/MusicPlayer.cs @@ -101,10 +101,10 @@ namespace NadekoBot.Services.Music // i don't want to spam connection attempts continue; } - var pcm = ac.CreatePCMStream(AudioApplication.Music, bufferMillis: 250); + var pcm = ac.CreatePCMStream(AudioApplication.Music, bufferMillis: 1000); OnStarted?.Invoke(this, data.Song); - byte[] buffer = new byte[3840]; + byte[] buffer = new byte[38400]; int bytesRead = 0; try { diff --git a/src/NadekoBot/Services/Music/SongBuffer.cs b/src/NadekoBot/Services/Music/SongBuffer.cs index 7c2a68ff..64fe4750 100644 --- a/src/NadekoBot/Services/Music/SongBuffer.cs +++ b/src/NadekoBot/Services/Music/SongBuffer.cs @@ -43,17 +43,17 @@ namespace NadekoBot.Services.Music while (!cancelToken.IsCancellationRequested) { var toRead = buffer.Length; - var remCap = _outStream.RemainingCapacity; - if (remCap < readSize) - { - if (_outStream.RemainingCapacity == 0) - { - Console.WriteLine("Buffer full, not gonnna read from ffmpeg"); - await Task.Delay(20); - continue; - } - toRead = remCap; - } + //var remCap = _outStream.RemainingCapacity; + //if (remCap < readSize) + //{ + // if (_outStream.RemainingCapacity == 0) + // { + // Console.WriteLine("Buffer full, not gonnna read from ffmpeg"); + // await Task.Delay(20); + // continue; + // } + // toRead = remCap; + //} int bytesRead = await p.StandardOutput.BaseStream.ReadAsync(buffer, 0, toRead, cancelToken).ConfigureAwait(false); if (bytesRead == 0) @@ -88,6 +88,7 @@ namespace NadekoBot.Services.Music public void Dispose() { + Console.WriteLine("DISPOSING"); try { this.p.Kill(); } catch { } _outStream.Dispose(); From 0e73372c23330558d742aea7ed129762b9abb21e Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Sun, 2 Jul 2017 13:19:05 +0200 Subject: [PATCH 064/346] buffer is now 50MB --- .../DataStructures/PoopyRingBuffer.cs | 8 +++--- src/NadekoBot/Services/Music/SongBuffer.cs | 25 ++++++++++--------- 2 files changed, 18 insertions(+), 15 deletions(-) diff --git a/src/NadekoBot/DataStructures/PoopyRingBuffer.cs b/src/NadekoBot/DataStructures/PoopyRingBuffer.cs index 0a2ca39f..214ee110 100644 --- a/src/NadekoBot/DataStructures/PoopyRingBuffer.cs +++ b/src/NadekoBot/DataStructures/PoopyRingBuffer.cs @@ -1,5 +1,7 @@ -using System; +using NadekoBot.Extensions; +using System; using System.Collections.Generic; +using System.IO; using System.Linq; using System.Text; using System.Threading; @@ -48,7 +50,7 @@ namespace NadekoBot.DataStructures private readonly SemaphoreSlim _locker = new SemaphoreSlim(1, 1); - public PoopyRingBuffer(int capacity = 3840 * 300) + public PoopyRingBuffer(int capacity = 50_000_000) { this.Capacity = capacity + 1; this.buffer = new byte[this.Capacity]; @@ -97,7 +99,7 @@ namespace NadekoBot.DataStructures public Task WriteAsync(byte[] b, int offset, int toWrite, CancellationToken cancelToken) => Task.Run(async () => { while (toWrite > RemainingCapacity) - await Task.Delay(100, cancelToken); + await Task.Delay(50, cancelToken); await _locker.WaitAsync(cancelToken); try diff --git a/src/NadekoBot/Services/Music/SongBuffer.cs b/src/NadekoBot/Services/Music/SongBuffer.cs index 64fe4750..1d9fa9ef 100644 --- a/src/NadekoBot/Services/Music/SongBuffer.cs +++ b/src/NadekoBot/Services/Music/SongBuffer.cs @@ -24,12 +24,17 @@ namespace NadekoBot.Services.Music this.p = Process.Start(new ProcessStartInfo { FileName = "ffmpeg", - Arguments = $"-i {songUri} -f s16le -ar 48000 -vn -ac 2 pipe:1 -loglevel quiet -nostdin", + Arguments = $"-ss 0 -i {songUri} -f s16le -ar 48000 -vn -ac 2 pipe:1 -loglevel quiet", UseShellExecute = false, RedirectStandardOutput = true, - RedirectStandardError = false, + RedirectStandardError = true, CreateNoWindow = true, }); + var t = Task.Run(() => + { + this.p.BeginErrorReadLine(); + this.p.WaitForExit(); + }); } public Task StartBuffering(CancellationToken cancelToken) @@ -40,7 +45,8 @@ namespace NadekoBot.Services.Music try { byte[] buffer = new byte[readSize]; - while (!cancelToken.IsCancellationRequested) + int bytesRead = -1; + while (!cancelToken.IsCancellationRequested && bytesRead != 0) { var toRead = buffer.Length; //var remCap = _outStream.RemainingCapacity; @@ -54,14 +60,8 @@ namespace NadekoBot.Services.Music // } // toRead = remCap; //} - - int bytesRead = await p.StandardOutput.BaseStream.ReadAsync(buffer, 0, toRead, cancelToken).ConfigureAwait(false); - if (bytesRead == 0) - { - Console.WriteLine("I'm not reading anything from ffmpeg"); - await Task.Delay(20); - continue; - } + + bytesRead = await p.StandardOutput.BaseStream.ReadAsync(buffer, 0, toRead, cancelToken).ConfigureAwait(false); await _outStream.WriteAsync(buffer, 0, bytesRead, cancelToken); if (_outStream.RemainingCapacity < _outStream.Capacity * 0.9f) @@ -70,8 +70,9 @@ namespace NadekoBot.Services.Music } Console.WriteLine("FFMPEG killed or song canceled"); } - catch + catch (Exception ex) { + Console.WriteLine(ex); if(toReturn.TrySetResult(false)) Console.WriteLine("Prebuffering failed"); //ignored From 5015b6ad95d8d594fd8607db1033659ef15df70d Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Sun, 2 Jul 2017 13:53:09 +0200 Subject: [PATCH 065/346] cleanup --- .../DataStructures/PoopyRingBuffer.cs | 9 ++-- .../DataStructures/SearchImageCacher.cs | 5 ++- .../Modules/Administration/Administration.cs | 1 - .../Modules/Searches/Commands/LoLCommands.cs | 2 - src/NadekoBot/NadekoBot.cs | 2 +- src/NadekoBot/Services/Music/MusicPlayer.cs | 5 --- src/NadekoBot/Services/Music/Song.cs | 1 - src/NadekoBot/Services/Music/SongBuffer.cs | 44 +++++++++---------- 8 files changed, 30 insertions(+), 39 deletions(-) diff --git a/src/NadekoBot/DataStructures/PoopyRingBuffer.cs b/src/NadekoBot/DataStructures/PoopyRingBuffer.cs index 214ee110..b0b9f19c 100644 --- a/src/NadekoBot/DataStructures/PoopyRingBuffer.cs +++ b/src/NadekoBot/DataStructures/PoopyRingBuffer.cs @@ -87,7 +87,6 @@ namespace NadekoBot.DataStructures ReadPos = secondRead; } } - Console.WriteLine("Readpos: {0} WritePos: {1}", ReadPos, WritePos); return toRead; } finally @@ -99,12 +98,14 @@ namespace NadekoBot.DataStructures public Task WriteAsync(byte[] b, int offset, int toWrite, CancellationToken cancelToken) => Task.Run(async () => { while (toWrite > RemainingCapacity) - await Task.Delay(50, cancelToken); + await Task.Delay(1000, cancelToken); // wait a lot, buffer should be large anyway + + if (toWrite == 0) + return; await _locker.WaitAsync(cancelToken); try { - Console.WriteLine("Writing {0}", toWrite); if (WritePos < ReadPos) { Buffer.BlockCopy(b, offset, buffer, WritePos, toWrite); @@ -130,8 +131,6 @@ namespace NadekoBot.DataStructures WritePos = 0; } } - Console.WriteLine("Readpos: {0} WritePos: {1} ({2})", ReadPos, WritePos, ReadPos - WritePos); - return toWrite; } finally { diff --git a/src/NadekoBot/DataStructures/SearchImageCacher.cs b/src/NadekoBot/DataStructures/SearchImageCacher.cs index 1296c311..c9795ecf 100644 --- a/src/NadekoBot/DataStructures/SearchImageCacher.cs +++ b/src/NadekoBot/DataStructures/SearchImageCacher.cs @@ -1,6 +1,7 @@ using NadekoBot.Extensions; using NadekoBot.Services; using Newtonsoft.Json; +using NLog; using System; using System.Collections.Concurrent; using System.Collections.Generic; @@ -19,9 +20,11 @@ namespace NadekoBot.DataStructures private readonly ConcurrentDictionary _locks = new ConcurrentDictionary(); private readonly SortedSet _cache; + private readonly Logger _log; public SearchImageCacher() { + _log = LogManager.GetCurrentClassLogger(); _rng = new NadekoRandom(); _cache = new SortedSet(); } @@ -85,7 +88,7 @@ namespace NadekoBot.DataStructures public async Task DownloadImages(string tag, bool isExplicit, DapiSearchType type) { - Console.WriteLine($"Loading extra images from {type}"); + _log.Info($"Loading extra images from {type}"); tag = tag?.Replace(" ", "_").ToLowerInvariant(); if (isExplicit) tag = "rating%3Aexplicit+" + tag; diff --git a/src/NadekoBot/Modules/Administration/Administration.cs b/src/NadekoBot/Modules/Administration/Administration.cs index 56a0affc..129970e1 100644 --- a/src/NadekoBot/Modules/Administration/Administration.cs +++ b/src/NadekoBot/Modules/Administration/Administration.cs @@ -136,7 +136,6 @@ namespace NadekoBot.Modules.Administration } catch (Exception ex) { - Console.WriteLine(ex); await ReplyErrorLocalized("rar_err").ConfigureAwait(false); } } diff --git a/src/NadekoBot/Modules/Searches/Commands/LoLCommands.cs b/src/NadekoBot/Modules/Searches/Commands/LoLCommands.cs index d7697a5c..0225725c 100644 --- a/src/NadekoBot/Modules/Searches/Commands/LoLCommands.cs +++ b/src/NadekoBot/Modules/Searches/Commands/LoLCommands.cs @@ -173,7 +173,6 @@ namespace NadekoBot.Modules.Searches // .FirstOrDefault(jt => jt["role"].ToString() == role)?["general"]; // if (general == null) // { -// //Console.WriteLine("General is null."); // return; // } // //get build data for this role @@ -309,7 +308,6 @@ namespace NadekoBot.Modules.Searches // } // catch (Exception ex) // { -// //Console.WriteLine(ex); // await channel.SendMessageAsync("💢 Failed retreiving data for that champion.").ConfigureAwait(false); // } // }); diff --git a/src/NadekoBot/NadekoBot.cs b/src/NadekoBot/NadekoBot.cs index d75cd963..00bf8319 100644 --- a/src/NadekoBot/NadekoBot.cs +++ b/src/NadekoBot/NadekoBot.cs @@ -352,7 +352,7 @@ namespace NadekoBot #if GLOBAL_NADEKO isPublicNadeko = true; #endif - //Console.WriteLine(string.Join(", ", CommandService.Commands + //_log.Info(string.Join(", ", CommandService.Commands // .Distinct(x => x.Name + x.Module.Name) // .SelectMany(x => x.Aliases) // .GroupBy(x => x) diff --git a/src/NadekoBot/Services/Music/MusicPlayer.cs b/src/NadekoBot/Services/Music/MusicPlayer.cs index 1ac0cbad..b31831e8 100644 --- a/src/NadekoBot/Services/Music/MusicPlayer.cs +++ b/src/NadekoBot/Services/Music/MusicPlayer.cs @@ -78,13 +78,9 @@ namespace NadekoBot.Services.Music } try { - _log.Info("Checking for songs"); if (data.Song == null) continue; - _log.Info("Connecting"); - - _log.Info("Starting"); using (var b = new SongBuffer(data.Song.Uri, "")) { @@ -117,7 +113,6 @@ namespace NadekoBot.Services.Music await (pauseTaskSource?.Task ?? Task.CompletedTask); } - _log.Info(">>>>>>>>>READ 0<<<<<<<<<<"); } catch (OperationCanceledException) { diff --git a/src/NadekoBot/Services/Music/Song.cs b/src/NadekoBot/Services/Music/Song.cs index 074238d6..a9567f9c 100644 --- a/src/NadekoBot/Services/Music/Song.cs +++ b/src/NadekoBot/Services/Music/Song.cs @@ -172,7 +172,6 @@ namespace NadekoBot.Services.Music // while (!cancelToken.IsCancellationRequested && //song canceled for whatever reason // !(MusicPlayer.MaxPlaytimeSeconds != 0 && CurrentTime.TotalSeconds >= MusicPlayer.MaxPlaytimeSeconds)) // or exceedded max playtime // { - // //Console.WriteLine($"Read: {songBuffer.ReadPosition}\nWrite: {songBuffer.WritePosition}\nContentLength:{songBuffer.ContentLength}\n---------"); // var read = await inStream.ReadAsync(buffer, 0, buffer.Length).ConfigureAwait(false); // //await inStream.CopyToAsync(voiceClient.OutputStream); // if (read < _frameBytes) diff --git a/src/NadekoBot/Services/Music/SongBuffer.cs b/src/NadekoBot/Services/Music/SongBuffer.cs index 1d9fa9ef..953e640a 100644 --- a/src/NadekoBot/Services/Music/SongBuffer.cs +++ b/src/NadekoBot/Services/Music/SongBuffer.cs @@ -1,4 +1,5 @@ using NadekoBot.DataStructures; +using NLog; using System; using System.Diagnostics; using System.IO; @@ -14,11 +15,13 @@ namespace NadekoBot.Services.Music private PoopyRingBuffer _outStream = new PoopyRingBuffer(); private readonly SemaphoreSlim _lock = new SemaphoreSlim(1, 1); + private readonly Logger _log; public string SongUri { get; private set; } public SongBuffer(string songUri, string skipTo) { + _log = LogManager.GetCurrentClassLogger(); this.SongUri = songUri; this.p = Process.Start(new ProcessStartInfo @@ -48,34 +51,31 @@ namespace NadekoBot.Services.Music int bytesRead = -1; while (!cancelToken.IsCancellationRequested && bytesRead != 0) { - var toRead = buffer.Length; - //var remCap = _outStream.RemainingCapacity; - //if (remCap < readSize) - //{ - // if (_outStream.RemainingCapacity == 0) - // { - // Console.WriteLine("Buffer full, not gonnna read from ffmpeg"); - // await Task.Delay(20); - // continue; - // } - // toRead = remCap; - //} - - bytesRead = await p.StandardOutput.BaseStream.ReadAsync(buffer, 0, toRead, cancelToken).ConfigureAwait(false); + bytesRead = await p.StandardOutput.BaseStream.ReadAsync(buffer, 0, readSize, cancelToken).ConfigureAwait(false); await _outStream.WriteAsync(buffer, 0, bytesRead, cancelToken); - if (_outStream.RemainingCapacity < _outStream.Capacity * 0.9f) + if (_outStream.RemainingCapacity < _outStream.Capacity * 0.5f) if (toReturn.TrySetResult(true)) - Console.WriteLine("Prebuffering finished"); + _log.Info("Prebuffering finished"); } - Console.WriteLine("FFMPEG killed or song canceled"); + _log.Info("FFMPEG killed, song canceled, or song fully downloaded"); + } + catch (System.ComponentModel.Win32Exception) + { + _log.Error(@"You have not properly installed or configured FFMPEG. +Please install and configure FFMPEG to play music. +Check the guides for your platform on how to setup ffmpeg correctly: + Windows Guide: https://goo.gl/OjKk8F + Linux Guide: https://goo.gl/ShjCUo"); } catch (Exception ex) { - Console.WriteLine(ex); - if(toReturn.TrySetResult(false)) - Console.WriteLine("Prebuffering failed"); - //ignored + _log.Info(ex); + } + finally + { + if (toReturn.TrySetResult(false)) + _log.Info("Prebuffering failed"); } }, cancelToken); @@ -89,7 +89,6 @@ namespace NadekoBot.Services.Music public void Dispose() { - Console.WriteLine("DISPOSING"); try { this.p.Kill(); } catch { } _outStream.Dispose(); @@ -203,7 +202,6 @@ namespace NadekoBot.Services.Music // { // if (outStream != null) // outStream.Dispose(); -// Console.WriteLine($"Buffering done."); // if (p != null) // { // try From 8e1c20624d7d40148056c7fa97c6940262f8fe6f Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Sun, 2 Jul 2017 14:49:37 +0200 Subject: [PATCH 066/346] Fixed bugs, added .play command which acts as .n 1 when used without arguments or as .q command when used with serach query --- .../DataStructures/PoopyRingBuffer.cs | 7 +--- .../Modules/Administration/Administration.cs | 2 +- src/NadekoBot/Modules/Music/Music.cs | 34 +++++++++++++----- src/NadekoBot/NadekoBot.cs | 3 -- src/NadekoBot/Resources/CommandStrings.resx | 9 +++++ src/NadekoBot/Services/Music/Exceptions.cs | 6 ++-- src/NadekoBot/Services/Music/MusicPlayer.cs | 25 ++++++++----- src/NadekoBot/Services/Music/MusicQueue.cs | 35 ++++++++++--------- src/NadekoBot/Services/Music/SongBuffer.cs | 1 + 9 files changed, 76 insertions(+), 46 deletions(-) diff --git a/src/NadekoBot/DataStructures/PoopyRingBuffer.cs b/src/NadekoBot/DataStructures/PoopyRingBuffer.cs index b0b9f19c..28b391e6 100644 --- a/src/NadekoBot/DataStructures/PoopyRingBuffer.cs +++ b/src/NadekoBot/DataStructures/PoopyRingBuffer.cs @@ -1,9 +1,4 @@ -using NadekoBot.Extensions; -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Text; +using System; using System.Threading; using System.Threading.Tasks; diff --git a/src/NadekoBot/Modules/Administration/Administration.cs b/src/NadekoBot/Modules/Administration/Administration.cs index 129970e1..fafd9f48 100644 --- a/src/NadekoBot/Modules/Administration/Administration.cs +++ b/src/NadekoBot/Modules/Administration/Administration.cs @@ -134,7 +134,7 @@ namespace NadekoBot.Modules.Administration await user.RemoveRolesAsync(userRoles).ConfigureAwait(false); await ReplyConfirmLocalized("rar", Format.Bold(user.ToString())).ConfigureAwait(false); } - catch (Exception ex) + catch (Exception) { await ReplyErrorLocalized("rar_err").ConfigureAwait(false); } diff --git a/src/NadekoBot/Modules/Music/Music.cs b/src/NadekoBot/Modules/Music/Music.cs index a8906e62..493e3c08 100644 --- a/src/NadekoBot/Modules/Music/Music.cs +++ b/src/NadekoBot/Modules/Music/Music.cs @@ -84,7 +84,16 @@ namespace NadekoBot.Modules.Music private async Task InternalQueue(MusicPlayer mp, SongInfo songInfo, bool silent) { - var qData = mp.Enqueue(songInfo); + (bool Success, int Index) qData; + try + { + qData = mp.Enqueue(songInfo); + } + catch (QueueFullException) + { + await ReplyErrorLocalized("queue_full", mp.MaxQueueSize).ConfigureAwait(false); + throw; + } if (qData.Success) { if (!silent) @@ -111,8 +120,16 @@ namespace NadekoBot.Modules.Music } } } - - //todo add play command. .play = .n, .play whatever = .q whatever + //todo test play + [NadekoCommand, Usage, Description, Aliases] + [RequireContext(ContextType.Guild)] + public Task Play([Remainder]string query = null) + { + if (string.IsNullOrWhiteSpace(query)) + try { return Queue(query); } catch (QueueFullException) { return Task.CompletedTask; } + else + return Next(); + } [NadekoCommand, Usage, Description, Aliases] [RequireContext(ContextType.Guild)] @@ -120,7 +137,7 @@ namespace NadekoBot.Modules.Music { var mp = await _music.GetOrCreatePlayer(Context); var songInfo = await _music.ResolveSong(query, Context.User.ToString()); - await InternalQueue(mp, songInfo, false); + try { await InternalQueue(mp, songInfo, false); } catch (QueueFullException) { return; } if ((await Context.Guild.GetCurrentUserAsync()).GetPermissions((IGuildChannel)Context.Channel).ManageMessages) { @@ -241,7 +258,7 @@ namespace NadekoBot.Modules.Music var mp = await _music.GetOrCreatePlayer(Context); - mp.Next(); + mp.Next(skipCount); } [NadekoCommand, Usage, Description, Aliases] @@ -520,7 +537,7 @@ namespace NadekoBot.Modules.Music await Context.Channel.EmbedAsync(embed).ConfigureAwait(false); } - //todo test shuffle + [NadekoCommand, Usage, Description, Aliases] [RequireContext(ContextType.Guild)] public async Task ShufflePlaylist() @@ -746,8 +763,7 @@ namespace NadekoBot.Modules.Music //} - - //todo test smq + [NadekoCommand, Usage, Description, Aliases] [RequireContext(ContextType.Guild)] public async Task SetMaxQueue(uint size = 0) @@ -756,7 +772,7 @@ namespace NadekoBot.Modules.Music return; var mp = await _music.GetOrCreatePlayer(Context); - mp.SetMaxQueueSize(size); + mp.MaxQueueSize = size; if (size == 0) await ReplyConfirmLocalized("max_queue_unlimited").ConfigureAwait(false); diff --git a/src/NadekoBot/NadekoBot.cs b/src/NadekoBot/NadekoBot.cs index 00bf8319..855c5652 100644 --- a/src/NadekoBot/NadekoBot.cs +++ b/src/NadekoBot/NadekoBot.cs @@ -212,8 +212,6 @@ namespace NadekoBot var pokemonService = new PokemonService(); #endregion - - //initialize Services Services = new NServiceProvider.ServiceProviderBuilder() .Add(Localization) @@ -269,7 +267,6 @@ namespace NadekoBot .Add(this) .Build(); - CommandHandler.AddServices(Services); //setup typereaders diff --git a/src/NadekoBot/Resources/CommandStrings.resx b/src/NadekoBot/Resources/CommandStrings.resx index 8ddcf04d..d8977412 100644 --- a/src/NadekoBot/Resources/CommandStrings.resx +++ b/src/NadekoBot/Resources/CommandStrings.resx @@ -1467,6 +1467,15 @@ `{0}n` or `{0}n 5` + + play start + + + If no arguments are specified, acts as `{0}next 1` command. If you specify a search query, acts as a `{0}q` command + + + `{0}play` or `{0}play Dream Of Venice` + stop s diff --git a/src/NadekoBot/Services/Music/Exceptions.cs b/src/NadekoBot/Services/Music/Exceptions.cs index af195c15..8d4dab72 100644 --- a/src/NadekoBot/Services/Music/Exceptions.cs +++ b/src/NadekoBot/Services/Music/Exceptions.cs @@ -2,12 +2,12 @@ namespace NadekoBot.Services.Music { - public class PlaylistFullException : Exception + public class QueueFullException : Exception { - public PlaylistFullException(string message) : base(message) + public QueueFullException(string message) : base(message) { } - public PlaylistFullException() : base("Queue is full.") { } + public QueueFullException() : base("Queue is full.") { } } public class SongNotFoundException : Exception diff --git a/src/NadekoBot/Services/Music/MusicPlayer.cs b/src/NadekoBot/Services/Music/MusicPlayer.cs index b31831e8..e49e8967 100644 --- a/src/NadekoBot/Services/Music/MusicPlayer.cs +++ b/src/NadekoBot/Services/Music/MusicPlayer.cs @@ -45,6 +45,11 @@ namespace NadekoBot.Services.Music public bool Shuffle { get; private set; } public bool Autoplay { get; private set; } public bool RepeatPlaylist { get; private set; } = true; + public uint MaxQueueSize + { + get => Queue.MaxQueueSize; + set => Queue.MaxQueueSize = value; + } private IAudioClient _audioClient; private readonly object locker = new object(); @@ -137,7 +142,6 @@ namespace NadekoBot.Services.Music } finally { - _log.Info("Next song"); do { await Task.Delay(500); @@ -146,7 +150,10 @@ namespace NadekoBot.Services.Music if (!RepeatCurrentSong) //if repeating current song, just ignore other settings, and play this song again (don't change the index) { if (Shuffle) + { + _log.Info("Random song"); Queue.Random(); //if shuffle is set, set current song index to a random number + } else { //if last song, and autoplay is enabled, and if it's a youtube song @@ -155,19 +162,25 @@ namespace NadekoBot.Services.Music { try { + _log.Info("Loading related song"); //todo test autoplay await _musicService.TryQueueRelatedSongAsync(data.Song.Query, OutputTextChannel, VoiceChannel); Queue.Next(); } - catch { } + catch + { + _log.Info("Loading related song failed."); + } } else if (Queue.Count == data.Index && !RepeatPlaylist) { //todo test repeatplaylist + _log.Info("Stopping because repeatplaylist is disabled"); Stop(); } else { + _log.Info("Next song"); Queue.Next(); } } @@ -208,10 +221,11 @@ namespace NadekoBot.Services.Music return (true, Queue.Count); } - public void Next() + public void Next(int skipCount) { lock (locker) { + Queue.Next(skipCount - 1); Stopped = false; Unpause(); CancelCurrentSong(); @@ -364,11 +378,6 @@ namespace NadekoBot.Services.Music } } - public void SetMaxQueueSize(uint size) - { - Queue.SetMaxQueueSize(size); - } - public void SetVoiceChannel(IVoiceChannel vch) { VoiceChannel = vch; diff --git a/src/NadekoBot/Services/Music/MusicQueue.cs b/src/NadekoBot/Services/Music/MusicQueue.cs index 0715ef2f..9156d0a7 100644 --- a/src/NadekoBot/Services/Music/MusicQueue.cs +++ b/src/NadekoBot/Services/Music/MusicQueue.cs @@ -50,23 +50,37 @@ namespace NadekoBot.Services.Music } } - public uint maxQueueSize { get; private set; } + private uint _maxQueueSize; + public uint MaxQueueSize + { + get => _maxQueueSize; + set + { + if (value < 0) + throw new ArgumentOutOfRangeException(nameof(value)); + + lock (locker) + { + _maxQueueSize = value; + } + } + } public void Add(SongInfo song) { song.ThrowIfNull(nameof(song)); lock (locker) { - if(maxQueueSize !=0 && CurrentIndex >= maxQueueSize) - throw new PlaylistFullException(); + if(MaxQueueSize != 0 && Songs.Count >= MaxQueueSize) + throw new QueueFullException(); Songs.AddLast(song); } } - public void Next() + public void Next(int skipCount = 1) { lock(locker) - CurrentIndex++; + CurrentIndex += skipCount; } public void Dispose() @@ -133,16 +147,5 @@ namespace NadekoBot.Services.Music CurrentIndex = new NadekoRandom().Next(Songs.Count); } } - - public void SetMaxQueueSize(uint size) - { - if (size < 0) - throw new ArgumentOutOfRangeException(nameof(size)); - - lock (locker) - { - maxQueueSize = size; - } - } } } diff --git a/src/NadekoBot/Services/Music/SongBuffer.cs b/src/NadekoBot/Services/Music/SongBuffer.cs index 953e640a..033ac9fd 100644 --- a/src/NadekoBot/Services/Music/SongBuffer.cs +++ b/src/NadekoBot/Services/Music/SongBuffer.cs @@ -68,6 +68,7 @@ Check the guides for your platform on how to setup ffmpeg correctly: Windows Guide: https://goo.gl/OjKk8F Linux Guide: https://goo.gl/ShjCUo"); } + catch (OperationCanceledException) { } catch (Exception ex) { _log.Info(ex); From 45e4816033e4d061661bd24fabb97edbf78c6fea Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Sun, 2 Jul 2017 15:44:00 +0200 Subject: [PATCH 067/346] (Re)added shuffle, rpl, rcs and autoplay to the top of the .lq --- src/NadekoBot/Modules/Music/Music.cs | 14 ++++++++--- src/NadekoBot/Services/Music/MusicPlayer.cs | 25 ++++++++++++------- .../_strings/ResponseStrings.en-US.json | 1 + 3 files changed, 27 insertions(+), 13 deletions(-) diff --git a/src/NadekoBot/Modules/Music/Music.cs b/src/NadekoBot/Modules/Music/Music.cs index 493e3c08..c1280613 100644 --- a/src/NadekoBot/Modules/Music/Music.cs +++ b/src/NadekoBot/Modules/Music/Music.cs @@ -227,10 +227,18 @@ namespace NadekoBot.Modules.Music desc = $"`🔊` {songs[current].PrettyFullName}\n\n" + desc; + if (mp.RepeatCurrentSong) desc = "🔂 " + GetText("repeating_cur_song") + "\n\n" + desc; else if (mp.Shuffle) desc = "🔀 " + GetText("shuffling_playlist") + "\n\n" + desc; + else + { + if(mp.Autoplay) + desc = "↪ " + GetText("autoplaying") + "\n\n" + desc; + if (mp.RepeatPlaylist) + desc = "🔁 " + GetText("repeating_playlist") + "\n\n" + desc; + } var embed = new EmbedBuilder() .WithAuthor(eab => eab.WithName(GetText("player_queue", curPage + 1, lastPage + 1)) @@ -797,8 +805,7 @@ namespace NadekoBot.Modules.Music // else // await ReplyConfirmLocalized("max_playtime_set", seconds).ConfigureAwait(false); //} - - //todo test rcs + [NadekoCommand, Usage, Description, Aliases] [RequireContext(ContextType.Guild)] public async Task ReptCurSong() @@ -819,8 +826,7 @@ namespace NadekoBot.Modules.Music await Context.Channel.SendConfirmAsync("🔂 " + GetText("repeating_track_stopped")) .ConfigureAwait(false); } - - //todo test rpl + [NadekoCommand, Usage, Description, Aliases] [RequireContext(ContextType.Guild)] public async Task RepeatPl() diff --git a/src/NadekoBot/Services/Music/MusicPlayer.cs b/src/NadekoBot/Services/Music/MusicPlayer.cs index e49e8967..5dd87f67 100644 --- a/src/NadekoBot/Services/Music/MusicPlayer.cs +++ b/src/NadekoBot/Services/Music/MusicPlayer.cs @@ -61,6 +61,9 @@ namespace NadekoBot.Services.Music public event Action OnPauseChanged; #endregion + + private bool manualSkip = false; + public MusicPlayer(MusicService musicService, IVoiceChannel vch, ITextChannel output, float volume) { _log = LogManager.GetCurrentClassLogger(); @@ -80,6 +83,7 @@ namespace NadekoBot.Services.Music { data = Queue.Current; cancelToken = SongCancelSource.Token; + manualSkip = false; } try { @@ -142,12 +146,10 @@ namespace NadekoBot.Services.Music } finally { - do - { - await Task.Delay(500); - } - while (Stopped && !Exited); - if (!RepeatCurrentSong) //if repeating current song, just ignore other settings, and play this song again (don't change the index) + //if repeating current song, just ignore other settings, + // and play this song again (don't change the index) + // ignore rcs if song is manually skipped + if (!RepeatCurrentSong || manualSkip) { if (Shuffle) { @@ -172,9 +174,8 @@ namespace NadekoBot.Services.Music _log.Info("Loading related song failed."); } } - else if (Queue.Count == data.Index && !RepeatPlaylist) + else if (Queue.Count - 1 == data.Index && !RepeatPlaylist && !manualSkip) { - //todo test repeatplaylist _log.Info("Stopping because repeatplaylist is disabled"); Stop(); } @@ -185,6 +186,11 @@ namespace NadekoBot.Services.Music } } } + do + { + await Task.Delay(500); + } + while (Stopped && !Exited); } } }, SongCancelSource.Token); @@ -221,10 +227,11 @@ namespace NadekoBot.Services.Music return (true, Queue.Count); } - public void Next(int skipCount) + public void Next(int skipCount = 1) { lock (locker) { + manualSkip = true; Queue.Next(skipCount - 1); Stopped = false; Unpause(); diff --git a/src/NadekoBot/_strings/ResponseStrings.en-US.json b/src/NadekoBot/_strings/ResponseStrings.en-US.json index ab8aea41..a83d8442 100644 --- a/src/NadekoBot/_strings/ResponseStrings.en-US.json +++ b/src/NadekoBot/_strings/ResponseStrings.en-US.json @@ -404,6 +404,7 @@ "music_attempting_to_queue": "Attempting to queue {0} songs...", "music_autoplay_disabled": "Autoplay disabled.", "music_autoplay_enabled": "Autoplay enabled.", + "music_autoplaying": "Auto-playing.", "music_defvol_set": "Default volume set to {0}%", "music_dir_queue_complete": "Directory queue complete.", "music_fairplay": "fairplay", From b33e4bdd80c3b90f74c42d0f7f159ef12412179f Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Sun, 2 Jul 2017 16:20:25 +0200 Subject: [PATCH 068/346] fixed .play --- src/NadekoBot/Modules/Music/Music.cs | 2 +- src/NadekoBot/Services/Music/MusicPlayer.cs | 3 ++- src/NadekoBot/Services/Music/SongBuffer.cs | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/NadekoBot/Modules/Music/Music.cs b/src/NadekoBot/Modules/Music/Music.cs index c1280613..c2dc3614 100644 --- a/src/NadekoBot/Modules/Music/Music.cs +++ b/src/NadekoBot/Modules/Music/Music.cs @@ -125,7 +125,7 @@ namespace NadekoBot.Modules.Music [RequireContext(ContextType.Guild)] public Task Play([Remainder]string query = null) { - if (string.IsNullOrWhiteSpace(query)) + if (!string.IsNullOrWhiteSpace(query)) try { return Queue(query); } catch (QueueFullException) { return Task.CompletedTask; } else return Next(); diff --git a/src/NadekoBot/Services/Music/MusicPlayer.cs b/src/NadekoBot/Services/Music/MusicPlayer.cs index 5dd87f67..577800b6 100644 --- a/src/NadekoBot/Services/Music/MusicPlayer.cs +++ b/src/NadekoBot/Services/Music/MusicPlayer.cs @@ -232,7 +232,8 @@ namespace NadekoBot.Services.Music lock (locker) { manualSkip = true; - Queue.Next(skipCount - 1); + // if player is stopped, and user uses .n, it should play current song. + // It's a bit weird, but that's the least annoying solution Stopped = false; Unpause(); CancelCurrentSong(); diff --git a/src/NadekoBot/Services/Music/SongBuffer.cs b/src/NadekoBot/Services/Music/SongBuffer.cs index 033ac9fd..fa356bb4 100644 --- a/src/NadekoBot/Services/Music/SongBuffer.cs +++ b/src/NadekoBot/Services/Music/SongBuffer.cs @@ -27,7 +27,7 @@ namespace NadekoBot.Services.Music this.p = Process.Start(new ProcessStartInfo { FileName = "ffmpeg", - Arguments = $"-ss 0 -i {songUri} -f s16le -ar 48000 -vn -ac 2 pipe:1 -loglevel quiet", + Arguments = $"-i {songUri} -f s16le -ar 48000 -vn -ac 2 pipe:1 -loglevel quiet", UseShellExecute = false, RedirectStandardOutput = true, RedirectStandardError = true, From 322424b0a1f94d30072550c6ab88f32ec7d92366 Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Sun, 2 Jul 2017 16:27:30 +0200 Subject: [PATCH 069/346] .move fixed, but it will cancel current song when moving --- src/NadekoBot/Modules/Music/Music.cs | 5 ++--- src/NadekoBot/Services/Music/MusicPlayer.cs | 6 +++++- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/NadekoBot/Modules/Music/Music.cs b/src/NadekoBot/Modules/Music/Music.cs index c2dc3614..8ca46e0a 100644 --- a/src/NadekoBot/Modules/Music/Music.cs +++ b/src/NadekoBot/Modules/Music/Music.cs @@ -120,7 +120,7 @@ namespace NadekoBot.Modules.Music } } } - //todo test play + [NadekoCommand, Usage, Description, Aliases] [RequireContext(ContextType.Guild)] public Task Play([Remainder]string query = null) @@ -722,7 +722,7 @@ namespace NadekoBot.Modules.Music if (mp == null) return; - //todo test move + mp.SetVoiceChannel(vch); } @@ -886,7 +886,6 @@ namespace NadekoBot.Modules.Music await ReplyConfirmLocalized("autoplay_enabled").ConfigureAwait(false); } - //todo test output text channel [NadekoCommand, Usage, Description, Aliases] [RequireContext(ContextType.Guild)] [RequireUserPermission(GuildPermission.ManageMessages)] diff --git a/src/NadekoBot/Services/Music/MusicPlayer.cs b/src/NadekoBot/Services/Music/MusicPlayer.cs index 577800b6..97ef8ee7 100644 --- a/src/NadekoBot/Services/Music/MusicPlayer.cs +++ b/src/NadekoBot/Services/Music/MusicPlayer.cs @@ -63,6 +63,7 @@ namespace NadekoBot.Services.Music private bool manualSkip = false; + private bool newVoiceChannel = false; public MusicPlayer(MusicService musicService, IVoiceChannel vch, ITextChannel output, float volume) { @@ -200,7 +201,8 @@ namespace NadekoBot.Services.Music { if (_audioClient == null || _audioClient.ConnectionState != ConnectionState.Connected || - reconnect) + reconnect || + newVoiceChannel) try { try @@ -211,6 +213,7 @@ namespace NadekoBot.Services.Music catch { } + newVoiceChannel = false; _audioClient = await VoiceChannel.ConnectAsync(); } catch @@ -389,6 +392,7 @@ namespace NadekoBot.Services.Music public void SetVoiceChannel(IVoiceChannel vch) { VoiceChannel = vch; + newVoiceChannel = true; Next(); } From 1d1b7de20aa818a705afce837e2f1075b828759a Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Sun, 2 Jul 2017 16:54:09 +0200 Subject: [PATCH 070/346] Fixed playing small songs --- src/NadekoBot/Modules/Music/Music.cs | 17 ++++++++++------- src/NadekoBot/Services/Music/MusicPlayer.cs | 16 ++++++++++++---- src/NadekoBot/Services/Music/SongBuffer.cs | 6 +++--- 3 files changed, 25 insertions(+), 14 deletions(-) diff --git a/src/NadekoBot/Modules/Music/Music.cs b/src/NadekoBot/Modules/Music/Music.cs index 8ca46e0a..a56d5cfa 100644 --- a/src/NadekoBot/Modules/Music/Music.cs +++ b/src/NadekoBot/Modules/Music/Music.cs @@ -109,7 +109,7 @@ namespace NadekoBot.Modules.Music .ConfigureAwait(false); if (mp.Stopped) { - (await ReplyErrorLocalized("music_queue_stopped", Format.Code(Prefix + "play")).ConfigureAwait(false)).DeleteAfter(10); + (await ReplyErrorLocalized("queue_stopped", Format.Code(Prefix + "play")).ConfigureAwait(false)).DeleteAfter(10); } queuedMessage?.DeleteAfter(10); } @@ -227,18 +227,21 @@ namespace NadekoBot.Modules.Music desc = $"`🔊` {songs[current].PrettyFullName}\n\n" + desc; - + var add = ""; if (mp.RepeatCurrentSong) - desc = "🔂 " + GetText("repeating_cur_song") + "\n\n" + desc; + add += "🔂 " + GetText("repeating_cur_song") + "\n"; else if (mp.Shuffle) - desc = "🔀 " + GetText("shuffling_playlist") + "\n\n" + desc; + add += "🔀 " + GetText("shuffling_playlist") + "\n"; else { - if(mp.Autoplay) - desc = "↪ " + GetText("autoplaying") + "\n\n" + desc; if (mp.RepeatPlaylist) - desc = "🔁 " + GetText("repeating_playlist") + "\n\n" + desc; + add += "🔁 " + GetText("repeating_playlist") + "\n"; + if (mp.Autoplay) + add += "↪ " + GetText("autoplaying") + "\n"; } + + if (!string.IsNullOrWhiteSpace(add)) + desc += add + "\n"; var embed = new EmbedBuilder() .WithAuthor(eab => eab.WithName(GetText("player_queue", curPage + 1, lastPage + 1)) diff --git a/src/NadekoBot/Services/Music/MusicPlayer.cs b/src/NadekoBot/Services/Music/MusicPlayer.cs index 97ef8ee7..48a037be 100644 --- a/src/NadekoBot/Services/Music/MusicPlayer.cs +++ b/src/NadekoBot/Services/Music/MusicPlayer.cs @@ -94,10 +94,18 @@ namespace NadekoBot.Services.Music _log.Info("Starting"); using (var b = new SongBuffer(data.Song.Uri, "")) { - var bufferSuccess = await b.StartBuffering(cancelToken); - - if (bufferSuccess == false) + var bufferTask = b.StartBuffering(cancelToken); + var timeout = Task.Delay(10000); + if (Task.WhenAny(bufferTask, timeout) == timeout) + { + _log.Info("Buffering failed due to a timeout."); continue; + } + else if (!bufferTask.Result) + { + _log.Info("Buffering failed due to a cancel or error."); + continue; + } var ac = await GetAudioClient(); if (ac == null) @@ -161,7 +169,7 @@ namespace NadekoBot.Services.Music { //if last song, and autoplay is enabled, and if it's a youtube song // do autplay magix - if (Queue.Count == data.Index && Autoplay && data.Song?.Provider == "YouTube") + if (Queue.Count - 1 == data.Index && Autoplay && data.Song?.Provider == "YouTube") { try { diff --git a/src/NadekoBot/Services/Music/SongBuffer.cs b/src/NadekoBot/Services/Music/SongBuffer.cs index fa356bb4..0e507344 100644 --- a/src/NadekoBot/Services/Music/SongBuffer.cs +++ b/src/NadekoBot/Services/Music/SongBuffer.cs @@ -48,13 +48,13 @@ namespace NadekoBot.Services.Music try { byte[] buffer = new byte[readSize]; - int bytesRead = -1; - while (!cancelToken.IsCancellationRequested && bytesRead != 0) + int bytesRead = 1; + while (!cancelToken.IsCancellationRequested && !this.p.HasExited && bytesRead > 0) { bytesRead = await p.StandardOutput.BaseStream.ReadAsync(buffer, 0, readSize, cancelToken).ConfigureAwait(false); await _outStream.WriteAsync(buffer, 0, bytesRead, cancelToken); - if (_outStream.RemainingCapacity < _outStream.Capacity * 0.5f) + if (_outStream.RemainingCapacity < _outStream.Capacity * 0.5f || bytesRead == 0) if (toReturn.TrySetResult(true)) _log.Info("Prebuffering finished"); } From 728aeab8093687ecf38654c5a12a49df434bd506 Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Sun, 2 Jul 2017 17:15:34 +0200 Subject: [PATCH 071/346] Fixes. .lq will say at the top if it's stopped. .lq will also default to the page current song is playing from --- src/NadekoBot/Modules/Music/Music.cs | 19 ++++++++++++------- .../Services/Games/ChatterbotService.cs | 1 - src/NadekoBot/Services/Music/MusicPlayer.cs | 3 ++- src/NadekoBot/Services/Music/MusicService.cs | 12 ------------ .../_strings/ResponseStrings.en-US.json | 1 + 5 files changed, 15 insertions(+), 21 deletions(-) diff --git a/src/NadekoBot/Modules/Music/Music.cs b/src/NadekoBot/Modules/Music/Music.cs index a56d5cfa..fbd01028 100644 --- a/src/NadekoBot/Modules/Music/Music.cs +++ b/src/NadekoBot/Modules/Music/Music.cs @@ -181,11 +181,10 @@ namespace NadekoBot.Modules.Music try { await msg.DeleteAsync().ConfigureAwait(false); } catch { } } } - - //todo, page should default to the page the current song is on + [NadekoCommand, Usage, Description, Aliases] [RequireContext(ContextType.Guild)] - public async Task ListQueue(int page = 1) + public async Task ListQueue(int page = 0) { var mp = await _music.GetOrCreatePlayer(Context); var (current, songs) = mp.QueueArray(); @@ -195,14 +194,18 @@ namespace NadekoBot.Modules.Music await ReplyErrorLocalized("no_player").ConfigureAwait(false); return; } - - if (--page < 0) + + if (--page < -1) return; - //todo say whether music player is stopped //try { await musicPlayer.UpdateSongDurationsAsync().ConfigureAwait(false); } catch { } const int itemsPerPage = 10; + if (page == -1) + page = current / itemsPerPage; + + //if page is 0 (-1 after this decrement) that means default to the page current song is playing from + //var total = musicPlayer.TotalPlaytime; //var totalStr = total == TimeSpan.MaxValue ? "∞" : GetText("time_format", // (int)total.TotalHours, @@ -228,6 +231,8 @@ namespace NadekoBot.Modules.Music desc = $"`🔊` {songs[current].PrettyFullName}\n\n" + desc; var add = ""; + if (mp.Stopped) + add += Format.Bold(GetText("queue_stopped", Format.Code(Prefix + "play"))) + "\n"; if (mp.RepeatCurrentSong) add += "🔂 " + GetText("repeating_cur_song") + "\n"; else if (mp.Shuffle) @@ -345,7 +350,7 @@ namespace NadekoBot.Modules.Music } catch (ArgumentOutOfRangeException) { - //todo error message + await ReplyErrorLocalized("removed_song_error").ConfigureAwait(false); } } diff --git a/src/NadekoBot/Services/Games/ChatterbotService.cs b/src/NadekoBot/Services/Games/ChatterbotService.cs index 833eef0b..7f9c8251 100644 --- a/src/NadekoBot/Services/Games/ChatterbotService.cs +++ b/src/NadekoBot/Services/Games/ChatterbotService.cs @@ -104,7 +104,6 @@ namespace NadekoBot.Services.Games { if (pc.Verbose) { - //todo move this to permissions var returnMsg = _strings.GetText("trigger", guild.Id, "Permissions".ToLowerInvariant(), index + 1, Format.Bold(pc.Permissions[index].GetCommand(_cmd.GetPrefix(guild), (SocketGuild)guild))); try { await usrMsg.Channel.SendErrorAsync(returnMsg).ConfigureAwait(false); } catch { } _log.Info(returnMsg); diff --git a/src/NadekoBot/Services/Music/MusicPlayer.cs b/src/NadekoBot/Services/Music/MusicPlayer.cs index 48a037be..e9f10ab3 100644 --- a/src/NadekoBot/Services/Music/MusicPlayer.cs +++ b/src/NadekoBot/Services/Music/MusicPlayer.cs @@ -174,7 +174,6 @@ namespace NadekoBot.Services.Music try { _log.Info("Loading related song"); - //todo test autoplay await _musicService.TryQueueRelatedSongAsync(data.Song.Query, OutputTextChannel, VoiceChannel); Queue.Next(); } @@ -245,6 +244,8 @@ namespace NadekoBot.Services.Music manualSkip = true; // if player is stopped, and user uses .n, it should play current song. // It's a bit weird, but that's the least annoying solution + if (!Stopped) + Queue.Next(skipCount - 1); Stopped = false; Unpause(); CancelCurrentSong(); diff --git a/src/NadekoBot/Services/Music/MusicService.cs b/src/NadekoBot/Services/Music/MusicService.cs index ccc70dbc..03f7b0d9 100644 --- a/src/NadekoBot/Services/Music/MusicService.cs +++ b/src/NadekoBot/Services/Music/MusicService.cs @@ -108,18 +108,6 @@ namespace NadekoBot.Services.Music { // ignored } - - //todo autoplay should be independent from event handlers - //if (mp.Autoplay && mp.Playlist.Count == 0 && song.SongInfo.ProviderType == MusicType.Normal) - //{ - // var relatedVideos = (await _google.GetRelatedVideosAsync(song.SongInfo.Query, 4)).ToList(); - // if (relatedVideos.Count > 0) - // await QueueSong(await textCh.Guild.GetCurrentUserAsync(), - // textCh, - // voiceCh, - // relatedVideos[new NadekoRandom().Next(0, relatedVideos.Count)], - // true).ConfigureAwait(false); - //} } catch { diff --git a/src/NadekoBot/_strings/ResponseStrings.en-US.json b/src/NadekoBot/_strings/ResponseStrings.en-US.json index a83d8442..705b3c34 100644 --- a/src/NadekoBot/_strings/ResponseStrings.en-US.json +++ b/src/NadekoBot/_strings/ResponseStrings.en-US.json @@ -440,6 +440,7 @@ "music_queue_full": "Queue is full at {0}/{0}.", "music_queue_stopped": "Player is stopped. Use {0} command to start playing.", "music_removed_song": "Removed song", + "music_removed_song_error": "Song on that index doesn't exist", "music_repeating_cur_song": "Repeating current song", "music_repeating_playlist": "Repeating playlist", "music_repeating_track": "Repeating track", From 196f40e648b888a25bdd2e92f24c12b89fd5ad4d Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Sun, 2 Jul 2017 19:00:25 +0200 Subject: [PATCH 072/346] readded .sq and .lo and .lopl, also .lopl will now explicitly avoid files with .jpg and .png extension (usually album images) --- src/NadekoBot/Modules/Music/Music.cs | 208 +++++----- .../Services/Database/Models/PlaylistSong.cs | 2 +- src/NadekoBot/Services/Music/MusicPlayer.cs | 8 +- src/NadekoBot/Services/Music/MusicService.cs | 369 +++++++++--------- src/NadekoBot/Services/Music/SongInfo.cs | 8 +- 5 files changed, 301 insertions(+), 294 deletions(-) diff --git a/src/NadekoBot/Modules/Music/Music.cs b/src/NadekoBot/Modules/Music/Music.cs index fbd01028..a1078a0f 100644 --- a/src/NadekoBot/Modules/Music/Music.cs +++ b/src/NadekoBot/Modules/Music/Music.cs @@ -12,6 +12,9 @@ using NadekoBot.Services.Database.Models; using NadekoBot.Services.Music; using NadekoBot.DataStructures; using System.Collections.Concurrent; +using System.IO; +using System.Net.Http; +using Newtonsoft.Json.Linq; namespace NadekoBot.Modules.Music { @@ -36,6 +39,8 @@ namespace NadekoBot.Modules.Music //_client.UserVoiceStateUpdated += Client_UserVoiceStateUpdated; } + //todo when someone drags nadeko from one voice channel to another + //private Task Client_UserVoiceStateUpdated(SocketUser iusr, SocketVoiceState oldState, SocketVoiceState newState) //{ // var usr = iusr as SocketGuildUser; @@ -84,6 +89,13 @@ namespace NadekoBot.Modules.Music private async Task InternalQueue(MusicPlayer mp, SongInfo songInfo, bool silent) { + if (songInfo == null) + { + if(!silent) + await ReplyErrorLocalized("song_not_found").ConfigureAwait(false); + return; + } + (bool Success, int Index) qData; try { @@ -137,6 +149,7 @@ namespace NadekoBot.Modules.Music { var mp = await _music.GetOrCreatePlayer(Context); var songInfo = await _music.ResolveSong(query, Context.User.ToString()); + try { await InternalQueue(mp, songInfo, false); } catch (QueueFullException) { return; } if ((await Context.Guild.GetCurrentUserAsync()).GetPermissions((IGuildChannel)Context.Channel).ManageMessages) @@ -486,9 +499,8 @@ namespace NadekoBot.Modules.Music try { await Task.Yield(); - //todo fix for all - if (item.ProviderType == MusicType.Normal) - await Task.WhenAll(Task.Delay(1000), InternalQueue(mp, await _music.ResolveSong(item.Query, Context.User.ToString(), item.ProviderType), true)).ConfigureAwait(false); + + await Task.WhenAll(Task.Delay(1000), InternalQueue(mp, await _music.ResolveSong(item.Query, Context.User.ToString(), item.ProviderType), true)).ConfigureAwait(false); } catch (SongNotFoundException) { } catch { break; } @@ -524,17 +536,43 @@ namespace NadekoBot.Modules.Music // } //} - //[NadekoCommand, Usage, Description, Aliases] - //[RequireContext(ContextType.Guild)] - //public async Task SoundCloudQueue([Remainder] string query) - //{ - // await _music.QueueSong(((IGuildUser)Context.User), (ITextChannel)Context.Channel, ((IGuildUser)Context.User).VoiceChannel, query, musicType: MusicType.Soundcloud).ConfigureAwait(false); - // if ((await Context.Guild.GetCurrentUserAsync()).GetPermissions((IGuildChannel)Context.Channel).ManageMessages) - // { - // Context.Message.DeleteAfter(10); - // } - //} + [NadekoCommand, Usage, Description, Aliases] + [RequireContext(ContextType.Guild)] + public async Task SoundCloudQueue([Remainder] string query) + { + var mp = await _music.GetOrCreatePlayer(Context); + var song = await _music.ResolveSong(query, Context.User.ToString(), MusicType.Soundcloud); + await InternalQueue(mp, song, false).ConfigureAwait(false); + } + //todo test soundcloudpl + [NadekoCommand, Usage, Description, Aliases] + [RequireContext(ContextType.Guild)] + public async Task SoundCloudPl([Remainder] string pl) + { + pl = pl?.Trim(); + + if (string.IsNullOrWhiteSpace(pl)) + return; + + var mp = await _music.GetOrCreatePlayer(Context); + + using (var http = new HttpClient()) + { + var scvids = JObject.Parse(await http.GetStringAsync($"https://scapi.nadekobot.me/resolve?url={pl}").ConfigureAwait(false))["tracks"].ToObject(); + + foreach (var svideo in scvids) + { + try + { + await InternalQueue(mp, await _music.SongInfoFromSVideo(svideo, Context.User.ToString()), true); + } + catch { break; } + } + } + } + + //todo fix playlist sync stuff [NadekoCommand, Usage, Description, Aliases] [RequireContext(ContextType.Guild)] public async Task NowPlaying() @@ -619,103 +657,59 @@ namespace NadekoBot.Modules.Music // await msg.ModifyAsync(m => m.Content = "✅ " + Format.Bold(GetText("playlist_queue_complete"))).ConfigureAwait(false); //} - //[NadekoCommand, Usage, Description, Aliases] - //[RequireContext(ContextType.Guild)] - //public async Task SoundCloudPl([Remainder] string pl) - //{ - // pl = pl?.Trim(); + [NadekoCommand, Usage, Description, Aliases] + [RequireContext(ContextType.Guild)] + public async Task Radio(string radioLink) + { + var mp = await _music.GetOrCreatePlayer(Context); + var song = await _music.ResolveSong(radioLink, Context.User.ToString(), MusicType.Radio); + await InternalQueue(mp, song, false).ConfigureAwait(false); + } - // if (string.IsNullOrWhiteSpace(pl)) - // return; + [NadekoCommand, Usage, Description, Aliases] + [RequireContext(ContextType.Guild)] + [OwnerOnly] + public async Task Local([Remainder] string path) + { + var mp = await _music.GetOrCreatePlayer(Context); + var song = await _music.ResolveSong(path, Context.User.ToString(), MusicType.Local); + await InternalQueue(mp, song, false).ConfigureAwait(false); + } + //todo test localpl + [NadekoCommand, Usage, Description, Aliases] + [RequireContext(ContextType.Guild)] + [OwnerOnly] + public async Task LocalPl([Remainder] string dirPath) + { + if (string.IsNullOrWhiteSpace(dirPath)) + return; - // using (var http = new HttpClient()) - // { - // var scvids = JObject.Parse(await http.GetStringAsync($"https://scapi.nadekobot.me/resolve?url={pl}").ConfigureAwait(false))["tracks"].ToObject(); - // await _music.QueueSong(((IGuildUser)Context.User), (ITextChannel)Context.Channel, ((IGuildUser)Context.User).VoiceChannel, scvids[0].TrackLink).ConfigureAwait(false); + var mp = await _music.GetOrCreatePlayer(Context); - // MusicPlayer musicPlayer; - // if ((musicPlayer = _music.GetPlayer(Context.Guild.Id)) == null) - // return; - - // foreach (var svideo in scvids.Skip(1)) - // { - // try - // { - // musicPlayer.AddSong(new Song(new SongInfo - // { - // Title = svideo.FullName, - // Provider = "SoundCloud", - // Uri = await svideo.StreamLink(), - // ProviderType = MusicType.Normal, - // Query = svideo.TrackLink, - // }), ((IGuildUser)Context.User).Username); - // } - // catch (PlaylistFullException) { break; } - // } - // } - //} - - //[NadekoCommand, Usage, Description, Aliases] - //[RequireContext(ContextType.Guild)] - //[OwnerOnly] - //public async Task LocalPl([Remainder] string directory) - //{ - - // var arg = directory; - // if (string.IsNullOrWhiteSpace(arg)) - // return; - // var dir = new DirectoryInfo(arg); - // var fileEnum = dir.GetFiles("*", SearchOption.AllDirectories) - // .Where(x => !x.Attributes.HasFlag(FileAttributes.Hidden | FileAttributes.System)); - // var gusr = (IGuildUser)Context.User; - // foreach (var file in fileEnum) - // { - // try - // { - // await _music.QueueSong(gusr, (ITextChannel)Context.Channel, gusr.VoiceChannel, file.FullName, true, MusicType.Local).ConfigureAwait(false); - // } - // catch (PlaylistFullException) - // { - // break; - // } - // catch - // { - // // ignored - // } - // } - // await ReplyConfirmLocalized("dir_queue_complete").ConfigureAwait(false); - //} - - //[NadekoCommand, Usage, Description, Aliases] - //[RequireContext(ContextType.Guild)] - //public async Task Radio(string radioLink) - //{ - - // if (((IGuildUser)Context.User).VoiceChannel?.Guild != Context.Guild) - // { - // await ReplyErrorLocalized("must_be_in_voice").ConfigureAwait(false); - // return; - // } - // await _music.QueueSong(((IGuildUser)Context.User), (ITextChannel)Context.Channel, ((IGuildUser)Context.User).VoiceChannel, radioLink, musicType: MusicType.Radio).ConfigureAwait(false); - // if ((await Context.Guild.GetCurrentUserAsync()).GetPermissions((IGuildChannel)Context.Channel).ManageMessages) - // { - // Context.Message.DeleteAfter(10); - // } - //} - - //[NadekoCommand, Usage, Description, Aliases] - //[RequireContext(ContextType.Guild)] - //[OwnerOnly] - //public async Task Local([Remainder] string path) - //{ - - // var arg = path; - // if (string.IsNullOrWhiteSpace(arg)) - // return; - // await _music.QueueSong(((IGuildUser)Context.User), (ITextChannel)Context.Channel, ((IGuildUser)Context.User).VoiceChannel, path, musicType: MusicType.Local).ConfigureAwait(false); - - //} + DirectoryInfo dir; + try { dir = new DirectoryInfo(dirPath); } catch { return; } + var fileEnum = dir.GetFiles("*", SearchOption.AllDirectories) + .Where(x => !x.Attributes.HasFlag(FileAttributes.Hidden | FileAttributes.System) && x.Extension != ".jpg" && x.Extension != ".png"); + foreach (var file in fileEnum) + { + try + { + var song = await _music.ResolveSong(file.FullName, Context.User.ToString(), MusicType.Local); + await InternalQueue(mp, song, true).ConfigureAwait(false); + } + catch (QueueFullException) + { + break; + } + catch (Exception ex) + { + _log.Warn(ex); + break; + } + } + await ReplyConfirmLocalized("dir_queue_complete").ConfigureAwait(false); + } [NadekoCommand, Usage, Description, Aliases] [RequireContext(ContextType.Guild)] @@ -776,8 +770,6 @@ namespace NadekoBot.Modules.Music // await Context.Channel.EmbedAsync(embed).ConfigureAwait(false); // //await channel.SendConfirmAsync($"🎵Moved {s.PrettyName} `from #{n1} to #{n2}`").ConfigureAwait(false); - - //} [NadekoCommand, Usage, Description, Aliases] diff --git a/src/NadekoBot/Services/Database/Models/PlaylistSong.cs b/src/NadekoBot/Services/Database/Models/PlaylistSong.cs index d1b09f9d..f938d242 100644 --- a/src/NadekoBot/Services/Database/Models/PlaylistSong.cs +++ b/src/NadekoBot/Services/Database/Models/PlaylistSong.cs @@ -12,7 +12,7 @@ public enum MusicType { Radio, - Normal, + YouTube, Local, Soundcloud } diff --git a/src/NadekoBot/Services/Music/MusicPlayer.cs b/src/NadekoBot/Services/Music/MusicPlayer.cs index e9f10ab3..38babb9c 100644 --- a/src/NadekoBot/Services/Music/MusicPlayer.cs +++ b/src/NadekoBot/Services/Music/MusicPlayer.cs @@ -115,10 +115,10 @@ namespace NadekoBot.Services.Music // i don't want to spam connection attempts continue; } - var pcm = ac.CreatePCMStream(AudioApplication.Music, bufferMillis: 1000); + var pcm = ac.CreatePCMStream(AudioApplication.Music, bufferMillis: 200); OnStarted?.Invoke(this, data.Song); - byte[] buffer = new byte[38400]; + byte[] buffer = new byte[3840]; int bytesRead = 0; try { @@ -127,7 +127,7 @@ namespace NadekoBot.Services.Music var vol = Volume; if (vol != 1) AdjustVolume(buffer, vol); - await Task.WhenAll(Task.Delay(10), pcm.WriteAsync(buffer, 0, bytesRead, cancelToken)).ConfigureAwait(false); + await pcm.WriteAsync(buffer, 0, bytesRead, cancelToken).ConfigureAwait(false); await (pauseTaskSource?.Task ?? Task.CompletedTask); } @@ -169,7 +169,7 @@ namespace NadekoBot.Services.Music { //if last song, and autoplay is enabled, and if it's a youtube song // do autplay magix - if (Queue.Count - 1 == data.Index && Autoplay && data.Song?.Provider == "YouTube") + if (Queue.Count - 1 == data.Index && Autoplay && data.Song?.ProviderType == Database.Models.MusicType.YouTube) { try { diff --git a/src/NadekoBot/Services/Music/MusicService.cs b/src/NadekoBot/Services/Music/MusicService.cs index 03f7b0d9..a7f9ef46 100644 --- a/src/NadekoBot/Services/Music/MusicService.cs +++ b/src/NadekoBot/Services/Music/MusicService.cs @@ -11,6 +11,8 @@ using VideoLibrary; using System.Collections.Generic; using Discord.Commands; using Discord.WebSocket; +using System.Text.RegularExpressions; +using System.Net.Http; namespace NadekoBot.Services.Music { @@ -156,24 +158,7 @@ namespace NadekoBot.Services.Music // ignored } }; - //mp.SongRemoved += async (song, index) => - //{ - // try - // { - // var embed = new EmbedBuilder() - // .WithAuthor(eab => eab.WithName(GetText("removed_song") + " #" + (index + 1)).WithMusicIcon()) - // .WithDescription(song.PrettyName) - // .WithFooter(ef => ef.WithText(song.PrettyInfo)) - // .WithErrorColor(); - - // await mp.OutputTextChannel.EmbedAsync(embed).ConfigureAwait(false); - - // } - // catch - // { - // // ignored - // } - //}; + return mp; }); } @@ -192,27 +177,109 @@ namespace NadekoBot.Services.Music if (!related.Any()) return; - var si = await ResolveSong(related[new NadekoRandom().Next(related.Length)], _client.CurrentUser.ToString(), MusicType.Normal); + var si = await ResolveSong(related[new NadekoRandom().Next(related.Length)], _client.CurrentUser.ToString(), MusicType.YouTube); + if (si == null) + throw new SongNotFoundException(); var mp = await GetOrCreatePlayer(txtCh.GuildId, vch, txtCh); mp.Enqueue(si); } - public async Task ResolveSong(string query, string queuerName, MusicType musicType = MusicType.Normal) + public async Task ResolveSong(string query, string queuerName, MusicType? musicType = null) { query.ThrowIfNull(nameof(query)); - SongInfo sinfo; - - sinfo = await ResolveYoutubeSong(query, queuerName).ConfigureAwait(false); + SongInfo sinfo = null; + switch (musicType) + { + case MusicType.YouTube: + sinfo = await ResolveYoutubeSong(query, queuerName); + break; + case MusicType.Radio: + try { sinfo = ResolveRadioSong(IsRadioLink(query) ? await HandleStreamContainers(query) : query, queuerName); } catch { }; + break; + case MusicType.Local: + sinfo = ResolveLocalSong(query, queuerName); + break; + case MusicType.Soundcloud: + sinfo = await ResolveSoundCloudSong(query, queuerName); + break; + case null: + if (_sc.IsSoundCloudLink(query)) + sinfo = await ResolveSoundCloudSong(query, queuerName); + else if (IsRadioLink(query)) + sinfo = ResolveRadioSong(await HandleStreamContainers(query), queuerName); + else + try + { + sinfo = await ResolveYoutubeSong(query, queuerName); + } + catch + { + sinfo = null; + } + break; + } return sinfo; } + public async Task ResolveSoundCloudSong(string query, string queuerName) + { + var svideo = !_sc.IsSoundCloudLink(query) ? + await _sc.GetVideoByQueryAsync(query).ConfigureAwait(false): + await _sc.ResolveVideoAsync(query).ConfigureAwait(false); + + if (svideo == null) + return null; + return await SongInfoFromSVideo(svideo, queuerName); + } + + public async Task SongInfoFromSVideo(SoundCloudVideo svideo, string queuerName) => + new SongInfo + { + Title = svideo.FullName, + Provider = "SoundCloud", + Uri = await svideo.StreamLink().ConfigureAwait(false), + ProviderType = MusicType.Soundcloud, + Query = svideo.TrackLink, + AlbumArt = svideo.artwork_url, + QueuerName = queuerName + }; + + public SongInfo ResolveLocalSong(string query, string queuerName) + { + return new SongInfo + { + Uri = "\"" + Path.GetFullPath(query) + "\"", + Title = Path.GetFileNameWithoutExtension(query), + Provider = "Local File", + ProviderType = MusicType.Local, + Query = query, + QueuerName = queuerName + }; + } + + public SongInfo ResolveRadioSong(string query, string queuerName) + { + return new SongInfo + { + Uri = query, + Title = query, + Provider = "Radio Stream", + ProviderType = MusicType.Radio, + Query = query, + QueuerName = queuerName + }; + } + public async Task ResolveYoutubeSong(string query, string queuerName) { var link = (await _google.GetVideoLinksByKeywordAsync(query).ConfigureAwait(false)).FirstOrDefault(); if (string.IsNullOrWhiteSpace(link)) - throw new OperationCanceledException("Not a valid youtube query."); + { + _log.Info("No song found."); + return null; + } var allVideos = await Task.Run(async () => { try { return await YouTube.Default.GetAllVideosAsync(link).ConfigureAwait(false); } catch { return Enumerable.Empty(); } }).ConfigureAwait(false); var videos = allVideos.Where(v => v.AdaptiveKind == AdaptiveKind.Audio); var video = videos @@ -221,7 +288,7 @@ namespace NadekoBot.Services.Music .FirstOrDefault(); if (video == null) // do something with this error - throw new Exception("Could not load any video elements based on the query."); + _log.Info("Could not load any video elements based on the query."); //var m = Regex.Match(query, @"\?t=(?\d*)"); //int gotoTime = 0; //if (m.Captures.Count > 0) @@ -232,171 +299,119 @@ namespace NadekoBot.Services.Music Provider = "YouTube", Uri = await video.GetUriAsync().ConfigureAwait(false), Query = link, - ProviderType = MusicType.Normal, + ProviderType = MusicType.YouTube, QueuerName = queuerName }; return song; } + private bool IsRadioLink(string query) => + (query.StartsWith("http") || + query.StartsWith("ww")) + && + (query.Contains(".pls") || + query.Contains(".m3u") || + query.Contains(".asx") || + query.Contains(".xspf")); + public async Task DestroyPlayer(ulong id) { if (MusicPlayers.TryRemove(id, out var mp)) await mp.Destroy(); } - // public async Task QueueSong(IGuildUser queuer, ITextChannel textCh, IVoiceChannel voiceCh, string query, bool silent = false, MusicType musicType = MusicType.Normal) - // { - // string GetText(string text, params object[] replacements) => - // _strings.GetText(text, _localization.GetCultureInfo(textCh.Guild), "Music".ToLowerInvariant(), replacements); + private readonly Regex plsRegex = new Regex("File1=(?.*?)\\n", RegexOptions.Compiled); + private readonly Regex m3uRegex = new Regex("(?^[^#].*)", RegexOptions.Compiled | RegexOptions.Multiline); + private readonly Regex asxRegex = new Regex(".*?)\"", RegexOptions.Compiled); + private readonly Regex xspfRegex = new Regex("(?.*?)", RegexOptions.Compiled); - //if (string.IsNullOrWhiteSpace(query) || query.Length< 3) - // throw new ArgumentException("Invalid song query.", nameof(query)); + private async Task HandleStreamContainers(string query) + { + string file = null; + try + { + using (var http = new HttpClient()) + { + file = await http.GetStringAsync(query).ConfigureAwait(false); + } + } + catch + { + return query; + } + if (query.Contains(".pls")) + { + //File1=http://armitunes.com:8000/ + //Regex.Match(query) + try + { + var m = plsRegex.Match(file); + var res = m.Groups["url"]?.ToString(); + return res?.Trim(); + } + catch + { + _log.Warn($"Failed reading .pls:\n{file}"); + return null; + } + } + if (query.Contains(".m3u")) + { + /* +# This is a comment + C:\xxx4xx\xxxxxx3x\xx2xxxx\xx.mp3 + C:\xxx5xx\x6xxxxxx\x7xxxxx\xx.mp3 + */ + try + { + var m = m3uRegex.Match(file); + var res = m.Groups["url"]?.ToString(); + return res?.Trim(); + } + catch + { + _log.Warn($"Failed reading .m3u:\n{file}"); + return null; + } - // var musicPlayer = GetOrCreatePlayer(textCh.Guild.Id, voiceCh, textCh); - // Song resolvedSong; - // try - // { - // musicPlayer.ThrowIfQueueFull(); - // resolvedSong = await ResolveSong(query, musicType).ConfigureAwait(false); + } + if (query.Contains(".asx")) + { + // + try + { + var m = asxRegex.Match(file); + var res = m.Groups["url"]?.ToString(); + return res?.Trim(); + } + catch + { + _log.Warn($"Failed reading .asx:\n{file}"); + return null; + } + } + if (query.Contains(".xspf")) + { + /* + + + + file:///mp3s/song_1.mp3 + */ + try + { + var m = xspfRegex.Match(file); + var res = m.Groups["url"]?.ToString(); + return res?.Trim(); + } + catch + { + _log.Warn($"Failed reading .xspf:\n{file}"); + return null; + } + } - // if (resolvedSong == null) - // throw new SongNotFoundException(); - - // musicPlayer.AddSong(resolvedSong, queuer.Username); - // } - // catch (PlaylistFullException) - // { - // try - // { - // await textCh.SendConfirmAsync(GetText("queue_full", musicPlayer.MaxQueueSize)); - // } - // catch - // { - // // ignored - // } - // throw; - // } - // if (!silent) - // { - // try - // { - // //var queuedMessage = await textCh.SendConfirmAsync($"🎵 Queued **{resolvedSong.SongInfo.Title}** at `#{musicPlayer.Playlist.Count + 1}`").ConfigureAwait(false); - // var queuedMessage = await textCh.EmbedAsync(new EmbedBuilder().WithOkColor() - // .WithAuthor(eab => eab.WithName(GetText("queued_song") + " #" + (musicPlayer.Playlist.Count + 1)).WithMusicIcon()) - // .WithDescription($"{resolvedSong.PrettyName}\n{GetText("queue")} ") - // .WithThumbnailUrl(resolvedSong.Thumbnail) - // .WithFooter(ef => ef.WithText(resolvedSong.PrettyProvider))) - // .ConfigureAwait(false); - // queuedMessage?.DeleteAfter(10); - // } - // catch - // { - // // ignored - // } // if queued message sending fails, don't attempt to delete it - // } - // } - - - - - - - // private async Task HandleStreamContainers(string query) - // { - // string file = null; - // try - // { - // using (var http = new HttpClient()) - // { - // file = await http.GetStringAsync(query).ConfigureAwait(false); - // } - // } - // catch - // { - // return query; - // } - // if (query.Contains(".pls")) - // { - // //File1=http://armitunes.com:8000/ - // //Regex.Match(query) - // try - // { - // var m = Regex.Match(file, "File1=(?.*?)\\n"); - // var res = m.Groups["url"]?.ToString(); - // return res?.Trim(); - // } - // catch - // { - // _log.Warn($"Failed reading .pls:\n{file}"); - // return null; - // } - // } - // if (query.Contains(".m3u")) - // { - // /* - //# This is a comment - // C:\xxx4xx\xxxxxx3x\xx2xxxx\xx.mp3 - // C:\xxx5xx\x6xxxxxx\x7xxxxx\xx.mp3 - // */ - // try - // { - // var m = Regex.Match(file, "(?^[^#].*)", RegexOptions.Multiline); - // var res = m.Groups["url"]?.ToString(); - // return res?.Trim(); - // } - // catch - // { - // _log.Warn($"Failed reading .m3u:\n{file}"); - // return null; - // } - - // } - // if (query.Contains(".asx")) - // { - // // - // try - // { - // var m = Regex.Match(file, ".*?)\""); - // var res = m.Groups["url"]?.ToString(); - // return res?.Trim(); - // } - // catch - // { - // _log.Warn($"Failed reading .asx:\n{file}"); - // return null; - // } - // } - // if (query.Contains(".xspf")) - // { - // /* - // - // - // - // file:///mp3s/song_1.mp3 - // */ - // try - // { - // var m = Regex.Match(file, "(?.*?)"); - // var res = m.Groups["url"]?.ToString(); - // return res?.Trim(); - // } - // catch - // { - // _log.Warn($"Failed reading .xspf:\n{file}"); - // return null; - // } - // } - - // return query; - // } - - // private bool IsRadioLink(string query) => - // (query.StartsWith("http") || - // query.StartsWith("ww")) - // && - // (query.Contains(".pls") || - // query.Contains(".m3u") || - // query.Contains(".asx") || - // query.Contains(".xspf")); + return query; + } } } \ No newline at end of file diff --git a/src/NadekoBot/Services/Music/SongInfo.cs b/src/NadekoBot/Services/Music/SongInfo.cs index 28d407ee..22accf2f 100644 --- a/src/NadekoBot/Services/Music/SongInfo.cs +++ b/src/NadekoBot/Services/Music/SongInfo.cs @@ -47,7 +47,7 @@ namespace NadekoBot.Services.Music { switch (ProviderType) { - case MusicType.Normal: + case MusicType.YouTube: return Query; case MusicType.Soundcloud: return Query; @@ -60,6 +60,7 @@ namespace NadekoBot.Services.Music } } } + private string videoId = null; private readonly Regex videoIdRegex = new Regex("<=v=[a-zA-Z0-9-]+(?=&)|(?<=[0-9])[^&\n]+|(?<=v=)[^&\n]+", RegexOptions.Compiled); public string Thumbnail { @@ -69,9 +70,8 @@ namespace NadekoBot.Services.Music { case MusicType.Radio: return "https://cdn.discordapp.com/attachments/155726317222887425/261850925063340032/1482522097_radio.png"; //test links - case MusicType.Normal: - //todo have videoid in songinfo from the start - var videoId = videoIdRegex.Match(Query); + case MusicType.YouTube: + videoId = videoId ?? videoIdRegex.Match(Query)?.ToString(); return $"https://img.youtube.com/vi/{ videoId }/0.jpg"; case MusicType.Local: return "https://cdn.discordapp.com/attachments/155726317222887425/261850914783100928/1482522077_music.png"; //test links From 9f3c04c93e47addd3125e380266746d3e5276e19 Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Sun, 2 Jul 2017 19:20:04 +0200 Subject: [PATCH 073/346] you can now load only 1 playlist at a time using .load, because it's expensive --- src/NadekoBot/Modules/Music/Music.cs | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/src/NadekoBot/Modules/Music/Music.cs b/src/NadekoBot/Modules/Music/Music.cs index a1078a0f..15f7f6a3 100644 --- a/src/NadekoBot/Modules/Music/Music.cs +++ b/src/NadekoBot/Modules/Music/Music.cs @@ -470,7 +470,7 @@ namespace NadekoBot.Modules.Music .AddField(efb => efb.WithName(GetText("id")).WithValue(playlist.Id.ToString()))); } - private readonly ConcurrentHashSet PlaylistLoadBlacklist = new ConcurrentHashSet(); + private static readonly ConcurrentHashSet PlaylistLoadBlacklist = new ConcurrentHashSet(); [NadekoCommand, Usage, Description, Aliases] [RequireContext(ContextType.Guild)] @@ -544,8 +544,7 @@ namespace NadekoBot.Modules.Music var song = await _music.ResolveSong(query, Context.User.ToString(), MusicType.Soundcloud); await InternalQueue(mp, song, false).ConfigureAwait(false); } - - //todo test soundcloudpl + [NadekoCommand, Usage, Description, Aliases] [RequireContext(ContextType.Guild)] public async Task SoundCloudPl([Remainder] string pl) @@ -560,19 +559,26 @@ namespace NadekoBot.Modules.Music using (var http = new HttpClient()) { var scvids = JObject.Parse(await http.GetStringAsync($"https://scapi.nadekobot.me/resolve?url={pl}").ConfigureAwait(false))["tracks"].ToObject(); - + IUserMessage msg = null; + try { msg = await Context.Channel.SendMessageAsync(GetText("attempting_to_queue", Format.Bold(scvids.Length.ToString()))).ConfigureAwait(false); } catch { } foreach (var svideo in scvids) { try { + await Task.Yield(); await InternalQueue(mp, await _music.SongInfoFromSVideo(svideo, Context.User.ToString()), true); } - catch { break; } + catch (Exception ex) + { + _log.Warn(ex); + break; + } } + if (msg != null) + await msg.ModifyAsync(m => m.Content = GetText("playlist_queue_complete")).ConfigureAwait(false); } } - - //todo fix playlist sync stuff + [NadekoCommand, Usage, Description, Aliases] [RequireContext(ContextType.Guild)] public async Task NowPlaying() @@ -676,7 +682,7 @@ namespace NadekoBot.Modules.Music var song = await _music.ResolveSong(path, Context.User.ToString(), MusicType.Local); await InternalQueue(mp, song, false).ConfigureAwait(false); } - //todo test localpl + [NadekoCommand, Usage, Description, Aliases] [RequireContext(ContextType.Guild)] [OwnerOnly] @@ -695,6 +701,7 @@ namespace NadekoBot.Modules.Music { try { + await Task.Yield(); var song = await _music.ResolveSong(file.FullName, Context.User.ToString(), MusicType.Local); await InternalQueue(mp, song, true).ConfigureAwait(false); } From 3c9b68e7398da404e1f3546a96553bb1278dcff8 Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Sun, 2 Jul 2017 20:58:45 +0200 Subject: [PATCH 074/346] Cleanup, .smp is reimplemented, and will now show in .lq too --- src/NadekoBot/Modules/Music/Music.cs | 36 +++++++++---------- src/NadekoBot/Resources/CommandStrings.resx | 2 +- src/NadekoBot/Services/Music/MusicPlayer.cs | 12 ++++++- src/NadekoBot/Services/Music/SongBuffer.cs | 1 + .../_strings/ResponseStrings.en-US.json | 1 + 5 files changed, 32 insertions(+), 20 deletions(-) diff --git a/src/NadekoBot/Modules/Music/Music.cs b/src/NadekoBot/Modules/Music/Music.cs index 15f7f6a3..ab131a0f 100644 --- a/src/NadekoBot/Modules/Music/Music.cs +++ b/src/NadekoBot/Modules/Music/Music.cs @@ -246,6 +246,9 @@ namespace NadekoBot.Modules.Music var add = ""; if (mp.Stopped) add += Format.Bold(GetText("queue_stopped", Format.Code(Prefix + "play"))) + "\n"; + var mps = mp.MaxPlaytimeSeconds; + if (mps > 0) + add += Format.Bold(GetText("song_skips_after", TimeSpan.FromSeconds(mps).ToString("g"))) + "\n"; if (mp.RepeatCurrentSong) add += "🔂 " + GetText("repeating_cur_song") + "\n"; else if (mp.Shuffle) @@ -259,7 +262,7 @@ namespace NadekoBot.Modules.Music } if (!string.IsNullOrWhiteSpace(add)) - desc += add + "\n"; + desc = add + "\n" + desc; var embed = new EmbedBuilder() .WithAuthor(eab => eab.WithName(GetText("player_queue", curPage + 1, lastPage + 1)) @@ -795,24 +798,21 @@ namespace NadekoBot.Modules.Music await ReplyConfirmLocalized("max_queue_x", size).ConfigureAwait(false); } - //[NadekoCommand, Usage, Description, Aliases] - //[RequireContext(ContextType.Guild)] - //public async Task SetMaxPlaytime(uint seconds) - //{ - // if (seconds < 15 && seconds != 0) - // return; + [NadekoCommand, Usage, Description, Aliases] + [RequireContext(ContextType.Guild)] + public async Task SetMaxPlaytime(uint seconds) + { + if (seconds < 15 && seconds != 0) + return; + + var mp = await _music.GetOrCreatePlayer(Context); + mp.MaxPlaytimeSeconds = seconds; + if (seconds == 0) + await ReplyConfirmLocalized("max_playtime_none").ConfigureAwait(false); + else + await ReplyConfirmLocalized("max_playtime_set", seconds).ConfigureAwait(false); + } - // var channel = (ITextChannel)Context.Channel; - // MusicPlayer musicPlayer; - // if ((musicPlayer = _music.GetPlayer(Context.Guild.Id)) == null) - // return; - // musicPlayer.MaxPlaytimeSeconds = seconds; - // if (seconds == 0) - // await ReplyConfirmLocalized("max_playtime_none").ConfigureAwait(false); - // else - // await ReplyConfirmLocalized("max_playtime_set", seconds).ConfigureAwait(false); - //} - [NadekoCommand, Usage, Description, Aliases] [RequireContext(ContextType.Guild)] public async Task ReptCurSong() diff --git a/src/NadekoBot/Resources/CommandStrings.resx b/src/NadekoBot/Resources/CommandStrings.resx index d8977412..78297dd9 100644 --- a/src/NadekoBot/Resources/CommandStrings.resx +++ b/src/NadekoBot/Resources/CommandStrings.resx @@ -1198,7 +1198,7 @@ `{0}drawnew` or `{0}drawnew 5` - playlistshuffle plsh + shuffle plsh Shuffles the current playlist. diff --git a/src/NadekoBot/Services/Music/MusicPlayer.cs b/src/NadekoBot/Services/Music/MusicPlayer.cs index 38babb9c..10d4d2ac 100644 --- a/src/NadekoBot/Services/Music/MusicPlayer.cs +++ b/src/NadekoBot/Services/Music/MusicPlayer.cs @@ -50,6 +50,13 @@ namespace NadekoBot.Services.Music get => Queue.MaxQueueSize; set => Queue.MaxQueueSize = value; } + public uint MaxPlaytimeSeconds { get; set; } + + + const int _frameBytes = 3840; + const float _miliseconds = 20.0f; + public TimeSpan CurrentTime => TimeSpan.FromSeconds(_bytesSent / (float)_frameBytes / (1000 / _miliseconds)); + private int _bytesSent = 0; private IAudioClient _audioClient; private readonly object locker = new object(); @@ -78,6 +85,7 @@ namespace NadekoBot.Services.Music { while (!Exited) { + _bytesSent = 0; CancellationToken cancelToken; (int Index, SongInfo Song) data; lock (locker) @@ -122,12 +130,14 @@ namespace NadekoBot.Services.Music int bytesRead = 0; try { - while ((bytesRead = await b.ReadAsync(buffer, 0, buffer.Length, cancelToken).ConfigureAwait(false)) > 0) + while ((bytesRead = await b.ReadAsync(buffer, 0, buffer.Length, cancelToken).ConfigureAwait(false)) > 0 + && (MaxPlaytimeSeconds <= 0 || MaxPlaytimeSeconds >= CurrentTime.TotalSeconds)) { var vol = Volume; if (vol != 1) AdjustVolume(buffer, vol); await pcm.WriteAsync(buffer, 0, bytesRead, cancelToken).ConfigureAwait(false); + unchecked { _bytesSent += bytesRead; } await (pauseTaskSource?.Task ?? Task.CompletedTask); } diff --git a/src/NadekoBot/Services/Music/SongBuffer.cs b/src/NadekoBot/Services/Music/SongBuffer.cs index 0e507344..9f26671d 100644 --- a/src/NadekoBot/Services/Music/SongBuffer.cs +++ b/src/NadekoBot/Services/Music/SongBuffer.cs @@ -69,6 +69,7 @@ Check the guides for your platform on how to setup ffmpeg correctly: Linux Guide: https://goo.gl/ShjCUo"); } catch (OperationCanceledException) { } + catch (InvalidOperationException) { } // when ffmpeg is disposed catch (Exception ex) { _log.Info(ex); diff --git a/src/NadekoBot/_strings/ResponseStrings.en-US.json b/src/NadekoBot/_strings/ResponseStrings.en-US.json index 705b3c34..2703d2fd 100644 --- a/src/NadekoBot/_strings/ResponseStrings.en-US.json +++ b/src/NadekoBot/_strings/ResponseStrings.en-US.json @@ -455,6 +455,7 @@ "music_songs_shuffle_disable": "Songs will no longer shuffle.", "music_song_moved": "Song moved", "music_song_not_found": "No song found.", + "music_song_skips_after": "Songs will skip after {0}", "music_time_format": "{0}h {1}m {2}s", "music_to_position": "To position", "music_unlimited": "unlimited", From a609e17717411a6afa46fc7cf5f7b1520dd949af Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Mon, 3 Jul 2017 12:40:12 +0200 Subject: [PATCH 075/346] .play help update, readded pausing when nobody is in voice channel, and also cleaning up music players if bot is kicked or leaves the server --- src/NadekoBot/Modules/Music/Music.cs | 267 +++++---- src/NadekoBot/Resources/CommandStrings.resx | 6 +- .../Administration/PlayingRotateService.cs | 1 - src/NadekoBot/Services/Music/MusicPlayer.cs | 565 +++++------------- src/NadekoBot/Services/Music/MusicQueue.cs | 4 +- src/NadekoBot/Services/Music/MusicService.cs | 10 +- .../_strings/ResponseStrings.en-US.json | 4 +- 7 files changed, 306 insertions(+), 551 deletions(-) diff --git a/src/NadekoBot/Modules/Music/Music.cs b/src/NadekoBot/Modules/Music/Music.cs index ab131a0f..51713c80 100644 --- a/src/NadekoBot/Modules/Music/Music.cs +++ b/src/NadekoBot/Modules/Music/Music.cs @@ -36,56 +36,62 @@ namespace NadekoBot.Modules.Music _db = db; _music = music; - //_client.UserVoiceStateUpdated += Client_UserVoiceStateUpdated; + _client.UserVoiceStateUpdated += Client_UserVoiceStateUpdated; + _client.LeftGuild += _client_LeftGuild; } - //todo when someone drags nadeko from one voice channel to another + private Task _client_LeftGuild(SocketGuild arg) + { + var t = _music.DestroyPlayer(arg.Id); + return Task.CompletedTask; + } - //private Task Client_UserVoiceStateUpdated(SocketUser iusr, SocketVoiceState oldState, SocketVoiceState newState) - //{ - // var usr = iusr as SocketGuildUser; - // if (usr == null || - // oldState.VoiceChannel == newState.VoiceChannel) - // return Task.CompletedTask; + private Task Client_UserVoiceStateUpdated(SocketUser iusr, SocketVoiceState oldState, SocketVoiceState newState) + { + var t = Task.Run(() => + { + var usr = iusr as SocketGuildUser; + if (usr == null || + oldState.VoiceChannel == newState.VoiceChannel) + return; - // MusicPlayer player; - // if ((player = _music.GetPlayer(usr.Guild.Id)) == null) - // return Task.CompletedTask; + var player = _music.GetPlayerOrDefault(usr.Guild.Id); - // try - // { - // //if bot moved - // if ((player.PlaybackVoiceChannel == oldState.VoiceChannel) && - // usr.Id == _client.CurrentUser.Id) - // { - // if (player.Paused && newState.VoiceChannel.Users.Count > 1) //unpause if there are people in the new channel - // player.TogglePause(); - // else if (!player.Paused && newState.VoiceChannel.Users.Count <= 1) // pause if there are no users in the new channel - // player.TogglePause(); + try + { + //if bot moved + if ((player.VoiceChannel == oldState.VoiceChannel) && + usr.Id == _client.CurrentUser.Id) + { + if (player.Paused && newState.VoiceChannel.Users.Count > 1) //unpause if there are people in the new channel + player.TogglePause(); + else if (!player.Paused && newState.VoiceChannel.Users.Count <= 1) // pause if there are no users in the new channel + player.TogglePause(); - // return Task.CompletedTask; - // } + player.SetVoiceChannel(newState.VoiceChannel); + return; + } - // //if some other user moved - // if ((player.PlaybackVoiceChannel == newState.VoiceChannel && //if joined first, and player paused, unpause - // player.Paused && - // newState.VoiceChannel.Users.Count == 2) || // keep in mind bot is in the channel (+1) - // (player.PlaybackVoiceChannel == oldState.VoiceChannel && // if left last, and player unpaused, pause - // !player.Paused && - // oldState.VoiceChannel.Users.Count == 1)) - // { - // player.TogglePause(); - // return Task.CompletedTask; - // } - - // } - // catch - // { - // // ignored - // } - // return Task.CompletedTask; - //} + //if some other user moved + if ((player.VoiceChannel == newState.VoiceChannel && //if joined first, and player paused, unpause + player.Paused && + newState.VoiceChannel.Users.Count >= 2) || // keep in mind bot is in the channel (+1) + (player.VoiceChannel == oldState.VoiceChannel && // if left last, and player unpaused, pause + !player.Paused && + oldState.VoiceChannel.Users.Count == 1)) + { + player.TogglePause(); + return; + } + } + catch + { + // ignored + } + }); + return Task.CompletedTask; + } private async Task InternalQueue(MusicPlayer mp, SongInfo songInfo, bool silent) { @@ -96,25 +102,24 @@ namespace NadekoBot.Modules.Music return; } - (bool Success, int Index) qData; + int index; try { - qData = mp.Enqueue(songInfo); + index = mp.Enqueue(songInfo); } catch (QueueFullException) { await ReplyErrorLocalized("queue_full", mp.MaxQueueSize).ConfigureAwait(false); throw; } - if (qData.Success) + if (index != -1) { if (!silent) { try { - //var queuedMessage = await textCh.SendConfirmAsync($"🎵 Queued **{resolvedSong.SongInfo.Title}** at `#{musicPlayer.Playlist.Count + 1}`").ConfigureAwait(false); var queuedMessage = await mp.OutputTextChannel.EmbedAsync(new EmbedBuilder().WithOkColor() - .WithAuthor(eab => eab.WithName(GetText("queued_song") + " #" + (qData.Index)).WithMusicIcon()) + .WithAuthor(eab => eab.WithName(GetText("queued_song") + " #" + (index)).WithMusicIcon()) .WithDescription($"{songInfo.PrettyName}\n{GetText("queue")} ") .WithThumbnailUrl(songInfo.Thumbnail) .WithFooter(ef => ef.WithText(songInfo.PrettyProvider))) @@ -135,12 +140,26 @@ namespace NadekoBot.Modules.Music [NadekoCommand, Usage, Description, Aliases] [RequireContext(ContextType.Guild)] - public Task Play([Remainder]string query = null) + public async Task Play([Remainder] string query = null) { - if (!string.IsNullOrWhiteSpace(query)) - try { return Queue(query); } catch (QueueFullException) { return Task.CompletedTask; } + var mp = await _music.GetOrCreatePlayer(Context); + if (string.IsNullOrWhiteSpace(query)) + { + await Next(); + } + else if (int.TryParse(query, out var index)) + if (index >= 1) + mp.SetIndex(index - 1); + else + return; else - return Next(); + { + try + { + await Queue(query); + } + catch { } + } } [NadekoCommand, Usage, Description, Aliases] @@ -218,13 +237,12 @@ namespace NadekoBot.Modules.Music page = current / itemsPerPage; //if page is 0 (-1 after this decrement) that means default to the page current song is playing from - - //var total = musicPlayer.TotalPlaytime; - //var totalStr = total == TimeSpan.MaxValue ? "∞" : GetText("time_format", - // (int)total.TotalHours, - // total.Minutes, - // total.Seconds); - //var maxPlaytime = musicPlayer.MaxPlaytimeSeconds; + var total = mp.TotalPlaytime; + var totalStr = total == TimeSpan.MaxValue ? "∞" : GetText("time_format", + (int)total.TotalHours, + total.Minutes, + total.Seconds); + var maxPlaytime = mp.MaxPlaytimeSeconds; var lastPage = songs.Length / itemsPerPage; Func printAction = curPage => { @@ -255,10 +273,12 @@ namespace NadekoBot.Modules.Music add += "🔀 " + GetText("shuffling_playlist") + "\n"; else { - if (mp.RepeatPlaylist) - add += "🔁 " + GetText("repeating_playlist") + "\n"; if (mp.Autoplay) add += "↪ " + GetText("autoplaying") + "\n"; + if (mp.FairPlay && !mp.Autoplay) + add += " " + GetText("fairplay") + "\n"; + else if (mp.RepeatPlaylist) + add += "🔁 " + GetText("repeating_playlist") + "\n"; } if (!string.IsNullOrWhiteSpace(add)) @@ -268,12 +288,8 @@ namespace NadekoBot.Modules.Music .WithAuthor(eab => eab.WithName(GetText("player_queue", curPage + 1, lastPage + 1)) .WithMusicIcon()) .WithDescription(desc) - //.WithFooter(ef => ef.WithText($"{musicPlayer.PrettyVolume} | {musicPlayer.Playlist.Count} " + - // $"{("tracks".SnPl(musicPlayer.Playlist.Count))} | {totalStr} | " + - // (musicPlayer.FairPlay - // ? "✔️" + GetText("fairplay") - // : "✖️" + GetText("fairplay")) + " | " + - // (maxPlaytime == 0 ? "unlimited" : GetText("play_limit", maxPlaytime)))) + .WithFooter(ef => ef.WithText($"{mp.PrettyVolume} | {songs.Length} " + + $"{("tracks".SnPl(songs.Length))} | {totalStr}")) .WithOkColor(); return embed; @@ -517,27 +533,22 @@ namespace NadekoBot.Modules.Music } } - //[NadekoCommand, Usage, Description, Aliases] - //[RequireContext(ContextType.Guild)] - //public async Task Fairplay() - //{ - // var channel = (ITextChannel)Context.Channel; - // MusicPlayer musicPlayer; - // if ((musicPlayer = _music.GetPlayer(Context.Guild.Id)) == null) - // return; - // if (((IGuildUser)Context.User).VoiceChannel != musicPlayer.PlaybackVoiceChannel) - // return; - // var val = musicPlayer.FairPlay = !musicPlayer.FairPlay; + [NadekoCommand, Usage, Description, Aliases] + [RequireContext(ContextType.Guild)] + public async Task Fairplay() + { + var mp = await _music.GetOrCreatePlayer(Context); + var val = mp.FairPlay = !mp.FairPlay; - // if (val) - // { - // await ReplyConfirmLocalized("fp_enabled").ConfigureAwait(false); - // } - // else - // { - // await ReplyConfirmLocalized("fp_disabled").ConfigureAwait(false); - // } - //} + if (val) + { + await ReplyConfirmLocalized("fp_enabled").ConfigureAwait(false); + } + else + { + await ReplyConfirmLocalized("fp_disabled").ConfigureAwait(false); + } + } [NadekoCommand, Usage, Description, Aliases] [RequireContext(ContextType.Guild)] @@ -613,58 +624,46 @@ namespace NadekoBot.Modules.Music await ReplyConfirmLocalized("songs_shuffle_disable").ConfigureAwait(false); } - //[NadekoCommand, Usage, Description, Aliases] - //[RequireContext(ContextType.Guild)] - //public async Task Playlist([Remainder] string playlist) - //{ + [NadekoCommand, Usage, Description, Aliases] + [RequireContext(ContextType.Guild)] + public async Task Playlist([Remainder] string playlist) + { + if (string.IsNullOrWhiteSpace(playlist)) + return; - // var arg = playlist; - // if (string.IsNullOrWhiteSpace(arg)) - // return; - // if (((IGuildUser)Context.User).VoiceChannel?.Guild != Context.Guild) - // { - // await ReplyErrorLocalized("must_be_in_voice").ConfigureAwait(false); - // return; - // } - // var plId = (await _google.GetPlaylistIdsByKeywordsAsync(arg).ConfigureAwait(false)).FirstOrDefault(); - // if (plId == null) - // { - // await ReplyErrorLocalized("no_search_results").ConfigureAwait(false); - // return; - // } - // var ids = await _google.GetPlaylistTracksAsync(plId, 500).ConfigureAwait(false); - // if (!ids.Any()) - // { - // await ReplyErrorLocalized("no_search_results").ConfigureAwait(false); - // return; - // } - // var count = ids.Count(); - // var msg = await Context.Channel.SendMessageAsync("🎵 " + GetText("attempting_to_queue", - // Format.Bold(count.ToString()))).ConfigureAwait(false); + var mp = await _music.GetOrCreatePlayer(Context); - // var cancelSource = new CancellationTokenSource(); + var plId = (await _google.GetPlaylistIdsByKeywordsAsync(playlist).ConfigureAwait(false)).FirstOrDefault(); + if (plId == null) + { + await ReplyErrorLocalized("no_search_results").ConfigureAwait(false); + return; + } + var ids = await _google.GetPlaylistTracksAsync(plId, 500).ConfigureAwait(false); + if (!ids.Any()) + { + await ReplyErrorLocalized("no_search_results").ConfigureAwait(false); + return; + } + var count = ids.Count(); + var msg = await Context.Channel.SendMessageAsync("🎵 " + GetText("attempting_to_queue", + Format.Bold(count.ToString()))).ConfigureAwait(false); + + foreach (var song in ids) + { + try + { + if (mp.Exited) + return; - // var gusr = (IGuildUser)Context.User; - // while (ids.Any() && !cancelSource.IsCancellationRequested) - // { - // var tasks = Task.WhenAll(ids.Take(5).Select(async id => - // { - // if (cancelSource.Token.IsCancellationRequested) - // return; - // try - // { - // await _music.QueueSong(gusr, (ITextChannel)Context.Channel, gusr.VoiceChannel, id, true).ConfigureAwait(false); - // } - // catch (SongNotFoundException) { } - // catch { try { cancelSource.Cancel(); } catch { } } - // })); + await Task.WhenAll(Task.Delay(100), InternalQueue(mp, await _music.ResolveSong(song, Context.User.ToString(), MusicType.YouTube), true)); + } + catch (SongNotFoundException) { } + catch { break; } + } - // await Task.WhenAny(tasks, Task.Delay(Timeout.Infinite, cancelSource.Token)); - // ids = ids.Skip(5); - // } - - // await msg.ModifyAsync(m => m.Content = "✅ " + Format.Bold(GetText("playlist_queue_complete"))).ConfigureAwait(false); - //} + await msg.ModifyAsync(m => m.Content = "✅ " + Format.Bold(GetText("playlist_queue_complete"))).ConfigureAwait(false); + } [NadekoCommand, Usage, Description, Aliases] diff --git a/src/NadekoBot/Resources/CommandStrings.resx b/src/NadekoBot/Resources/CommandStrings.resx index 78297dd9..e3504acb 100644 --- a/src/NadekoBot/Resources/CommandStrings.resx +++ b/src/NadekoBot/Resources/CommandStrings.resx @@ -1198,7 +1198,7 @@ `{0}drawnew` or `{0}drawnew 5` - shuffle plsh + shuffle sh plsh Shuffles the current playlist. @@ -1471,10 +1471,10 @@ play start - If no arguments are specified, acts as `{0}next 1` command. If you specify a search query, acts as a `{0}q` command + If no arguments are specified, acts as `{0}next 1` command. If you specify a song number, it will jump to that song. If you specify a search query, acts as a `{0}q` command - `{0}play` or `{0}play Dream Of Venice` + `{0}play` or `{0}play 5` or `{0}play Dream Of Venice` stop s diff --git a/src/NadekoBot/Services/Administration/PlayingRotateService.cs b/src/NadekoBot/Services/Administration/PlayingRotateService.cs index d4e09ad1..aa9321c6 100644 --- a/src/NadekoBot/Services/Administration/PlayingRotateService.cs +++ b/src/NadekoBot/Services/Administration/PlayingRotateService.cs @@ -1,6 +1,5 @@ using Discord.WebSocket; using NadekoBot.DataStructures.Replacements; -using NadekoBot.Services; using NadekoBot.Services.Database.Models; using NadekoBot.Services.Music; using NLog; diff --git a/src/NadekoBot/Services/Music/MusicPlayer.cs b/src/NadekoBot/Services/Music/MusicPlayer.cs index 10d4d2ac..c42d56da 100644 --- a/src/NadekoBot/Services/Music/MusicPlayer.cs +++ b/src/NadekoBot/Services/Music/MusicPlayer.cs @@ -4,7 +4,9 @@ using System; using System.Threading; using System.Threading.Tasks; using NLog; -using System.Diagnostics; +using System.Linq; +using System.Collections.Concurrent; +using NadekoBot.Extensions; namespace NadekoBot.Services.Music { @@ -18,7 +20,7 @@ namespace NadekoBot.Services.Music public class MusicPlayer { private readonly Task _player; - private IVoiceChannel VoiceChannel { get; set; } + public IVoiceChannel VoiceChannel { get; private set; } private readonly Logger _log; private MusicQueue Queue { get; } = new MusicQueue(); @@ -27,6 +29,7 @@ namespace NadekoBot.Services.Music public bool Stopped { get; private set; } = false; public float Volume { get; private set; } = 1.0f; public string PrettyVolume => $"🔉 {(int)(Volume * 100)}%"; + public bool Paused => pauseTaskSource != null; private TaskCompletionSource pauseTaskSource { get; set; } = null; private CancellationTokenSource SongCancelSource { get; set; } @@ -48,7 +51,27 @@ namespace NadekoBot.Services.Music public uint MaxQueueSize { get => Queue.MaxQueueSize; - set => Queue.MaxQueueSize = value; + set { lock (locker) Queue.MaxQueueSize = value; } + } + private bool _fairPlay; + public bool FairPlay + { + get => _fairPlay; + set + { + if (value) + { + var cur = Queue.Current; + if (cur.Song != null) + RecentlyPlayedUsers.Add(cur.Song.QueuerName); + } + else + { + RecentlyPlayedUsers.Clear(); + } + + _fairPlay = value; + } } public uint MaxPlaytimeSeconds { get; set; } @@ -56,6 +79,7 @@ namespace NadekoBot.Services.Music const int _frameBytes = 3840; const float _miliseconds = 20.0f; public TimeSpan CurrentTime => TimeSpan.FromSeconds(_bytesSent / (float)_frameBytes / (1000 / _miliseconds)); + private int _bytesSent = 0; private IAudioClient _audioClient; @@ -63,15 +87,19 @@ namespace NadekoBot.Services.Music private MusicService _musicService; #region events - public event Action OnStarted; + public event Action OnStarted; public event Action OnCompleted; public event Action OnPauseChanged; #endregion private bool manualSkip = false; + private bool manualIndex = false; private bool newVoiceChannel = false; + private ConcurrentHashSet RecentlyPlayedUsers { get; } = new ConcurrentHashSet(); + public TimeSpan TotalPlaytime => TimeSpan.MaxValue; + public MusicPlayer(MusicService musicService, IVoiceChannel vch, ITextChannel output, float volume) { _log = LogManager.GetCurrentClassLogger(); @@ -93,6 +121,7 @@ namespace NadekoBot.Services.Music data = Queue.Current; cancelToken = SongCancelSource.Token; manualSkip = false; + manualIndex = false; } try { @@ -123,19 +152,17 @@ namespace NadekoBot.Services.Music // i don't want to spam connection attempts continue; } - var pcm = ac.CreatePCMStream(AudioApplication.Music, bufferMillis: 200); - OnStarted?.Invoke(this, data.Song); + var pcm = ac.CreatePCMStream(AudioApplication.Music, bufferMillis: 500); + OnStarted?.Invoke(this, data); byte[] buffer = new byte[3840]; int bytesRead = 0; try { - while ((bytesRead = await b.ReadAsync(buffer, 0, buffer.Length, cancelToken).ConfigureAwait(false)) > 0 + while ((bytesRead = await b.ReadAsync(buffer, 0, buffer.Length, cancelToken).ConfigureAwait(false)) > 0 && (MaxPlaytimeSeconds <= 0 || MaxPlaytimeSeconds >= CurrentTime.TotalSeconds)) { - var vol = Volume; - if (vol != 1) - AdjustVolume(buffer, vol); + AdjustVolume(buffer, Volume); await pcm.WriteAsync(buffer, 0, bytesRead, cancelToken).ConfigureAwait(false); unchecked { _bytesSent += bytesRead; } @@ -165,60 +192,118 @@ namespace NadekoBot.Services.Music } finally { - //if repeating current song, just ignore other settings, - // and play this song again (don't change the index) - // ignore rcs if song is manually skipped - if (!RepeatCurrentSong || manualSkip) + try { - if (Shuffle) + //if repeating current song, just ignore other settings, + // and play this song again (don't change the index) + // ignore rcs if song is manually skipped + + int queueCount; + lock (locker) + queueCount = Queue.Count; + + if (!manualIndex && (!RepeatCurrentSong || manualSkip)) { - _log.Info("Random song"); - Queue.Random(); //if shuffle is set, set current song index to a random number - } - else - { - //if last song, and autoplay is enabled, and if it's a youtube song - // do autplay magix - if (Queue.Count - 1 == data.Index && Autoplay && data.Song?.ProviderType == Database.Models.MusicType.YouTube) + if (Shuffle) { - try - { - _log.Info("Loading related song"); - await _musicService.TryQueueRelatedSongAsync(data.Song.Query, OutputTextChannel, VoiceChannel); - Queue.Next(); - } - catch - { - _log.Info("Loading related song failed."); - } - } - else if (Queue.Count - 1 == data.Index && !RepeatPlaylist && !manualSkip) - { - _log.Info("Stopping because repeatplaylist is disabled"); - Stop(); + _log.Info("Random song"); + Queue.Random(); //if shuffle is set, set current song index to a random number } else { - _log.Info("Next song"); - Queue.Next(); + //if last song, and autoplay is enabled, and if it's a youtube song + // do autplay magix + if (queueCount - 1 == data.Index && Autoplay && data.Song?.ProviderType == Database.Models.MusicType.YouTube) + { + try + { + _log.Info("Loading related song"); + await _musicService.TryQueueRelatedSongAsync(data.Song.Query, OutputTextChannel, VoiceChannel); + Queue.Next(); + } + catch + { + _log.Info("Loading related song failed."); + } + } + else if (FairPlay) + { + lock (locker) + { + _log.Info("Next fair song"); + var q = Queue.ToArray().Songs.Shuffle().ToArray(); + + bool found = false; + for (var i = 0; i < q.Length; i++) //first try to find a queuer who didn't have their song played recently + { + var item = q[i]; + if (RecentlyPlayedUsers.Add(item.QueuerName)) // if it's found, set current song to that index + { + Queue.CurrentIndex = i; + found = true; + break; + } + } + if (!found) //if it's not + { + RecentlyPlayedUsers.Clear(); //clear all recently played users (that means everyone from the playlist has had their song played) + Queue.Random(); //go to a random song (to prevent looping on the first few songs) + var cur = Current; + if (cur.Current != null) // add newely scheduled song's queuer to the recently played list + RecentlyPlayedUsers.Add(cur.Current.QueuerName); + } + } + } + else if (queueCount - 1 == data.Index && !RepeatPlaylist && !manualSkip) + { + _log.Info("Stopping because repeatplaylist is disabled"); + lock (locker) + { + Stop(); + } + } + else + { + _log.Info("Next song"); + lock (locker) + { + Queue.Next(); + } + } } } } + catch (Exception ex) + { + _log.Error(ex); + } do { await Task.Delay(500); } - while (Stopped && !Exited); + while ((Queue.Count == 0 || Stopped) && !Exited); } } }, SongCancelSource.Token); } + public void SetIndex(int index) + { + if (index < 0) + throw new ArgumentOutOfRangeException(nameof(index)); + lock (locker) + { + Queue.CurrentIndex = index; + manualIndex = true; + CancelCurrentSong(); + } + } + private async Task GetAudioClient(bool reconnect = false) { if (_audioClient == null || _audioClient.ConnectionState != ConnectionState.Connected || - reconnect || + reconnect || newVoiceChannel) try { @@ -240,17 +325,24 @@ namespace NadekoBot.Services.Music return _audioClient; } - public (bool Success, int Index) Enqueue(SongInfo song) + public int Enqueue(SongInfo song) { _log.Info("Adding song"); - Queue.Add(song); - return (true, Queue.Count); + lock (locker) + { + if (Exited) + return -1; + Queue.Add(song); + return Queue.Count; + } } public void Next(int skipCount = 1) { lock (locker) { + if (Exited) + return; manualSkip = true; // if player is stopped, and user uses .n, it should play current song. // It's a bit weird, but that's the least annoying solution @@ -277,10 +369,13 @@ namespace NadekoBot.Services.Music private void Unpause() { - if (pauseTaskSource != null) + lock (locker) { - pauseTaskSource.TrySetResult(true); - pauseTaskSource = null; + if (pauseTaskSource != null) + { + pauseTaskSource.TrySetResult(true); + pauseTaskSource = null; + } } } @@ -302,7 +397,10 @@ namespace NadekoBot.Services.Music { if (volume < 0 || volume > 100) throw new ArgumentOutOfRangeException(nameof(volume)); - Volume = ((float)volume) / 100; + lock (locker) + { + Volume = ((float)volume) / 100; + } } public SongInfo RemoveAt(int index) @@ -335,7 +433,10 @@ namespace NadekoBot.Services.Music } public (int CurrentIndex, SongInfo[] Songs) QueueArray() - => Queue.ToArray(); + { + lock (locker) + return Queue.ToArray(); + } //aidiakapi ftw public static unsafe byte[] AdjustVolume(byte[] audioSamples, float volume) @@ -410,366 +511,18 @@ namespace NadekoBot.Services.Music public void SetVoiceChannel(IVoiceChannel vch) { - VoiceChannel = vch; - newVoiceChannel = true; - Next(); + lock (locker) + { + if (Exited) + return; + VoiceChannel = vch; + } } - - //private IAudioClient AudioClient { get; set; } - - ///// - ///// Player will prioritize different queuer name - ///// over the song position in the playlist - ///// - //public bool FairPlay { get; set; } = false; - - ///// - ///// Song will stop playing after this amount of time. - ///// To prevent people queueing radio or looped songs - ///// while other people want to listen to other songs too. - ///// - //public uint MaxPlaytimeSeconds { get; set; } = 0; - - //// this should be written better //public TimeSpan TotalPlaytime => // _playlist.Any(s => s.TotalTime == TimeSpan.MaxValue) ? // TimeSpan.MaxValue : - // new TimeSpan(_playlist.Sum(s => s.TotalTime.Ticks)); - - ///// - ///// Users who recently got their music wish - ///// - //private ConcurrentHashSet RecentlyPlayedUsers { get; } = new ConcurrentHashSet(); - - //private readonly List _playlist = new List(); - //private readonly Logger _log; - //private readonly IGoogleApiService _google; - - //public IReadOnlyCollection Playlist => _playlist; - - //public Song CurrentSong { get; private set; } - //public CancellationTokenSource SongCancelSource { get; private set; } - //private CancellationToken CancelToken { get; set; } - - //public bool Paused { get; set; } - - //public float Volume { get; private set; } - - //public event Action OnCompleted = delegate { }; - //public event Action OnStarted = delegate { }; - //public event Action OnPauseChanged = delegate { }; - - //public IVoiceChannel PlaybackVoiceChannel { get; private set; } - //public ITextChannel OutputTextChannel { get; set; } - - //private bool Destroyed { get; set; } - //public bool RepeatSong { get; private set; } - //public bool RepeatPlaylist { get; private set; } - //public bool Autoplay { get; set; } - //public uint MaxQueueSize { get; set; } = 0; - - //private ConcurrentQueue ActionQueue { get; } = new ConcurrentQueue(); - - //public string PrettyVolume => $"🔉 {(int)(Volume * 100)}%"; - - //public event Action SongRemoved = delegate { }; - - //public MusicPlayer(IVoiceChannel startingVoiceChannel, ITextChannel outputChannel, float? defaultVolume, IGoogleApiService google) - //{ - // _log = LogManager.GetCurrentClassLogger(); - // _google = google; - - // OutputTextChannel = outputChannel; - // Volume = defaultVolume ?? 1.0f; - - // PlaybackVoiceChannel = startingVoiceChannel ?? throw new ArgumentNullException(nameof(startingVoiceChannel)); - // SongCancelSource = new CancellationTokenSource(); - // CancelToken = SongCancelSource.Token; - - // Task.Run(async () => - // { - // try - // { - // while (!Destroyed) - // { - // try - // { - // if (ActionQueue.TryDequeue(out Action action)) - // { - // action(); - // } - // } - // finally - // { - // await Task.Delay(100).ConfigureAwait(false); - // } - // } - // } - // catch (Exception ex) - // { - // _log.Warn("Action queue crashed"); - // _log.Warn(ex); - // } - // }).ConfigureAwait(false); - - // var t = new Thread(async () => - // { - // while (!Destroyed) - // { - // try - // { - // CurrentSong = GetNextSong(); - - // if (CurrentSong == null) - // continue; - - // while (AudioClient?.ConnectionState == ConnectionState.Disconnecting || - // AudioClient?.ConnectionState == ConnectionState.Connecting) - // { - // _log.Info("Waiting for Audio client"); - // await Task.Delay(200).ConfigureAwait(false); - // } - - // if (AudioClient == null || AudioClient.ConnectionState == ConnectionState.Disconnected) - // AudioClient = await PlaybackVoiceChannel.ConnectAsync().ConfigureAwait(false); - - // var index = _playlist.IndexOf(CurrentSong); - // if (index != -1) - // RemoveSongAt(index, true); - - // OnStarted(this, CurrentSong); - // try - // { - // await CurrentSong.Play(AudioClient, CancelToken); - // } - // catch (OperationCanceledException) - // { - // } - // finally - // { - // OnCompleted(this, CurrentSong); - // } - - - // if (RepeatPlaylist & !RepeatSong) - // AddSong(CurrentSong, CurrentSong.QueuerName); - - // if (RepeatSong) - // AddSong(CurrentSong, 0); - - // } - // catch (Exception ex) - // { - // _log.Warn("Music thread almost crashed."); - // _log.Warn(ex); - // await Task.Delay(3000).ConfigureAwait(false); - // } - // finally - // { - // if (!CancelToken.IsCancellationRequested) - // { - // SongCancelSource.Cancel(); - // } - // SongCancelSource = new CancellationTokenSource(); - // CancelToken = SongCancelSource.Token; - // CurrentSong = null; - // await Task.Delay(300).ConfigureAwait(false); - // } - // } - // }); - - // t.Start(); - //} - - //public void Next() - //{ - // ActionQueue.Enqueue(() => - // { - // Paused = false; - // SongCancelSource.Cancel(); - // }); - //} - - //public void Stop() - //{ - // ActionQueue.Enqueue(() => - // { - // RepeatPlaylist = false; - // RepeatSong = false; - // Autoplay = false; - // _playlist.Clear(); - // if (!SongCancelSource.IsCancellationRequested) - // SongCancelSource.Cancel(); - // }); - //} - - //public void TogglePause() => OnPauseChanged(Paused = !Paused); - - //public int SetVolume(int volume) - //{ - // if (volume < 0) - // volume = 0; - // if (volume > 100) - // volume = 100; - - // Volume = volume / 100.0f; - // return volume; - //} - - //private Song GetNextSong() - //{ - // if (!FairPlay) - // { - // return _playlist.FirstOrDefault(); - // } - // var song = _playlist.FirstOrDefault(c => !RecentlyPlayedUsers.Contains(c.QueuerName)) - // ?? _playlist.FirstOrDefault(); - - // if (song == null) - // return null; - - // if (RecentlyPlayedUsers.Contains(song.QueuerName)) - // { - // RecentlyPlayedUsers.Clear(); - // } - - // RecentlyPlayedUsers.Add(song.QueuerName); - // return song; - //} - - //public void Shuffle() - //{ - // ActionQueue.Enqueue(() => - // { - // var oldPlaylist = _playlist.ToArray(); - // _playlist.Clear(); - // _playlist.AddRange(oldPlaylist.Shuffle()); - // }); - //} - - //public void AddSong(Song s, string username) - //{ - // if (s == null) - // throw new ArgumentNullException(nameof(s)); - // ThrowIfQueueFull(); - // ActionQueue.Enqueue(() => - // { - // s.MusicPlayer = this; - // s.QueuerName = username.TrimTo(10); - // _playlist.Add(s); - // }); - //} - - //public void AddSong(Song s, int index) - //{ - // if (s == null) - // throw new ArgumentNullException(nameof(s)); - // ActionQueue.Enqueue(() => - // { - // _playlist.Insert(index, s); - // }); - //} - - //public void RemoveSong(Song s) - //{ - // if (s == null) - // throw new ArgumentNullException(nameof(s)); - // ActionQueue.Enqueue(() => - // { - // _playlist.Remove(s); - // }); - //} - - //public void RemoveSongAt(int index, bool silent = false) - //{ - // ActionQueue.Enqueue(() => - // { - // if (index < 0 || index >= _playlist.Count) - // return; - // var song = _playlist.ElementAtOrDefault(index); - // if (_playlist.Remove(song) && !silent) - // { - // SongRemoved(song, index); - // } - - // }); - //} - - //public void ClearQueue() - //{ - // ActionQueue.Enqueue(() => - // { - // _playlist.Clear(); - // }); - //} - - //public async Task UpdateSongDurationsAsync() - //{ - // var curSong = CurrentSong; - // var toUpdate = _playlist.Where(s => s.SongInfo.ProviderType == MusicType.Normal && - // s.TotalTime == TimeSpan.Zero) - // .ToArray(); - // if (curSong != null) - // { - // Array.Resize(ref toUpdate, toUpdate.Length + 1); - // toUpdate[toUpdate.Length - 1] = curSong; - // } - // var ids = toUpdate.Select(s => s.SongInfo.Query.Substring(s.SongInfo.Query.LastIndexOf("?v=") + 3)) - // .Distinct(); - - // var durations = await _google.GetVideoDurationsAsync(ids); - - // toUpdate.ForEach(s => - // { - // foreach (var kvp in durations) - // { - // if (s.SongInfo.Query.EndsWith(kvp.Key)) - // { - // s.TotalTime = kvp.Value; - // return; - // } - // } - // }); - //} - - //public void Destroy() - //{ - // ActionQueue.Enqueue(async () => - // { - // RepeatPlaylist = false; - // RepeatSong = false; - // Autoplay = false; - // Destroyed = true; - // _playlist.Clear(); - - // try { await AudioClient.StopAsync(); } catch { } - // if (!SongCancelSource.IsCancellationRequested) - // SongCancelSource.Cancel(); - // }); - //} - - ////public async Task MoveToVoiceChannel(IVoiceChannel voiceChannel) - ////{ - //// if (audioClient?.ConnectionState != ConnectionState.Connected) - //// throw new InvalidOperationException("Can't move while bot is not connected to voice channel."); - //// PlaybackVoiceChannel = voiceChannel; - //// audioClient = await voiceChannel.ConnectAsync().ConfigureAwait(false); - ////} - - //public bool ToggleRepeatSong() => RepeatSong = !RepeatSong; - - //public bool ToggleRepeatPlaylist() => RepeatPlaylist = !RepeatPlaylist; - - //public bool ToggleAutoplay() => Autoplay = !Autoplay; - - //public void ThrowIfQueueFull() - //{ - // if (MaxQueueSize == 0) - // return; - // if (_playlist.Count >= MaxQueueSize) - // throw new PlaylistFullException(); - //} + // new TimeSpan(_playlist.Sum(s => s.TotalTime.Ticks)); } -} +} \ No newline at end of file diff --git a/src/NadekoBot/Services/Music/MusicQueue.cs b/src/NadekoBot/Services/Music/MusicQueue.cs index 9156d0a7..967a5197 100644 --- a/src/NadekoBot/Services/Music/MusicQueue.cs +++ b/src/NadekoBot/Services/Music/MusicQueue.cs @@ -11,7 +11,7 @@ namespace NadekoBot.Services.Music { private LinkedList Songs { get; } = new LinkedList(); private int _currentIndex = 0; - private int CurrentIndex + public int CurrentIndex { get { @@ -124,7 +124,7 @@ namespace NadekoBot.Services.Music } } - public (int, SongInfo[]) ToArray() + public (int CurrentIndex, SongInfo[] Songs) ToArray() { lock (locker) { diff --git a/src/NadekoBot/Services/Music/MusicService.cs b/src/NadekoBot/Services/Music/MusicService.cs index a7f9ef46..2ed203ac 100644 --- a/src/NadekoBot/Services/Music/MusicService.cs +++ b/src/NadekoBot/Services/Music/MusicService.cs @@ -131,9 +131,9 @@ namespace NadekoBot.Services.Music playingMessage?.DeleteAfter(0); playingMessage = await mp.OutputTextChannel.EmbedAsync(new EmbedBuilder().WithOkColor() - .WithAuthor(eab => eab.WithName(GetText("playing_song")).WithMusicIcon()) - .WithDescription(song.PrettyName) - .WithFooter(ef => ef.WithText(mp.PrettyVolume + " | " + song.PrettyInfo))) + .WithAuthor(eab => eab.WithName(GetText("playing_song", song.Index + 1)).WithMusicIcon()) + .WithDescription(song.Song.PrettyName) + .WithFooter(ef => ef.WithText(mp.PrettyVolume + " | " + song.Song.PrettyInfo))) .ConfigureAwait(false); } catch @@ -288,11 +288,15 @@ namespace NadekoBot.Services.Music .FirstOrDefault(); if (video == null) // do something with this error + { _log.Info("Could not load any video elements based on the query."); + return null; + } //var m = Regex.Match(query, @"\?t=(?\d*)"); //int gotoTime = 0; //if (m.Captures.Count > 0) // int.TryParse(m.Groups["t"].ToString(), out gotoTime); + var song = new SongInfo { Title = video.Title.Substring(0, video.Title.Length - 10), // removing trailing "- You Tube" diff --git a/src/NadekoBot/_strings/ResponseStrings.en-US.json b/src/NadekoBot/_strings/ResponseStrings.en-US.json index 2703d2fd..fa4bea1a 100644 --- a/src/NadekoBot/_strings/ResponseStrings.en-US.json +++ b/src/NadekoBot/_strings/ResponseStrings.en-US.json @@ -407,7 +407,7 @@ "music_autoplaying": "Auto-playing.", "music_defvol_set": "Default volume set to {0}%", "music_dir_queue_complete": "Directory queue complete.", - "music_fairplay": "fairplay", + "music_fairplay": "Fairplay", "music_finished_song": "Finished song", "music_fp_disabled": "Fair play disabled.", "music_fp_enabled": "Fair play enabled.", @@ -425,7 +425,7 @@ "music_no_search_results": "No search results.", "music_paused": "Music playback paused.", "music_player_queue": "Player queue - Page {0}/{1}", - "music_playing_song": "Playing song", + "music_playing_song": "Playing song #{0}", "music_playlists": "`#{0}` - **{1}** by *{2}* ({3} songs)", "music_playlists_page": "Page {0} of saved playlists", "music_playlist_deleted": "Playlist deleted.", From 99049a6ace16343f39936574655ca1b897353ffa Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Mon, 3 Jul 2017 12:46:51 +0200 Subject: [PATCH 076/346] Prebuffering time drastically decreased --- src/NadekoBot/DataStructures/PoopyRingBuffer.cs | 5 +++++ src/NadekoBot/Services/Music/SongBuffer.cs | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/NadekoBot/DataStructures/PoopyRingBuffer.cs b/src/NadekoBot/DataStructures/PoopyRingBuffer.cs index 28b391e6..a09af647 100644 --- a/src/NadekoBot/DataStructures/PoopyRingBuffer.cs +++ b/src/NadekoBot/DataStructures/PoopyRingBuffer.cs @@ -38,6 +38,11 @@ namespace NadekoBot.DataStructures } } + public int LightLength => + _readPos <= _writePos? + _writePos - _readPos : + Capacity - (_readPos - _writePos); + public int RemainingCapacity { get { lock (posLock) return Capacity - Length - 1; } diff --git a/src/NadekoBot/Services/Music/SongBuffer.cs b/src/NadekoBot/Services/Music/SongBuffer.cs index 9f26671d..08737c49 100644 --- a/src/NadekoBot/Services/Music/SongBuffer.cs +++ b/src/NadekoBot/Services/Music/SongBuffer.cs @@ -54,7 +54,7 @@ namespace NadekoBot.Services.Music bytesRead = await p.StandardOutput.BaseStream.ReadAsync(buffer, 0, readSize, cancelToken).ConfigureAwait(false); await _outStream.WriteAsync(buffer, 0, bytesRead, cancelToken); - if (_outStream.RemainingCapacity < _outStream.Capacity * 0.5f || bytesRead == 0) + if (_outStream.LightLength > 200_000 || bytesRead == 0) if (toReturn.TrySetResult(true)) _log.Info("Prebuffering finished"); } From 421431d01dd871fa341927223e0b7248c2b249ff Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Mon, 3 Jul 2017 13:18:13 +0200 Subject: [PATCH 077/346] pausing disabled again due to issues --- src/NadekoBot/Modules/Music/Music.cs | 30 ++++++++++++++-------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/src/NadekoBot/Modules/Music/Music.cs b/src/NadekoBot/Modules/Music/Music.cs index 51713c80..6cb3ebe8 100644 --- a/src/NadekoBot/Modules/Music/Music.cs +++ b/src/NadekoBot/Modules/Music/Music.cs @@ -63,27 +63,27 @@ namespace NadekoBot.Modules.Music if ((player.VoiceChannel == oldState.VoiceChannel) && usr.Id == _client.CurrentUser.Id) { - if (player.Paused && newState.VoiceChannel.Users.Count > 1) //unpause if there are people in the new channel - player.TogglePause(); - else if (!player.Paused && newState.VoiceChannel.Users.Count <= 1) // pause if there are no users in the new channel - player.TogglePause(); + //if (player.Paused && newState.VoiceChannel.Users.Count > 1) //unpause if there are people in the new channel + // player.TogglePause(); + //else if (!player.Paused && newState.VoiceChannel.Users.Count <= 1) // pause if there are no users in the new channel + // player.TogglePause(); player.SetVoiceChannel(newState.VoiceChannel); return; } - //if some other user moved - if ((player.VoiceChannel == newState.VoiceChannel && //if joined first, and player paused, unpause - player.Paused && - newState.VoiceChannel.Users.Count >= 2) || // keep in mind bot is in the channel (+1) - (player.VoiceChannel == oldState.VoiceChannel && // if left last, and player unpaused, pause - !player.Paused && - oldState.VoiceChannel.Users.Count == 1)) - { - player.TogglePause(); - return; - } + ////if some other user moved + //if ((player.VoiceChannel == newState.VoiceChannel && //if joined first, and player paused, unpause + // player.Paused && + // newState.VoiceChannel.Users.Count >= 2) || // keep in mind bot is in the channel (+1) + // (player.VoiceChannel == oldState.VoiceChannel && // if left last, and player unpaused, pause + // !player.Paused && + // oldState.VoiceChannel.Users.Count == 1)) + //{ + // player.TogglePause(); + // return; + //} } catch { From 44859529d53a0e4d469f206289b4014876ce7339 Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Mon, 3 Jul 2017 20:26:17 +0200 Subject: [PATCH 078/346] Cleanup and fixes --- .../DataStructures/PoopyRingBuffer.cs | 143 +++++++----------- src/NadekoBot/Modules/Music/Music.cs | 2 +- src/NadekoBot/NadekoBot.cs | 1 + src/NadekoBot/Services/Music/MusicPlayer.cs | 82 +++++----- src/NadekoBot/Services/Music/SongBuffer.cs | 36 ++++- 5 files changed, 134 insertions(+), 130 deletions(-) diff --git a/src/NadekoBot/DataStructures/PoopyRingBuffer.cs b/src/NadekoBot/DataStructures/PoopyRingBuffer.cs index a09af647..6c1e1118 100644 --- a/src/NadekoBot/DataStructures/PoopyRingBuffer.cs +++ b/src/NadekoBot/DataStructures/PoopyRingBuffer.cs @@ -10,42 +10,27 @@ namespace NadekoBot.DataStructures // writepos == readpos - 1 means full private readonly byte[] buffer; - private readonly object posLock = new object(); public int Capacity { get; } - private volatile int _readPos = 0; + private int _readPos = 0; private int ReadPos { get => _readPos; - set { lock (posLock) _readPos = value; } + set => _readPos = value; } - private volatile int _writePos = 0; + private int _writePos = 0; private int WritePos { get => _writePos; - set { lock (posLock) _writePos = value; } + set => _writePos = value; } - private int Length - { - get - { - lock (posLock) - { - return ReadPos <= WritePos ? - WritePos - ReadPos : - Capacity - (ReadPos - WritePos); - } - } - } - - public int LightLength => - _readPos <= _writePos? - _writePos - _readPos : - Capacity - (_readPos - _writePos); + public int Length => ReadPos <= WritePos + ? WritePos - ReadPos + : Capacity - (ReadPos - WritePos); public int RemainingCapacity { - get { lock (posLock) return Capacity - Length - 1; } + get => Capacity - Length - 1; } private readonly SemaphoreSlim _locker = new SemaphoreSlim(1, 1); @@ -56,90 +41,76 @@ namespace NadekoBot.DataStructures this.buffer = new byte[this.Capacity]; } - public Task ReadAsync(byte[] b, int offset, int toRead, CancellationToken cancelToken) => Task.Run(async () => + public int Read(byte[] b, int offset, int toRead) { - await _locker.WaitAsync(cancelToken); - try + if (WritePos == ReadPos) + return 0; + + if (toRead > Length) + toRead = Length; + + if (WritePos > ReadPos) { - if (WritePos == ReadPos) - return 0; - - if (toRead > Length) - toRead = Length; - - if (WritePos > ReadPos) - { - Buffer.BlockCopy(buffer, ReadPos, b, offset, toRead); - ReadPos += toRead; - } - else - { - var toEnd = Capacity - ReadPos; - var firstRead = toRead > toEnd ? - toEnd : - toRead; - Buffer.BlockCopy(buffer, ReadPos, b, offset, firstRead); - ReadPos += firstRead; - var secondRead = toRead - firstRead; - if (secondRead > 0) - { - Buffer.BlockCopy(buffer, 0, b, offset + firstRead, secondRead); - ReadPos = secondRead; - } - } - return toRead; + Array.Copy(buffer, ReadPos, b, offset, toRead); + ReadPos += toRead; } - finally + else { - _locker.Release(); + var toEnd = Capacity - ReadPos; + var firstRead = toRead > toEnd ? + toEnd : + toRead; + Array.Copy(buffer, ReadPos, b, offset, firstRead); + ReadPos += firstRead; + var secondRead = toRead - firstRead; + if (secondRead > 0) + { + Array.Copy(buffer, 0, b, offset + firstRead, secondRead); + ReadPos = secondRead; + } } - }); + return toRead; + } - public Task WriteAsync(byte[] b, int offset, int toWrite, CancellationToken cancelToken) => Task.Run(async () => + public bool Write(byte[] b, int offset, int toWrite) { while (toWrite > RemainingCapacity) - await Task.Delay(1000, cancelToken); // wait a lot, buffer should be large anyway + return false; if (toWrite == 0) - return; + return true; - await _locker.WaitAsync(cancelToken); - try + if (WritePos < ReadPos) { - if (WritePos < ReadPos) + Array.Copy(b, offset, buffer, WritePos, toWrite); + WritePos += toWrite; + } + else + { + var toEnd = Capacity - WritePos; + var firstWrite = toWrite > toEnd ? + toEnd : + toWrite; + Array.Copy(b, offset, buffer, WritePos, firstWrite); + var secondWrite = toWrite - firstWrite; + if (secondWrite > 0) { - Buffer.BlockCopy(b, offset, buffer, WritePos, toWrite); - WritePos += toWrite; + Array.Copy(b, offset + firstWrite, buffer, 0, secondWrite); + WritePos = secondWrite; } else { - var toEnd = Capacity - WritePos; - var firstWrite = toWrite > toEnd ? - toEnd : - toWrite; - Buffer.BlockCopy(b, offset, buffer, WritePos, firstWrite); - var secondWrite = toWrite - firstWrite; - if (secondWrite > 0) - { - Buffer.BlockCopy(b, offset + firstWrite, buffer, 0, secondWrite); - WritePos = secondWrite; - } - else - { - WritePos += firstWrite; - if (WritePos == Capacity) - WritePos = 0; - } + WritePos += firstWrite; + if (WritePos == Capacity) + WritePos = 0; } } - finally - { - _locker.Release(); - } - }); + return true; + } public void Dispose() { + } } } diff --git a/src/NadekoBot/Modules/Music/Music.cs b/src/NadekoBot/Modules/Music/Music.cs index 6cb3ebe8..bc88ff2d 100644 --- a/src/NadekoBot/Modules/Music/Music.cs +++ b/src/NadekoBot/Modules/Music/Music.cs @@ -45,7 +45,7 @@ namespace NadekoBot.Modules.Music var t = _music.DestroyPlayer(arg.Id); return Task.CompletedTask; } - + //todo changing server region is bugged again private Task Client_UserVoiceStateUpdated(SocketUser iusr, SocketVoiceState oldState, SocketVoiceState newState) { var t = Task.Run(() => diff --git a/src/NadekoBot/NadekoBot.cs b/src/NadekoBot/NadekoBot.cs index 855c5652..5560437f 100644 --- a/src/NadekoBot/NadekoBot.cs +++ b/src/NadekoBot/NadekoBot.cs @@ -31,6 +31,7 @@ using NadekoBot.Extensions; namespace NadekoBot { + //todo log when joining or leaving the server public class NadekoBot { private Logger _log; diff --git a/src/NadekoBot/Services/Music/MusicPlayer.cs b/src/NadekoBot/Services/Music/MusicPlayer.cs index c42d56da..a9a12f25 100644 --- a/src/NadekoBot/Services/Music/MusicPlayer.cs +++ b/src/NadekoBot/Services/Music/MusicPlayer.cs @@ -131,38 +131,40 @@ namespace NadekoBot.Services.Music _log.Info("Starting"); using (var b = new SongBuffer(data.Song.Uri, "")) { - var bufferTask = b.StartBuffering(cancelToken); - var timeout = Task.Delay(10000); - if (Task.WhenAny(bufferTask, timeout) == timeout) - { - _log.Info("Buffering failed due to a timeout."); - continue; - } - else if (!bufferTask.Result) - { - _log.Info("Buffering failed due to a cancel or error."); - continue; - } - - var ac = await GetAudioClient(); - if (ac == null) - { - await Task.Delay(900, cancelToken); - // just wait some time, maybe bot doesn't even have perms to join that voice channel, - // i don't want to spam connection attempts - continue; - } - var pcm = ac.CreatePCMStream(AudioApplication.Music, bufferMillis: 500); - OnStarted?.Invoke(this, data); - - byte[] buffer = new byte[3840]; - int bytesRead = 0; + AudioOutStream pcm = null; try { - while ((bytesRead = await b.ReadAsync(buffer, 0, buffer.Length, cancelToken).ConfigureAwait(false)) > 0 + var bufferTask = b.StartBuffering(cancelToken); + var timeout = Task.Delay(10000); + if (Task.WhenAny(bufferTask, timeout) == timeout) + { + _log.Info("Buffering failed due to a timeout."); + continue; + } + else if (!bufferTask.Result) + { + _log.Info("Buffering failed due to a cancel or error."); + continue; + } + + var ac = await GetAudioClient(); + if (ac == null) + { + await Task.Delay(900, cancelToken); + // just wait some time, maybe bot doesn't even have perms to join that voice channel, + // i don't want to spam connection attempts + continue; + } + pcm = ac.CreatePCMStream(AudioApplication.Music, bufferMillis: 500); + OnStarted?.Invoke(this, data); + + byte[] buffer = new byte[3840]; + int bytesRead = 0; + + while ((bytesRead = b.Read(buffer, 0, buffer.Length)) > 0 && (MaxPlaytimeSeconds <= 0 || MaxPlaytimeSeconds >= CurrentTime.TotalSeconds)) { - AdjustVolume(buffer, Volume); + //AdjustVolume(buffer, Volume); await pcm.WriteAsync(buffer, 0, bytesRead, cancelToken).ConfigureAwait(false); unchecked { _bytesSent += bytesRead; } @@ -179,12 +181,16 @@ namespace NadekoBot.Services.Music } finally { - //flush is known to get stuck from time to time, just cancel it if it takes more than 1 second - var flushCancel = new CancellationTokenSource(); - var flushToken = flushCancel.Token; - var flushDelay = Task.Delay(1000, flushToken); - await Task.WhenAny(flushDelay, pcm.FlushAsync(flushToken)); - flushCancel.Cancel(); + if (pcm != null) + { + // flush is known to get stuck from time to time, + // just skip flushing if it takes more than 1 second + var flushCancel = new CancellationTokenSource(); + var flushToken = flushCancel.Token; + var flushDelay = Task.Delay(1000, flushToken); + await Task.WhenAny(flushDelay, pcm.FlushAsync(flushToken)); + flushCancel.Cancel(); + } OnCompleted?.Invoke(this, data.Song); } @@ -309,8 +315,12 @@ namespace NadekoBot.Services.Music { try { - await _audioClient?.StopAsync(); - _audioClient?.Dispose(); + var t = _audioClient?.StopAsync(); + if (t != null) + { + await t; + _audioClient?.Dispose(); + } } catch { diff --git a/src/NadekoBot/Services/Music/SongBuffer.cs b/src/NadekoBot/Services/Music/SongBuffer.cs index 08737c49..527c65f3 100644 --- a/src/NadekoBot/Services/Music/SongBuffer.cs +++ b/src/NadekoBot/Services/Music/SongBuffer.cs @@ -36,10 +36,17 @@ namespace NadekoBot.Services.Music var t = Task.Run(() => { this.p.BeginErrorReadLine(); + this.p.ErrorDataReceived += P_ErrorDataReceived; this.p.WaitForExit(); }); } + private void P_ErrorDataReceived(object sender, DataReceivedEventArgs e) + { + _log.Error(">>> " + e.Data); + } + + private readonly object locker = new object(); public Task StartBuffering(CancellationToken cancelToken) { var toReturn = new TaskCompletionSource(); @@ -49,14 +56,26 @@ namespace NadekoBot.Services.Music { byte[] buffer = new byte[readSize]; int bytesRead = 1; - while (!cancelToken.IsCancellationRequested && !this.p.HasExited && bytesRead > 0) + while (!cancelToken.IsCancellationRequested && !this.p.HasExited) { bytesRead = await p.StandardOutput.BaseStream.ReadAsync(buffer, 0, readSize, cancelToken).ConfigureAwait(false); - await _outStream.WriteAsync(buffer, 0, bytesRead, cancelToken); + if (bytesRead == 0) + break; + bool written; + do + { + lock (locker) + written = _outStream.Write(buffer, 0, bytesRead); + if (!written) + await Task.Delay(32, cancelToken); + } + while (!written); + lock (locker) + if (_outStream.Length > 200_000 || bytesRead == 0) + if (toReturn.TrySetResult(true)) + _log.Info("Prebuffering finished"); - if (_outStream.LightLength > 200_000 || bytesRead == 0) - if (toReturn.TrySetResult(true)) - _log.Info("Prebuffering finished"); + await Task.Delay(5); // @.@ } _log.Info("FFMPEG killed, song canceled, or song fully downloaded"); } @@ -84,9 +103,10 @@ Check the guides for your platform on how to setup ffmpeg correctly: return toReturn.Task; } - public Task ReadAsync(byte[] b, int offset, int toRead, CancellationToken cancelToken) + public int Read(byte[] b, int offset, int toRead) { - return _outStream.ReadAsync(b, offset, toRead, cancelToken); + lock (locker) + return _outStream.Read(b, offset, toRead); } public void Dispose() @@ -94,6 +114,8 @@ Check the guides for your platform on how to setup ffmpeg correctly: try { this.p.Kill(); } catch { } _outStream.Dispose(); + this.p.StandardError.Dispose(); + this.p.StandardOutput.Dispose(); this.p.Dispose(); } } From 89eabc7c14657f93636316e3cf7d16c5091d0025 Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Mon, 3 Jul 2017 20:29:32 +0200 Subject: [PATCH 079/346] Fixed crash --- src/NadekoBot/Modules/Music/Music.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/NadekoBot/Modules/Music/Music.cs b/src/NadekoBot/Modules/Music/Music.cs index bc88ff2d..09bcde52 100644 --- a/src/NadekoBot/Modules/Music/Music.cs +++ b/src/NadekoBot/Modules/Music/Music.cs @@ -57,6 +57,9 @@ namespace NadekoBot.Modules.Music var player = _music.GetPlayerOrDefault(usr.Guild.Id); + if (player == null) + return; + try { //if bot moved From f826fb97f6dff871d8f5270230144f554b23f59f Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Mon, 3 Jul 2017 21:05:35 +0200 Subject: [PATCH 080/346] Super weird fixes, i must've broke something else. Bot has to reconnect after restart now --- src/NadekoBot/Modules/Music/Music.cs | 4 +- src/NadekoBot/Services/Music/MusicPlayer.cs | 311 ++++++++++---------- src/NadekoBot/Services/Music/SongBuffer.cs | 10 +- 3 files changed, 166 insertions(+), 159 deletions(-) diff --git a/src/NadekoBot/Modules/Music/Music.cs b/src/NadekoBot/Modules/Music/Music.cs index 09bcde52..8f80e726 100644 --- a/src/NadekoBot/Modules/Music/Music.cs +++ b/src/NadekoBot/Modules/Music/Music.cs @@ -70,8 +70,8 @@ namespace NadekoBot.Modules.Music // player.TogglePause(); //else if (!player.Paused && newState.VoiceChannel.Users.Count <= 1) // pause if there are no users in the new channel // player.TogglePause(); - - player.SetVoiceChannel(newState.VoiceChannel); + + // player.SetVoiceChannel(newState.VoiceChannel); return; } diff --git a/src/NadekoBot/Services/Music/MusicPlayer.cs b/src/NadekoBot/Services/Music/MusicPlayer.cs index a9a12f25..5df2955a 100644 --- a/src/NadekoBot/Services/Music/MusicPlayer.cs +++ b/src/NadekoBot/Services/Music/MusicPlayer.cs @@ -123,172 +123,166 @@ namespace NadekoBot.Services.Music manualSkip = false; manualIndex = false; } - try - { - if (data.Song == null) - continue; - - _log.Info("Starting"); - using (var b = new SongBuffer(data.Song.Uri, "")) - { - AudioOutStream pcm = null; - try - { - var bufferTask = b.StartBuffering(cancelToken); - var timeout = Task.Delay(10000); - if (Task.WhenAny(bufferTask, timeout) == timeout) - { - _log.Info("Buffering failed due to a timeout."); - continue; - } - else if (!bufferTask.Result) - { - _log.Info("Buffering failed due to a cancel or error."); - continue; - } - - var ac = await GetAudioClient(); - if (ac == null) - { - await Task.Delay(900, cancelToken); - // just wait some time, maybe bot doesn't even have perms to join that voice channel, - // i don't want to spam connection attempts - continue; - } - pcm = ac.CreatePCMStream(AudioApplication.Music, bufferMillis: 500); - OnStarted?.Invoke(this, data); - - byte[] buffer = new byte[3840]; - int bytesRead = 0; - - while ((bytesRead = b.Read(buffer, 0, buffer.Length)) > 0 - && (MaxPlaytimeSeconds <= 0 || MaxPlaytimeSeconds >= CurrentTime.TotalSeconds)) - { - //AdjustVolume(buffer, Volume); - await pcm.WriteAsync(buffer, 0, bytesRead, cancelToken).ConfigureAwait(false); - unchecked { _bytesSent += bytesRead; } - - await (pauseTaskSource?.Task ?? Task.CompletedTask); - } - } - catch (OperationCanceledException) - { - _log.Info("Song Canceled"); - } - catch (Exception ex) - { - _log.Warn(ex); - } - finally - { - if (pcm != null) - { - // flush is known to get stuck from time to time, - // just skip flushing if it takes more than 1 second - var flushCancel = new CancellationTokenSource(); - var flushToken = flushCancel.Token; - var flushDelay = Task.Delay(1000, flushToken); - await Task.WhenAny(flushDelay, pcm.FlushAsync(flushToken)); - flushCancel.Cancel(); - } - - OnCompleted?.Invoke(this, data.Song); - } - } - } - finally + if (data.Song == null) + continue; + + _log.Info("Starting"); + using (var b = new SongBuffer(data.Song.Uri, "")) { + AudioOutStream pcm = null; try { - //if repeating current song, just ignore other settings, - // and play this song again (don't change the index) - // ignore rcs if song is manually skipped - - int queueCount; - lock (locker) - queueCount = Queue.Count; - - if (!manualIndex && (!RepeatCurrentSong || manualSkip)) + var bufferTask = b.StartBuffering(cancelToken); + var timeout = Task.Delay(10000); + if (Task.WhenAny(bufferTask, timeout) == timeout) { - if (Shuffle) - { - _log.Info("Random song"); - Queue.Random(); //if shuffle is set, set current song index to a random number - } - else - { - //if last song, and autoplay is enabled, and if it's a youtube song - // do autplay magix - if (queueCount - 1 == data.Index && Autoplay && data.Song?.ProviderType == Database.Models.MusicType.YouTube) - { - try - { - _log.Info("Loading related song"); - await _musicService.TryQueueRelatedSongAsync(data.Song.Query, OutputTextChannel, VoiceChannel); - Queue.Next(); - } - catch - { - _log.Info("Loading related song failed."); - } - } - else if (FairPlay) - { - lock (locker) - { - _log.Info("Next fair song"); - var q = Queue.ToArray().Songs.Shuffle().ToArray(); - - bool found = false; - for (var i = 0; i < q.Length; i++) //first try to find a queuer who didn't have their song played recently - { - var item = q[i]; - if (RecentlyPlayedUsers.Add(item.QueuerName)) // if it's found, set current song to that index - { - Queue.CurrentIndex = i; - found = true; - break; - } - } - if (!found) //if it's not - { - RecentlyPlayedUsers.Clear(); //clear all recently played users (that means everyone from the playlist has had their song played) - Queue.Random(); //go to a random song (to prevent looping on the first few songs) - var cur = Current; - if (cur.Current != null) // add newely scheduled song's queuer to the recently played list - RecentlyPlayedUsers.Add(cur.Current.QueuerName); - } - } - } - else if (queueCount - 1 == data.Index && !RepeatPlaylist && !manualSkip) - { - _log.Info("Stopping because repeatplaylist is disabled"); - lock (locker) - { - Stop(); - } - } - else - { - _log.Info("Next song"); - lock (locker) - { - Queue.Next(); - } - } - } + _log.Info("Buffering failed due to a timeout."); + continue; } + else if (!bufferTask.Result) + { + _log.Info("Buffering failed due to a cancel or error."); + continue; + } + + var ac = await GetAudioClient(); + if (ac == null) + { + await Task.Delay(900, cancelToken); + // just wait some time, maybe bot doesn't even have perms to join that voice channel, + // i don't want to spam connection attempts + continue; + } + pcm = ac.CreatePCMStream(AudioApplication.Music, bufferMillis: 500); + OnStarted?.Invoke(this, data); + + byte[] buffer = new byte[3840]; + int bytesRead = 0; + + while ((bytesRead = b.Read(buffer, 0, buffer.Length)) > 0 + && (MaxPlaytimeSeconds <= 0 || MaxPlaytimeSeconds >= CurrentTime.TotalSeconds)) + { + //AdjustVolume(buffer, Volume); + await pcm.WriteAsync(buffer, 0, bytesRead, cancelToken).ConfigureAwait(false); + unchecked { _bytesSent += bytesRead; } + + await (pauseTaskSource?.Task ?? Task.CompletedTask); + } + } + catch (OperationCanceledException) + { + _log.Info("Song Canceled"); } catch (Exception ex) { - _log.Error(ex); + _log.Warn(ex); } - do + finally { - await Task.Delay(500); + if (pcm != null) + { + // flush is known to get stuck from time to time, + // just skip flushing if it takes more than 1 second + var flushCancel = new CancellationTokenSource(); + var flushToken = flushCancel.Token; + var flushDelay = Task.Delay(1000, flushToken); + await Task.WhenAny(flushDelay, pcm.FlushAsync(flushToken)); + flushCancel.Cancel(); + } + + OnCompleted?.Invoke(this, data.Song); } - while ((Queue.Count == 0 || Stopped) && !Exited); } + try + { + //if repeating current song, just ignore other settings, + // and play this song again (don't change the index) + // ignore rcs if song is manually skipped + + int queueCount; + lock (locker) + queueCount = Queue.Count; + + if (!manualIndex && (!RepeatCurrentSong || manualSkip)) + { + if (Shuffle) + { + _log.Info("Random song"); + Queue.Random(); //if shuffle is set, set current song index to a random number + } + else + { + //if last song, and autoplay is enabled, and if it's a youtube song + // do autplay magix + if (queueCount - 1 == data.Index && Autoplay && data.Song?.ProviderType == Database.Models.MusicType.YouTube) + { + try + { + _log.Info("Loading related song"); + await _musicService.TryQueueRelatedSongAsync(data.Song.Query, OutputTextChannel, VoiceChannel); + Queue.Next(); + } + catch + { + _log.Info("Loading related song failed."); + } + } + else if (FairPlay) + { + lock (locker) + { + _log.Info("Next fair song"); + var q = Queue.ToArray().Songs.Shuffle().ToArray(); + + bool found = false; + for (var i = 0; i < q.Length; i++) //first try to find a queuer who didn't have their song played recently + { + var item = q[i]; + if (RecentlyPlayedUsers.Add(item.QueuerName)) // if it's found, set current song to that index + { + Queue.CurrentIndex = i; + found = true; + break; + } + } + if (!found) //if it's not + { + RecentlyPlayedUsers.Clear(); //clear all recently played users (that means everyone from the playlist has had their song played) + Queue.Random(); //go to a random song (to prevent looping on the first few songs) + var cur = Current; + if (cur.Current != null) // add newely scheduled song's queuer to the recently played list + RecentlyPlayedUsers.Add(cur.Current.QueuerName); + } + } + } + else if (queueCount - 1 == data.Index && !RepeatPlaylist && !manualSkip) + { + _log.Info("Stopping because repeatplaylist is disabled"); + lock (locker) + { + Stop(); + } + } + else + { + _log.Info("Next song"); + lock (locker) + { + Queue.Next(); + } + } + } + } + } + catch (Exception ex) + { + _log.Error(ex); + } + do + { + await Task.Delay(500); + } + while ((Queue.Count == 0 || Stopped) && !Exited); } }, SongCancelSource.Token); } @@ -319,13 +313,20 @@ namespace NadekoBot.Services.Music if (t != null) { await t; - _audioClient?.Dispose(); + _audioClient.Dispose(); } } catch { } newVoiceChannel = false; + var curUser = await VoiceChannel.Guild.GetCurrentUserAsync(); + _audioClient = await VoiceChannel.ConnectAsync(); + if (curUser.VoiceChannel != null) + { + await _audioClient.StopAsync(); + await Task.Delay(1000); + } _audioClient = await VoiceChannel.ConnectAsync(); } catch diff --git a/src/NadekoBot/Services/Music/SongBuffer.cs b/src/NadekoBot/Services/Music/SongBuffer.cs index 527c65f3..8cc23462 100644 --- a/src/NadekoBot/Services/Music/SongBuffer.cs +++ b/src/NadekoBot/Services/Music/SongBuffer.cs @@ -111,11 +111,17 @@ Check the guides for your platform on how to setup ffmpeg correctly: public void Dispose() { + try + { + this.p.StandardOutput.Dispose(); + } + catch (Exception ex) + { + _log.Error(ex); + } try { this.p.Kill(); } catch { } _outStream.Dispose(); - this.p.StandardError.Dispose(); - this.p.StandardOutput.Dispose(); this.p.Dispose(); } } From eba804b5ce1baff5185afefcfff28187630d462b Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Mon, 3 Jul 2017 23:27:17 +0200 Subject: [PATCH 081/346] Cleanup, improvements --- .../DataStructures/PoopyRingBuffer.cs | 2 +- .../Administration/GuildTimezoneService.cs | 5 +++- src/NadekoBot/Services/Music/MusicPlayer.cs | 4 +-- src/NadekoBot/Services/Music/SongBuffer.cs | 29 +++++++++++++++---- 4 files changed, 30 insertions(+), 10 deletions(-) diff --git a/src/NadekoBot/DataStructures/PoopyRingBuffer.cs b/src/NadekoBot/DataStructures/PoopyRingBuffer.cs index 6c1e1118..acb7db8e 100644 --- a/src/NadekoBot/DataStructures/PoopyRingBuffer.cs +++ b/src/NadekoBot/DataStructures/PoopyRingBuffer.cs @@ -35,7 +35,7 @@ namespace NadekoBot.DataStructures private readonly SemaphoreSlim _locker = new SemaphoreSlim(1, 1); - public PoopyRingBuffer(int capacity = 50_000_000) + public PoopyRingBuffer(int capacity = 81920 * 100) { this.Capacity = capacity + 1; this.buffer = new byte[this.Capacity]; diff --git a/src/NadekoBot/Services/Administration/GuildTimezoneService.cs b/src/NadekoBot/Services/Administration/GuildTimezoneService.cs index 1d469211..6e4757fe 100644 --- a/src/NadekoBot/Services/Administration/GuildTimezoneService.cs +++ b/src/NadekoBot/Services/Administration/GuildTimezoneService.cs @@ -21,7 +21,10 @@ namespace NadekoBot.Services.Administration TimeZoneInfo tz; try { - tz = TimeZoneInfo.FindSystemTimeZoneById(x.TimeZoneId); + if (x.TimeZoneId == null) + tz = null; + else + tz = TimeZoneInfo.FindSystemTimeZoneById(x.TimeZoneId); } catch { diff --git a/src/NadekoBot/Services/Music/MusicPlayer.cs b/src/NadekoBot/Services/Music/MusicPlayer.cs index 5df2955a..fc11e1ab 100644 --- a/src/NadekoBot/Services/Music/MusicPlayer.cs +++ b/src/NadekoBot/Services/Music/MusicPlayer.cs @@ -321,10 +321,10 @@ namespace NadekoBot.Services.Music } newVoiceChannel = false; var curUser = await VoiceChannel.Guild.GetCurrentUserAsync(); - _audioClient = await VoiceChannel.ConnectAsync(); if (curUser.VoiceChannel != null) { - await _audioClient.StopAsync(); + var ac = await VoiceChannel.ConnectAsync(); + await ac.StopAsync(); await Task.Delay(1000); } _audioClient = await VoiceChannel.ConnectAsync(); diff --git a/src/NadekoBot/Services/Music/SongBuffer.cs b/src/NadekoBot/Services/Music/SongBuffer.cs index 8cc23462..d0d550b3 100644 --- a/src/NadekoBot/Services/Music/SongBuffer.cs +++ b/src/NadekoBot/Services/Music/SongBuffer.cs @@ -10,7 +10,7 @@ namespace NadekoBot.Services.Music { public class SongBuffer : IDisposable { - const int readSize = 38400; + const int readSize = 81920; private Process p; private PoopyRingBuffer _outStream = new PoopyRingBuffer(); @@ -52,8 +52,13 @@ namespace NadekoBot.Services.Music var toReturn = new TaskCompletionSource(); var _ = Task.Run(async () => { + int maxLoopsPerSec = 25; + var sw = Stopwatch.StartNew(); + var delay = 1000 / maxLoopsPerSec; + int currentLoops = 0; try { + ++currentLoops; byte[] buffer = new byte[readSize]; int bytesRead = 1; while (!cancelToken.IsCancellationRequested && !this.p.HasExited) @@ -67,7 +72,7 @@ namespace NadekoBot.Services.Music lock (locker) written = _outStream.Write(buffer, 0, bytesRead); if (!written) - await Task.Delay(32, cancelToken); + await Task.Delay(2000, cancelToken); } while (!written); lock (locker) @@ -75,9 +80,15 @@ namespace NadekoBot.Services.Music if (toReturn.TrySetResult(true)) _log.Info("Prebuffering finished"); - await Task.Delay(5); // @.@ + _log.Info(_outStream.Length); + await Task.Delay(10); } - _log.Info("FFMPEG killed, song canceled, or song fully downloaded"); + if (cancelToken.IsCancellationRequested) + _log.Info("Song canceled"); + else if (p.HasExited) + _log.Info("Song buffered completely (FFmpeg exited)"); + else if (bytesRead == 0) + _log.Info("Nothing read"); } catch (System.ComponentModel.Win32Exception) { @@ -119,8 +130,14 @@ Check the guides for your platform on how to setup ffmpeg correctly: { _log.Error(ex); } - try { this.p.Kill(); } - catch { } + try + { + if(!this.p.HasExited) + this.p.Kill(); + } + catch + { + } _outStream.Dispose(); this.p.Dispose(); } From fc941770e9494a90ceef9137791b254c099a4430 Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Mon, 3 Jul 2017 23:59:56 +0200 Subject: [PATCH 082/346] Removed spam --- src/NadekoBot/Services/Music/SongBuffer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/NadekoBot/Services/Music/SongBuffer.cs b/src/NadekoBot/Services/Music/SongBuffer.cs index d0d550b3..7f79eac5 100644 --- a/src/NadekoBot/Services/Music/SongBuffer.cs +++ b/src/NadekoBot/Services/Music/SongBuffer.cs @@ -80,7 +80,7 @@ namespace NadekoBot.Services.Music if (toReturn.TrySetResult(true)) _log.Info("Prebuffering finished"); - _log.Info(_outStream.Length); + //_log.Info(_outStream.Length); await Task.Delay(10); } if (cancelToken.IsCancellationRequested) From 684dba0d9c3b6de5a13a7876151595f995daba7c Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Tue, 4 Jul 2017 00:21:45 +0200 Subject: [PATCH 083/346] added some debugging --- src/NadekoBot/Services/Music/SongBuffer.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/NadekoBot/Services/Music/SongBuffer.cs b/src/NadekoBot/Services/Music/SongBuffer.cs index 7f79eac5..5ff91e88 100644 --- a/src/NadekoBot/Services/Music/SongBuffer.cs +++ b/src/NadekoBot/Services/Music/SongBuffer.cs @@ -27,7 +27,7 @@ namespace NadekoBot.Services.Music this.p = Process.Start(new ProcessStartInfo { FileName = "ffmpeg", - Arguments = $"-i {songUri} -f s16le -ar 48000 -vn -ac 2 pipe:1 -loglevel quiet", + Arguments = $"-i {songUri} -f s16le -ar 48000 -vn -ac 2 pipe:1 -loglevel error -threads 0", UseShellExecute = false, RedirectStandardOutput = true, RedirectStandardError = true, @@ -78,7 +78,7 @@ namespace NadekoBot.Services.Music lock (locker) if (_outStream.Length > 200_000 || bytesRead == 0) if (toReturn.TrySetResult(true)) - _log.Info("Prebuffering finished"); + _log.Info("Prebuffering finished in {0}", sw.Elapsed.TotalSeconds.ToString("F2")); //_log.Info(_outStream.Length); await Task.Delay(10); From 842b45178d9f0eeadced464d0414bc2e1e2588e9 Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Tue, 4 Jul 2017 10:16:18 +0200 Subject: [PATCH 084/346] Added reconnect arguments to ffmpeg --- src/NadekoBot/Services/Music/SongBuffer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/NadekoBot/Services/Music/SongBuffer.cs b/src/NadekoBot/Services/Music/SongBuffer.cs index 5ff91e88..f6f72b4e 100644 --- a/src/NadekoBot/Services/Music/SongBuffer.cs +++ b/src/NadekoBot/Services/Music/SongBuffer.cs @@ -27,7 +27,7 @@ namespace NadekoBot.Services.Music this.p = Process.Start(new ProcessStartInfo { FileName = "ffmpeg", - Arguments = $"-i {songUri} -f s16le -ar 48000 -vn -ac 2 pipe:1 -loglevel error -threads 0", + Arguments = $"-i {songUri} -f s16le -ar 48000 -vn -ac 2 pipe:1 -loglevel error -threads 0 -reconnect 1 -reconnect_streamed 1 -reconnect_delay_max 5", UseShellExecute = false, RedirectStandardOutput = true, RedirectStandardError = true, From 556174ec897f72207c7fda8dab88b291acc9dc79 Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Tue, 4 Jul 2017 11:23:34 +0200 Subject: [PATCH 085/346] Restart ffmpeg if it crashes? Maybe i should reconsider file-based cache. Ffmpeg doesn't like being slowed down it seems --- src/NadekoBot/Services/Music/SongBuffer.cs | 100 +++++++++++++-------- 1 file changed, 65 insertions(+), 35 deletions(-) diff --git a/src/NadekoBot/Services/Music/SongBuffer.cs b/src/NadekoBot/Services/Music/SongBuffer.cs index f6f72b4e..f962f919 100644 --- a/src/NadekoBot/Services/Music/SongBuffer.cs +++ b/src/NadekoBot/Services/Music/SongBuffer.cs @@ -19,20 +19,14 @@ namespace NadekoBot.Services.Music public string SongUri { get; private set; } + private volatile bool restart = false; + public SongBuffer(string songUri, string skipTo) { _log = LogManager.GetCurrentClassLogger(); this.SongUri = songUri; - this.p = Process.Start(new ProcessStartInfo - { - FileName = "ffmpeg", - Arguments = $"-i {songUri} -f s16le -ar 48000 -vn -ac 2 pipe:1 -loglevel error -threads 0 -reconnect 1 -reconnect_streamed 1 -reconnect_delay_max 5", - UseShellExecute = false, - RedirectStandardOutput = true, - RedirectStandardError = true, - CreateNoWindow = true, - }); + this.p = StartFFmpegProcess(songUri, 0); var t = Task.Run(() => { this.p.BeginErrorReadLine(); @@ -41,9 +35,27 @@ namespace NadekoBot.Services.Music }); } + private Process StartFFmpegProcess(string songUri, float skipTo = 0) + { + return Process.Start(new ProcessStartInfo + { + FileName = "ffmpeg", + Arguments = $"-ss {skipTo:F4} -i {songUri} -f s16le -ar 48000 -vn -ac 2 pipe:1 -loglevel error -threads 0 -reconnect 1 -reconnect_streamed 1 -reconnect_delay_max 5", + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = true, + CreateNoWindow = true, + }); + } + private void P_ErrorDataReceived(object sender, DataReceivedEventArgs e) { _log.Error(">>> " + e.Data); + if (e.Data.Contains("Error in the pull function")) + { + _log.Info("Got error in the pull function!"); + restart = true; + } } private readonly object locker = new object(); @@ -56,39 +68,57 @@ namespace NadekoBot.Services.Music var sw = Stopwatch.StartNew(); var delay = 1000 / maxLoopsPerSec; int currentLoops = 0; + int _bytesSent = 0; try { - ++currentLoops; - byte[] buffer = new byte[readSize]; - int bytesRead = 1; - while (!cancelToken.IsCancellationRequested && !this.p.HasExited) + do { - bytesRead = await p.StandardOutput.BaseStream.ReadAsync(buffer, 0, readSize, cancelToken).ConfigureAwait(false); - if (bytesRead == 0) - break; - bool written; - do + if (restart) { - lock (locker) - written = _outStream.Write(buffer, 0, bytesRead); - if (!written) - await Task.Delay(2000, cancelToken); + var cur = _bytesSent / 3840 / (1000 / 20.0f); + _log.Info("Restarting"); + try { this.p.StandardOutput.Dispose(); } catch { } + try { this.p.Dispose(); } catch { } + this.p = StartFFmpegProcess(SongUri, cur); } - while (!written); - lock (locker) - if (_outStream.Length > 200_000 || bytesRead == 0) - if (toReturn.TrySetResult(true)) - _log.Info("Prebuffering finished in {0}", sw.Elapsed.TotalSeconds.ToString("F2")); + restart = false; + ++currentLoops; + byte[] buffer = new byte[readSize]; + int bytesRead = 1; + while (!cancelToken.IsCancellationRequested && !this.p.HasExited) + { + bytesRead = await p.StandardOutput.BaseStream.ReadAsync(buffer, 0, readSize, cancelToken).ConfigureAwait(false); + _bytesSent += bytesRead; + if (bytesRead == 0) + break; + bool written; + do + { + lock (locker) + written = _outStream.Write(buffer, 0, bytesRead); + if (!written) + await Task.Delay(2000, cancelToken); + } + while (!written); + lock (locker) + if (_outStream.Length > 200_000 || bytesRead == 0) + if (toReturn.TrySetResult(true)) + _log.Info("Prebuffering finished in {0}", sw.Elapsed.TotalSeconds.ToString("F2")); - //_log.Info(_outStream.Length); - await Task.Delay(10); + //_log.Info(_outStream.Length); + await Task.Delay(10); + } + if (cancelToken.IsCancellationRequested) + _log.Info("Song canceled"); + else if (p.HasExited) + _log.Info("Song buffered completely (FFmpeg exited)"); + else if (bytesRead == 0) + _log.Info("Nothing read"); + + if (restart) + _log.Info("Lets do some magix"); } - if (cancelToken.IsCancellationRequested) - _log.Info("Song canceled"); - else if (p.HasExited) - _log.Info("Song buffered completely (FFmpeg exited)"); - else if (bytesRead == 0) - _log.Info("Nothing read"); + while (restart && !cancelToken.IsCancellationRequested); } catch (System.ComponentModel.Win32Exception) { From b1a4aa6a2109249a821a4f61afe6e5715ec3a1b3 Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Tue, 4 Jul 2017 11:27:48 +0200 Subject: [PATCH 086/346] Fix --- src/NadekoBot/Services/Music/SongBuffer.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/NadekoBot/Services/Music/SongBuffer.cs b/src/NadekoBot/Services/Music/SongBuffer.cs index f962f919..29979b59 100644 --- a/src/NadekoBot/Services/Music/SongBuffer.cs +++ b/src/NadekoBot/Services/Music/SongBuffer.cs @@ -40,7 +40,7 @@ namespace NadekoBot.Services.Music return Process.Start(new ProcessStartInfo { FileName = "ffmpeg", - Arguments = $"-ss {skipTo:F4} -i {songUri} -f s16le -ar 48000 -vn -ac 2 pipe:1 -loglevel error -threads 0 -reconnect 1 -reconnect_streamed 1 -reconnect_delay_max 5", + Arguments = $"-ss {skipTo:F4} -i {songUri} -f s16le -ar 48000 -vn -ac 2 pipe:1 -loglevel error -reconnect 1 -reconnect_streamed 1 -reconnect_delay_max 5", UseShellExecute = false, RedirectStandardOutput = true, RedirectStandardError = true, @@ -51,7 +51,7 @@ namespace NadekoBot.Services.Music private void P_ErrorDataReceived(object sender, DataReceivedEventArgs e) { _log.Error(">>> " + e.Data); - if (e.Data.Contains("Error in the pull function")) + if (e.Data?.Contains("Error in the pull function") == true) { _log.Info("Got error in the pull function!"); restart = true; From a6d432de7f275d101be89621989744e17ef9a210 Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Tue, 4 Jul 2017 12:38:01 +0200 Subject: [PATCH 087/346] Try ignoring errors --- src/NadekoBot/Services/Music/SongBuffer.cs | 32 +++++++++++----------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/src/NadekoBot/Services/Music/SongBuffer.cs b/src/NadekoBot/Services/Music/SongBuffer.cs index 29979b59..0bf6f089 100644 --- a/src/NadekoBot/Services/Music/SongBuffer.cs +++ b/src/NadekoBot/Services/Music/SongBuffer.cs @@ -40,7 +40,7 @@ namespace NadekoBot.Services.Music return Process.Start(new ProcessStartInfo { FileName = "ffmpeg", - Arguments = $"-ss {skipTo:F4} -i {songUri} -f s16le -ar 48000 -vn -ac 2 pipe:1 -loglevel error -reconnect 1 -reconnect_streamed 1 -reconnect_delay_max 5", + Arguments = $"-ss {skipTo:F4} -err_detect ignore_err -reconnect 1 -reconnect_streamed 1 -reconnect_delay_max 5 -i {songUri} -f s16le -ar 48000 -vn -ac 2 pipe:1 -loglevel error", UseShellExecute = false, RedirectStandardOutput = true, RedirectStandardError = true, @@ -64,24 +64,24 @@ namespace NadekoBot.Services.Music var toReturn = new TaskCompletionSource(); var _ = Task.Run(async () => { - int maxLoopsPerSec = 25; + //int maxLoopsPerSec = 25; var sw = Stopwatch.StartNew(); - var delay = 1000 / maxLoopsPerSec; + //var delay = 1000 / maxLoopsPerSec; int currentLoops = 0; int _bytesSent = 0; try { - do - { - if (restart) - { - var cur = _bytesSent / 3840 / (1000 / 20.0f); - _log.Info("Restarting"); - try { this.p.StandardOutput.Dispose(); } catch { } - try { this.p.Dispose(); } catch { } - this.p = StartFFmpegProcess(SongUri, cur); - } - restart = false; + //do + //{ + // if (restart) + // { + // var cur = _bytesSent / 3840 / (1000 / 20.0f); + // _log.Info("Restarting"); + // try { this.p.StandardOutput.Dispose(); } catch { } + // try { this.p.Dispose(); } catch { } + // this.p = StartFFmpegProcess(SongUri, cur); + // } + // restart = false; ++currentLoops; byte[] buffer = new byte[readSize]; int bytesRead = 1; @@ -117,8 +117,8 @@ namespace NadekoBot.Services.Music if (restart) _log.Info("Lets do some magix"); - } - while (restart && !cancelToken.IsCancellationRequested); + //} + //while (restart && !cancelToken.IsCancellationRequested); } catch (System.ComponentModel.Win32Exception) { From 17158d5e8095ebf73268980e5f31764bc8cb43f5 Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Tue, 4 Jul 2017 13:05:23 +0200 Subject: [PATCH 088/346] Testing something --- src/NadekoBot/Services/CommandHandler.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/NadekoBot/Services/CommandHandler.cs b/src/NadekoBot/Services/CommandHandler.cs index a1de8871..4c46d46d 100644 --- a/src/NadekoBot/Services/CommandHandler.cs +++ b/src/NadekoBot/Services/CommandHandler.cs @@ -187,6 +187,7 @@ namespace NadekoBot.Services private async Task MessageReceivedHandler(SocketMessage msg) { + await Task.Delay(1500); try { if (msg.Author.IsBot || !_bot.Ready) //no bots, wait until bot connected and initialized From 8b72447b0f917ddef656d09ad7320a05919a33ee Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Tue, 4 Jul 2017 15:32:38 +0200 Subject: [PATCH 089/346] Removed testing delay --- src/NadekoBot/Services/CommandHandler.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/NadekoBot/Services/CommandHandler.cs b/src/NadekoBot/Services/CommandHandler.cs index 4c46d46d..a1de8871 100644 --- a/src/NadekoBot/Services/CommandHandler.cs +++ b/src/NadekoBot/Services/CommandHandler.cs @@ -187,7 +187,6 @@ namespace NadekoBot.Services private async Task MessageReceivedHandler(SocketMessage msg) { - await Task.Delay(1500); try { if (msg.Author.IsBot || !_bot.Ready) //no bots, wait until bot connected and initialized From d5903a1e25552ddf5dc00130cae7ed439d4620cf Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Tue, 4 Jul 2017 15:38:19 +0200 Subject: [PATCH 090/346] Readded current time and song durations --- src/NadekoBot/Modules/Music/Music.cs | 10 ++--- src/NadekoBot/Services/Music/MusicPlayer.cs | 45 +++++++++++++++++++- src/NadekoBot/Services/Music/MusicService.cs | 2 +- src/NadekoBot/Services/Music/SongBuffer.cs | 17 +++----- src/NadekoBot/Services/Music/SongInfo.cs | 20 +++++++-- 5 files changed, 72 insertions(+), 22 deletions(-) diff --git a/src/NadekoBot/Modules/Music/Music.cs b/src/NadekoBot/Modules/Music/Music.cs index 8f80e726..4d3584c5 100644 --- a/src/NadekoBot/Modules/Music/Music.cs +++ b/src/NadekoBot/Modules/Music/Music.cs @@ -75,7 +75,6 @@ namespace NadekoBot.Modules.Music return; } - ////if some other user moved //if ((player.VoiceChannel == newState.VoiceChannel && //if joined first, and player paused, unpause // player.Paused && @@ -232,7 +231,8 @@ namespace NadekoBot.Modules.Music if (--page < -1) return; - //try { await musicPlayer.UpdateSongDurationsAsync().ConfigureAwait(false); } catch { } + + try { await mp.UpdateSongDurationsAsync().ConfigureAwait(false); } catch { } const int itemsPerPage = 10; @@ -604,13 +604,13 @@ namespace NadekoBot.Modules.Music var (_, currentSong) = mp.Current; if (currentSong == null) return; - //try { await mp.UpdateSongDurationsAsync().ConfigureAwait(false); } catch { } + try { await mp.UpdateSongDurationsAsync().ConfigureAwait(false); } catch { } var embed = new EmbedBuilder().WithOkColor() .WithAuthor(eab => eab.WithName(GetText("now_playing")).WithMusicIcon()) .WithDescription(currentSong.PrettyName) .WithThumbnailUrl(currentSong.Thumbnail) - .WithFooter(ef => ef.WithText(mp.PrettyVolume + " | " + /*currentSong.PrettyFullTime +*/ $" | {currentSong.PrettyProvider} | {currentSong.QueuerName}")); + .WithFooter(ef => ef.WithText(mp.PrettyVolume + " | " + mp.PrettyFullTime + $" | {currentSong.PrettyProvider} | {currentSong.QueuerName}")); await Context.Channel.EmbedAsync(embed).ConfigureAwait(false); } @@ -847,7 +847,7 @@ namespace NadekoBot.Modules.Music else await ReplyConfirmLocalized("rpl_disabled").ConfigureAwait(false); } - + //todo readd goto //[NadekoCommand, Usage, Description, Aliases] //[RequireContext(ContextType.Guild)] //public async Task Goto(int time) diff --git a/src/NadekoBot/Services/Music/MusicPlayer.cs b/src/NadekoBot/Services/Music/MusicPlayer.cs index fc11e1ab..9dd806e1 100644 --- a/src/NadekoBot/Services/Music/MusicPlayer.cs +++ b/src/NadekoBot/Services/Music/MusicPlayer.cs @@ -7,6 +7,7 @@ using NLog; using System.Linq; using System.Collections.Concurrent; using NadekoBot.Extensions; +using System.Diagnostics; namespace NadekoBot.Services.Music { @@ -28,10 +29,24 @@ namespace NadekoBot.Services.Music public bool Exited { get; set; } = false; public bool Stopped { get; private set; } = false; public float Volume { get; private set; } = 1.0f; - public string PrettyVolume => $"🔉 {(int)(Volume * 100)}%"; public bool Paused => pauseTaskSource != null; private TaskCompletionSource pauseTaskSource { get; set; } = null; + public string PrettyVolume => $"🔉 {(int)(Volume * 100)}%"; + public string PrettyCurrentTime + { + get + { + var time = CurrentTime.ToString(@"mm\:ss"); + var hrs = (int)CurrentTime.TotalHours; + + if (hrs > 0) + return hrs + ":" + time; + else + return time; + } + } + public string PrettyFullTime => PrettyCurrentTime + " / " + (Queue.Current.Song?.PrettyTotalTime ?? "?"); private CancellationTokenSource SongCancelSource { get; set; } public ITextChannel OutputTextChannel { get; set; } public (int Index, SongInfo Current) Current @@ -96,11 +111,12 @@ namespace NadekoBot.Services.Music private bool manualSkip = false; private bool manualIndex = false; private bool newVoiceChannel = false; + private readonly IGoogleApiService _google; private ConcurrentHashSet RecentlyPlayedUsers { get; } = new ConcurrentHashSet(); public TimeSpan TotalPlaytime => TimeSpan.MaxValue; - public MusicPlayer(MusicService musicService, IVoiceChannel vch, ITextChannel output, float volume) + public MusicPlayer(MusicService musicService, IGoogleApiService google, IVoiceChannel vch, ITextChannel output, float volume) { _log = LogManager.GetCurrentClassLogger(); this.Volume = volume; @@ -108,6 +124,7 @@ namespace NadekoBot.Services.Music this.SongCancelSource = new CancellationTokenSource(); this.OutputTextChannel = output; this._musicService = musicService; + this._google = google; _player = Task.Run(async () => { @@ -530,6 +547,30 @@ namespace NadekoBot.Services.Music } } + public async Task UpdateSongDurationsAsync() + { + var sw = Stopwatch.StartNew(); + var (_, songs) = Queue.ToArray(); + var toUpdate = songs + .Where(x => x.ProviderType == Database.Models.MusicType.YouTube + && x.TotalTime == TimeSpan.Zero); + + var vIds = toUpdate.Select(x => x.VideoId); + + sw.Stop(); + _log.Info(sw.Elapsed.TotalSeconds); + if (!vIds.Any()) + return; + + var durations = await _google.GetVideoDurationsAsync(vIds); + + foreach (var x in toUpdate) + { + if (durations.TryGetValue(x.VideoId, out var dur)) + x.TotalTime = dur; + } + } + //// this should be written better //public TimeSpan TotalPlaytime => // _playlist.Any(s => s.TotalTime == TimeSpan.MaxValue) ? diff --git a/src/NadekoBot/Services/Music/MusicService.cs b/src/NadekoBot/Services/Music/MusicService.cs index 2ed203ac..bcd00b29 100644 --- a/src/NadekoBot/Services/Music/MusicService.cs +++ b/src/NadekoBot/Services/Music/MusicService.cs @@ -88,7 +88,7 @@ namespace NadekoBot.Services.Music return MusicPlayers.GetOrAdd(guildId, _ => { var vol = GetDefaultVolume(guildId); - var mp = new MusicPlayer(this, voiceCh, textCh, vol); + var mp = new MusicPlayer(this, _google, voiceCh, textCh, vol); IUserMessage playingMessage = null; IUserMessage lastFinishedMessage = null; diff --git a/src/NadekoBot/Services/Music/SongBuffer.cs b/src/NadekoBot/Services/Music/SongBuffer.cs index 0bf6f089..af6bac8b 100644 --- a/src/NadekoBot/Services/Music/SongBuffer.cs +++ b/src/NadekoBot/Services/Music/SongBuffer.cs @@ -53,7 +53,7 @@ namespace NadekoBot.Services.Music _log.Error(">>> " + e.Data); if (e.Data?.Contains("Error in the pull function") == true) { - _log.Info("Got error in the pull function!"); + _log.Error("Ignore this."); restart = true; } } @@ -108,15 +108,12 @@ namespace NadekoBot.Services.Music //_log.Info(_outStream.Length); await Task.Delay(10); } - if (cancelToken.IsCancellationRequested) - _log.Info("Song canceled"); - else if (p.HasExited) - _log.Info("Song buffered completely (FFmpeg exited)"); - else if (bytesRead == 0) - _log.Info("Nothing read"); - - if (restart) - _log.Info("Lets do some magix"); + //if (cancelToken.IsCancellationRequested) + // _log.Info("Song canceled"); + //else if (p.HasExited) + // _log.Info("Song buffered completely (FFmpeg exited)"); + //else if (bytesRead == 0) + // _log.Info("Nothing read"); //} //while (restart && !cancelToken.IsCancellationRequested); } diff --git a/src/NadekoBot/Services/Music/SongInfo.cs b/src/NadekoBot/Services/Music/SongInfo.cs index 22accf2f..04d48d55 100644 --- a/src/NadekoBot/Services/Music/SongInfo.cs +++ b/src/NadekoBot/Services/Music/SongInfo.cs @@ -17,7 +17,7 @@ namespace NadekoBot.Services.Music public string Uri { get; set; } public string AlbumArt { get; set; } public string QueuerName { get; set; } - public TimeSpan TotalTime = TimeSpan.Zero; + public TimeSpan TotalTime { get; set; } = TimeSpan.Zero; public string PrettyProvider => (Provider ?? "???"); //public string PrettyFullTime => PrettyCurrentTime + " / " + PrettyTotalTime; @@ -60,7 +60,20 @@ namespace NadekoBot.Services.Music } } } - private string videoId = null; + private string _videoId = null; + public string VideoId + { + get + { + if (ProviderType == MusicType.YouTube) + return _videoId = _videoId ?? videoIdRegex.Match(Query)?.ToString(); + + return _videoId ?? ""; + } + + set => _videoId = value; + } + private readonly Regex videoIdRegex = new Regex("<=v=[a-zA-Z0-9-]+(?=&)|(?<=[0-9])[^&\n]+|(?<=v=)[^&\n]+", RegexOptions.Compiled); public string Thumbnail { @@ -71,8 +84,7 @@ namespace NadekoBot.Services.Music case MusicType.Radio: return "https://cdn.discordapp.com/attachments/155726317222887425/261850925063340032/1482522097_radio.png"; //test links case MusicType.YouTube: - videoId = videoId ?? videoIdRegex.Match(Query)?.ToString(); - return $"https://img.youtube.com/vi/{ videoId }/0.jpg"; + return $"https://img.youtube.com/vi/{ VideoId }/0.jpg"; case MusicType.Local: return "https://cdn.discordapp.com/attachments/155726317222887425/261850914783100928/1482522077_music.png"; //test links case MusicType.Soundcloud: From c33c2bce60e5b1f7c480ca3eda1bbc7be40aa6fa Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Tue, 4 Jul 2017 15:42:07 +0200 Subject: [PATCH 091/346] Readded total playtime --- src/NadekoBot/Services/Music/MusicPlayer.cs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/NadekoBot/Services/Music/MusicPlayer.cs b/src/NadekoBot/Services/Music/MusicPlayer.cs index 9dd806e1..b810c413 100644 --- a/src/NadekoBot/Services/Music/MusicPlayer.cs +++ b/src/NadekoBot/Services/Music/MusicPlayer.cs @@ -114,7 +114,17 @@ namespace NadekoBot.Services.Music private readonly IGoogleApiService _google; private ConcurrentHashSet RecentlyPlayedUsers { get; } = new ConcurrentHashSet(); - public TimeSpan TotalPlaytime => TimeSpan.MaxValue; + public TimeSpan TotalPlaytime + { + get + { + var songs = Queue.ToArray().Songs; + return songs.Any(s => s.TotalTime == TimeSpan.MaxValue) + ? TimeSpan.MaxValue + : new TimeSpan(songs.Sum(s => s.TotalTime.Ticks)); + } + } + public MusicPlayer(MusicService musicService, IGoogleApiService google, IVoiceChannel vch, ITextChannel output, float volume) { From 9bb8f3d666124fd3bc93cc32435be6c8f7a4d247 Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Tue, 4 Jul 2017 23:38:11 +0200 Subject: [PATCH 092/346] .ms readded --- src/NadekoBot/Modules/Music/Music.cs | 72 +++++++++------------ src/NadekoBot/Services/Music/MusicPlayer.cs | 7 +- src/NadekoBot/Services/Music/MusicQueue.cs | 18 +++++- src/NadekoBot/Services/Music/SongBuffer.cs | 8 ++- 4 files changed, 58 insertions(+), 47 deletions(-) diff --git a/src/NadekoBot/Modules/Music/Music.cs b/src/NadekoBot/Modules/Music/Music.cs index 4d3584c5..d8bc759d 100644 --- a/src/NadekoBot/Modules/Music/Music.cs +++ b/src/NadekoBot/Modules/Music/Music.cs @@ -45,6 +45,7 @@ namespace NadekoBot.Modules.Music var t = _music.DestroyPlayer(arg.Id); return Task.CompletedTask; } + //todo changing server region is bugged again private Task Client_UserVoiceStateUpdated(SocketUser iusr, SocketVoiceState oldState, SocketVoiceState newState) { @@ -725,7 +726,7 @@ namespace NadekoBot.Modules.Music [NadekoCommand, Usage, Description, Aliases] [RequireContext(ContextType.Guild)] - public void Move() + public async Task Move() { var vch = ((IGuildUser)Context.User).VoiceChannel; @@ -737,53 +738,42 @@ namespace NadekoBot.Modules.Music if (mp == null) return; - mp.SetVoiceChannel(vch); + await mp.SetVoiceChannel(vch); } - //[NadekoCommand, Usage, Description, Aliases] - //[RequireContext(ContextType.Guild)] - //public async Task MoveSong([Remainder] string fromto) - //{ - // if (string.IsNullOrWhiteSpace(fromto)) - // return; + [NadekoCommand, Usage, Description, Aliases] + [RequireContext(ContextType.Guild)] + public async Task MoveSong([Remainder] string fromto) + { + if (string.IsNullOrWhiteSpace(fromto)) + return; - // MusicPlayer musicPlayer; - // if ((musicPlayer = _music.GetPlayer(Context.Guild.Id)) == null) - // return; + MusicPlayer mp = _music.GetPlayerOrDefault(Context.Guild.Id); + if (mp == null) + return; - // fromto = fromto?.Trim(); - // var fromtoArr = fromto.Split('>'); + fromto = fromto?.Trim(); + var fromtoArr = fromto.Split('>'); - // int n1; - // int n2; + SongInfo s; + if (fromtoArr.Length != 2 || !int.TryParse(fromtoArr[0], out var n1) || + !int.TryParse(fromtoArr[1], out var n2) || n1 < 1 || n2 < 1 || n1 == n2 + || (s = mp.MoveSong(n1, n2)) == null) + { + await ReplyConfirmLocalized("invalid_input").ConfigureAwait(false); + return; + } - // var playlist = musicPlayer.Playlist as List ?? musicPlayer.Playlist.ToList(); + var embed = new EmbedBuilder() + .WithTitle(s.Title.TrimTo(65)) + .WithUrl(s.SongUrl) + .WithAuthor(eab => eab.WithName(GetText("song_moved")).WithIconUrl("https://cdn.discordapp.com/attachments/155726317222887425/258605269972549642/music1.png")) + .AddField(fb => fb.WithName(GetText("from_position")).WithValue($"#{n1}").WithIsInline(true)) + .AddField(fb => fb.WithName(GetText("to_position")).WithValue($"#{n2}").WithIsInline(true)) + .WithColor(NadekoBot.OkColor); + await Context.Channel.EmbedAsync(embed).ConfigureAwait(false); + } - // if (fromtoArr.Length != 2 || !int.TryParse(fromtoArr[0], out n1) || - // !int.TryParse(fromtoArr[1], out n2) || n1 < 1 || n2 < 1 || n1 == n2 || - // n1 > playlist.Count || n2 > playlist.Count) - // { - // await ReplyConfirmLocalized("invalid_input").ConfigureAwait(false); - // return; - // } - - // var s = playlist[n1 - 1]; - // playlist.Insert(n2 - 1, s); - // var nn1 = n2 < n1 ? n1 : n1 - 1; - // playlist.RemoveAt(nn1); - - // var embed = new EmbedBuilder() - // .WithTitle($"{s.SongInfo.Title.TrimTo(70)}") - // .WithUrl(s.SongUrl) - // .WithAuthor(eab => eab.WithName(GetText("song_moved")).WithIconUrl("https://cdn.discordapp.com/attachments/155726317222887425/258605269972549642/music1.png")) - // .AddField(fb => fb.WithName(GetText("from_position")).WithValue($"#{n1}").WithIsInline(true)) - // .AddField(fb => fb.WithName(GetText("to_position")).WithValue($"#{n2}").WithIsInline(true)) - // .WithColor(NadekoBot.OkColor); - // await Context.Channel.EmbedAsync(embed).ConfigureAwait(false); - - // //await channel.SendConfirmAsync($"🎵Moved {s.PrettyName} `from #{n1} to #{n2}`").ConfigureAwait(false); - //} - [NadekoCommand, Usage, Description, Aliases] [RequireContext(ContextType.Guild)] public async Task SetMaxQueue(uint size = 0) diff --git a/src/NadekoBot/Services/Music/MusicPlayer.cs b/src/NadekoBot/Services/Music/MusicPlayer.cs index b810c413..2064fb45 100644 --- a/src/NadekoBot/Services/Music/MusicPlayer.cs +++ b/src/NadekoBot/Services/Music/MusicPlayer.cs @@ -365,7 +365,6 @@ namespace NadekoBot.Services.Music public int Enqueue(SongInfo song) { - _log.Info("Adding song"); lock (locker) { if (Exited) @@ -547,7 +546,7 @@ namespace NadekoBot.Services.Music } } - public void SetVoiceChannel(IVoiceChannel vch) + public async Task SetVoiceChannel(IVoiceChannel vch) { lock (locker) { @@ -555,6 +554,7 @@ namespace NadekoBot.Services.Music return; VoiceChannel = vch; } + _audioClient = await vch.ConnectAsync(); } public async Task UpdateSongDurationsAsync() @@ -581,6 +581,9 @@ namespace NadekoBot.Services.Music } } + public SongInfo MoveSong(int n1, int n2) + => Queue.MoveSong(n1, n2); + //// this should be written better //public TimeSpan TotalPlaytime => // _playlist.Any(s => s.TotalTime == TimeSpan.MaxValue) ? diff --git a/src/NadekoBot/Services/Music/MusicQueue.cs b/src/NadekoBot/Services/Music/MusicQueue.cs index 967a5197..c8890484 100644 --- a/src/NadekoBot/Services/Music/MusicQueue.cs +++ b/src/NadekoBot/Services/Music/MusicQueue.cs @@ -9,7 +9,7 @@ namespace NadekoBot.Services.Music { public class MusicQueue : IDisposable { - private LinkedList Songs { get; } = new LinkedList(); + private LinkedList Songs { get; set; } = new LinkedList(); private int _currentIndex = 0; public int CurrentIndex { @@ -147,5 +147,21 @@ namespace NadekoBot.Services.Music CurrentIndex = new NadekoRandom().Next(Songs.Count); } } + + public SongInfo MoveSong(int n1, int n2) + { + lock (locker) + { + var playlist = Songs.ToList(); + if (n1 > playlist.Count || n2 > playlist.Count) + return null; + var s = playlist[n1 - 1]; + playlist.Insert(n2 - 1, s); + var nn1 = n2 < n1 ? n1 : n1 - 1; + playlist.RemoveAt(nn1); + Songs = new LinkedList(playlist); + return s; + } + } } } diff --git a/src/NadekoBot/Services/Music/SongBuffer.cs b/src/NadekoBot/Services/Music/SongBuffer.cs index af6bac8b..a3ec23a4 100644 --- a/src/NadekoBot/Services/Music/SongBuffer.cs +++ b/src/NadekoBot/Services/Music/SongBuffer.cs @@ -19,7 +19,7 @@ namespace NadekoBot.Services.Music public string SongUri { get; private set; } - private volatile bool restart = false; + //private volatile bool restart = false; public SongBuffer(string songUri, string skipTo) { @@ -50,11 +50,13 @@ namespace NadekoBot.Services.Music private void P_ErrorDataReceived(object sender, DataReceivedEventArgs e) { + if (string.IsNullOrWhiteSpace(e.Data)) + return; _log.Error(">>> " + e.Data); if (e.Data?.Contains("Error in the pull function") == true) { _log.Error("Ignore this."); - restart = true; + //restart = true; } } @@ -99,7 +101,7 @@ namespace NadekoBot.Services.Music if (!written) await Task.Delay(2000, cancelToken); } - while (!written); + while (!written && !cancelToken.IsCancellationRequested); lock (locker) if (_outStream.Length > 200_000 || bytesRead == 0) if (toReturn.TrySetResult(true)) From b4cf9fee84b1fe2602e851f142f196f5e274c1b9 Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Wed, 5 Jul 2017 11:07:54 +0200 Subject: [PATCH 093/346] closes #1321 --- src/NadekoBot/Modules/Utility/Commands/PatreonCommands.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/NadekoBot/Modules/Utility/Commands/PatreonCommands.cs b/src/NadekoBot/Modules/Utility/Commands/PatreonCommands.cs index 293e2c42..6e287374 100644 --- a/src/NadekoBot/Modules/Utility/Commands/PatreonCommands.cs +++ b/src/NadekoBot/Modules/Utility/Commands/PatreonCommands.cs @@ -32,6 +32,7 @@ namespace NadekoBot.Modules.Utility [NadekoCommand, Usage, Description, Aliases] [OwnerOnly] + [RequireContext(ContextType.DM)] public async Task PatreonRewardsReload() { if (string.IsNullOrWhiteSpace(_creds.PatreonAccessToken)) @@ -42,6 +43,7 @@ namespace NadekoBot.Modules.Utility } [NadekoCommand, Usage, Description, Aliases] + [RequireContext(ContextType.DM)] public async Task ClaimPatreonRewards() { if (string.IsNullOrWhiteSpace(_creds.PatreonAccessToken)) From 54a91b4a793a7d8014029a8f6174f81bd30c8915 Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Wed, 5 Jul 2017 11:23:31 +0200 Subject: [PATCH 094/346] Dispose pcm --- src/NadekoBot/Services/Music/MusicPlayer.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/NadekoBot/Services/Music/MusicPlayer.cs b/src/NadekoBot/Services/Music/MusicPlayer.cs index 2064fb45..88332be6 100644 --- a/src/NadekoBot/Services/Music/MusicPlayer.cs +++ b/src/NadekoBot/Services/Music/MusicPlayer.cs @@ -180,14 +180,13 @@ namespace NadekoBot.Services.Music // i don't want to spam connection attempts continue; } - pcm = ac.CreatePCMStream(AudioApplication.Music, bufferMillis: 500); OnStarted?.Invoke(this, data); byte[] buffer = new byte[3840]; int bytesRead = 0; while ((bytesRead = b.Read(buffer, 0, buffer.Length)) > 0 - && (MaxPlaytimeSeconds <= 0 || MaxPlaytimeSeconds >= CurrentTime.TotalSeconds)) + && (MaxPlaytimeSeconds <= 0 || MaxPlaytimeSeconds >= CurrentTime.TotalSeconds)) { //AdjustVolume(buffer, Volume); await pcm.WriteAsync(buffer, 0, bytesRead, cancelToken).ConfigureAwait(false); @@ -215,6 +214,7 @@ namespace NadekoBot.Services.Music var flushDelay = Task.Delay(1000, flushToken); await Task.WhenAny(flushDelay, pcm.FlushAsync(flushToken)); flushCancel.Cancel(); + pcm.Dispose(); } OnCompleted?.Invoke(this, data.Song); From 6591dd7c74be514b70a34ddfb19dfe59251189ce Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Wed, 5 Jul 2017 11:43:36 +0200 Subject: [PATCH 095/346] Bot will now show in the console when it joins or leaves a server --- src/NadekoBot/NadekoBot.cs | 14 ++++++++++++++ src/NadekoBot/Services/IBotCredentials.cs | 1 + src/NadekoBot/Services/Impl/BotCredentials.cs | 4 ++++ .../Services/Utility/PatreonRewardsService.cs | 2 +- src/NadekoBot/credentials_example.json | 1 + 5 files changed, 21 insertions(+), 1 deletion(-) diff --git a/src/NadekoBot/NadekoBot.cs b/src/NadekoBot/NadekoBot.cs index 5560437f..b91b0004 100644 --- a/src/NadekoBot/NadekoBot.cs +++ b/src/NadekoBot/NadekoBot.cs @@ -317,9 +317,23 @@ namespace NadekoBot Client.Ready += SetClientReady; await clientReady.Task.ConfigureAwait(false); Client.Ready -= SetClientReady; + Client.JoinedGuild += Client_JoinedGuild; + Client.LeftGuild += Client_LeftGuild; _log.Info("Shard {0} logged in.", ShardId); } + private Task Client_LeftGuild(SocketGuild arg) + { + _log.Info("Left server: {0} [{1}]", arg?.Name, arg?.Id); + return Task.CompletedTask; + } + + private Task Client_JoinedGuild(SocketGuild arg) + { + _log.Info("Joined server: {0} [{1}]", arg?.Name, arg?.Id); + return Task.CompletedTask; + } + public async Task RunAsync(params string[] args) { if(ShardId == 0) diff --git a/src/NadekoBot/Services/IBotCredentials.cs b/src/NadekoBot/Services/IBotCredentials.cs index e0c271b5..9ed5ca1b 100644 --- a/src/NadekoBot/Services/IBotCredentials.cs +++ b/src/NadekoBot/Services/IBotCredentials.cs @@ -22,6 +22,7 @@ namespace NadekoBot.Services int TotalShards { get; } string ShardRunCommand { get; } string ShardRunArguments { get; } + string PatreonCampaignId { get; } } public class DBConfig diff --git a/src/NadekoBot/Services/Impl/BotCredentials.cs b/src/NadekoBot/Services/Impl/BotCredentials.cs index 3d47a207..3c69a3ec 100644 --- a/src/NadekoBot/Services/Impl/BotCredentials.cs +++ b/src/NadekoBot/Services/Impl/BotCredentials.cs @@ -36,6 +36,8 @@ namespace NadekoBot.Services.Impl public string ShardRunArguments { get; } public int ShardRunPort { get; } + public string PatreonCampaignId { get; } + public BotCredentials() { _log = LogManager.GetCurrentClassLogger(); @@ -64,6 +66,7 @@ namespace NadekoBot.Services.Impl MashapeKey = data[nameof(MashapeKey)]; OsuApiKey = data[nameof(OsuApiKey)]; PatreonAccessToken = data[nameof(PatreonAccessToken)]; + PatreonCampaignId = data[nameof(PatreonCampaignId)] ?? "334038"; ShardRunCommand = data[nameof(ShardRunCommand)]; ShardRunArguments = data[nameof(ShardRunArguments)]; if (string.IsNullOrWhiteSpace(ShardRunCommand)) @@ -121,6 +124,7 @@ namespace NadekoBot.Services.Impl public DBConfig Db { get; set; } = new DBConfig("sqlite", "Filename=./data/NadekoBot.db"); public int TotalShards { get; set; } = 1; public string PatreonAccessToken { get; set; } = ""; + public string PatreonCampaignId { get; set; } = "334038"; public string ShardRunCommand { get; set; } = ""; public string ShardRunArguments { get; set; } = ""; diff --git a/src/NadekoBot/Services/Utility/PatreonRewardsService.cs b/src/NadekoBot/Services/Utility/PatreonRewardsService.cs index 3eef3d72..7c08f747 100644 --- a/src/NadekoBot/Services/Utility/PatreonRewardsService.cs +++ b/src/NadekoBot/Services/Utility/PatreonRewardsService.cs @@ -64,7 +64,7 @@ namespace NadekoBot.Services.Utility { Links = new PatreonDataLinks() { - next = "https://api.patreon.com/oauth2/api/campaigns/334038/pledges" + next = $"https://api.patreon.com/oauth2/api/campaigns/{_creds.PatreonCampaignId}/pledges" } }; do diff --git a/src/NadekoBot/credentials_example.json b/src/NadekoBot/credentials_example.json index 8f977d87..9745558d 100644 --- a/src/NadekoBot/credentials_example.json +++ b/src/NadekoBot/credentials_example.json @@ -16,6 +16,7 @@ }, "TotalShards": 1, "PatreonAccessToken": "", + "PatreonCampaignId": "334038", "ShardRunCommand": "", "ShardRunArguments": "", "ShardRunPort": null From c244cb7de026c0564edf2082f7956de324402889 Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Wed, 5 Jul 2017 11:46:29 +0200 Subject: [PATCH 096/346] Commandlist and docs update --- docs/Commands List.md | 32 +++++++++++++++++--------------- docs/JSON Explanations.md | 3 +++ src/NadekoBot/NadekoBot.cs | 1 - 3 files changed, 20 insertions(+), 16 deletions(-) diff --git a/docs/Commands List.md b/docs/Commands List.md index 23cef108..0deeaa6b 100644 --- a/docs/Commands List.md +++ b/docs/Commands List.md @@ -104,6 +104,7 @@ Commands and aliases | Description | Usage `.timezone` | Sets this guilds timezone. This affects bot's time output in this server (logs, etc..) | `.timezone` or `.timezone GMT Standard Time` `.warn` | Warns a user. **Requires BanMembers server permission.** | `.warn @b1nzy Very rude person` `.warnlog` | See a list of warnings of a certain user. **Requires BanMembers server permission.** | `.warnlog @b1nzy` +`.warnlogall` | See a list of all warnings on the server. 15 users per page. **Requires BanMembers server permission.** | `.warnlogall` or `.warnlogall 2` `.warnclear` `.warnc` | Clears all warnings from a certain user. **Requires BanMembers server permission.** | `.warnclear @PoorDude` `.warnpunish` `.warnp` | Sets a punishment for a certain number of warnings. Provide no punishment to remove. **Requires BanMembers server permission.** | `.warnpunish 5 Ban` or `.warnpunish 3` `.warnpunishlist` `.warnpl` | Lists punishments for warnings. | `.warnpunishlist` @@ -232,35 +233,36 @@ Commands and aliases | Description | Usage ### Music Commands and aliases | Description | Usage ----------------|--------------|------- +`.play` `.start` | If no arguments are specified, acts as `.next 1` command. If you specify a song number, it will jump to that song. If you specify a search query, acts as a `.q` command | `.play` or `.play 5` or `.play Dream Of Venice` +`.queue` `.q` `.yq` | Queue a song using keywords or a link. Bot will join your voice channel. **You must be in a voice channel**. | `.q Dream Of Venice` +`.queuesearch` `.qs` `.yqs` | Search for top 5 youtube song result using keywords, and type the index of the song to play that song. Bot will join your voice channel. **You must be in a voice channel**. | `.qs Dream Of Venice` +`.listqueue` `.lq` | Lists 15 currently queued songs per page. Default page is 1. | `.lq` or `.lq 2` `.next` `.n` | Goes to the next song in the queue. You have to be in the same voice channel as the bot. You can skip multiple songs, but in that case songs will not be requeued if .rcs or .rpl is enabled. | `.n` or `.n 5` `.stop` `.s` | Stops the music and clears the playlist. Stays in the channel. | `.s` `.destroy` `.d` | Completely stops the music and unbinds the bot from the channel. (may cause weird behaviour) | `.d` `.pause` `.p` | Pauses or Unpauses the song. | `.p` -`.fairplay` `.fp` | Toggles fairplay. While enabled, the bot will prioritize songs from users who didn't have their song recently played instead of the song's position in the queue. | `.fp` -`.queue` `.q` `.yq` | Queue a song using keywords or a link. Bot will join your voice channel. **You must be in a voice channel**. | `.q Dream Of Venice` -`.queuesearch` `.qs` `.yqs` | Search for top 5 youtube song result using keywords, and type the index of the song to play that song. Bot will join your voice channel. **You must be in a voice channel**. | `.qs Dream Of Venice` -`.soundcloudqueue` `.sq` | Queue a soundcloud song using keywords. Bot will join your voice channel. **You must be in a voice channel**. | `.sq Dream Of Venice` -`.listqueue` `.lq` | Lists 15 currently queued songs per page. Default page is 1. | `.lq` or `.lq 2` -`.nowplaying` `.np` | Shows the song that the bot is currently playing. | `.np` `.volume` `.vol` | Sets the music playback volume (0-100%) | `.vol 50` `.defvol` `.dv` | Sets the default music volume when music playback is started (0-100). Persists through restarts. | `.dv 80` -`.playlistshuffle` `.plsh` | Shuffles the current playlist. | `.plsh` -`.playlist` `.pl` | Queues up to 500 songs from a youtube playlist specified by a link, or keywords. | `.pl playlist link or name` +`.songremove` `.srm` | Remove a song by its # in the queue, or 'all' to remove all songs from the queue. | `.srm 5` +`.playlists` `.pls` | Lists all playlists. Paginated, 20 per page. Default page is 0. | `.pls 1` +`.deleteplaylist` `.delpls` | Deletes a saved playlist. Works only if you made it or if you are the bot owner. | `.delpls animu-5` +`.save` | Saves a playlist under a certain name. Playlist name must be no longer than 20 characters and must not contain dashes. | `.save classical1` +`.load` | Loads a saved playlist using its ID. Use `.pls` to list all saved playlists and `.save` to save new ones. | `.load 5` +`.fairplay` `.fp` | Toggles fairplay. While enabled, the bot will prioritize songs from users who didn't have their song recently played instead of the song's position in the queue. | `.fp` +`.soundcloudqueue` `.sq` | Queue a soundcloud song using keywords. Bot will join your voice channel. **You must be in a voice channel**. | `.sq Dream Of Venice` `.soundcloudpl` `.scpl` | Queue a Soundcloud playlist using a link. | `.scpl soundcloudseturl` -`.localplaylst` `.lopl` | Queues all songs from a directory. **Bot owner only** | `.lopl C:/music/classical` +`.nowplaying` `.np` | Shows the song that the bot is currently playing. | `.np` +`.shuffle` `.sh` `.plsh` | Shuffles the current playlist. | `.plsh` +`.playlist` `.pl` | Queues up to 500 songs from a youtube playlist specified by a link, or keywords. | `.pl playlist link or name` `.radio` `.ra` | Queues a radio stream from a link. It can be a direct mp3 radio stream, .m3u, .pls .asx or .xspf (Usage Video: ) | `.ra radio link here` `.local` `.lo` | Queues a local file by specifying a full path. **Bot owner only** | `.lo C:/music/mysong.mp3` -`.songremove` `.srm` | Remove a song by its # in the queue, or 'all' to remove all songs from the queue. | `.srm 5` +`.localplaylst` `.lopl` | Queues all songs from a directory. **Bot owner only** | `.lopl C:/music/classical` +`.move` `.mv` | Moves the bot to your voice channel. (works only if music is already playing) | `.mv` `.movesong` `.ms` | Moves a song from one position to another. | `.ms 5>3` `.setmaxqueue` `.smq` | Sets a maximum queue size. Supply 0 or no argument to have no limit. | `.smq 50` or `.smq` `.setmaxplaytime` `.smp` | Sets a maximum number of seconds (>14) a song can run before being skipped automatically. Set 0 to have no limit. | `.smp 0` or `.smp 270` `.reptcursong` `.rcs` | Toggles repeat of current song. | `.rcs` `.rpeatplaylst` `.rpl` | Toggles repeat of all songs in the queue (every song that finishes is added to the end of the queue). | `.rpl` -`.save` | Saves a playlist under a certain name. Playlist name must be no longer than 20 characters and must not contain dashes. | `.save classical1` -`.load` | Loads a saved playlist using its ID. Use `.pls` to list all saved playlists and `.save` to save new ones. | `.load 5` -`.playlists` `.pls` | Lists all playlists. Paginated, 20 per page. Default page is 0. | `.pls 1` -`.deleteplaylist` `.delpls` | Deletes a saved playlist. Works only if you made it or if you are the bot owner. | `.delpls animu-5` -`.goto` | Goes to a specific time in seconds in a song. | `.goto 30` `.autoplay` `.ap` | Toggles autoplay - When the song is finished, automatically queue a related Youtube song. (Works only for Youtube songs and when queue is empty) | `.ap` `.setmusicchannel` `.smch` | Sets the current channel as the default music output channel. This will output playing, finished, paused and removed songs to that channel instead of the channel where the first song was queued in. **Requires ManageMessages server permission.** | `.smch` diff --git a/docs/JSON Explanations.md b/docs/JSON Explanations.md index 14595dd4..028b1891 100644 --- a/docs/JSON Explanations.md +++ b/docs/JSON Explanations.md @@ -17,6 +17,7 @@ If you do not see `credentials.json` you will need to rename `credentials_exampl "MashapeKey": "4UrKpcWXc2mshS8RKi00000y8Kf5p1Q8kI6jsn32bmd8oVWiY7", "OsuApiKey": "4c8c8fdff8e1234581725db27fd140a7d93320d6", "PatreonAccessToken": "", + "PatreonCampaignId": "334038", "Db": null, "TotalShards": 1, "ShardRunCommand": "", @@ -155,6 +156,8 @@ It should look like: - You can get this key [here.](https://osu.ppy.sh/p/api) - **PatreonAccessToken** - For Patreon creators only. +- **PatreonCampaignId** + - For Patreon creators only. Id of your campaign. - **TotalShards** - Required if the bot will be connected to more than 1500 servers. - Most likely unnecessary to change until your bot is added to more than 1500 servers. diff --git a/src/NadekoBot/NadekoBot.cs b/src/NadekoBot/NadekoBot.cs index b91b0004..5f0a6513 100644 --- a/src/NadekoBot/NadekoBot.cs +++ b/src/NadekoBot/NadekoBot.cs @@ -31,7 +31,6 @@ using NadekoBot.Extensions; namespace NadekoBot { - //todo log when joining or leaving the server public class NadekoBot { private Logger _log; From 768c8b20ee9a80b8ba676b0e068ee6a5ff29b9a6 Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Wed, 5 Jul 2017 11:47:35 +0200 Subject: [PATCH 097/346] Fixed volume, closes #1332 --- src/NadekoBot/Services/Music/MusicPlayer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/NadekoBot/Services/Music/MusicPlayer.cs b/src/NadekoBot/Services/Music/MusicPlayer.cs index 88332be6..3485760b 100644 --- a/src/NadekoBot/Services/Music/MusicPlayer.cs +++ b/src/NadekoBot/Services/Music/MusicPlayer.cs @@ -188,7 +188,7 @@ namespace NadekoBot.Services.Music while ((bytesRead = b.Read(buffer, 0, buffer.Length)) > 0 && (MaxPlaytimeSeconds <= 0 || MaxPlaytimeSeconds >= CurrentTime.TotalSeconds)) { - //AdjustVolume(buffer, Volume); + AdjustVolume(buffer, Volume); await pcm.WriteAsync(buffer, 0, bytesRead, cancelToken).ConfigureAwait(false); unchecked { _bytesSent += bytesRead; } From 221a30b5768289916a6e666cde6e9b851577352c Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Wed, 5 Jul 2017 12:02:02 +0200 Subject: [PATCH 098/346] Fixed .ow for some users --- src/NadekoBot/Modules/Searches/Commands/OverwatchCommands.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/NadekoBot/Modules/Searches/Commands/OverwatchCommands.cs b/src/NadekoBot/Modules/Searches/Commands/OverwatchCommands.cs index 98b7b583..d4ffffbe 100644 --- a/src/NadekoBot/Modules/Searches/Commands/OverwatchCommands.cs +++ b/src/NadekoBot/Modules/Searches/Commands/OverwatchCommands.cs @@ -62,8 +62,8 @@ namespace NadekoBot.Modules.Searches .AddField(fb => fb.WithName(GetText("quick_wins")).WithValue(qp.OverallStats.wins.ToString()).WithIsInline(true)) .AddField(fb => fb.WithName(GetText("compet_wins")).WithValue(compet.OverallStats.wins.ToString()).WithIsInline(true)) .AddField(fb => fb.WithName(GetText("compet_loses")).WithValue(compet.OverallStats.losses.ToString()).WithIsInline(true)) - .AddField(fb => fb.WithName(GetText("compet_played")).WithValue(compet.OverallStats.games.ToString()).WithIsInline(true)) - .AddField(fb => fb.WithName(GetText("compet_rank")).WithValue(compet.OverallStats.comprank.ToString()).WithIsInline(true)) + .AddField(fb => fb.WithName(GetText("compet_played")).WithValue(compet.OverallStats.games.ToString() ?? "-").WithIsInline(true)) + .AddField(fb => fb.WithName(GetText("compet_rank")).WithValue(compet.OverallStats.comprank?.ToString() ?? "-").WithIsInline(true)) .AddField(fb => fb.WithName(GetText("compet_playtime")).WithValue(compet.GameStats.timePlayed + "hrs").WithIsInline(true)) .AddField(fb => fb.WithName(GetText("quick_playtime")).WithValue(qp.GameStats.timePlayed.ToString("F1") + "hrs").WithIsInline(true)) .WithColor(NadekoBot.OkColor); From 65b76ec48e84801d2e781a71b0da989fc24204f9 Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Wed, 5 Jul 2017 12:23:37 +0200 Subject: [PATCH 099/346] .die should now stop all music players, #1329 --- .../Modules/Administration/Commands/SelfCommands.cs | 6 +++++- src/NadekoBot/Services/Music/MusicService.cs | 8 ++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/src/NadekoBot/Modules/Administration/Commands/SelfCommands.cs b/src/NadekoBot/Modules/Administration/Commands/SelfCommands.cs index 449398b4..bd1a8eb6 100644 --- a/src/NadekoBot/Modules/Administration/Commands/SelfCommands.cs +++ b/src/NadekoBot/Modules/Administration/Commands/SelfCommands.cs @@ -15,6 +15,7 @@ using Microsoft.EntityFrameworkCore; using NadekoBot.Services.Administration; using System.Diagnostics; using NadekoBot.DataStructures; +using NadekoBot.Services.Music; namespace NadekoBot.Modules.Administration { @@ -29,14 +30,16 @@ namespace NadekoBot.Modules.Administration private readonly SelfService _service; private readonly DiscordSocketClient _client; private readonly IImagesService _images; + private readonly MusicService _music; public SelfCommands(DbService db, SelfService service, DiscordSocketClient client, - IImagesService images) + MusicService music, IImagesService images) { _db = db; _service = service; _client = client; _images = images; + _music = music; } [NadekoCommand, Usage, Description, Aliases] @@ -268,6 +271,7 @@ namespace NadekoBot.Modules.Administration // ignored } await Task.Delay(2000).ConfigureAwait(false); + try { await _music.DestroyAllPlayers().ConfigureAwait(false); } catch { } Environment.Exit(0); } diff --git a/src/NadekoBot/Services/Music/MusicService.cs b/src/NadekoBot/Services/Music/MusicService.cs index bcd00b29..f3f8b982 100644 --- a/src/NadekoBot/Services/Music/MusicService.cs +++ b/src/NadekoBot/Services/Music/MusicService.cs @@ -272,6 +272,14 @@ namespace NadekoBot.Services.Music }; } + public async Task DestroyAllPlayers() + { + foreach (var key in MusicPlayers.Keys) + { + await DestroyPlayer(key); + } + } + public async Task ResolveYoutubeSong(string query, string queuerName) { var link = (await _google.GetVideoLinksByKeywordAsync(query).ConfigureAwait(false)).FirstOrDefault(); From d089a37bf0708bace332edc8b6da6d3e00654583 Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Wed, 5 Jul 2017 13:17:59 +0200 Subject: [PATCH 100/346] You can now use %server_time% placeholder to get your server's time, if you've set your timezone --- docs/Placeholders.md | 1 + .../Replacements/ReplacementBuilder.cs | 19 +++++++++++++++++-- src/NadekoBot/NadekoBot.cs | 2 +- .../Administration/GuildTimezoneService.cs | 8 +++++++- 4 files changed, 26 insertions(+), 4 deletions(-) diff --git a/docs/Placeholders.md b/docs/Placeholders.md index 0039e0a3..88146afb 100644 --- a/docs/Placeholders.md +++ b/docs/Placeholders.md @@ -18,6 +18,7 @@ Some features have their own specific placeholders which are noted in that featu - `%userdiscrim%` - discriminator (for example 1234) - `%rngX-Y%` - Replace X and Y with the range (for example `%rng5-10%` - random between 5 and 10) - `%time%` - Bot time +- `%server_time%` - Time on this server, set with `.timezone` command **If you're using placeholders in embeds, don't use %user% and in titles, footers and field names. They will not show properly.** diff --git a/src/NadekoBot/DataStructures/Replacements/ReplacementBuilder.cs b/src/NadekoBot/DataStructures/Replacements/ReplacementBuilder.cs index 2a7ac6bf..e7e7d2d4 100644 --- a/src/NadekoBot/DataStructures/Replacements/ReplacementBuilder.cs +++ b/src/NadekoBot/DataStructures/Replacements/ReplacementBuilder.cs @@ -3,6 +3,7 @@ using Discord.Commands; using Discord.WebSocket; using NadekoBot.Extensions; using NadekoBot.Services; +using NadekoBot.Services.Administration; using NadekoBot.Services.Music; using System; using System.Collections.Concurrent; @@ -26,7 +27,7 @@ namespace NadekoBot.DataStructures.Replacements { return this.WithUser(usr) .WithChannel(ch) - .WithServer(g) + .WithServer(client, g) .WithClient(client); } @@ -41,10 +42,24 @@ namespace NadekoBot.DataStructures.Replacements return this; } - public ReplacementBuilder WithServer(IGuild g) + public ReplacementBuilder WithServer(DiscordSocketClient client, IGuild g) { + _reps.TryAdd("%sid%", () => g == null ? "DM" : g.Id.ToString()); _reps.TryAdd("%server%", () => g == null ? "DM" : g.Name); + _reps.TryAdd("%server_time%", () => + { + TimeZoneInfo to = TimeZoneInfo.Local; + if (g != null) + { + if (GuildTimezoneService.AllServices.TryGetValue(client.CurrentUser.Id, out var tz)) + to = tz.GetTimeZoneOrDefault(g.Id) ?? TimeZoneInfo.Local; + } + + return TimeZoneInfo.ConvertTime(DateTime.UtcNow, + TimeZoneInfo.Utc, + to).ToString("HH:mm ") + to.StandardName.GetInitials(); + }); return this; } diff --git a/src/NadekoBot/NadekoBot.cs b/src/NadekoBot/NadekoBot.cs index 5f0a6513..513386c7 100644 --- a/src/NadekoBot/NadekoBot.cs +++ b/src/NadekoBot/NadekoBot.cs @@ -205,7 +205,7 @@ namespace NadekoBot var gameVcService = new GameVoiceChannelService(Client, Db, AllGuildConfigs); var autoAssignRoleService = new AutoAssignRoleService(Client, AllGuildConfigs); var logCommandService = new LogCommandService(Client, Strings, AllGuildConfigs, Db, muteService, protectionService); - var guildTimezoneService = new GuildTimezoneService(AllGuildConfigs, Db); + var guildTimezoneService = new GuildTimezoneService(Client, AllGuildConfigs, Db); #endregion #region pokemon diff --git a/src/NadekoBot/Services/Administration/GuildTimezoneService.cs b/src/NadekoBot/Services/Administration/GuildTimezoneService.cs index 6e4757fe..66736679 100644 --- a/src/NadekoBot/Services/Administration/GuildTimezoneService.cs +++ b/src/NadekoBot/Services/Administration/GuildTimezoneService.cs @@ -5,15 +5,18 @@ using System.Collections.Generic; using System.Linq; using System.Collections.Concurrent; using NadekoBot.Services; +using Discord.WebSocket; namespace NadekoBot.Services.Administration { public class GuildTimezoneService { + //hack >.> + public static ConcurrentDictionary AllServices { get; } = new ConcurrentDictionary(); private ConcurrentDictionary _timezones; private readonly DbService _db; - public GuildTimezoneService(IEnumerable gcs, DbService db) + public GuildTimezoneService(DiscordSocketClient client, IEnumerable gcs, DbService db) { _timezones = gcs .Select(x => @@ -36,6 +39,9 @@ namespace NadekoBot.Services.Administration .ToDictionary(x => x.Item1, x => x.Item2) .ToConcurrent(); + var curUser = client.CurrentUser; + if (curUser != null) + AllServices.TryAdd(curUser.Id, this); _db = db; } From d0326ad680088c315b80dcb9c24120e449d9a641 Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Wed, 5 Jul 2017 13:31:41 +0200 Subject: [PATCH 101/346] readded %playing% and %queued% placeholders --- .../Replacements/ReplacementBuilder.cs | 39 ++++++++++--------- 1 file changed, 20 insertions(+), 19 deletions(-) diff --git a/src/NadekoBot/DataStructures/Replacements/ReplacementBuilder.cs b/src/NadekoBot/DataStructures/Replacements/ReplacementBuilder.cs index e7e7d2d4..0dc367a6 100644 --- a/src/NadekoBot/DataStructures/Replacements/ReplacementBuilder.cs +++ b/src/NadekoBot/DataStructures/Replacements/ReplacementBuilder.cs @@ -89,25 +89,26 @@ namespace NadekoBot.DataStructures.Replacements return this; } - //public ReplacementBuilder WithMusic(MusicService ms) - //{ - // _reps.TryAdd("%playing%", () => - // { - // var cnt = ms.MusicPlayers.Count(kvp => kvp.Value.CurrentSong != null); - // if (cnt != 1) return cnt.ToString(); - // try - // { - // var mp = ms.MusicPlayers.FirstOrDefault(); - // return mp.Value.CurrentSong.SongInfo.Title; - // } - // catch - // { - // return "No songs"; - // } - // }); - // _reps.TryAdd("%queued%", () => ms.MusicPlayers.Sum(kvp => kvp.Value.Playlist.Count).ToString()); - // return this; - //} + public ReplacementBuilder WithMusic(MusicService ms) + { + _reps.TryAdd("%playing%", () => + { + var cnt = ms.MusicPlayers.Count(kvp => kvp.Value.Current.Current != null); + if (cnt != 1) return cnt.ToString(); + try + { + var mp = ms.MusicPlayers.FirstOrDefault(); + var title = mp.Value?.Current.Current?.Title; + return title ?? "No songs"; + } + catch + { + return "error"; + } + }); + _reps.TryAdd("%queued%", () => ms.MusicPlayers.Sum(kvp => kvp.Value.QueueArray().Songs.Length).ToString()); + return this; + } public ReplacementBuilder WithRngRegex() { From 6b51dbd3300ccb959ce83e6fcdd80f8e30798113 Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Wed, 5 Jul 2017 13:41:50 +0200 Subject: [PATCH 102/346] Added correct time guild time based on set .timezone to reminders confirmation message, and logs, close #1328 --- .../Modules/Utility/Commands/Remind.cs | 8 ++- src/NadekoBot/NadekoBot.cs | 2 +- .../Administration/LogCommandService.cs | 59 ++++++++++++------- 3 files changed, 44 insertions(+), 25 deletions(-) diff --git a/src/NadekoBot/Modules/Utility/Commands/Remind.cs b/src/NadekoBot/Modules/Utility/Commands/Remind.cs index ce046467..8957d1a8 100644 --- a/src/NadekoBot/Modules/Utility/Commands/Remind.cs +++ b/src/NadekoBot/Modules/Utility/Commands/Remind.cs @@ -3,6 +3,7 @@ using Discord.Commands; using NadekoBot.Attributes; using NadekoBot.Extensions; using NadekoBot.Services; +using NadekoBot.Services.Administration; using NadekoBot.Services.Database.Models; using NadekoBot.Services.Utility; using System; @@ -19,11 +20,13 @@ namespace NadekoBot.Modules.Utility { private readonly RemindService _service; private readonly DbService _db; + private readonly GuildTimezoneService _tz; - public RemindCommands(RemindService service, DbService db) + public RemindCommands(RemindService service, DbService db, GuildTimezoneService tz) { _service = service; _db = db; + _tz = tz; } public enum MeOrHere @@ -119,6 +122,7 @@ namespace NadekoBot.Modules.Utility await uow.CompleteAsync(); } + var gTime = TimeZoneInfo.ConvertTime(time, _tz.GetTimeZoneOrUtc(Context.Guild.Id)); try { await Context.Channel.SendConfirmAsync( @@ -126,7 +130,7 @@ namespace NadekoBot.Modules.Utility Format.Bold(!isPrivate ? $"<#{targetId}>" : Context.User.Username), Format.Bold(message.SanitizeMentions()), Format.Bold(output), - time, time)).ConfigureAwait(false); + gTime, gTime)).ConfigureAwait(false); } catch { diff --git a/src/NadekoBot/NadekoBot.cs b/src/NadekoBot/NadekoBot.cs index 513386c7..82c384bc 100644 --- a/src/NadekoBot/NadekoBot.cs +++ b/src/NadekoBot/NadekoBot.cs @@ -204,8 +204,8 @@ namespace NadekoBot var playingRotateService = new PlayingRotateService(Client, BotConfig, musicService, Db); var gameVcService = new GameVoiceChannelService(Client, Db, AllGuildConfigs); var autoAssignRoleService = new AutoAssignRoleService(Client, AllGuildConfigs); - var logCommandService = new LogCommandService(Client, Strings, AllGuildConfigs, Db, muteService, protectionService); var guildTimezoneService = new GuildTimezoneService(Client, AllGuildConfigs, Db); + var logCommandService = new LogCommandService(Client, Strings, AllGuildConfigs, Db, muteService, protectionService, guildTimezoneService); #endregion #region pokemon diff --git a/src/NadekoBot/Services/Administration/LogCommandService.cs b/src/NadekoBot/Services/Administration/LogCommandService.cs index 420ada80..dcd00a6f 100644 --- a/src/NadekoBot/Services/Administration/LogCommandService.cs +++ b/src/NadekoBot/Services/Administration/LogCommandService.cs @@ -19,8 +19,21 @@ namespace NadekoBot.Services.Administration private readonly DiscordSocketClient _client; private readonly Logger _log; - private string PrettyCurrentTime => $"【{DateTime.UtcNow:HH:mm:ss}】"; - private string CurrentTime => $"{DateTime.UtcNow:HH:mm:ss}"; + private string PrettyCurrentTime(IGuild g) + { + var time = DateTime.UtcNow; + if(g != null) + time = TimeZoneInfo.ConvertTime(time, _tz.GetTimeZoneOrUtc(g.Id)); + return $"【{time:HH:mm:ss}】"; + } + private string CurrentTime(IGuild g) + { + DateTime time = DateTime.UtcNow; + if (g != null) + time = TimeZoneInfo.ConvertTime(time, _tz.GetTimeZoneOrUtc(g.Id)); + + return $"{time:HH:mm:ss}"; + } public ConcurrentDictionary GuildLogSettings { get; } @@ -30,9 +43,10 @@ namespace NadekoBot.Services.Administration private readonly DbService _db; private readonly MuteService _mute; private readonly ProtectionService _prot; + private readonly GuildTimezoneService _tz; public LogCommandService(DiscordSocketClient client, NadekoStrings strings, - IEnumerable gcs, DbService db, MuteService mute, ProtectionService prot) + IEnumerable gcs, DbService db, MuteService mute, ProtectionService prot, GuildTimezoneService tz) { _client = client; _log = LogManager.GetCurrentClassLogger(); @@ -40,6 +54,7 @@ namespace NadekoBot.Services.Administration _db = db; _mute = mute; _prot = prot; + _tz = tz; GuildLogSettings = gcs .ToDictionary(g => g.GuildId, g => g.LogSetting) @@ -124,7 +139,7 @@ namespace NadekoBot.Services.Administration .WithDescription($"{before.Username}#{before.Discriminator} | {before.Id}") .AddField(fb => fb.WithName("Old Name").WithValue($"{before.Username}").WithIsInline(true)) .AddField(fb => fb.WithName("New Name").WithValue($"{after.Username}").WithIsInline(true)) - .WithFooter(fb => fb.WithText(CurrentTime)) + .WithFooter(fb => fb.WithText(CurrentTime(g))) .WithOkColor(); } else if (before.AvatarId != after.AvatarId) @@ -133,7 +148,7 @@ namespace NadekoBot.Services.Administration .WithDescription($"{before.Username}#{before.Discriminator} | {before.Id}") .WithThumbnailUrl(before.GetAvatarUrl()) .WithImageUrl(after.GetAvatarUrl()) - .WithFooter(fb => fb.WithText(CurrentTime)) + .WithFooter(fb => fb.WithText(CurrentTime(g))) .WithOkColor(); } else @@ -244,7 +259,7 @@ namespace NadekoBot.Services.Administration var embed = new EmbedBuilder().WithAuthor(eab => eab.WithName(mutes)) .WithTitle($"{usr.Username}#{usr.Discriminator} | {usr.Id}") - .WithFooter(fb => fb.WithText(CurrentTime)) + .WithFooter(fb => fb.WithText(CurrentTime(usr.Guild))) .WithOkColor(); await logChannel.EmbedAsync(embed).ConfigureAwait(false); @@ -287,7 +302,7 @@ namespace NadekoBot.Services.Administration var embed = new EmbedBuilder().WithAuthor(eab => eab.WithName(mutes)) .WithTitle($"{usr.Username}#{usr.Discriminator} | {usr.Id}") - .WithFooter(fb => fb.WithText($"{CurrentTime}")) + .WithFooter(fb => fb.WithText($"{CurrentTime(usr.Guild)}")) .WithOkColor(); await logChannel.EmbedAsync(embed).ConfigureAwait(false); @@ -335,7 +350,7 @@ namespace NadekoBot.Services.Administration var embed = new EmbedBuilder().WithAuthor(eab => eab.WithName($"🛡 Anti-{protection}")) .WithTitle(GetText(logChannel.Guild, "users") + " " + punishment) .WithDescription(string.Join("\n", users.Select(u => u.ToString()))) - .WithFooter(fb => fb.WithText($"{CurrentTime}")) + .WithFooter(fb => fb.WithText(CurrentTime(logChannel.Guild))) .WithOkColor(); await logChannel.EmbedAsync(embed).ConfigureAwait(false); @@ -361,7 +376,7 @@ namespace NadekoBot.Services.Administration ITextChannel logChannel; if ((logChannel = await TryGetLogChannel(before.Guild, logSetting, LogType.UserUpdated)) == null) return; - var embed = new EmbedBuilder().WithOkColor().WithFooter(efb => efb.WithText(CurrentTime)) + var embed = new EmbedBuilder().WithOkColor().WithFooter(efb => efb.WithText(CurrentTime(before.Guild))) .WithTitle($"{before.Username}#{before.Discriminator} | {before.Id}"); if (before.Nickname != after.Nickname) { @@ -417,7 +432,7 @@ namespace NadekoBot.Services.Administration if ((logChannel = await TryGetLogChannel(before.Guild, logSetting, LogType.ChannelUpdated)) == null) return; - var embed = new EmbedBuilder().WithOkColor().WithFooter(efb => efb.WithText(CurrentTime)); + var embed = new EmbedBuilder().WithOkColor().WithFooter(efb => efb.WithText(CurrentTime(before.Guild))); var beforeTextChannel = cbefore as ITextChannel; var afterTextChannel = cafter as ITextChannel; @@ -477,7 +492,7 @@ namespace NadekoBot.Services.Administration .WithOkColor() .WithTitle("🆕 " + title) .WithDescription($"{ch.Name} | {ch.Id}") - .WithFooter(efb => efb.WithText(CurrentTime))).ConfigureAwait(false); + .WithFooter(efb => efb.WithText(CurrentTime(ch.Guild)))).ConfigureAwait(false); } catch { @@ -515,7 +530,7 @@ namespace NadekoBot.Services.Administration .WithOkColor() .WithTitle("🆕 " + title) .WithDescription($"{ch.Name} | {ch.Id}") - .WithFooter(efb => efb.WithText(CurrentTime))).ConfigureAwait(false); + .WithFooter(efb => efb.WithText(CurrentTime(ch.Guild)))).ConfigureAwait(false); } catch (Exception ex) { _log.Warn(ex); } }); @@ -549,19 +564,19 @@ namespace NadekoBot.Services.Administration string str = null; if (beforeVch?.Guild == afterVch?.Guild) { - str = "🎙" + Format.Code(PrettyCurrentTime) + GetText(logChannel.Guild, "user_vmoved", + str = "🎙" + Format.Code(PrettyCurrentTime(usr.Guild)) + GetText(logChannel.Guild, "user_vmoved", "👤" + Format.Bold(usr.Username + "#" + usr.Discriminator), Format.Bold(beforeVch?.Name ?? ""), Format.Bold(afterVch?.Name ?? "")); } else if (beforeVch == null) { - str = "🎙" + Format.Code(PrettyCurrentTime) + GetText(logChannel.Guild, "user_vjoined", + str = "🎙" + Format.Code(PrettyCurrentTime(usr.Guild)) + GetText(logChannel.Guild, "user_vjoined", "👤" + Format.Bold(usr.Username + "#" + usr.Discriminator), Format.Bold(afterVch.Name ?? "")); } else if (afterVch == null) { - str = "🎙" + Format.Code(PrettyCurrentTime) + GetText(logChannel.Guild, "user_vleft", + str = "🎙" + Format.Code(PrettyCurrentTime(usr.Guild)) + GetText(logChannel.Guild, "user_vleft", "👤" + Format.Bold(usr.Username + "#" + usr.Discriminator), Format.Bold(beforeVch.Name ?? "")); } @@ -597,7 +612,7 @@ namespace NadekoBot.Services.Administration // return; // string str = ""; // if (before.Status != after.Status) - // str = "🎭" + Format.Code(PrettyCurrentTime) + + // str = "🎭" + Format.Code(PrettyCurrentTime(g)) + // GetText(logChannel.Guild, "user_status_change", // "👤" + Format.Bold(usr.Username), // Format.Bold(after.Status.ToString())); @@ -639,7 +654,7 @@ namespace NadekoBot.Services.Administration .WithThumbnailUrl(usr.GetAvatarUrl()) .WithDescription(usr.ToString()) .AddField(efb => efb.WithName("Id").WithValue(usr.Id.ToString())) - .WithFooter(efb => efb.WithText(CurrentTime))).ConfigureAwait(false); + .WithFooter(efb => efb.WithText(CurrentTime(usr.Guild)))).ConfigureAwait(false); } catch { @@ -669,7 +684,7 @@ namespace NadekoBot.Services.Administration .WithThumbnailUrl(usr.GetAvatarUrl()) .WithDescription($"{usr}") .AddField(efb => efb.WithName("Id").WithValue(usr.Id.ToString())) - .WithFooter(efb => efb.WithText(CurrentTime))).ConfigureAwait(false); + .WithFooter(efb => efb.WithText(CurrentTime(usr.Guild)))).ConfigureAwait(false); } catch (Exception ex) { _log.Warn(ex); } }); @@ -696,7 +711,7 @@ namespace NadekoBot.Services.Administration .WithThumbnailUrl(usr.GetAvatarUrl()) .WithDescription(usr.ToString()) .AddField(efb => efb.WithName("Id").WithValue(usr.Id.ToString())) - .WithFooter(efb => efb.WithText(CurrentTime))).ConfigureAwait(false); + .WithFooter(efb => efb.WithText(CurrentTime(guild)))).ConfigureAwait(false); } catch (Exception ex) { _log.Warn(ex); } }); @@ -722,7 +737,7 @@ namespace NadekoBot.Services.Administration .WithThumbnailUrl(usr.GetAvatarUrl()) .WithDescription(usr.ToString()) .AddField(efb => efb.WithName("Id").WithValue(usr.Id.ToString())) - .WithFooter(efb => efb.WithText(CurrentTime))).ConfigureAwait(false); + .WithFooter(efb => efb.WithText(CurrentTime(guild)))).ConfigureAwait(false); } catch (Exception ex) { _log.Warn(ex); } }); @@ -757,7 +772,7 @@ namespace NadekoBot.Services.Administration .WithDescription(msg.Author.ToString()) .AddField(efb => efb.WithName(GetText(logChannel.Guild, "content")).WithValue(string.IsNullOrWhiteSpace(msg.Content) ? "-" : msg.Resolve(userHandling: TagHandling.FullName)).WithIsInline(false)) .AddField(efb => efb.WithName("Id").WithValue(msg.Id.ToString()).WithIsInline(false)) - .WithFooter(efb => efb.WithText(CurrentTime)); + .WithFooter(efb => efb.WithText(CurrentTime(channel.Guild))); if (msg.Attachments.Any()) embed.AddField(efb => efb.WithName(GetText(logChannel.Guild, "attachments")).WithValue(string.Join(", ", msg.Attachments.Select(a => a.Url))).WithIsInline(false)); @@ -809,7 +824,7 @@ namespace NadekoBot.Services.Administration .AddField(efb => efb.WithName(GetText(logChannel.Guild, "old_msg")).WithValue(string.IsNullOrWhiteSpace(before.Content) ? "-" : before.Resolve(userHandling: TagHandling.FullName)).WithIsInline(false)) .AddField(efb => efb.WithName(GetText(logChannel.Guild, "new_msg")).WithValue(string.IsNullOrWhiteSpace(after.Content) ? "-" : after.Resolve(userHandling: TagHandling.FullName)).WithIsInline(false)) .AddField(efb => efb.WithName("Id").WithValue(after.Id.ToString()).WithIsInline(false)) - .WithFooter(efb => efb.WithText(CurrentTime)); + .WithFooter(efb => efb.WithText(CurrentTime(channel.Guild))); await logChannel.EmbedAsync(embed).ConfigureAwait(false); } From 9fddfe77c4653fc4da6fa55b77c72613c5bed928 Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Wed, 5 Jul 2017 15:16:06 +0200 Subject: [PATCH 103/346] Shards will now show when they're unresponsive in .shardstats command --- .../DataStructures/ShardCom/IShardComMessage.cs | 1 + src/NadekoBot/Modules/Music/Music.cs | 2 +- src/NadekoBot/Modules/Utility/Utility.cs | 9 +++++++-- src/NadekoBot/NadekoBot.cs | 3 ++- src/NadekoBot/ShardsCoordinator.cs | 6 ++++-- src/NadekoBot/_strings/ResponseStrings.en-US.json | 2 +- 6 files changed, 16 insertions(+), 7 deletions(-) diff --git a/src/NadekoBot/DataStructures/ShardCom/IShardComMessage.cs b/src/NadekoBot/DataStructures/ShardCom/IShardComMessage.cs index 9fb1f5d0..6f54df59 100644 --- a/src/NadekoBot/DataStructures/ShardCom/IShardComMessage.cs +++ b/src/NadekoBot/DataStructures/ShardCom/IShardComMessage.cs @@ -12,5 +12,6 @@ namespace NadekoBot.DataStructures.ShardCom public int ShardId { get; set; } public ConnectionState ConnectionState { get; set; } public int Guilds { get; set; } + public DateTime Time { get; set; } } } diff --git a/src/NadekoBot/Modules/Music/Music.cs b/src/NadekoBot/Modules/Music/Music.cs index d8bc759d..a5d4fc26 100644 --- a/src/NadekoBot/Modules/Music/Music.cs +++ b/src/NadekoBot/Modules/Music/Music.cs @@ -270,7 +270,7 @@ namespace NadekoBot.Modules.Music add += Format.Bold(GetText("queue_stopped", Format.Code(Prefix + "play"))) + "\n"; var mps = mp.MaxPlaytimeSeconds; if (mps > 0) - add += Format.Bold(GetText("song_skips_after", TimeSpan.FromSeconds(mps).ToString("g"))) + "\n"; + add += Format.Bold(GetText("song_skips_after", TimeSpan.FromSeconds(mps).ToString("HH\\:mm\\:ss"))) + "\n"; if (mp.RepeatCurrentSong) add += "🔂 " + GetText("repeating_cur_song") + "\n"; else if (mp.Shuffle) diff --git a/src/NadekoBot/Modules/Utility/Utility.cs b/src/NadekoBot/Modules/Utility/Utility.cs index 7a6f9ae1..02d93d2c 100644 --- a/src/NadekoBot/Modules/Utility/Utility.cs +++ b/src/NadekoBot/Modules/Utility/Utility.cs @@ -295,8 +295,13 @@ namespace NadekoBot.Modules.Utility var allShardStrings = statuses .Select(x => - GetText("shard_stats_txt", x.ShardId.ToString(), - Format.Bold(x.ConnectionState.ToString()), Format.Bold(x.Guilds.ToString()))) + { + var timeDiff = DateTime.UtcNow - x.Time; + if (timeDiff > TimeSpan.FromSeconds(20)) + return $"Shard #{x.ShardId.ToString()} **UNRESPONSIVE** for {timeDiff.ToString(@"hh\:mm\:ss")}"; + return GetText("shard_stats_txt", x.ShardId.ToString(), + Format.Bold(x.ConnectionState.ToString()), Format.Bold(x.Guilds.ToString()), timeDiff.ToString(@"hh\:mm\:ss")); + }) .ToArray(); await Context.Channel.SendPaginatedConfirmAsync(_client, page, (curPage) => diff --git a/src/NadekoBot/NadekoBot.cs b/src/NadekoBot/NadekoBot.cs index 82c384bc..1661511c 100644 --- a/src/NadekoBot/NadekoBot.cs +++ b/src/NadekoBot/NadekoBot.cs @@ -130,8 +130,9 @@ namespace NadekoBot ConnectionState = Client.ConnectionState, Guilds = Client.ConnectionState == ConnectionState.Connected ? Client.Guilds.Count : 0, ShardId = Client.ShardId, + Time = DateTime.UtcNow, }); - await Task.Delay(1000); + await Task.Delay(5000); } }); } diff --git a/src/NadekoBot/ShardsCoordinator.cs b/src/NadekoBot/ShardsCoordinator.cs index 18725566..1516f868 100644 --- a/src/NadekoBot/ShardsCoordinator.cs +++ b/src/NadekoBot/ShardsCoordinator.cs @@ -3,6 +3,7 @@ using NadekoBot.Services; using NadekoBot.Services.Impl; using NLog; using System; +using System.Collections.Concurrent; using System.Diagnostics; using System.Linq; using System.Threading.Tasks; @@ -21,7 +22,7 @@ namespace NadekoBot private readonly Logger _log; private readonly ShardComServer _comServer; private readonly int _port; - + public ShardsCoordinator(int port) { LogSetup.SetupLogger(); @@ -40,6 +41,7 @@ namespace NadekoBot private Task _comServer_OnDataReceived(ShardComMessage msg) { Statuses[msg.ShardId] = msg; + if (msg.ConnectionState == Discord.ConnectionState.Disconnected || msg.ConnectionState == Discord.ConnectionState.Disconnecting) _log.Error("!!! SHARD {0} IS IN {1} STATE", msg.ShardId, msg.ConnectionState.ToString()); return Task.CompletedTask; @@ -87,7 +89,7 @@ namespace NadekoBot _log.Info(string.Join("\n", Statuses .ToArray() .Where(x => x != null) - .Select(x => $"Shard {x.ShardId} is in {x.ConnectionState.ToString()} state with {x.Guilds} servers")) + "\n" + groupStr); + .Select(x => $"Shard {x.ShardId} is in {x.ConnectionState.ToString()} state with {x.Guilds} servers. {(DateTime.UtcNow - x.Time).ToString(@"hh\:mm\:ss")} ago")) + "\n" + groupStr); break; default: break; diff --git a/src/NadekoBot/_strings/ResponseStrings.en-US.json b/src/NadekoBot/_strings/ResponseStrings.en-US.json index fa4bea1a..06d62f39 100644 --- a/src/NadekoBot/_strings/ResponseStrings.en-US.json +++ b/src/NadekoBot/_strings/ResponseStrings.en-US.json @@ -678,7 +678,7 @@ "utility_server_info": "Server info", "utility_shard": "Shard", "utility_shard_stats": "Shard stats", - "utility_shard_stats_txt": "Shard **#{0}** is in {1} state with {2} servers", + "utility_shard_stats_txt": "Shard **#{0}** is in {1} state with {2} servers - {3} ago", "utility_showemojis": "**Name:** {0} **Link:** {1}", "utility_showemojis_none": "No special emojis found.", "utility_stats_songs": "Playing {0} songs, {1} queued.", From 3a71f63754387df551dfe5d86160b24dfbb924f4 Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Wed, 5 Jul 2017 15:32:57 +0200 Subject: [PATCH 104/346] Fixed music --- src/NadekoBot/Services/Music/MusicPlayer.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/NadekoBot/Services/Music/MusicPlayer.cs b/src/NadekoBot/Services/Music/MusicPlayer.cs index 3485760b..cc165837 100644 --- a/src/NadekoBot/Services/Music/MusicPlayer.cs +++ b/src/NadekoBot/Services/Music/MusicPlayer.cs @@ -180,6 +180,7 @@ namespace NadekoBot.Services.Music // i don't want to spam connection attempts continue; } + pcm = ac.CreatePCMStream(AudioApplication.Music, bufferMillis: 500); OnStarted?.Invoke(this, data); byte[] buffer = new byte[3840]; From ab07199a1ede9de4be374986fa3dd28ee9f0be9a Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Wed, 5 Jul 2017 16:41:51 +0200 Subject: [PATCH 105/346] Fixed converter? --- src/NadekoBot/NadekoBot.cs | 4 +- .../Services/Utility/ConverterService.cs | 48 +++++++++++-------- 2 files changed, 30 insertions(+), 22 deletions(-) diff --git a/src/NadekoBot/NadekoBot.cs b/src/NadekoBot/NadekoBot.cs index 1661511c..1c983731 100644 --- a/src/NadekoBot/NadekoBot.cs +++ b/src/NadekoBot/NadekoBot.cs @@ -162,7 +162,7 @@ namespace NadekoBot #region utility var remindService = new RemindService(Client, BotConfig, Db, startingGuildIdList, uow); var repeaterService = new MessageRepeaterService(this, Client, AllGuildConfigs); - //var converterService = new ConverterService(Db); + var converterService = new ConverterService(Client, Db); var commandMapService = new CommandMapService(AllGuildConfigs); var patreonRewardsService = new PatreonRewardsService(Credentials, Db, Currency, Client); var verboseErrorsService = new VerboseErrorsService(AllGuildConfigs, Db, CommandHandler, helpService); @@ -232,7 +232,7 @@ namespace NadekoBot .Add(commandMapService) .Add(remindService) .Add(repeaterService) - //.Add(converterService) + .Add(converterService) .Add(verboseErrorsService) .Add(patreonRewardsService) .Add(pruneService) diff --git a/src/NadekoBot/Services/Utility/ConverterService.cs b/src/NadekoBot/Services/Utility/ConverterService.cs index 40f7fafa..64100955 100644 --- a/src/NadekoBot/Services/Utility/ConverterService.cs +++ b/src/NadekoBot/Services/Utility/ConverterService.cs @@ -25,33 +25,41 @@ namespace NadekoBot.Services.Utility { _log = LogManager.GetCurrentClassLogger(); _db = db; - try - { - var data = JsonConvert.DeserializeObject>( - File.ReadAllText("data/units.json")) - .Select(u => new ConvertUnit() - { - Modifier = u.Modifier, - UnitType = u.UnitType, - InternalTrigger = string.Join("|", u.Triggers) - }).ToArray(); - using (var uow = _db.UnitOfWork) + if (client.ShardId == 0) + { + try { - if (uow.ConverterUnits.Empty()) + var data = JsonConvert.DeserializeObject>( + File.ReadAllText("data/units.json")) + .Select(u => new ConvertUnit() + { + Modifier = u.Modifier, + UnitType = u.UnitType, + InternalTrigger = string.Join("|", u.Triggers) + }).ToArray(); + + using (var uow = _db.UnitOfWork) { - uow.ConverterUnits.AddRange(data); - uow.Complete(); + if (uow.ConverterUnits.Empty()) + { + uow.ConverterUnits.AddRange(data); + + Units = uow.ConverterUnits.GetAll().ToList(); + uow.Complete(); + } } } - Units = data.ToList(); - } - catch (Exception ex) - { - _log.Warn("Could not load units: " + ex.Message); + catch (Exception ex) + { + _log.Warn("Could not load units: " + ex.Message); + } } - _currencyUpdater = new Timer(async (shouldLoad) => await UpdateCurrency((bool)shouldLoad), client.ShardId == 0, _updateInterval, _updateInterval); + _currencyUpdater = new Timer(async (shouldLoad) => await UpdateCurrency((bool)shouldLoad), + client.ShardId == 0, + TimeSpan.FromSeconds(1), + _updateInterval); } private async Task GetCurrencyRates() From 78be4598cfa726a84f1139c74da596dc25063314 Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Wed, 5 Jul 2017 16:59:59 +0200 Subject: [PATCH 106/346] Repull video with it's uri if it's a youtube song, every time a song is played, #1334 --- src/NadekoBot/Modules/Music/Music.cs | 10 ++-- src/NadekoBot/Services/Music/MusicPlayer.cs | 2 +- src/NadekoBot/Services/Music/MusicService.cs | 51 ++++++++++++-------- src/NadekoBot/Services/Music/SongBuffer.cs | 1 + src/NadekoBot/Services/Music/SongInfo.cs | 2 +- 5 files changed, 40 insertions(+), 26 deletions(-) diff --git a/src/NadekoBot/Modules/Music/Music.cs b/src/NadekoBot/Modules/Music/Music.cs index a5d4fc26..d9057723 100644 --- a/src/NadekoBot/Modules/Music/Music.cs +++ b/src/NadekoBot/Modules/Music/Music.cs @@ -463,15 +463,15 @@ namespace NadekoBot.Modules.Music { var mp = await _music.GetOrCreatePlayer(Context); - var songs = mp.QueueArray().Songs - .Select(s => new PlaylistSong() + var songs = await Task.WhenAll(mp.QueueArray().Songs + .Select(async s => new PlaylistSong() { Provider = s.Provider, ProviderType = s.ProviderType, Title = s.Title, - Uri = s.Uri, + Uri = await s.Uri(), Query = s.Query, - }).ToList(); + }).ToList()); MusicPlaylist playlist; using (var uow = _db.UnitOfWork) @@ -481,7 +481,7 @@ namespace NadekoBot.Modules.Music Name = name, Author = Context.User.Username, AuthorId = Context.User.Id, - Songs = songs, + Songs = songs.ToList(), }; uow.MusicPlaylists.Add(playlist); await uow.CompleteAsync().ConfigureAwait(false); diff --git a/src/NadekoBot/Services/Music/MusicPlayer.cs b/src/NadekoBot/Services/Music/MusicPlayer.cs index cc165837..15888dec 100644 --- a/src/NadekoBot/Services/Music/MusicPlayer.cs +++ b/src/NadekoBot/Services/Music/MusicPlayer.cs @@ -154,7 +154,7 @@ namespace NadekoBot.Services.Music continue; _log.Info("Starting"); - using (var b = new SongBuffer(data.Song.Uri, "")) + using (var b = new SongBuffer(await data.Song.Uri(), "")) { AudioOutStream pcm = null; try diff --git a/src/NadekoBot/Services/Music/MusicService.cs b/src/NadekoBot/Services/Music/MusicService.cs index f3f8b982..2291b59a 100644 --- a/src/NadekoBot/Services/Music/MusicService.cs +++ b/src/NadekoBot/Services/Music/MusicService.cs @@ -234,23 +234,23 @@ namespace NadekoBot.Services.Music return await SongInfoFromSVideo(svideo, queuerName); } - public async Task SongInfoFromSVideo(SoundCloudVideo svideo, string queuerName) => - new SongInfo + public Task SongInfoFromSVideo(SoundCloudVideo svideo, string queuerName) => + Task.FromResult(new SongInfo { Title = svideo.FullName, Provider = "SoundCloud", - Uri = await svideo.StreamLink().ConfigureAwait(false), + Uri = () => svideo.StreamLink(), ProviderType = MusicType.Soundcloud, Query = svideo.TrackLink, AlbumArt = svideo.artwork_url, QueuerName = queuerName - }; + }); public SongInfo ResolveLocalSong(string query, string queuerName) { return new SongInfo { - Uri = "\"" + Path.GetFullPath(query) + "\"", + Uri = () => Task.FromResult("\"" + Path.GetFullPath(query) + "\""), Title = Path.GetFileNameWithoutExtension(query), Provider = "Local File", ProviderType = MusicType.Local, @@ -263,7 +263,7 @@ namespace NadekoBot.Services.Music { return new SongInfo { - Uri = query, + Uri = () => Task.FromResult(query), Title = query, Provider = "Radio Stream", ProviderType = MusicType.Radio, @@ -282,18 +282,7 @@ namespace NadekoBot.Services.Music public async Task ResolveYoutubeSong(string query, string queuerName) { - var link = (await _google.GetVideoLinksByKeywordAsync(query).ConfigureAwait(false)).FirstOrDefault(); - if (string.IsNullOrWhiteSpace(link)) - { - _log.Info("No song found."); - return null; - } - var allVideos = await Task.Run(async () => { try { return await YouTube.Default.GetAllVideosAsync(link).ConfigureAwait(false); } catch { return Enumerable.Empty(); } }).ConfigureAwait(false); - var videos = allVideos.Where(v => v.AdaptiveKind == AdaptiveKind.Audio); - var video = videos - .Where(v => v.AudioBitrate < 256) - .OrderByDescending(v => v.AudioBitrate) - .FirstOrDefault(); + var (link, video) = await GetYoutubeVideo(query); if (video == null) // do something with this error { @@ -309,7 +298,13 @@ namespace NadekoBot.Services.Music { Title = video.Title.Substring(0, video.Title.Length - 10), // removing trailing "- You Tube" Provider = "YouTube", - Uri = await video.GetUriAsync().ConfigureAwait(false), + Uri = async () => { + var vid = await GetYoutubeVideo(query); + if (vid.Item2 == null) + throw new HttpRequestException(); + + return await vid.Item2.GetUriAsync(); + }, Query = link, ProviderType = MusicType.YouTube, QueuerName = queuerName @@ -317,6 +312,24 @@ namespace NadekoBot.Services.Music return song; } + private async Task<(string, YouTubeVideo)> GetYoutubeVideo(string query) + { + var link = (await _google.GetVideoLinksByKeywordAsync(query).ConfigureAwait(false)).FirstOrDefault(); + if (string.IsNullOrWhiteSpace(link)) + { + _log.Info("No song found."); + return (null, null); + } + var allVideos = await Task.Run(async () => { try { return await YouTube.Default.GetAllVideosAsync(link).ConfigureAwait(false); } catch { return Enumerable.Empty(); } }).ConfigureAwait(false); + var videos = allVideos.Where(v => v.AdaptiveKind == AdaptiveKind.Audio); + var video = videos + .Where(v => v.AudioBitrate < 256) + .OrderByDescending(v => v.AudioBitrate) + .FirstOrDefault(); + + return (link, video); + } + private bool IsRadioLink(string query) => (query.StartsWith("http") || query.StartsWith("ww")) diff --git a/src/NadekoBot/Services/Music/SongBuffer.cs b/src/NadekoBot/Services/Music/SongBuffer.cs index a3ec23a4..43661e3d 100644 --- a/src/NadekoBot/Services/Music/SongBuffer.cs +++ b/src/NadekoBot/Services/Music/SongBuffer.cs @@ -24,6 +24,7 @@ namespace NadekoBot.Services.Music public SongBuffer(string songUri, string skipTo) { _log = LogManager.GetCurrentClassLogger(); + _log.Warn(songUri); this.SongUri = songUri; this.p = StartFFmpegProcess(songUri, 0); diff --git a/src/NadekoBot/Services/Music/SongInfo.cs b/src/NadekoBot/Services/Music/SongInfo.cs index 04d48d55..cb3798d9 100644 --- a/src/NadekoBot/Services/Music/SongInfo.cs +++ b/src/NadekoBot/Services/Music/SongInfo.cs @@ -14,7 +14,7 @@ namespace NadekoBot.Services.Music public MusicType ProviderType { get; set; } public string Query { get; set; } public string Title { get; set; } - public string Uri { get; set; } + public Func> Uri { get; set; } public string AlbumArt { get; set; } public string QueuerName { get; set; } public TimeSpan TotalTime { get; set; } = TimeSpan.Zero; From 94bb6f15a71b23b3079a984ebe71f396acd3d386 Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Wed, 5 Jul 2017 17:02:40 +0200 Subject: [PATCH 107/346] danbooru fix --- src/NadekoBot/DataStructures/SearchImageCacher.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/NadekoBot/DataStructures/SearchImageCacher.cs b/src/NadekoBot/DataStructures/SearchImageCacher.cs index c9795ecf..8e71db03 100644 --- a/src/NadekoBot/DataStructures/SearchImageCacher.cs +++ b/src/NadekoBot/DataStructures/SearchImageCacher.cs @@ -102,7 +102,7 @@ namespace NadekoBot.DataStructures website = $"https://e621.net/post/index.json?limit=1000&tags={tag}"; break; case DapiSearchType.Danbooru: - website = $"https://danbooru.donmai.us/posts.json?limit=200&tags={tag}"; + website = $"http://danbooru.donmai.us/posts.json?limit=200&tags={tag}"; break; case DapiSearchType.Gelbooru: website = $"http://gelbooru.com/index.php?page=dapi&s=post&q=index&limit=1000&tags={tag}"; From 8f844d38d3ad69f8463db7e981295335f35a2067 Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Wed, 5 Jul 2017 17:05:29 +0200 Subject: [PATCH 108/346] Removed leftover log --- src/NadekoBot/Services/Music/SongBuffer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/NadekoBot/Services/Music/SongBuffer.cs b/src/NadekoBot/Services/Music/SongBuffer.cs index 43661e3d..71401119 100644 --- a/src/NadekoBot/Services/Music/SongBuffer.cs +++ b/src/NadekoBot/Services/Music/SongBuffer.cs @@ -24,7 +24,7 @@ namespace NadekoBot.Services.Music public SongBuffer(string songUri, string skipTo) { _log = LogManager.GetCurrentClassLogger(); - _log.Warn(songUri); + //_log.Warn(songUri); this.SongUri = songUri; this.p = StartFFmpegProcess(songUri, 0); From 269a4e3157ecc43bc1cfeef890727e3a0468c60f Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Wed, 5 Jul 2017 17:38:38 +0200 Subject: [PATCH 109/346] Upgraded dicord.net --- .../TypeReaders/BotCommandTypeReader.cs | 7 ++++--- .../TypeReaders/GuildDateTimeTypeReader.cs | 2 +- .../TypeReaders/GuildTypeReader.cs | 3 ++- .../TypeReaders/ModuleTypeReader.cs | 5 +++-- .../TypeReaders/PermissionActionTypeReader.cs | 3 ++- .../Commands/UserPunishCommands.cs | 2 +- .../Modules/Gambling/Commands/FlowerShop.cs | 2 +- src/NadekoBot/Modules/Help/Help.cs | 2 +- src/NadekoBot/Modules/NadekoModule.cs | 2 +- src/NadekoBot/Modules/Searches/Searches.cs | 2 +- src/NadekoBot/NadekoBot.csproj | 2 +- .../Services/Administration/SelfService.cs | 2 +- src/NadekoBot/Services/CommandHandler.cs | 2 +- .../Services/CustomReactions/Extensions.cs | 2 +- src/NadekoBot/Services/GreetSettingsService.cs | 2 +- src/NadekoBot/Services/Impl/StatsService.cs | 2 +- .../Services/Utility/RemindService.cs | 2 +- src/NadekoBot/ShardsCoordinator.cs | 8 +++++--- src/NadekoBot/_Extensions/Extensions.cs | 18 +++++++++--------- 19 files changed, 38 insertions(+), 32 deletions(-) diff --git a/src/NadekoBot/DataStructures/TypeReaders/BotCommandTypeReader.cs b/src/NadekoBot/DataStructures/TypeReaders/BotCommandTypeReader.cs index 6a292833..8a415eae 100644 --- a/src/NadekoBot/DataStructures/TypeReaders/BotCommandTypeReader.cs +++ b/src/NadekoBot/DataStructures/TypeReaders/BotCommandTypeReader.cs @@ -1,6 +1,7 @@ using Discord.Commands; using NadekoBot.Services; using NadekoBot.Services.CustomReactions; +using System; using System.Linq; using System.Threading.Tasks; @@ -17,7 +18,7 @@ namespace NadekoBot.TypeReaders _cmdHandler = cmdHandler; } - public override Task Read(ICommandContext context, string input) + public override Task Read(ICommandContext context, string input, IServiceProvider _) { input = input.ToUpperInvariant(); var prefix = _cmdHandler.GetPrefix(context.Guild); @@ -44,7 +45,7 @@ namespace NadekoBot.TypeReaders _crs = crs; } - public override async Task Read(ICommandContext context, string input) + public override async Task Read(ICommandContext context, string input, IServiceProvider _) { input = input.ToUpperInvariant(); @@ -64,7 +65,7 @@ namespace NadekoBot.TypeReaders } } - var cmd = await base.Read(context, input); + var cmd = await base.Read(context, input, _); if (cmd.IsSuccess) { return TypeReaderResult.FromSuccess(new CommandOrCrInfo(((CommandInfo)cmd.Values.First().Value).Name)); diff --git a/src/NadekoBot/DataStructures/TypeReaders/GuildDateTimeTypeReader.cs b/src/NadekoBot/DataStructures/TypeReaders/GuildDateTimeTypeReader.cs index 798caf62..7e394a21 100644 --- a/src/NadekoBot/DataStructures/TypeReaders/GuildDateTimeTypeReader.cs +++ b/src/NadekoBot/DataStructures/TypeReaders/GuildDateTimeTypeReader.cs @@ -14,7 +14,7 @@ namespace NadekoBot.TypeReaders _gts = gts; } - public override Task Read(ICommandContext context, string input) + public override Task Read(ICommandContext context, string input, IServiceProvider _) { if (!DateTime.TryParse(input, out var dt)) return Task.FromResult(TypeReaderResult.FromError(CommandError.ParseFailed, "Input string is in an incorrect format.")); diff --git a/src/NadekoBot/DataStructures/TypeReaders/GuildTypeReader.cs b/src/NadekoBot/DataStructures/TypeReaders/GuildTypeReader.cs index 3bb72d4c..068514fb 100644 --- a/src/NadekoBot/DataStructures/TypeReaders/GuildTypeReader.cs +++ b/src/NadekoBot/DataStructures/TypeReaders/GuildTypeReader.cs @@ -1,5 +1,6 @@ using Discord.Commands; using Discord.WebSocket; +using System; using System.Linq; using System.Threading.Tasks; @@ -13,7 +14,7 @@ namespace NadekoBot.TypeReaders { _client = client; } - public override Task Read(ICommandContext context, string input) + public override Task Read(ICommandContext context, string input, IServiceProvider _) { input = input.Trim().ToLowerInvariant(); var guilds = _client.Guilds; diff --git a/src/NadekoBot/DataStructures/TypeReaders/ModuleTypeReader.cs b/src/NadekoBot/DataStructures/TypeReaders/ModuleTypeReader.cs index 88645835..60adfc26 100644 --- a/src/NadekoBot/DataStructures/TypeReaders/ModuleTypeReader.cs +++ b/src/NadekoBot/DataStructures/TypeReaders/ModuleTypeReader.cs @@ -1,5 +1,6 @@ using Discord.Commands; using NadekoBot.Extensions; +using System; using System.Linq; using System.Threading.Tasks; @@ -14,7 +15,7 @@ namespace NadekoBot.TypeReaders _cmds = cmds; } - public override Task Read(ICommandContext context, string input) + public override Task Read(ICommandContext context, string input, IServiceProvider _) { input = input.ToUpperInvariant(); var module = _cmds.Modules.GroupBy(m => m.GetTopLevelModule()).FirstOrDefault(m => m.Key.Name.ToUpperInvariant() == input)?.Key; @@ -34,7 +35,7 @@ namespace NadekoBot.TypeReaders _cmds = cmds; } - public override Task Read(ICommandContext context, string input) + public override Task Read(ICommandContext context, string input, IServiceProvider _) { input = input.ToLowerInvariant(); var module = _cmds.Modules.GroupBy(m => m.GetTopLevelModule()).FirstOrDefault(m => m.Key.Name.ToLowerInvariant() == input)?.Key; diff --git a/src/NadekoBot/DataStructures/TypeReaders/PermissionActionTypeReader.cs b/src/NadekoBot/DataStructures/TypeReaders/PermissionActionTypeReader.cs index aa3510a6..9ca20229 100644 --- a/src/NadekoBot/DataStructures/TypeReaders/PermissionActionTypeReader.cs +++ b/src/NadekoBot/DataStructures/TypeReaders/PermissionActionTypeReader.cs @@ -1,6 +1,7 @@ using Discord.Commands; using System.Threading.Tasks; using NadekoBot.Modules.Permissions; +using System; namespace NadekoBot.TypeReaders { @@ -9,7 +10,7 @@ namespace NadekoBot.TypeReaders /// public class PermissionActionTypeReader : TypeReader { - public override Task Read(ICommandContext context, string input) + public override Task Read(ICommandContext context, string input, IServiceProvider _) { input = input.ToUpperInvariant(); switch (input) diff --git a/src/NadekoBot/Modules/Administration/Commands/UserPunishCommands.cs b/src/NadekoBot/Modules/Administration/Commands/UserPunishCommands.cs index 7e2a453a..9e7df4ec 100644 --- a/src/NadekoBot/Modules/Administration/Commands/UserPunishCommands.cs +++ b/src/NadekoBot/Modules/Administration/Commands/UserPunishCommands.cs @@ -109,7 +109,7 @@ namespace NadekoBot.Modules.Administration { try { - await (await user.CreateDMChannelAsync()).EmbedAsync(new EmbedBuilder().WithErrorColor() + await (await user.GetOrCreateDMChannelAsync()).EmbedAsync(new EmbedBuilder().WithErrorColor() .WithDescription(GetText("warned_on", Context.Guild.ToString())) .AddField(efb => efb.WithName(GetText("moderator")).WithValue(Context.User.ToString())) .AddField(efb => efb.WithName(GetText("reason")).WithValue(reason ?? "-"))) diff --git a/src/NadekoBot/Modules/Gambling/Commands/FlowerShop.cs b/src/NadekoBot/Modules/Gambling/Commands/FlowerShop.cs index dd4fd309..942ead4c 100644 --- a/src/NadekoBot/Modules/Gambling/Commands/FlowerShop.cs +++ b/src/NadekoBot/Modules/Gambling/Commands/FlowerShop.cs @@ -154,7 +154,7 @@ namespace NadekoBot.Modules.Gambling } try { - await (await Context.User.CreateDMChannelAsync()) + await (await Context.User.GetOrCreateDMChannelAsync()) .EmbedAsync(new EmbedBuilder().WithOkColor() .WithTitle(GetText("shop_purchase", Context.Guild.Name)) .AddField(efb => efb.WithName(GetText("item")).WithValue(item.Text).WithIsInline(false)) diff --git a/src/NadekoBot/Modules/Help/Help.cs b/src/NadekoBot/Modules/Help/Help.cs index 18ddf039..4b15b72e 100644 --- a/src/NadekoBot/Modules/Help/Help.cs +++ b/src/NadekoBot/Modules/Help/Help.cs @@ -96,7 +96,7 @@ namespace NadekoBot.Modules.Help if (com == null) { - IMessageChannel ch = channel is ITextChannel ? await ((IGuildUser)Context.User).CreateDMChannelAsync() : channel; + IMessageChannel ch = channel is ITextChannel ? await ((IGuildUser)Context.User).GetOrCreateDMChannelAsync() : channel; await ch.SendMessageAsync(HelpString).ConfigureAwait(false); return; } diff --git a/src/NadekoBot/Modules/NadekoModule.cs b/src/NadekoBot/Modules/NadekoModule.cs index 45abaea9..db7086fa 100644 --- a/src/NadekoBot/Modules/NadekoModule.cs +++ b/src/NadekoBot/Modules/NadekoModule.cs @@ -32,7 +32,7 @@ namespace NadekoBot.Modules _log = LogManager.GetCurrentClassLogger(); } - protected override void BeforeExecute() + protected override void BeforeExecute(CommandInfo cmd) { _cultureInfo = _localization.GetCultureInfo(Context.Guild?.Id); } diff --git a/src/NadekoBot/Modules/Searches/Searches.cs b/src/NadekoBot/Modules/Searches/Searches.cs index 0a8eec54..ea2990f5 100644 --- a/src/NadekoBot/Modules/Searches/Searches.cs +++ b/src/NadekoBot/Modules/Searches/Searches.cs @@ -667,7 +667,7 @@ namespace NadekoBot.Modules.Searches str += new NadekoRandom().Next(); foreach (var usr in allUsrsArray) { - await (await usr.CreateDMChannelAsync()).SendConfirmAsync(str).ConfigureAwait(false); + await (await usr.GetOrCreateDMChannelAsync()).SendConfirmAsync(str).ConfigureAwait(false); } } diff --git a/src/NadekoBot/NadekoBot.csproj b/src/NadekoBot/NadekoBot.csproj index 18ad4911..d5d72b38 100644 --- a/src/NadekoBot/NadekoBot.csproj +++ b/src/NadekoBot/NadekoBot.csproj @@ -55,7 +55,7 @@ - + diff --git a/src/NadekoBot/Services/Administration/SelfService.cs b/src/NadekoBot/Services/Administration/SelfService.cs index 2a12ffca..4134393b 100644 --- a/src/NadekoBot/Services/Administration/SelfService.cs +++ b/src/NadekoBot/Services/Administration/SelfService.cs @@ -84,7 +84,7 @@ namespace NadekoBot.Services.Administration { if (hs.Remove(u.Id)) { - channels.Add(u.Id, new AsyncLazy(async () => await u.CreateDMChannelAsync())); + channels.Add(u.Id, new AsyncLazy(async () => await u.GetOrCreateDMChannelAsync())); if (hs.Count == 0) break; } diff --git a/src/NadekoBot/Services/CommandHandler.cs b/src/NadekoBot/Services/CommandHandler.cs index a1de8871..58acff4d 100644 --- a/src/NadekoBot/Services/CommandHandler.cs +++ b/src/NadekoBot/Services/CommandHandler.cs @@ -355,7 +355,7 @@ namespace NadekoBot.Services } } - var execResult = await commands[i].ExecuteAsync(context, parseResult, serviceProvider); + var execResult = (ExecuteResult)(await commands[i].ExecuteAsync(context, parseResult, serviceProvider)); if (execResult.Exception != null && (!(execResult.Exception is HttpException he) || he.DiscordCode != 50013)) { lock (errorLogLock) diff --git a/src/NadekoBot/Services/CustomReactions/Extensions.cs b/src/NadekoBot/Services/CustomReactions/Extensions.cs index af968192..351ac6d2 100644 --- a/src/NadekoBot/Services/CustomReactions/Extensions.cs +++ b/src/NadekoBot/Services/CustomReactions/Extensions.cs @@ -81,7 +81,7 @@ namespace NadekoBot.Services.CustomReactions public static async Task Send(this CustomReaction cr, IUserMessage ctx, DiscordSocketClient client, CustomReactionsService crs) { - var channel = cr.DmResponse ? await ctx.Author.CreateDMChannelAsync() : ctx.Channel; + var channel = cr.DmResponse ? await ctx.Author.GetOrCreateDMChannelAsync() : ctx.Channel; crs.ReactionStats.AddOrUpdate(cr.Trigger, 1, (k, old) => ++old); diff --git a/src/NadekoBot/Services/GreetSettingsService.cs b/src/NadekoBot/Services/GreetSettingsService.cs index b38ac5bd..7e4aaee6 100644 --- a/src/NadekoBot/Services/GreetSettingsService.cs +++ b/src/NadekoBot/Services/GreetSettingsService.cs @@ -139,7 +139,7 @@ namespace NadekoBot.Services if (conf.SendDmGreetMessage) { - var channel = await user.CreateDMChannelAsync(); + var channel = await user.GetOrCreateDMChannelAsync(); if (channel != null) { diff --git a/src/NadekoBot/Services/Impl/StatsService.cs b/src/NadekoBot/Services/Impl/StatsService.cs index ed7ec815..60d4a1e2 100644 --- a/src/NadekoBot/Services/Impl/StatsService.cs +++ b/src/NadekoBot/Services/Impl/StatsService.cs @@ -17,7 +17,7 @@ namespace NadekoBot.Services.Impl private readonly IBotCredentials _creds; private readonly DateTime _started; - public const string BotVersion = "1.53"; + public const string BotVersion = "1.54"; public string Author => "Kwoth#2560"; public string Library => "Discord.Net"; diff --git a/src/NadekoBot/Services/Utility/RemindService.cs b/src/NadekoBot/Services/Utility/RemindService.cs index 592564c3..48f3f869 100644 --- a/src/NadekoBot/Services/Utility/RemindService.cs +++ b/src/NadekoBot/Services/Utility/RemindService.cs @@ -67,7 +67,7 @@ namespace NadekoBot.Services.Utility var user = _client.GetGuild(r.ServerId).GetUser(r.ChannelId); if (user == null) return; - ch = await user.CreateDMChannelAsync().ConfigureAwait(false); + ch = await user.GetOrCreateDMChannelAsync().ConfigureAwait(false); } else { diff --git a/src/NadekoBot/ShardsCoordinator.cs b/src/NadekoBot/ShardsCoordinator.cs index 1516f868..ad112cbf 100644 --- a/src/NadekoBot/ShardsCoordinator.cs +++ b/src/NadekoBot/ShardsCoordinator.cs @@ -22,7 +22,8 @@ namespace NadekoBot private readonly Logger _log; private readonly ShardComServer _comServer; private readonly int _port; - + private readonly int _curProcessId; + public ShardsCoordinator(int port) { LogSetup.SetupLogger(); @@ -36,6 +37,8 @@ namespace NadekoBot _comServer.Start(); _comServer.OnDataReceived += _comServer_OnDataReceived; + + _curProcessId = Process.GetCurrentProcess().Id; } private Task _comServer_OnDataReceived(ShardComMessage msg) @@ -49,13 +52,12 @@ namespace NadekoBot public async Task RunAsync() { - var curProcessId = Process.GetCurrentProcess().Id; for (int i = 1; i < Credentials.TotalShards; i++) { var p = Process.Start(new ProcessStartInfo() { FileName = Credentials.ShardRunCommand, - Arguments = string.Format(Credentials.ShardRunArguments, i, curProcessId, _port) + Arguments = string.Format(Credentials.ShardRunArguments, i, _curProcessId, _port) }); await Task.Delay(5000); } diff --git a/src/NadekoBot/_Extensions/Extensions.cs b/src/NadekoBot/_Extensions/Extensions.cs index 8488071b..880e9fbe 100644 --- a/src/NadekoBot/_Extensions/Extensions.cs +++ b/src/NadekoBot/_Extensions/Extensions.cs @@ -186,7 +186,7 @@ namespace NadekoBot.Extensions public static async Task SendMessageToOwnerAsync(this IGuild guild, string message) { - var ownerPrivate = await (await guild.GetOwnerAsync().ConfigureAwait(false)).CreateDMChannelAsync() + var ownerPrivate = await (await guild.GetOwnerAsync().ConfigureAwait(false)).GetOrCreateDMChannelAsync() .ConfigureAwait(false); return await ownerPrivate.SendMessageAsync(message).ConfigureAwait(false); @@ -230,28 +230,28 @@ namespace NadekoBot.Extensions public static double UnixTimestamp(this DateTime dt) => dt.ToUniversalTime().Subtract(new DateTime(1970, 1, 1, 0, 0, 0)).TotalSeconds; - public static async Task SendMessageAsync(this IUser user, string message, bool isTTS = false) => - await (await user.CreateDMChannelAsync().ConfigureAwait(false)).SendMessageAsync(message, isTTS).ConfigureAwait(false); + //public static async Task SendMessageAsync(this IUser user, string message, bool isTTS = false) => + // await (await user.CreateDMChannelAsync().ConfigureAwait(false)).SendMessageAsync(message, isTTS).ConfigureAwait(false); public static async Task SendConfirmAsync(this IUser user, string text) - => await (await user.CreateDMChannelAsync()).SendMessageAsync("", embed: new EmbedBuilder().WithOkColor().WithDescription(text)); + => await (await user.GetOrCreateDMChannelAsync()).SendMessageAsync("", embed: new EmbedBuilder().WithOkColor().WithDescription(text)); public static async Task SendConfirmAsync(this IUser user, string title, string text, string url = null) - => await (await user.CreateDMChannelAsync()).SendMessageAsync("", embed: new EmbedBuilder().WithOkColor().WithDescription(text) + => await (await user.GetOrCreateDMChannelAsync()).SendMessageAsync("", embed: new EmbedBuilder().WithOkColor().WithDescription(text) .WithTitle(title).WithUrl(url)); public static async Task SendErrorAsync(this IUser user, string title, string error, string url = null) - => await (await user.CreateDMChannelAsync()).SendMessageAsync("", embed: new EmbedBuilder().WithErrorColor().WithDescription(error) + => await (await user.GetOrCreateDMChannelAsync()).SendMessageAsync("", embed: new EmbedBuilder().WithErrorColor().WithDescription(error) .WithTitle(title).WithUrl(url)); public static async Task SendErrorAsync(this IUser user, string error) - => await (await user.CreateDMChannelAsync()).SendMessageAsync("", embed: new EmbedBuilder().WithErrorColor().WithDescription(error)); + => await (await user.GetOrCreateDMChannelAsync()).SendMessageAsync("", embed: new EmbedBuilder().WithErrorColor().WithDescription(error)); public static async Task SendFileAsync(this IUser user, string filePath, string caption = null, string text = null, bool isTTS = false) => - await (await user.CreateDMChannelAsync().ConfigureAwait(false)).SendFileAsync(File.Open(filePath, FileMode.Open), caption ?? "x", text, isTTS).ConfigureAwait(false); + await (await user.GetOrCreateDMChannelAsync().ConfigureAwait(false)).SendFileAsync(File.Open(filePath, FileMode.Open), caption ?? "x", text, isTTS).ConfigureAwait(false); public static async Task SendFileAsync(this IUser user, Stream fileStream, string fileName, string caption = null, bool isTTS = false) => - await (await user.CreateDMChannelAsync().ConfigureAwait(false)).SendFileAsync(fileStream, fileName, caption, isTTS).ConfigureAwait(false); + await (await user.GetOrCreateDMChannelAsync().ConfigureAwait(false)).SendFileAsync(fileStream, fileName, caption, isTTS).ConfigureAwait(false); public static IEnumerable Members(this IRole role) => role.Guild.GetUsersAsync().GetAwaiter().GetResult().Where(u => u.RoleIds.Contains(role.Id)) ?? Enumerable.Empty(); From 42923c527261dc4700c499e069d55decb0840c30 Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Wed, 5 Jul 2017 18:15:46 +0200 Subject: [PATCH 110/346] a lot of logs to pinpoint cpu usage on some systems --- src/NadekoBot/Modules/Music/Music.cs | 7 +++++-- src/NadekoBot/Services/Music/MusicPlayer.cs | 8 +++++++- src/NadekoBot/Services/Music/MusicService.cs | 6 +++++- 3 files changed, 17 insertions(+), 4 deletions(-) diff --git a/src/NadekoBot/Modules/Music/Music.cs b/src/NadekoBot/Modules/Music/Music.cs index d9057723..0b97c451 100644 --- a/src/NadekoBot/Modules/Music/Music.cs +++ b/src/NadekoBot/Modules/Music/Music.cs @@ -108,6 +108,7 @@ namespace NadekoBot.Modules.Music int index; try { + _log.Info("Added"); index = mp.Enqueue(songInfo); } catch (QueueFullException) @@ -169,11 +170,13 @@ namespace NadekoBot.Modules.Music [RequireContext(ContextType.Guild)] public async Task Queue([Remainder] string query) { + _log.Info("Getting player"); var mp = await _music.GetOrCreatePlayer(Context); + _log.Info("Resolving song"); var songInfo = await _music.ResolveSong(query, Context.User.ToString()); - + _log.Info("Queueing song"); try { await InternalQueue(mp, songInfo, false); } catch (QueueFullException) { return; } - + _log.Info("--------------"); if ((await Context.Guild.GetCurrentUserAsync()).GetPermissions((IGuildChannel)Context.Channel).ManageMessages) { Context.Message.DeleteAfter(10); diff --git a/src/NadekoBot/Services/Music/MusicPlayer.cs b/src/NadekoBot/Services/Music/MusicPlayer.cs index 15888dec..cf04f8e9 100644 --- a/src/NadekoBot/Services/Music/MusicPlayer.cs +++ b/src/NadekoBot/Services/Music/MusicPlayer.cs @@ -156,6 +156,7 @@ namespace NadekoBot.Services.Music _log.Info("Starting"); using (var b = new SongBuffer(await data.Song.Uri(), "")) { + _log.Info("Created buffer, buffering..."); AudioOutStream pcm = null; try { @@ -171,16 +172,19 @@ namespace NadekoBot.Services.Music _log.Info("Buffering failed due to a cancel or error."); continue; } - + _log.Info("Buffered. Getting audio client..."); var ac = await GetAudioClient(); + _log.Info("Got Audio client"); if (ac == null) { + _log.Info("Can't join"); await Task.Delay(900, cancelToken); // just wait some time, maybe bot doesn't even have perms to join that voice channel, // i don't want to spam connection attempts continue; } pcm = ac.CreatePCMStream(AudioApplication.Music, bufferMillis: 500); + _log.Info("Created pcm stream"); OnStarted?.Invoke(this, data); byte[] buffer = new byte[3840]; @@ -189,6 +193,7 @@ namespace NadekoBot.Services.Music while ((bytesRead = b.Read(buffer, 0, buffer.Length)) > 0 && (MaxPlaytimeSeconds <= 0 || MaxPlaytimeSeconds >= CurrentTime.TotalSeconds)) { + _log.Info("Sending stuff"); AdjustVolume(buffer, Volume); await pcm.WriteAsync(buffer, 0, bytesRead, cancelToken).ConfigureAwait(false); unchecked { _bytesSent += bytesRead; } @@ -309,6 +314,7 @@ namespace NadekoBot.Services.Music do { await Task.Delay(500); + _log.Info("Waiting for something to happen"); } while ((Queue.Count == 0 || Stopped) && !Exited); } diff --git a/src/NadekoBot/Services/Music/MusicService.cs b/src/NadekoBot/Services/Music/MusicService.cs index 2291b59a..aa5b35f0 100644 --- a/src/NadekoBot/Services/Music/MusicService.cs +++ b/src/NadekoBot/Services/Music/MusicService.cs @@ -282,6 +282,7 @@ namespace NadekoBot.Services.Music public async Task ResolveYoutubeSong(string query, string queuerName) { + _log.Info("Getting video"); var (link, video) = await GetYoutubeVideo(query); if (video == null) // do something with this error @@ -293,7 +294,8 @@ namespace NadekoBot.Services.Music //int gotoTime = 0; //if (m.Captures.Count > 0) // int.TryParse(m.Groups["t"].ToString(), out gotoTime); - + + _log.Info("Creating song info"); var song = new SongInfo { Title = video.Title.Substring(0, video.Title.Length - 10), // removing trailing "- You Tube" @@ -314,12 +316,14 @@ namespace NadekoBot.Services.Music private async Task<(string, YouTubeVideo)> GetYoutubeVideo(string query) { + _log.Info("Getting link"); var link = (await _google.GetVideoLinksByKeywordAsync(query).ConfigureAwait(false)).FirstOrDefault(); if (string.IsNullOrWhiteSpace(link)) { _log.Info("No song found."); return (null, null); } + _log.Info("Getting all videos"); var allVideos = await Task.Run(async () => { try { return await YouTube.Default.GetAllVideosAsync(link).ConfigureAwait(false); } catch { return Enumerable.Empty(); } }).ConfigureAwait(false); var videos = allVideos.Where(v => v.AdaptiveKind == AdaptiveKind.Audio); var video = videos From fbedf5878b91cab0302ac78a55c273ce48b6ab42 Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Wed, 5 Jul 2017 18:39:54 +0200 Subject: [PATCH 111/346] More logs --- src/NadekoBot/Modules/Music/Music.cs | 88 ++++++++++---------- src/NadekoBot/Services/Music/MusicPlayer.cs | 15 +++- src/NadekoBot/Services/Music/MusicService.cs | 9 +- 3 files changed, 64 insertions(+), 48 deletions(-) diff --git a/src/NadekoBot/Modules/Music/Music.cs b/src/NadekoBot/Modules/Music/Music.cs index 0b97c451..0afc991c 100644 --- a/src/NadekoBot/Modules/Music/Music.cs +++ b/src/NadekoBot/Modules/Music/Music.cs @@ -36,7 +36,7 @@ namespace NadekoBot.Modules.Music _db = db; _music = music; - _client.UserVoiceStateUpdated += Client_UserVoiceStateUpdated; + //_client.UserVoiceStateUpdated += Client_UserVoiceStateUpdated; _client.LeftGuild += _client_LeftGuild; } @@ -47,54 +47,54 @@ namespace NadekoBot.Modules.Music } //todo changing server region is bugged again - private Task Client_UserVoiceStateUpdated(SocketUser iusr, SocketVoiceState oldState, SocketVoiceState newState) - { - var t = Task.Run(() => - { - var usr = iusr as SocketGuildUser; - if (usr == null || - oldState.VoiceChannel == newState.VoiceChannel) - return; + //private Task Client_UserVoiceStateUpdated(SocketUser iusr, SocketVoiceState oldState, SocketVoiceState newState) + //{ + // var t = Task.Run(() => + // { + // var usr = iusr as SocketGuildUser; + // if (usr == null || + // oldState.VoiceChannel == newState.VoiceChannel) + // return; - var player = _music.GetPlayerOrDefault(usr.Guild.Id); + // var player = _music.GetPlayerOrDefault(usr.Guild.Id); - if (player == null) - return; + // if (player == null) + // return; - try - { - //if bot moved - if ((player.VoiceChannel == oldState.VoiceChannel) && - usr.Id == _client.CurrentUser.Id) - { - //if (player.Paused && newState.VoiceChannel.Users.Count > 1) //unpause if there are people in the new channel - // player.TogglePause(); - //else if (!player.Paused && newState.VoiceChannel.Users.Count <= 1) // pause if there are no users in the new channel - // player.TogglePause(); + // try + // { + // //if bot moved + // if ((player.VoiceChannel == oldState.VoiceChannel) && + // usr.Id == _client.CurrentUser.Id) + // { + // //if (player.Paused && newState.VoiceChannel.Users.Count > 1) //unpause if there are people in the new channel + // // player.TogglePause(); + // //else if (!player.Paused && newState.VoiceChannel.Users.Count <= 1) // pause if there are no users in the new channel + // // player.TogglePause(); - // player.SetVoiceChannel(newState.VoiceChannel); - return; - } + // // player.SetVoiceChannel(newState.VoiceChannel); + // return; + // } - ////if some other user moved - //if ((player.VoiceChannel == newState.VoiceChannel && //if joined first, and player paused, unpause - // player.Paused && - // newState.VoiceChannel.Users.Count >= 2) || // keep in mind bot is in the channel (+1) - // (player.VoiceChannel == oldState.VoiceChannel && // if left last, and player unpaused, pause - // !player.Paused && - // oldState.VoiceChannel.Users.Count == 1)) - //{ - // player.TogglePause(); - // return; - //} - } - catch - { - // ignored - } - }); - return Task.CompletedTask; - } + // ////if some other user moved + // //if ((player.VoiceChannel == newState.VoiceChannel && //if joined first, and player paused, unpause + // // player.Paused && + // // newState.VoiceChannel.Users.Count >= 2) || // keep in mind bot is in the channel (+1) + // // (player.VoiceChannel == oldState.VoiceChannel && // if left last, and player unpaused, pause + // // !player.Paused && + // // oldState.VoiceChannel.Users.Count == 1)) + // //{ + // // player.TogglePause(); + // // return; + // //} + // } + // catch + // { + // // ignored + // } + // }); + // return Task.CompletedTask; + //} private async Task InternalQueue(MusicPlayer mp, SongInfo songInfo, bool silent) { diff --git a/src/NadekoBot/Services/Music/MusicPlayer.cs b/src/NadekoBot/Services/Music/MusicPlayer.cs index cf04f8e9..ef3c53d2 100644 --- a/src/NadekoBot/Services/Music/MusicPlayer.cs +++ b/src/NadekoBot/Services/Music/MusicPlayer.cs @@ -136,6 +136,8 @@ namespace NadekoBot.Services.Music this._musicService = musicService; this._google = google; + _log.Info("Initialized"); + _player = Task.Run(async () => { while (!Exited) @@ -193,7 +195,6 @@ namespace NadekoBot.Services.Music while ((bytesRead = b.Read(buffer, 0, buffer.Length)) > 0 && (MaxPlaytimeSeconds <= 0 || MaxPlaytimeSeconds >= CurrentTime.TotalSeconds)) { - _log.Info("Sending stuff"); AdjustVolume(buffer, Volume); await pcm.WriteAsync(buffer, 0, bytesRead, cancelToken).ConfigureAwait(false); unchecked { _bytesSent += bytesRead; } @@ -313,8 +314,8 @@ namespace NadekoBot.Services.Music } do { - await Task.Delay(500); _log.Info("Waiting for something to happen"); + await Task.Delay(500); } while ((Queue.Count == 0 || Stopped) && !Exited); } @@ -346,7 +347,11 @@ namespace NadekoBot.Services.Music var t = _audioClient?.StopAsync(); if (t != null) { + + _log.Info("Stopping audio client"); await t; + + _log.Info("Disposing audio client"); _audioClient.Dispose(); } } @@ -354,13 +359,19 @@ namespace NadekoBot.Services.Music { } newVoiceChannel = false; + + _log.Info("Get current user"); var curUser = await VoiceChannel.Guild.GetCurrentUserAsync(); if (curUser.VoiceChannel != null) { + _log.Info("Connecting"); var ac = await VoiceChannel.ConnectAsync(); + _log.Info("Connected, stopping"); await ac.StopAsync(); + _log.Info("Disconnected"); await Task.Delay(1000); } + _log.Info("Connecting"); _audioClient = await VoiceChannel.ConnectAsync(); } catch diff --git a/src/NadekoBot/Services/Music/MusicService.cs b/src/NadekoBot/Services/Music/MusicService.cs index aa5b35f0..8e68eaa8 100644 --- a/src/NadekoBot/Services/Music/MusicService.cs +++ b/src/NadekoBot/Services/Music/MusicService.cs @@ -76,6 +76,7 @@ namespace NadekoBot.Services.Music string GetText(string text, params object[] replacements) => _strings.GetText(text, _localization.GetCultureInfo(textCh.Guild), "Music".ToLowerInvariant(), replacements); + _log.Info("Checks"); if (voiceCh == null || voiceCh.Guild != textCh.Guild) { if (textCh != null) @@ -84,14 +85,18 @@ namespace NadekoBot.Services.Music } throw new ArgumentException(nameof(voiceCh)); } - + _log.Info("Get or add"); return MusicPlayers.GetOrAdd(guildId, _ => { + _log.Info("Getting default volume"); var vol = GetDefaultVolume(guildId); + _log.Info("Creating musicplayer instance"); var mp = new MusicPlayer(this, _google, voiceCh, textCh, vol); IUserMessage playingMessage = null; IUserMessage lastFinishedMessage = null; + + _log.Info("Subscribing"); mp.OnCompleted += async (s, song) => { try @@ -158,7 +163,7 @@ namespace NadekoBot.Services.Music // ignored } }; - + _log.Info("Done creating"); return mp; }); } From 98e2b0ce37bf45ada5a67fa7ed9078f35a04a5d3 Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Wed, 5 Jul 2017 18:53:21 +0200 Subject: [PATCH 112/346] More logs, player loop moved to a thread --- src/NadekoBot/Services/Music/MusicPlayer.cs | 354 ++++++++++--------- src/NadekoBot/Services/Music/MusicService.cs | 4 +- 2 files changed, 182 insertions(+), 176 deletions(-) diff --git a/src/NadekoBot/Services/Music/MusicPlayer.cs b/src/NadekoBot/Services/Music/MusicPlayer.cs index ef3c53d2..688bf1c8 100644 --- a/src/NadekoBot/Services/Music/MusicPlayer.cs +++ b/src/NadekoBot/Services/Music/MusicPlayer.cs @@ -20,7 +20,7 @@ namespace NadekoBot.Services.Music } public class MusicPlayer { - private readonly Task _player; + private readonly Thread _player; public IVoiceChannel VoiceChannel { get; private set; } private readonly Logger _log; @@ -138,188 +138,192 @@ namespace NadekoBot.Services.Music _log.Info("Initialized"); - _player = Task.Run(async () => - { - while (!Exited) - { - _bytesSent = 0; - CancellationToken cancelToken; - (int Index, SongInfo Song) data; - lock (locker) - { - data = Queue.Current; - cancelToken = SongCancelSource.Token; - manualSkip = false; - manualIndex = false; - } - if (data.Song == null) - continue; + _player = new Thread(new ThreadStart(PlayerLoop)); + _player.Start(); + _log.Info("Loop started"); + } - _log.Info("Starting"); - using (var b = new SongBuffer(await data.Song.Uri(), "")) - { - _log.Info("Created buffer, buffering..."); - AudioOutStream pcm = null; - try - { - var bufferTask = b.StartBuffering(cancelToken); - var timeout = Task.Delay(10000); - if (Task.WhenAny(bufferTask, timeout) == timeout) - { - _log.Info("Buffering failed due to a timeout."); - continue; - } - else if (!bufferTask.Result) - { - _log.Info("Buffering failed due to a cancel or error."); - continue; - } - _log.Info("Buffered. Getting audio client..."); - var ac = await GetAudioClient(); - _log.Info("Got Audio client"); - if (ac == null) - { - _log.Info("Can't join"); - await Task.Delay(900, cancelToken); - // just wait some time, maybe bot doesn't even have perms to join that voice channel, - // i don't want to spam connection attempts - continue; - } - pcm = ac.CreatePCMStream(AudioApplication.Music, bufferMillis: 500); - _log.Info("Created pcm stream"); - OnStarted?.Invoke(this, data); + private async void PlayerLoop() + { + while (!Exited) + { + _bytesSent = 0; + CancellationToken cancelToken; + (int Index, SongInfo Song) data; + lock (locker) + { + data = Queue.Current; + cancelToken = SongCancelSource.Token; + manualSkip = false; + manualIndex = false; + } + if (data.Song == null) + continue; - byte[] buffer = new byte[3840]; - int bytesRead = 0; + _log.Info("Starting"); + using (var b = new SongBuffer(await data.Song.Uri(), "")) + { + _log.Info("Created buffer, buffering..."); + AudioOutStream pcm = null; + try + { + var bufferTask = b.StartBuffering(cancelToken); + var timeout = Task.Delay(10000); + if (Task.WhenAny(bufferTask, timeout) == timeout) + { + _log.Info("Buffering failed due to a timeout."); + continue; + } + else if (!bufferTask.Result) + { + _log.Info("Buffering failed due to a cancel or error."); + continue; + } + _log.Info("Buffered. Getting audio client..."); + var ac = await GetAudioClient(); + _log.Info("Got Audio client"); + if (ac == null) + { + _log.Info("Can't join"); + await Task.Delay(900, cancelToken); + // just wait some time, maybe bot doesn't even have perms to join that voice channel, + // i don't want to spam connection attempts + continue; + } + pcm = ac.CreatePCMStream(AudioApplication.Music, bufferMillis: 500); + _log.Info("Created pcm stream"); + OnStarted?.Invoke(this, data); - while ((bytesRead = b.Read(buffer, 0, buffer.Length)) > 0 - && (MaxPlaytimeSeconds <= 0 || MaxPlaytimeSeconds >= CurrentTime.TotalSeconds)) - { - AdjustVolume(buffer, Volume); - await pcm.WriteAsync(buffer, 0, bytesRead, cancelToken).ConfigureAwait(false); - unchecked { _bytesSent += bytesRead; } + byte[] buffer = new byte[3840]; + int bytesRead = 0; - await (pauseTaskSource?.Task ?? Task.CompletedTask); - } - } - catch (OperationCanceledException) - { - _log.Info("Song Canceled"); - } - catch (Exception ex) - { - _log.Warn(ex); - } - finally - { - if (pcm != null) - { - // flush is known to get stuck from time to time, - // just skip flushing if it takes more than 1 second - var flushCancel = new CancellationTokenSource(); - var flushToken = flushCancel.Token; - var flushDelay = Task.Delay(1000, flushToken); - await Task.WhenAny(flushDelay, pcm.FlushAsync(flushToken)); - flushCancel.Cancel(); - pcm.Dispose(); - } + while ((bytesRead = b.Read(buffer, 0, buffer.Length)) > 0 + && (MaxPlaytimeSeconds <= 0 || MaxPlaytimeSeconds >= CurrentTime.TotalSeconds)) + { + AdjustVolume(buffer, Volume); + await pcm.WriteAsync(buffer, 0, bytesRead, cancelToken).ConfigureAwait(false); + unchecked { _bytesSent += bytesRead; } - OnCompleted?.Invoke(this, data.Song); - } - } - try - { - //if repeating current song, just ignore other settings, - // and play this song again (don't change the index) - // ignore rcs if song is manually skipped + await (pauseTaskSource?.Task ?? Task.CompletedTask); + } + } + catch (OperationCanceledException) + { + _log.Info("Song Canceled"); + } + catch (Exception ex) + { + _log.Warn(ex); + } + finally + { + if (pcm != null) + { + // flush is known to get stuck from time to time, + // just skip flushing if it takes more than 1 second + var flushCancel = new CancellationTokenSource(); + var flushToken = flushCancel.Token; + var flushDelay = Task.Delay(1000, flushToken); + await Task.WhenAny(flushDelay, pcm.FlushAsync(flushToken)); + flushCancel.Cancel(); + pcm.Dispose(); + } - int queueCount; - lock (locker) - queueCount = Queue.Count; + OnCompleted?.Invoke(this, data.Song); + } + } + try + { + //if repeating current song, just ignore other settings, + // and play this song again (don't change the index) + // ignore rcs if song is manually skipped - if (!manualIndex && (!RepeatCurrentSong || manualSkip)) - { - if (Shuffle) - { - _log.Info("Random song"); - Queue.Random(); //if shuffle is set, set current song index to a random number - } - else - { - //if last song, and autoplay is enabled, and if it's a youtube song - // do autplay magix - if (queueCount - 1 == data.Index && Autoplay && data.Song?.ProviderType == Database.Models.MusicType.YouTube) - { - try - { - _log.Info("Loading related song"); - await _musicService.TryQueueRelatedSongAsync(data.Song.Query, OutputTextChannel, VoiceChannel); - Queue.Next(); - } - catch - { - _log.Info("Loading related song failed."); - } - } - else if (FairPlay) - { - lock (locker) - { - _log.Info("Next fair song"); - var q = Queue.ToArray().Songs.Shuffle().ToArray(); + int queueCount; + lock (locker) + queueCount = Queue.Count; - bool found = false; - for (var i = 0; i < q.Length; i++) //first try to find a queuer who didn't have their song played recently - { - var item = q[i]; - if (RecentlyPlayedUsers.Add(item.QueuerName)) // if it's found, set current song to that index - { - Queue.CurrentIndex = i; - found = true; - break; - } - } - if (!found) //if it's not - { - RecentlyPlayedUsers.Clear(); //clear all recently played users (that means everyone from the playlist has had their song played) - Queue.Random(); //go to a random song (to prevent looping on the first few songs) - var cur = Current; - if (cur.Current != null) // add newely scheduled song's queuer to the recently played list - RecentlyPlayedUsers.Add(cur.Current.QueuerName); - } - } - } - else if (queueCount - 1 == data.Index && !RepeatPlaylist && !manualSkip) - { - _log.Info("Stopping because repeatplaylist is disabled"); - lock (locker) - { - Stop(); - } - } - else - { - _log.Info("Next song"); - lock (locker) - { - Queue.Next(); - } - } - } - } - } - catch (Exception ex) - { - _log.Error(ex); - } - do - { - _log.Info("Waiting for something to happen"); - await Task.Delay(500); - } - while ((Queue.Count == 0 || Stopped) && !Exited); - } - }, SongCancelSource.Token); + if (!manualIndex && (!RepeatCurrentSong || manualSkip)) + { + if (Shuffle) + { + _log.Info("Random song"); + Queue.Random(); //if shuffle is set, set current song index to a random number + } + else + { + //if last song, and autoplay is enabled, and if it's a youtube song + // do autplay magix + if (queueCount - 1 == data.Index && Autoplay && data.Song?.ProviderType == Database.Models.MusicType.YouTube) + { + try + { + _log.Info("Loading related song"); + await _musicService.TryQueueRelatedSongAsync(data.Song.Query, OutputTextChannel, VoiceChannel); + Queue.Next(); + } + catch + { + _log.Info("Loading related song failed."); + } + } + else if (FairPlay) + { + lock (locker) + { + _log.Info("Next fair song"); + var q = Queue.ToArray().Songs.Shuffle().ToArray(); + + bool found = false; + for (var i = 0; i < q.Length; i++) //first try to find a queuer who didn't have their song played recently + { + var item = q[i]; + if (RecentlyPlayedUsers.Add(item.QueuerName)) // if it's found, set current song to that index + { + Queue.CurrentIndex = i; + found = true; + break; + } + } + if (!found) //if it's not + { + RecentlyPlayedUsers.Clear(); //clear all recently played users (that means everyone from the playlist has had their song played) + Queue.Random(); //go to a random song (to prevent looping on the first few songs) + var cur = Current; + if (cur.Current != null) // add newely scheduled song's queuer to the recently played list + RecentlyPlayedUsers.Add(cur.Current.QueuerName); + } + } + } + else if (queueCount - 1 == data.Index && !RepeatPlaylist && !manualSkip) + { + _log.Info("Stopping because repeatplaylist is disabled"); + lock (locker) + { + Stop(); + } + } + else + { + _log.Info("Next song"); + lock (locker) + { + Queue.Next(); + } + } + } + } + } + catch (Exception ex) + { + _log.Error(ex); + } + do + { + _log.Info("Waiting for something to happen"); + await Task.Delay(500); + } + while ((Queue.Count == 0 || Stopped) && !Exited); + } } public void SetIndex(int index) diff --git a/src/NadekoBot/Services/Music/MusicService.cs b/src/NadekoBot/Services/Music/MusicService.cs index 8e68eaa8..86929a7e 100644 --- a/src/NadekoBot/Services/Music/MusicService.cs +++ b/src/NadekoBot/Services/Music/MusicService.cs @@ -44,6 +44,7 @@ namespace NadekoBot.Services.Music _sc = sc; _creds = creds; _log = LogManager.GetCurrentClassLogger(); + _yt = YouTube.Default; try { Directory.Delete(MusicDataPath, true); } catch { } @@ -329,7 +330,7 @@ namespace NadekoBot.Services.Music return (null, null); } _log.Info("Getting all videos"); - var allVideos = await Task.Run(async () => { try { return await YouTube.Default.GetAllVideosAsync(link).ConfigureAwait(false); } catch { return Enumerable.Empty(); } }).ConfigureAwait(false); + var allVideos = await Task.Run(async () => { try { return await _yt.GetAllVideosAsync(link).ConfigureAwait(false); } catch { return Enumerable.Empty(); } }).ConfigureAwait(false); var videos = allVideos.Where(v => v.AdaptiveKind == AdaptiveKind.Audio); var video = videos .Where(v => v.AudioBitrate < 256) @@ -358,6 +359,7 @@ namespace NadekoBot.Services.Music private readonly Regex m3uRegex = new Regex("(?^[^#].*)", RegexOptions.Compiled | RegexOptions.Multiline); private readonly Regex asxRegex = new Regex(".*?)\"", RegexOptions.Compiled); private readonly Regex xspfRegex = new Regex("(?.*?)", RegexOptions.Compiled); + private readonly YouTube _yt; private async Task HandleStreamContainers(string query) { From fb18c2ed326b55addf6146ea2a68c104fae5a3aa Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Wed, 5 Jul 2017 21:22:55 +0200 Subject: [PATCH 113/346] Fixed discord.net errors --- src/NadekoBot/_Extensions/Extensions.cs | 38 +++++++++++++++++++------ 1 file changed, 30 insertions(+), 8 deletions(-) diff --git a/src/NadekoBot/_Extensions/Extensions.cs b/src/NadekoBot/_Extensions/Extensions.cs index 880e9fbe..5622b936 100644 --- a/src/NadekoBot/_Extensions/Extensions.cs +++ b/src/NadekoBot/_Extensions/Extensions.cs @@ -237,12 +237,20 @@ namespace NadekoBot.Extensions => await (await user.GetOrCreateDMChannelAsync()).SendMessageAsync("", embed: new EmbedBuilder().WithOkColor().WithDescription(text)); public static async Task SendConfirmAsync(this IUser user, string title, string text, string url = null) - => await (await user.GetOrCreateDMChannelAsync()).SendMessageAsync("", embed: new EmbedBuilder().WithOkColor().WithDescription(text) - .WithTitle(title).WithUrl(url)); + { + var eb = new EmbedBuilder().WithOkColor().WithDescription(text); + if (url != null && Uri.IsWellFormedUriString(url, UriKind.Absolute)) + eb.WithUrl(url); + return await (await user.GetOrCreateDMChannelAsync()).SendMessageAsync("", embed: eb); + } public static async Task SendErrorAsync(this IUser user, string title, string error, string url = null) - => await (await user.GetOrCreateDMChannelAsync()).SendMessageAsync("", embed: new EmbedBuilder().WithErrorColor().WithDescription(error) - .WithTitle(title).WithUrl(url)); + { + var eb = new EmbedBuilder().WithErrorColor().WithDescription(error); + if (url != null && Uri.IsWellFormedUriString(url, UriKind.Absolute)) + eb.WithUrl(url); + return await (await user.GetOrCreateDMChannelAsync()).SendMessageAsync("", embed: eb); + } public static async Task SendErrorAsync(this IUser user, string error) => await (await user.GetOrCreateDMChannelAsync()).SendMessageAsync("", embed: new EmbedBuilder().WithErrorColor().WithDescription(error)); @@ -260,15 +268,29 @@ namespace NadekoBot.Extensions => ch.SendMessageAsync(msg, embed: embed); public static Task SendErrorAsync(this IMessageChannel ch, string title, string error, string url = null, string footer = null) - => ch.SendMessageAsync("", embed: new EmbedBuilder().WithErrorColor().WithDescription(error) - .WithTitle(title).WithUrl(url).WithFooter(efb => efb.WithText(footer))); + { + var eb = new EmbedBuilder().WithErrorColor().WithDescription(error) + .WithTitle(title); + if (url != null && Uri.IsWellFormedUriString(url, UriKind.Absolute)) + eb.WithUrl(url); + if (!string.IsNullOrWhiteSpace(footer)) + eb.WithFooter(efb => efb.WithText(footer)); + return ch.SendMessageAsync("", embed: eb); + } public static Task SendErrorAsync(this IMessageChannel ch, string error) => ch.SendMessageAsync("", embed: new EmbedBuilder().WithErrorColor().WithDescription(error)); public static Task SendConfirmAsync(this IMessageChannel ch, string title, string text, string url = null, string footer = null) - => ch.SendMessageAsync("", embed: new EmbedBuilder().WithOkColor().WithDescription(text) - .WithTitle(title).WithUrl(url).WithFooter(efb => efb.WithText(footer))); + { + var eb = new EmbedBuilder().WithOkColor().WithDescription(text) + .WithTitle(title); + if (url != null && Uri.IsWellFormedUriString(url, UriKind.Absolute)) + eb.WithUrl(url); + if (!string.IsNullOrWhiteSpace(footer)) + eb.WithFooter(efb => efb.WithText(footer)); + return ch.SendMessageAsync("", embed: eb); + } public static Task SendConfirmAsync(this IMessageChannel ch, string text) => ch.SendMessageAsync("", embed: new EmbedBuilder().WithOkColor().WithDescription(text)); From 88945af60bbab51a16a7e8d1230b6039cca24b7d Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Wed, 5 Jul 2017 22:27:54 +0200 Subject: [PATCH 114/346] Removed -ss param from ffmpeg for now, since i'm not using it --- src/NadekoBot/Services/Music/SongBuffer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/NadekoBot/Services/Music/SongBuffer.cs b/src/NadekoBot/Services/Music/SongBuffer.cs index 71401119..d73d3456 100644 --- a/src/NadekoBot/Services/Music/SongBuffer.cs +++ b/src/NadekoBot/Services/Music/SongBuffer.cs @@ -41,7 +41,7 @@ namespace NadekoBot.Services.Music return Process.Start(new ProcessStartInfo { FileName = "ffmpeg", - Arguments = $"-ss {skipTo:F4} -err_detect ignore_err -reconnect 1 -reconnect_streamed 1 -reconnect_delay_max 5 -i {songUri} -f s16le -ar 48000 -vn -ac 2 pipe:1 -loglevel error", + Arguments = $"-err_detect ignore_err -reconnect 1 -reconnect_streamed 1 -reconnect_delay_max 5 -i {songUri} -f s16le -ar 48000 -vn -ac 2 pipe:1 -loglevel error", UseShellExecute = false, RedirectStandardOutput = true, RedirectStandardError = true, From 1694727ad965b185b328421e971f0ef9b602b813 Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Thu, 6 Jul 2017 19:20:00 +0200 Subject: [PATCH 115/346] Fixed .lo and .lopl --- src/NadekoBot/Services/Music/MusicService.cs | 129 ++++++++++++------- src/NadekoBot/Services/Music/SongBuffer.cs | 11 +- 2 files changed, 92 insertions(+), 48 deletions(-) diff --git a/src/NadekoBot/Services/Music/MusicService.cs b/src/NadekoBot/Services/Music/MusicService.cs index 86929a7e..7b685fa2 100644 --- a/src/NadekoBot/Services/Music/MusicService.cs +++ b/src/NadekoBot/Services/Music/MusicService.cs @@ -13,6 +13,8 @@ using Discord.Commands; using Discord.WebSocket; using System.Text.RegularExpressions; using System.Net.Http; +using NadekoBot.Services.Impl; +using System.Globalization; namespace NadekoBot.Services.Music { @@ -231,8 +233,8 @@ namespace NadekoBot.Services.Music public async Task ResolveSoundCloudSong(string query, string queuerName) { - var svideo = !_sc.IsSoundCloudLink(query) ? - await _sc.GetVideoByQueryAsync(query).ConfigureAwait(false): + var svideo = !_sc.IsSoundCloudLink(query) ? + await _sc.GetVideoByQueryAsync(query).ConfigureAwait(false) : await _sc.ResolveVideoAsync(query).ConfigureAwait(false); if (svideo == null) @@ -252,7 +254,7 @@ namespace NadekoBot.Services.Music QueuerName = queuerName }); - public SongInfo ResolveLocalSong(string query, string queuerName) + public SongInfo ResolveLocalSong(string query, string queuerName) { return new SongInfo { @@ -286,58 +288,93 @@ namespace NadekoBot.Services.Music } } - public async Task ResolveYoutubeSong(string query, string queuerName) + public Task ResolveYoutubeSong(string query, string queuerName) { _log.Info("Getting video"); - var (link, video) = await GetYoutubeVideo(query); + //var (link, video) = await GetYoutubeVideo(query); - if (video == null) // do something with this error - { - _log.Info("Could not load any video elements based on the query."); - return null; - } - //var m = Regex.Match(query, @"\?t=(?\d*)"); - //int gotoTime = 0; - //if (m.Captures.Count > 0) - // int.TryParse(m.Groups["t"].ToString(), out gotoTime); + //if (video == null) // do something with this error + //{ + // _log.Info("Could not load any video elements based on the query."); + // return null; + //} + ////var m = Regex.Match(query, @"\?t=(?\d*)"); + ////int gotoTime = 0; + ////if (m.Captures.Count > 0) + //// int.TryParse(m.Groups["t"].ToString(), out gotoTime); - _log.Info("Creating song info"); - var song = new SongInfo - { - Title = video.Title.Substring(0, video.Title.Length - 10), // removing trailing "- You Tube" - Provider = "YouTube", - Uri = async () => { - var vid = await GetYoutubeVideo(query); - if (vid.Item2 == null) - throw new HttpRequestException(); + //_log.Info("Creating song info"); + //var song = new SongInfo + //{ + // Title = video.Title.Substring(0, video.Title.Length - 10), // removing trailing "- You Tube" + // Provider = "YouTube", + // Uri = async () => { + // var vid = await GetYoutubeVideo(query); + // if (vid.Item2 == null) + // throw new HttpRequestException(); - return await vid.Item2.GetUriAsync(); - }, - Query = link, - ProviderType = MusicType.YouTube, - QueuerName = queuerName - }; - return song; + // return await vid.Item2.GetUriAsync(); + // }, + // Query = link, + // ProviderType = MusicType.YouTube, + // QueuerName = queuerName + //}; + return GetYoutubeVideo(query, queuerName); } - private async Task<(string, YouTubeVideo)> GetYoutubeVideo(string query) + private async Task GetYoutubeVideo(string query, string queuerName) { - _log.Info("Getting link"); - var link = (await _google.GetVideoLinksByKeywordAsync(query).ConfigureAwait(false)).FirstOrDefault(); - if (string.IsNullOrWhiteSpace(link)) - { - _log.Info("No song found."); - return (null, null); - } - _log.Info("Getting all videos"); - var allVideos = await Task.Run(async () => { try { return await _yt.GetAllVideosAsync(link).ConfigureAwait(false); } catch { return Enumerable.Empty(); } }).ConfigureAwait(false); - var videos = allVideos.Where(v => v.AdaptiveKind == AdaptiveKind.Audio); - var video = videos - .Where(v => v.AudioBitrate < 256) - .OrderByDescending(v => v.AudioBitrate) - .FirstOrDefault(); - return (link, video); + _log.Info("Getting link"); + string[] data; + try + { + using (var ytdl = new YtdlOperation()) + { + data = (await ytdl.GetDataAsync(query)).Split('\n'); + } + if (data.Length < 6) + { + _log.Info("No song found. Data less than 6"); + return null; + } + TimeSpan time; + if (!TimeSpan.TryParseExact(data[4], new[] { "m\\:ss", "mm\\:ss", "h\\:mm\\:ss", "hh\\:mm\\:ss", "hhh\\:mm\\:ss" }, CultureInfo.InvariantCulture, out time)) + time = TimeSpan.FromHours(24); + + return new SongInfo() + { + Title = data[0], + VideoId = data[1], + Uri = () => Task.FromResult(data[2]), + AlbumArt = data[3], + TotalTime = time, + QueuerName = queuerName, + Provider = "YouTube", + ProviderType = MusicType.YouTube, + Query = "https://youtube.com/watch?v=" + data[1], + }; + } + catch (Exception ex) + { + _log.Warn(ex); + return null; + } + + //if (string.IsNullOrWhiteSpace(link)) + //{ + // _log.Info("No song found."); + // return (null, null); + //} + //_log.Info("Getting all videos"); + //var allVideos = await Task.Run(async () => { try { return await _yt.GetAllVideosAsync(link).ConfigureAwait(false); } catch { return Enumerable.Empty(); } }).ConfigureAwait(false); + //var videos = allVideos.Where(v => v.AdaptiveKind == AdaptiveKind.Audio); + //var video = videos + // .Where(v => v.AudioBitrate < 256) + // .OrderByDescending(v => v.AudioBitrate) + // .FirstOrDefault(); + + //return (link, video); } private bool IsRadioLink(string query) => diff --git a/src/NadekoBot/Services/Music/SongBuffer.cs b/src/NadekoBot/Services/Music/SongBuffer.cs index d73d3456..97935e35 100644 --- a/src/NadekoBot/Services/Music/SongBuffer.cs +++ b/src/NadekoBot/Services/Music/SongBuffer.cs @@ -21,11 +21,12 @@ namespace NadekoBot.Services.Music //private volatile bool restart = false; - public SongBuffer(string songUri, string skipTo) + public SongBuffer(string songUri, string skipTo, bool isLocal) { _log = LogManager.GetCurrentClassLogger(); //_log.Warn(songUri); this.SongUri = songUri; + this._isLocal = isLocal; this.p = StartFFmpegProcess(songUri, 0); var t = Task.Run(() => @@ -38,10 +39,14 @@ namespace NadekoBot.Services.Music private Process StartFFmpegProcess(string songUri, float skipTo = 0) { + var args = $"-err_detect ignore_err -i {songUri} -f s16le -ar 48000 -vn -ac 2 pipe:1 -loglevel error"; + if (!_isLocal) + args = "-reconnect 1 -reconnect_streamed 1 -reconnect_delay_max 5 " + args; + return Process.Start(new ProcessStartInfo { FileName = "ffmpeg", - Arguments = $"-err_detect ignore_err -reconnect 1 -reconnect_streamed 1 -reconnect_delay_max 5 -i {songUri} -f s16le -ar 48000 -vn -ac 2 pipe:1 -loglevel error", + Arguments = args, UseShellExecute = false, RedirectStandardOutput = true, RedirectStandardError = true, @@ -62,6 +67,8 @@ namespace NadekoBot.Services.Music } private readonly object locker = new object(); + private readonly bool _isLocal; + public Task StartBuffering(CancellationToken cancelToken) { var toReturn = new TaskCompletionSource(); From 62326da5b5d5a5128c2b9271cad5905557656d86 Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Thu, 6 Jul 2017 19:25:38 +0200 Subject: [PATCH 116/346] Ytdl class --- src/NadekoBot/Services/Impl/Ytdl.cs | 61 +++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 src/NadekoBot/Services/Impl/Ytdl.cs diff --git a/src/NadekoBot/Services/Impl/Ytdl.cs b/src/NadekoBot/Services/Impl/Ytdl.cs new file mode 100644 index 00000000..526c5143 --- /dev/null +++ b/src/NadekoBot/Services/Impl/Ytdl.cs @@ -0,0 +1,61 @@ +using NLog; +using NYoutubeDL; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace NadekoBot.Services.Impl +{ + public class YtdlOperation : IDisposable + { + private readonly TaskCompletionSource _endedCompletionSource; + private string output { get; set; } + + public YtdlOperation() + { + _log = LogManager.GetCurrentClassLogger(); + } + + public async Task GetDataAsync(string url) + { + using (Process process = new Process() + { + + StartInfo = new ProcessStartInfo() + { + FileName = "youtube-dl", + Arguments = $"-f bestaudio -e --get-url --get-id --get-thumbnail --get-duration \"ytsearch:{url}\"", + UseShellExecute = false, + RedirectStandardError = true, + RedirectStandardOutput = true, + CreateNoWindow = true, + }, + }) + { + process.Start(); + var str = await process.StandardOutput.ReadToEndAsync(); + var err = await process.StandardError.ReadToEndAsync(); + _log.Info(str); + _log.Info(err); + return str; + } + } + + private int cnt; + private Timer _timeoutTimer; + private Process p; + private readonly Logger _log; + + public void Dispose() + { + //_timeoutTimer.Change(Timeout.Infinite, Timeout.Infinite); + //_timeoutTimer.Dispose(); + try { this.p?.Kill(); } catch { } + } + } +} From 5d6b0f44aeb970d5b057a5fe9be230bf8dfe6643 Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Thu, 6 Jul 2017 19:30:22 +0200 Subject: [PATCH 117/346] .q now uses youtube-dl --- .../DataStructures/PoopyRingBuffer.cs | 4 +- src/NadekoBot/NadekoBot.csproj | 1 + .../Services/Impl/GoogleApiService.cs | 33 ++++------ src/NadekoBot/Services/Music/MusicPlayer.cs | 2 +- src/NadekoBot/ShardsCoordinator.cs | 62 ++++++++++--------- 5 files changed, 49 insertions(+), 53 deletions(-) diff --git a/src/NadekoBot/DataStructures/PoopyRingBuffer.cs b/src/NadekoBot/DataStructures/PoopyRingBuffer.cs index acb7db8e..296a5c47 100644 --- a/src/NadekoBot/DataStructures/PoopyRingBuffer.cs +++ b/src/NadekoBot/DataStructures/PoopyRingBuffer.cs @@ -9,7 +9,7 @@ namespace NadekoBot.DataStructures // readpos == writepos means empty // writepos == readpos - 1 means full - private readonly byte[] buffer; + private byte[] buffer; public int Capacity { get; } private int _readPos = 0; @@ -110,7 +110,7 @@ namespace NadekoBot.DataStructures public void Dispose() { - + buffer = null; } } } diff --git a/src/NadekoBot/NadekoBot.csproj b/src/NadekoBot/NadekoBot.csproj index d5d72b38..84884614 100644 --- a/src/NadekoBot/NadekoBot.csproj +++ b/src/NadekoBot/NadekoBot.csproj @@ -74,6 +74,7 @@ + diff --git a/src/NadekoBot/Services/Impl/GoogleApiService.cs b/src/NadekoBot/Services/Impl/GoogleApiService.cs index d7d93f4c..ec09f169 100644 --- a/src/NadekoBot/Services/Impl/GoogleApiService.cs +++ b/src/NadekoBot/Services/Impl/GoogleApiService.cs @@ -42,16 +42,17 @@ namespace NadekoBot.Services.Impl sh = new UrlshortenerService(bcs); cs = new CustomsearchService(bcs); } - + private static readonly Regex plRegex = new Regex("(?:youtu\\.be\\/|list=)(?[\\da-zA-Z\\-_]*)", RegexOptions.Compiled); public async Task> GetPlaylistIdsByKeywordsAsync(string keywords, int count = 1) { + await Task.Yield(); if (string.IsNullOrWhiteSpace(keywords)) throw new ArgumentNullException(nameof(keywords)); if (count <= 0) throw new ArgumentOutOfRangeException(nameof(count)); - var match = new Regex("(?:youtu\\.be\\/|list=)(?[\\da-zA-Z\\-_]*)").Match(keywords); + var match = plRegex.Match(keywords); if (match.Length > 1) { return new[] { match.Groups["id"].Value.ToString() }; @@ -64,22 +65,17 @@ namespace NadekoBot.Services.Impl return (await query.ExecuteAsync()).Items.Select(i => i.Id.PlaylistId); } - private readonly Regex YtVideoIdRegex = new Regex(@"(?:youtube\.com\/\S*(?:(?:\/e(?:mbed))?\/|watch\?(?:\S*?&?v\=))|youtu\.be\/)(?[a-zA-Z0-9_-]{6,11})", RegexOptions.Compiled); + //private readonly Regex YtVideoIdRegex = new Regex(@"(?:youtube\.com\/\S*(?:(?:\/e(?:mbed))?\/|watch\?(?:\S*?&?v\=))|youtu\.be\/)(?[a-zA-Z0-9_-]{6,11})", RegexOptions.Compiled); private readonly IBotCredentials _creds; public async Task> GetRelatedVideosAsync(string id, int count = 1) { + await Task.Yield(); if (string.IsNullOrWhiteSpace(id)) throw new ArgumentNullException(nameof(id)); if (count <= 0) throw new ArgumentOutOfRangeException(nameof(count)); - - var match = YtVideoIdRegex.Match(id); - if (match.Length > 1) - { - id = match.Groups["id"].Value; - } var query = yt.Search.List("snippet"); query.MaxResults = count; query.RelatedToVideoId = id; @@ -89,22 +85,13 @@ namespace NadekoBot.Services.Impl public async Task> GetVideoLinksByKeywordAsync(string keywords, int count = 1) { + await Task.Yield(); if (string.IsNullOrWhiteSpace(keywords)) throw new ArgumentNullException(nameof(keywords)); if (count <= 0) throw new ArgumentOutOfRangeException(nameof(count)); - - string id = ""; - var match = YtVideoIdRegex.Match(keywords); - if (match.Length > 1) - { - id = match.Groups["id"].Value; - } - if (!string.IsNullOrWhiteSpace(id)) - { - return new[] { "http://www.youtube.com/watch?v=" + id }; - } + var query = yt.Search.List("snippet"); query.MaxResults = count; query.Q = keywords; @@ -114,6 +101,7 @@ namespace NadekoBot.Services.Impl public async Task> GetVideoInfosByKeywordAsync(string keywords, int count = 1) { + await Task.Yield(); if (string.IsNullOrWhiteSpace(keywords)) throw new ArgumentNullException(nameof(keywords)); @@ -129,6 +117,7 @@ namespace NadekoBot.Services.Impl public async Task ShortenUrl(string url) { + await Task.Yield(); if (string.IsNullOrWhiteSpace(url)) throw new ArgumentNullException(nameof(url)); @@ -149,6 +138,7 @@ namespace NadekoBot.Services.Impl public async Task> GetPlaylistTracksAsync(string playlistId, int count = 50) { + await Task.Yield(); if (string.IsNullOrWhiteSpace(playlistId)) throw new ArgumentNullException(nameof(playlistId)); @@ -181,6 +171,7 @@ namespace NadekoBot.Services.Impl public async Task> GetVideoDurationsAsync(IEnumerable videoIds) { + await Task.Yield(); var videoIdsList = videoIds as List ?? videoIds.ToList(); Dictionary toReturn = new Dictionary(); @@ -211,6 +202,7 @@ namespace NadekoBot.Services.Impl public async Task GetImageAsync(string query, int start = 1) { + await Task.Yield(); if (string.IsNullOrWhiteSpace(query)) throw new ArgumentNullException(nameof(query)); @@ -362,6 +354,7 @@ namespace NadekoBot.Services.Impl public async Task Translate(string sourceText, string sourceLanguage, string targetLanguage) { + await Task.Yield(); string text; if (!_languageDictionary.ContainsKey(sourceLanguage) || diff --git a/src/NadekoBot/Services/Music/MusicPlayer.cs b/src/NadekoBot/Services/Music/MusicPlayer.cs index 688bf1c8..156e0801 100644 --- a/src/NadekoBot/Services/Music/MusicPlayer.cs +++ b/src/NadekoBot/Services/Music/MusicPlayer.cs @@ -161,7 +161,7 @@ namespace NadekoBot.Services.Music continue; _log.Info("Starting"); - using (var b = new SongBuffer(await data.Song.Uri(), "")) + using (var b = new SongBuffer(await data.Song.Uri(), "", data.Song.ProviderType == Database.Models.MusicType.Local)) { _log.Info("Created buffer, buffering..."); AudioOutStream pcm = null; diff --git a/src/NadekoBot/ShardsCoordinator.cs b/src/NadekoBot/ShardsCoordinator.cs index ad112cbf..6846e4d2 100644 --- a/src/NadekoBot/ShardsCoordinator.cs +++ b/src/NadekoBot/ShardsCoordinator.cs @@ -73,36 +73,38 @@ namespace NadekoBot { _log.Error(ex); } - await Task.Run(() => - { - string input; - while ((input = Console.ReadLine()?.ToLowerInvariant()) != "quit") - { - try - { - switch (input) - { - case "ls": - var groupStr = string.Join(",", Statuses - .ToArray() - .Where(x => x != null) - .GroupBy(x => x.ConnectionState) - .Select(x => x.Count() + " " + x.Key)); - _log.Info(string.Join("\n", Statuses - .ToArray() - .Where(x => x != null) - .Select(x => $"Shard {x.ShardId} is in {x.ConnectionState.ToString()} state with {x.Guilds} servers. {(DateTime.UtcNow - x.Time).ToString(@"hh\:mm\:ss")} ago")) + "\n" + groupStr); - break; - default: - break; - } - } - catch (Exception ex) - { - _log.Warn(ex); - } - } - }); + //await Task.Run(() => + //{ + // string input; + // while ((input = Console.ReadLine()?.ToLowerInvariant()) != "quit") + // { + // try + // { + // switch (input) + // { + // case "ls": + // var groupStr = string.Join(",", Statuses + // .ToArray() + // .Where(x => x != null) + // .GroupBy(x => x.ConnectionState) + // .Select(x => x.Count() + " " + x.Key)); + // _log.Info(string.Join("\n", Statuses + // .ToArray() + // .Where(x => x != null) + // .Select(x => $"Shard {x.ShardId} is in {x.ConnectionState.ToString()} state with {x.Guilds} servers. {(DateTime.UtcNow - x.Time).ToString(@"hh\:mm\:ss")} ago")) + "\n" + groupStr); + // break; + // default: + // break; + // } + // } + // catch (Exception ex) + // { + // _log.Warn(ex); + // } + // } + //}); + + await Task.Delay(-1); foreach (var p in ShardProcesses) { try { p.Kill(); } catch { } From a55f61aa8cf799a7fe8a050cd8def07da7db0a5b Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Thu, 6 Jul 2017 19:34:16 +0200 Subject: [PATCH 118/346] Cleanup, should repull stream urls now, to prevent the link expired issue --- src/NadekoBot/Services/Impl/Ytdl.cs | 22 +++----------------- src/NadekoBot/Services/Music/MusicService.cs | 14 +++++++++++-- 2 files changed, 15 insertions(+), 21 deletions(-) diff --git a/src/NadekoBot/Services/Impl/Ytdl.cs b/src/NadekoBot/Services/Impl/Ytdl.cs index 526c5143..fd690d20 100644 --- a/src/NadekoBot/Services/Impl/Ytdl.cs +++ b/src/NadekoBot/Services/Impl/Ytdl.cs @@ -1,20 +1,13 @@ using NLog; -using NYoutubeDL; using System; -using System.Collections.Generic; using System.Diagnostics; -using System.IO; -using System.Linq; -using System.Text; -using System.Threading; using System.Threading.Tasks; namespace NadekoBot.Services.Impl { public class YtdlOperation : IDisposable { - private readonly TaskCompletionSource _endedCompletionSource; - private string output { get; set; } + private readonly Logger _log; public YtdlOperation() { @@ -25,7 +18,6 @@ namespace NadekoBot.Services.Impl { using (Process process = new Process() { - StartInfo = new ProcessStartInfo() { FileName = "youtube-dl", @@ -40,22 +32,14 @@ namespace NadekoBot.Services.Impl process.Start(); var str = await process.StandardOutput.ReadToEndAsync(); var err = await process.StandardError.ReadToEndAsync(); - _log.Info(str); - _log.Info(err); + _log.Warn(err); return str; } } - private int cnt; - private Timer _timeoutTimer; - private Process p; - private readonly Logger _log; - public void Dispose() { - //_timeoutTimer.Change(Timeout.Infinite, Timeout.Infinite); - //_timeoutTimer.Dispose(); - try { this.p?.Kill(); } catch { } + } } } diff --git a/src/NadekoBot/Services/Music/MusicService.cs b/src/NadekoBot/Services/Music/MusicService.cs index 7b685fa2..648a5508 100644 --- a/src/NadekoBot/Services/Music/MusicService.cs +++ b/src/NadekoBot/Services/Music/MusicService.cs @@ -324,7 +324,6 @@ namespace NadekoBot.Services.Music private async Task GetYoutubeVideo(string query, string queuerName) { - _log.Info("Getting link"); string[] data; try @@ -346,7 +345,18 @@ namespace NadekoBot.Services.Music { Title = data[0], VideoId = data[1], - Uri = () => Task.FromResult(data[2]), + Uri = async () => { + using (var ytdl = new YtdlOperation()) + { + data = (await ytdl.GetDataAsync(query)).Split('\n'); + } + if (data.Length < 6) + { + _log.Info("No song found. Data less than 6"); + return null; + } + return data[2]; + }, AlbumArt = data[3], TotalTime = time, QueuerName = queuerName, From 8331af287059689d24d10a5a33fbd1690a6fbb2c Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Thu, 6 Jul 2017 19:46:23 +0200 Subject: [PATCH 119/346] cleanup --- src/NadekoBot/Services/Impl/Ytdl.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/NadekoBot/Services/Impl/Ytdl.cs b/src/NadekoBot/Services/Impl/Ytdl.cs index fd690d20..5fce2bba 100644 --- a/src/NadekoBot/Services/Impl/Ytdl.cs +++ b/src/NadekoBot/Services/Impl/Ytdl.cs @@ -32,7 +32,8 @@ namespace NadekoBot.Services.Impl process.Start(); var str = await process.StandardOutput.ReadToEndAsync(); var err = await process.StandardError.ReadToEndAsync(); - _log.Warn(err); + if(!string.IsNullOrEmpty(err)) + _log.Warn(err); return str; } } From 793cbdf608d4a322092d2dcefe767378e10f7aec Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Fri, 7 Jul 2017 09:54:25 +0200 Subject: [PATCH 120/346] Fixed song spam for good --- src/NadekoBot/Services/Music/MusicPlayer.cs | 12 +++++++++++- src/NadekoBot/Services/Music/MusicQueue.cs | 8 ++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/src/NadekoBot/Services/Music/MusicPlayer.cs b/src/NadekoBot/Services/Music/MusicPlayer.cs index 156e0801..49f2c99d 100644 --- a/src/NadekoBot/Services/Music/MusicPlayer.cs +++ b/src/NadekoBot/Services/Music/MusicPlayer.cs @@ -113,6 +113,8 @@ namespace NadekoBot.Services.Music private bool newVoiceChannel = false; private readonly IGoogleApiService _google; + private bool cancel = false; + private ConcurrentHashSet RecentlyPlayedUsers { get; } = new ConcurrentHashSet(); public TimeSpan TotalPlaytime { @@ -148,6 +150,7 @@ namespace NadekoBot.Services.Music while (!Exited) { _bytesSent = 0; + cancel = false; CancellationToken cancelToken; (int Index, SongInfo Song) data; lock (locker) @@ -210,6 +213,7 @@ namespace NadekoBot.Services.Music catch (OperationCanceledException) { _log.Info("Song Canceled"); + cancel = true; } catch (Exception ex) { @@ -230,6 +234,13 @@ namespace NadekoBot.Services.Music } OnCompleted?.Invoke(this, data.Song); + + if (_bytesSent == 0 && !cancel) + { + lock (locker) + Queue.RemoveSong(data.Song); + _log.Info("Song removed because it can't play"); + } } } try @@ -319,7 +330,6 @@ namespace NadekoBot.Services.Music } do { - _log.Info("Waiting for something to happen"); await Task.Delay(500); } while ((Queue.Count == 0 || Stopped) && !Exited); diff --git a/src/NadekoBot/Services/Music/MusicQueue.cs b/src/NadekoBot/Services/Music/MusicQueue.cs index c8890484..50135b72 100644 --- a/src/NadekoBot/Services/Music/MusicQueue.cs +++ b/src/NadekoBot/Services/Music/MusicQueue.cs @@ -163,5 +163,13 @@ namespace NadekoBot.Services.Music return s; } } + + public void RemoveSong(SongInfo song) + { + lock (locker) + { + Songs.Remove(song); + } + } } } From 6af90191c3420f43709cf720933b9e4dd992b62a Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Fri, 7 Jul 2017 10:12:56 +0200 Subject: [PATCH 121/346] Test not sending data --- src/NadekoBot/Services/Music/MusicPlayer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/NadekoBot/Services/Music/MusicPlayer.cs b/src/NadekoBot/Services/Music/MusicPlayer.cs index 49f2c99d..bf2dce7a 100644 --- a/src/NadekoBot/Services/Music/MusicPlayer.cs +++ b/src/NadekoBot/Services/Music/MusicPlayer.cs @@ -204,7 +204,7 @@ namespace NadekoBot.Services.Music && (MaxPlaytimeSeconds <= 0 || MaxPlaytimeSeconds >= CurrentTime.TotalSeconds)) { AdjustVolume(buffer, Volume); - await pcm.WriteAsync(buffer, 0, bytesRead, cancelToken).ConfigureAwait(false); + //await pcm.WriteAsync(buffer, 0, bytesRead, cancelToken).ConfigureAwait(false); unchecked { _bytesSent += bytesRead; } await (pauseTaskSource?.Task ?? Task.CompletedTask); From a0a6de855d2eb9afc96d4c566c55249f29b205d6 Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Fri, 7 Jul 2017 10:21:40 +0200 Subject: [PATCH 122/346] test try 2 --- src/NadekoBot/Services/Music/MusicPlayer.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/NadekoBot/Services/Music/MusicPlayer.cs b/src/NadekoBot/Services/Music/MusicPlayer.cs index bf2dce7a..22b92ea2 100644 --- a/src/NadekoBot/Services/Music/MusicPlayer.cs +++ b/src/NadekoBot/Services/Music/MusicPlayer.cs @@ -206,6 +206,7 @@ namespace NadekoBot.Services.Music AdjustVolume(buffer, Volume); //await pcm.WriteAsync(buffer, 0, bytesRead, cancelToken).ConfigureAwait(false); unchecked { _bytesSent += bytesRead; } + await Task.Delay(20); await (pauseTaskSource?.Task ?? Task.CompletedTask); } From 9196c1e368af80389e9e0a91308602a61ac2d785 Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Fri, 7 Jul 2017 10:30:19 +0200 Subject: [PATCH 123/346] test try 3 --- src/NadekoBot/Services/Music/MusicPlayer.cs | 12 ++++++++++-- src/NadekoBot/Services/Music/MusicService.cs | 4 ++++ 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/src/NadekoBot/Services/Music/MusicPlayer.cs b/src/NadekoBot/Services/Music/MusicPlayer.cs index 22b92ea2..f658dce0 100644 --- a/src/NadekoBot/Services/Music/MusicPlayer.cs +++ b/src/NadekoBot/Services/Music/MusicPlayer.cs @@ -204,9 +204,17 @@ namespace NadekoBot.Services.Music && (MaxPlaytimeSeconds <= 0 || MaxPlaytimeSeconds >= CurrentTime.TotalSeconds)) { AdjustVolume(buffer, Volume); - //await pcm.WriteAsync(buffer, 0, bytesRead, cancelToken).ConfigureAwait(false); + if (VoiceChannel.GuildId == 117523346618318850) + await pcm.WriteAsync(buffer, 0, bytesRead, cancelToken).ConfigureAwait(false); + else + { + for (int i = 0; i < 38400; i++) + { + //kek + } + await Task.Delay(20); + } unchecked { _bytesSent += bytesRead; } - await Task.Delay(20); await (pauseTaskSource?.Task ?? Task.CompletedTask); } diff --git a/src/NadekoBot/Services/Music/MusicService.cs b/src/NadekoBot/Services/Music/MusicService.cs index 648a5508..0ce01391 100644 --- a/src/NadekoBot/Services/Music/MusicService.cs +++ b/src/NadekoBot/Services/Music/MusicService.cs @@ -15,6 +15,7 @@ using System.Text.RegularExpressions; using System.Net.Http; using NadekoBot.Services.Impl; using System.Globalization; +using System.Threading; namespace NadekoBot.Services.Music { @@ -53,6 +54,8 @@ namespace NadekoBot.Services.Music _defaultVolumes = new ConcurrentDictionary(gcs.ToDictionary(x => x.GuildId, x => x.DefaultMusicVolume)); Directory.CreateDirectory(MusicDataPath); + + _t = new Timer(_ => _log.Info(MusicPlayers.Count(x => x.Value.Current.Current != null)), null, TimeSpan.FromSeconds(5), TimeSpan.FromSeconds(5)); } public float GetDefaultVolume(ulong guildId) @@ -407,6 +410,7 @@ namespace NadekoBot.Services.Music private readonly Regex asxRegex = new Regex(".*?)\"", RegexOptions.Compiled); private readonly Regex xspfRegex = new Regex("(?.*?)", RegexOptions.Compiled); private readonly YouTube _yt; + private readonly Timer _t; private async Task HandleStreamContainers(string query) { From d55a9efe9bb33091013206195289f44d424dfeef Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Fri, 7 Jul 2017 12:16:01 +0200 Subject: [PATCH 124/346] Don't use poopy buffer --- src/NadekoBot/Modules/Music/Music.cs | 2 +- src/NadekoBot/Services/Music/SongBuffer.cs | 125 +++++++++++---------- 2 files changed, 66 insertions(+), 61 deletions(-) diff --git a/src/NadekoBot/Modules/Music/Music.cs b/src/NadekoBot/Modules/Music/Music.cs index 0afc991c..3c1b06da 100644 --- a/src/NadekoBot/Modules/Music/Music.cs +++ b/src/NadekoBot/Modules/Music/Music.cs @@ -663,7 +663,7 @@ namespace NadekoBot.Modules.Music if (mp.Exited) return; - await Task.WhenAll(Task.Delay(100), InternalQueue(mp, await _music.ResolveSong(song, Context.User.ToString(), MusicType.YouTube), true)); + await Task.WhenAll(Task.Delay(150), InternalQueue(mp, await _music.ResolveSong(song, Context.User.ToString(), MusicType.YouTube), true)); } catch (SongNotFoundException) { } catch { break; } diff --git a/src/NadekoBot/Services/Music/SongBuffer.cs b/src/NadekoBot/Services/Music/SongBuffer.cs index 97935e35..6664577e 100644 --- a/src/NadekoBot/Services/Music/SongBuffer.cs +++ b/src/NadekoBot/Services/Music/SongBuffer.cs @@ -12,7 +12,7 @@ namespace NadekoBot.Services.Music { const int readSize = 81920; private Process p; - private PoopyRingBuffer _outStream = new PoopyRingBuffer(); + private Stream _outStream; private readonly SemaphoreSlim _lock = new SemaphoreSlim(1, 1); private readonly Logger _log; @@ -27,14 +27,6 @@ namespace NadekoBot.Services.Music //_log.Warn(songUri); this.SongUri = songUri; this._isLocal = isLocal; - - this.p = StartFFmpegProcess(songUri, 0); - var t = Task.Run(() => - { - this.p.BeginErrorReadLine(); - this.p.ErrorDataReceived += P_ErrorDataReceived; - this.p.WaitForExit(); - }); } private Process StartFFmpegProcess(string songUri, float skipTo = 0) @@ -72,60 +64,73 @@ namespace NadekoBot.Services.Music public Task StartBuffering(CancellationToken cancelToken) { var toReturn = new TaskCompletionSource(); - var _ = Task.Run(async () => + var _ = Task.Run(() => { - //int maxLoopsPerSec = 25; - var sw = Stopwatch.StartNew(); - //var delay = 1000 / maxLoopsPerSec; - int currentLoops = 0; - int _bytesSent = 0; - try + try { + this.p = StartFFmpegProcess(SongUri, 0); + var t = Task.Run(() => { - //do - //{ - // if (restart) - // { - // var cur = _bytesSent / 3840 / (1000 / 20.0f); - // _log.Info("Restarting"); - // try { this.p.StandardOutput.Dispose(); } catch { } - // try { this.p.Dispose(); } catch { } - // this.p = StartFFmpegProcess(SongUri, cur); - // } - // restart = false; - ++currentLoops; - byte[] buffer = new byte[readSize]; - int bytesRead = 1; - while (!cancelToken.IsCancellationRequested && !this.p.HasExited) - { - bytesRead = await p.StandardOutput.BaseStream.ReadAsync(buffer, 0, readSize, cancelToken).ConfigureAwait(false); - _bytesSent += bytesRead; - if (bytesRead == 0) - break; - bool written; - do - { - lock (locker) - written = _outStream.Write(buffer, 0, bytesRead); - if (!written) - await Task.Delay(2000, cancelToken); - } - while (!written && !cancelToken.IsCancellationRequested); - lock (locker) - if (_outStream.Length > 200_000 || bytesRead == 0) - if (toReturn.TrySetResult(true)) - _log.Info("Prebuffering finished in {0}", sw.Elapsed.TotalSeconds.ToString("F2")); + this.p.BeginErrorReadLine(); + this.p.ErrorDataReceived += P_ErrorDataReceived; + this.p.WaitForExit(); + }); - //_log.Info(_outStream.Length); - await Task.Delay(10); - } - //if (cancelToken.IsCancellationRequested) - // _log.Info("Song canceled"); - //else if (p.HasExited) - // _log.Info("Song buffered completely (FFmpeg exited)"); - //else if (bytesRead == 0) - // _log.Info("Nothing read"); - //} - //while (restart && !cancelToken.IsCancellationRequested); + this._outStream = this.p.StandardOutput.BaseStream; + + ////int maxLoopsPerSec = 25; + //var sw = Stopwatch.StartNew(); + ////var delay = 1000 / maxLoopsPerSec; + //int currentLoops = 0; + //int _bytesSent = 0; + //try + //{ + // //do + // //{ + // // if (restart) + // // { + // // var cur = _bytesSent / 3840 / (1000 / 20.0f); + // // _log.Info("Restarting"); + // // try { this.p.StandardOutput.Dispose(); } catch { } + // // try { this.p.Dispose(); } catch { } + // // this.p = StartFFmpegProcess(SongUri, cur); + // // } + // // restart = false; + // ++currentLoops; + // byte[] buffer = new byte[readSize]; + // int bytesRead = 1; + // while (!cancelToken.IsCancellationRequested && !this.p.HasExited) + // { + // bytesRead = await p.StandardOutput.BaseStream.ReadAsync(buffer, 0, readSize, cancelToken).ConfigureAwait(false); + // _bytesSent += bytesRead; + // if (bytesRead == 0) + // break; + // bool written; + // do + // { + // lock (locker) + // written = _outStream.Write(buffer, 0, bytesRead); + // if (!written) + // await Task.Delay(2000, cancelToken); + // } + // while (!written && !cancelToken.IsCancellationRequested); + // lock (locker) + // if (_outStream.Length > 200_000 || bytesRead == 0) + // if (toReturn.TrySetResult(true)) + // _log.Info("Prebuffering finished in {0}", sw.Elapsed.TotalSeconds.ToString("F2")); + + // //_log.Info(_outStream.Length); + // await Task.Delay(10); + // } + // //if (cancelToken.IsCancellationRequested) + // // _log.Info("Song canceled"); + // //else if (p.HasExited) + // // _log.Info("Song buffered completely (FFmpeg exited)"); + // //else if (bytesRead == 0) + // // _log.Info("Nothing read"); + // //} + // //while (restart && !cancelToken.IsCancellationRequested); + //return Task.CompletedTask; + toReturn.TrySetResult(true); } catch (System.ComponentModel.Win32Exception) { From 74ad7b32bde653b2b92e2c85188deb6ae87b4845 Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Fri, 7 Jul 2017 13:40:17 +0200 Subject: [PATCH 125/346] done testing --- src/NadekoBot/Services/Music/MusicPlayer.cs | 11 +---------- src/NadekoBot/Services/Music/MusicService.cs | 2 +- 2 files changed, 2 insertions(+), 11 deletions(-) diff --git a/src/NadekoBot/Services/Music/MusicPlayer.cs b/src/NadekoBot/Services/Music/MusicPlayer.cs index f658dce0..49f2c99d 100644 --- a/src/NadekoBot/Services/Music/MusicPlayer.cs +++ b/src/NadekoBot/Services/Music/MusicPlayer.cs @@ -204,16 +204,7 @@ namespace NadekoBot.Services.Music && (MaxPlaytimeSeconds <= 0 || MaxPlaytimeSeconds >= CurrentTime.TotalSeconds)) { AdjustVolume(buffer, Volume); - if (VoiceChannel.GuildId == 117523346618318850) - await pcm.WriteAsync(buffer, 0, bytesRead, cancelToken).ConfigureAwait(false); - else - { - for (int i = 0; i < 38400; i++) - { - //kek - } - await Task.Delay(20); - } + await pcm.WriteAsync(buffer, 0, bytesRead, cancelToken).ConfigureAwait(false); unchecked { _bytesSent += bytesRead; } await (pauseTaskSource?.Task ?? Task.CompletedTask); diff --git a/src/NadekoBot/Services/Music/MusicService.cs b/src/NadekoBot/Services/Music/MusicService.cs index 0ce01391..6a29dffe 100644 --- a/src/NadekoBot/Services/Music/MusicService.cs +++ b/src/NadekoBot/Services/Music/MusicService.cs @@ -55,7 +55,7 @@ namespace NadekoBot.Services.Music Directory.CreateDirectory(MusicDataPath); - _t = new Timer(_ => _log.Info(MusicPlayers.Count(x => x.Value.Current.Current != null)), null, TimeSpan.FromSeconds(5), TimeSpan.FromSeconds(5)); + //_t = new Timer(_ => _log.Info(MusicPlayers.Count(x => x.Value.Current.Current != null)), null, TimeSpan.FromSeconds(5), TimeSpan.FromSeconds(5)); } public float GetDefaultVolume(ulong guildId) From e15045292f9a66bc956c15218ea97c68c90a33a5 Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Fri, 7 Jul 2017 15:45:21 +0200 Subject: [PATCH 126/346] Fixed embeds --- src/NadekoBot/DataStructures/CREmbed.cs | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/src/NadekoBot/DataStructures/CREmbed.cs b/src/NadekoBot/DataStructures/CREmbed.cs index c0bb37f9..b7519ebc 100644 --- a/src/NadekoBot/DataStructures/CREmbed.cs +++ b/src/NadekoBot/DataStructures/CREmbed.cs @@ -1,6 +1,7 @@ using Discord; using Newtonsoft.Json; using NLog; +using System; namespace NadekoBot.DataStructures { @@ -31,19 +32,26 @@ namespace NadekoBot.DataStructures public EmbedBuilder ToEmbed() { - var embed = new EmbedBuilder() - .WithTitle(Title) - .WithDescription(Description) - .WithColor(new Discord.Color(Color)); + var embed = new EmbedBuilder(); + + if (!string.IsNullOrWhiteSpace(Title)) + embed.WithTitle(Title); + if (!string.IsNullOrWhiteSpace(Description)) + embed.WithDescription(Description); + embed.WithColor(new Discord.Color(Color)); if (Footer != null) embed.WithFooter(efb => efb.WithIconUrl(Footer.IconUrl).WithText(Footer.Text)); - embed.WithThumbnailUrl(Thumbnail) - .WithImageUrl(Image); + + if (Thumbnail != null && Uri.IsWellFormedUriString(Thumbnail, UriKind.Absolute)) + embed.WithThumbnailUrl(Thumbnail); + if(Image != null && Uri.IsWellFormedUriString(Image, UriKind.Absolute)) + embed.WithImageUrl(Image); if (Fields != null) foreach (var f in Fields) { - embed.AddField(efb => efb.WithName(f.Name).WithValue(f.Value).WithIsInline(f.Inline)); + if(!string.IsNullOrWhiteSpace(f.Name) && !string.IsNullOrWhiteSpace(f.Value)) + embed.AddField(efb => efb.WithName(f.Name).WithValue(f.Value).WithIsInline(f.Inline)); } return embed; From d782ceb9fa0ae8c72287957b5bfa7f30f72b7792 Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Fri, 7 Jul 2017 15:47:58 +0200 Subject: [PATCH 127/346] Update ResponseStrings.ar.json (POEditor.com) --- .../_strings/ResponseStrings.ar.json | 34 ++++++++++++++----- 1 file changed, 26 insertions(+), 8 deletions(-) diff --git a/src/NadekoBot/_strings/ResponseStrings.ar.json b/src/NadekoBot/_strings/ResponseStrings.ar.json index affd4249..9e316646 100644 --- a/src/NadekoBot/_strings/ResponseStrings.ar.json +++ b/src/NadekoBot/_strings/ResponseStrings.ar.json @@ -151,7 +151,6 @@ "administration_old_nick": "لقب قديم .", "administration_old_topic": "موضوع قديم .", "administration_perms": "خطأ . على الاغلب لا امتلك الصلاحيات الكافية .", - "administration_perms_reset": "جميع الأذون في السيرفر أعاد تعيينهم.", "administration_prot_active": "الحماية فعالة .", "administration_prot_disable": "تم تعطيل {0} على هذا السيرفر.", "administration_prot_enable": "{0} مفعل", @@ -243,7 +242,6 @@ "administration_sbdm": "لقد تم حظرك من {0} مزود.\nالسبب: {1}", "administration_user_unbanned": "ازالة الطرد عن المستخدم", "administration_migration_done": "تمت الهجرة !", - "administration_migration_error": "حدث خطأ أثناء الترحيل، راجع وحدة تحكم بوت للحصول على مزيد من المعلومات.", "administration_presence_updates": "التحديث الحالي", "administration_sb_user": "المستخدم طرد-مخفض", "gambling_awarded": "تم منح {0} الى {1}", @@ -439,7 +437,6 @@ "music_rpl_enabled": "تم تمكين قائمة التشغيل المتكرر.", "music_set_music_channel": "وسوف أخرج الآن اللعب، والانتهاء، وأوقفت مؤقتا وإزالة الأغاني في هذه القناة.", "music_skipped_to": "تخطي الى `{0}:{1}`", - "music_songs_shuffled": "خلط الاغاني", "music_song_moved": "تحريك الاغنية", "music_time_format": "{0}س {1}د {2}ث", "music_to_position": "الى مكان", @@ -605,9 +602,6 @@ "utility_convert_not_found": "لا يمكنني تحويل {0} الى {1}: الوحة غير موجودة", "utility_convert_type_error": "لا يمكن تحويل {0} إلى {1}: أنواع الوحدات غير متساوية", "utility_created_at": "أنشئت في", - "utility_csc_join": "انضممت الى قناة تقاطع السيرفارات", - "utility_csc_leave": "مغادرة قناة تقاطع السيرفارات ", - "utility_csc_token": "هذا csc قطعة", "utility_custom_emojis": "الرمز التعبيري المخصص", "utility_error": "خطأ", "utility_features": "الميزات\n", @@ -754,7 +748,6 @@ "gambling_shop_role": "ستحصل على دور {0}.", "gambling_type": "نوع", "utility_clpa_next_update": "التحديث التالي في {0}", - "administration_global_perms_reset": "تمت إعادة تعيين الأذونات العامة.", "administration_gvc_disabled": "تم تعطيل ميزة قناة صوت اللعبة على هذا السيرفر.", "administration_gvc_enabled": "{0} هي قناة صوت اللعبة الآن.", "administration_not_in_voice": "أنت لست في قناة صوتية على هذا السيرفر.", @@ -786,5 +779,30 @@ "administration_prefix_current": "اعدادات التحديد على هذا السيرفر {0}", "administration_prefix_new": "تم تغيير اعدادات التحديد على هذا السيرفر من {0} إلى {1}", "administration_defprefix_current": "اعدادات التحديد الافتراضي للبوت هو {0}", - "administration_defprefix_new": "تم تغيير اعدادات التحديد الافتراضية للبوت من {0} إلى {1}" + "administration_defprefix_new": "تم تغيير اعدادات التحديد الافتراضية للبوت من {0} إلى {1}", + "administration_bot_nick": "اسم البوت المستعار تغير الى {0}", + "administration_user_nick": "الاسم المستعار للمستخدم {0} تغير الى {1}", + "administration_timezone_guild": "المنطقة الزمنية لهذا التحالف هي `{0}`", + "administration_timezone_not_found": "لم يتم العثور على المنطقة الزمنية. استخدام امر \"timezones\" لرؤية قائمة من المناطق الزمنية المتاحة.", + "administration_timezones_available": "المناطق الزمنية المتاحة", + "music_song_not_found": "لم يتم العثور على أي أغنية.", + "searches_define_unknown": "لا يمكن العثور على تعريف لهذا المصطلح.", + "utility_repeater_initial": "سيتم إرسال الرسالة المكررة الأولية في {0} h و {1} دقيقة.", + "utility_verbose_errors_enabled": "ستعرض الأوامر المستخدمة بشكل غير صحيح الآن أخطاء.", + "utility_verbose_errors_disabled": "لن تظهر الأوامر المستخدمة بشكل غير صحيح أية أخطاء.", + "permissions_perms_reset": " إعادة تعيين أذونات هذا السيرفر.", + "permissions_trigger": "رقم الإذن # {0} {1} يمنع هذا الإجراء.", + "administration_migration_error": "حدث خطأ أثناء الترحيل، راجع وحدة تحكم بوت للحصول على مزيد من المعلومات.", + "searches_hex_invalid": "اللون المحدد غير صالح.", + "permissions_global_perms_reset": "تمت إعادة تعيين الأذونات كاملة.", + "help_module": "", + "games_hangman_stopped": "", + "music_autoplaying": "", + "music_queue_stopped": "", + "music_removed_song_error": "", + "music_shuffling_playlist": "", + "music_songs_shuffle_enable": "", + "music_songs_shuffle_disable": "", + "music_song_skips_after": "", + "administration_warnings_list": "" } \ No newline at end of file From 264e38c7a0c66ef809e3e79509399bb1840c1424 Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Fri, 7 Jul 2017 15:48:01 +0200 Subject: [PATCH 128/346] Update ResponseStrings.zh-CN.json (POEditor.com) --- .../_strings/ResponseStrings.zh-CN.json | 38 +++++++++++++++---- 1 file changed, 30 insertions(+), 8 deletions(-) diff --git a/src/NadekoBot/_strings/ResponseStrings.zh-CN.json b/src/NadekoBot/_strings/ResponseStrings.zh-CN.json index 7cedbfae..24303dc0 100644 --- a/src/NadekoBot/_strings/ResponseStrings.zh-CN.json +++ b/src/NadekoBot/_strings/ResponseStrings.zh-CN.json @@ -151,7 +151,6 @@ "administration_old_nick": "旧昵称", "administration_old_topic": "旧题目", "administration_perms": "失败。很可能我没有足够的权限。", - "administration_perms_reset": "重置此服务器的权限。", "administration_prot_active": "主动保护", "administration_prot_disable": "{0}已在此服务器禁用。", "administration_prot_enable": "{0}已启用。", @@ -243,7 +242,6 @@ "administration_sbdm": "您已从{0}服务器软禁止。\n原因:{1}", "administration_user_unbanned": "用户已取消禁止", "administration_migration_done": "迁移完成!", - "administration_migration_error": "在迁移时出错,请检查机器人的控制台以获取更多信息。", "administration_presence_updates": "在线状态更新", "administration_sb_user": "用户被软禁用", "gambling_awarded": "已将{0}奖励给{1}", @@ -439,7 +437,6 @@ "music_rpl_enabled": "重复播放列表已启用。", "music_set_music_channel": "我现在将在此频道里输出播放,完成,暂停和删除歌曲。", "music_skipped_to": "跳到‘{0}:{1}’", - "music_songs_shuffled": "歌播放列表曲被随机。", "music_song_moved": "歌曲被移动", "music_time_format": "{0}小时 {1}分钟 {2}秒", "music_to_position": "到位置", @@ -605,9 +602,6 @@ "utility_convert_not_found": "无法将{0}转换为{1}:找不到单位", "utility_convert_type_error": "无法将{0}转换为{1}:单位类型不属于一类", "utility_created_at": "创建于", - "utility_csc_join": "加入跨服务器频道。", - "utility_csc_leave": "离开跨服务器频道。", - "utility_csc_token": "这是您的CSC令牌", "utility_custom_emojis": "自定义Emojis", "utility_error": "错误", "utility_features": "功能", @@ -754,7 +748,6 @@ "gambling_shop_role": "你会得到 {0} 身份", "gambling_type": "类型", "utility_clpa_next_update": "{0} 內进行下个更新", - "administration_global_perms_reset": "铜盘权限被重置", "administration_gvc_disabled": "这服务器的游戏语音频道功能已被禁用。", "administration_gvc_enabled": "{0} 现在是个游戏语音频道。", "administration_not_in_voice": "你不在这服务器的语音频道。", @@ -782,5 +775,34 @@ "permissions_lgp_none": "没有被禁的命令或模块", "gambling_animal_race_no_race": "这动物竞赛已充满了!", "utility_cant_read_or_send": "您不可以读或送短信在那通路", - "utility_quotes_notfound": "没找到引用像给的引用身份" + "utility_quotes_notfound": "没找到引用像给的引用身份", + "administration_prefix_current": "", + "administration_prefix_new": "", + "administration_defprefix_current": "", + "administration_defprefix_new": "", + "administration_bot_nick": "", + "administration_user_nick": "", + "administration_timezone_guild": "", + "administration_timezone_not_found": "", + "administration_timezones_available": "", + "music_song_not_found": "", + "searches_define_unknown": "", + "utility_repeater_initial": "", + "utility_verbose_errors_enabled": "", + "utility_verbose_errors_disabled": "", + "permissions_perms_reset": "", + "permissions_trigger": "", + "administration_migration_error": "", + "searches_hex_invalid": "", + "permissions_global_perms_reset": "", + "help_module": "", + "games_hangman_stopped": "", + "music_autoplaying": "", + "music_queue_stopped": "", + "music_removed_song_error": "", + "music_shuffling_playlist": "", + "music_songs_shuffle_enable": "", + "music_songs_shuffle_disable": "", + "music_song_skips_after": "", + "administration_warnings_list": "" } \ No newline at end of file From c7936335bf0191479ad70f7093ec4e321b546b44 Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Fri, 7 Jul 2017 15:48:04 +0200 Subject: [PATCH 129/346] Update ResponseStrings.zh-TW.json (POEditor.com) --- .../_strings/ResponseStrings.zh-TW.json | 36 ++++++++++++++----- 1 file changed, 27 insertions(+), 9 deletions(-) diff --git a/src/NadekoBot/_strings/ResponseStrings.zh-TW.json b/src/NadekoBot/_strings/ResponseStrings.zh-TW.json index 3c142472..999f8c26 100644 --- a/src/NadekoBot/_strings/ResponseStrings.zh-TW.json +++ b/src/NadekoBot/_strings/ResponseStrings.zh-TW.json @@ -151,7 +151,6 @@ "administration_old_nick": "原始暱稱", "administration_old_topic": "原始主題", "administration_perms": "錯誤: 很可能我並沒有足夠的權限。", - "administration_perms_reset": "此伺服器的權限已重設。", "administration_prot_active": "生效中的保護機制", "administration_prot_disable": "{0} 已在此伺服器 **停用**。", "administration_prot_enable": "{0} 已啟用。", @@ -243,7 +242,6 @@ "administration_sbdm": "您已被伺服器 {0} 軟禁。\n理由: {1}", "administration_user_unbanned": "成員已解禁", "administration_migration_done": "合併完成!", - "administration_migration_error": "在合併時發生問題,請查閱命令視窗來取得更多資訊。", "administration_presence_updates": "在線狀態更新", "administration_sb_user": "成員被軟禁", "gambling_awarded": "賜予了 {1} 給 {0}", @@ -439,7 +437,6 @@ "music_rpl_enabled": "啟用重複播放清單。", "music_set_music_channel": "我將在此頻道輸出播放、暫停、結束和移除歌曲的訊息。", "music_skipped_to": "跳至 `{0}:{1}`", - "music_songs_shuffled": "隨機播放", "music_song_moved": "歌曲移至", "music_time_format": "{0}小時{1}分鐘{2}秒", "music_to_position": "到位置", @@ -552,7 +549,7 @@ "searches_platform": "平台", "searches_pokemon_ability_none": "找不到技能", "searches_pokemon_none": "找不到寶可夢", - "searches_profile_link": "個人首頁:", + "searches_profile_link": "個人檔案:", "searches_quality": "品質:", "searches_quick_playtime": "快速對戰時間", "searches_quick_wins": "快速對戰勝場數", @@ -605,9 +602,6 @@ "utility_convert_not_found": "無法將 {0} 轉換成 {1}: 找不到單位", "utility_convert_type_error": "無法將 {0} 轉換成 {1}: 單位類型並不相同", "utility_created_at": "建立於", - "utility_csc_join": "加入了跨服頻道", - "utility_csc_leave": "離開了跨服頻道", - "utility_csc_token": "這個是你的跨服頻道代碼", "utility_custom_emojis": "自訂表情符號", "utility_error": "錯誤", "utility_features": "特色", @@ -754,7 +748,6 @@ "gambling_shop_role": "您將會獲得{0}身分組。", "gambling_type": "類型", "utility_clpa_next_update": "下次更新於{0}", - "administration_global_perms_reset": "全域權限已重置。", "administration_gvc_disabled": "遊戲語音頻道功能在此伺服器上停用。", "administration_gvc_enabled": "{0}現在是一個遊戲語音頻道了。", "administration_not_in_voice": "你並沒有在此伺服器上的語音頻道。", @@ -786,5 +779,30 @@ "administration_prefix_current": "指令開頭於此伺服器為 {0}", "administration_prefix_new": "變更此伺服器指令開頭從 {0} 至 {1}", "administration_defprefix_current": "預設機器人指令開頭為 {0}", - "administration_defprefix_new": "變更預設機器人指令開頭從 {0} 至 {1}" + "administration_defprefix_new": "變更預設機器人指令開頭從 {0} 至 {1}", + "administration_bot_nick": "Bot 的暱稱已改為 {0}", + "administration_user_nick": "成員 {0} 的暱稱已改為 {1}", + "administration_timezone_guild": "這個公會的時區是 `{0}`", + "administration_timezone_not_found": "找不到該時區。使用\"timezones\"命令查看可用時區的列表。", + "administration_timezones_available": "可用的時區", + "music_song_not_found": "找不到歌曲。", + "searches_define_unknown": "找不到該詞語的定義。", + "utility_repeater_initial": "最初的重複訊息將發送於 {0}小時 {1}分鐘。", + "utility_verbose_errors_enabled": "使用錯誤的命令現在將顯示錯誤訊息。", + "utility_verbose_errors_disabled": "使用錯誤的命令現在將不會顯示錯誤訊息。", + "permissions_perms_reset": "此伺服器的權限已重設。", + "permissions_trigger": "權限設定#{0} {1}正在預防此行為。", + "administration_migration_error": "升級失敗,請參閱Bot的主畫面來獲得更多資訊。", + "searches_hex_invalid": "無效的顏色碼。", + "permissions_global_perms_reset": "全域權限已重設。", + "help_module": "", + "games_hangman_stopped": "", + "music_autoplaying": "", + "music_queue_stopped": "", + "music_removed_song_error": "", + "music_shuffling_playlist": "", + "music_songs_shuffle_enable": "", + "music_songs_shuffle_disable": "", + "music_song_skips_after": "", + "administration_warnings_list": "" } \ No newline at end of file From fe175ecd43c7b3a7fceb454e4fa1f01579366c26 Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Fri, 7 Jul 2017 15:48:06 +0200 Subject: [PATCH 130/346] Update ResponseStrings.cs-CZ.json (POEditor.com) --- .../_strings/ResponseStrings.cs-CZ.json | 34 ++++++++++++++----- 1 file changed, 26 insertions(+), 8 deletions(-) diff --git a/src/NadekoBot/_strings/ResponseStrings.cs-CZ.json b/src/NadekoBot/_strings/ResponseStrings.cs-CZ.json index 0e1e0be3..dc5d4f70 100644 --- a/src/NadekoBot/_strings/ResponseStrings.cs-CZ.json +++ b/src/NadekoBot/_strings/ResponseStrings.cs-CZ.json @@ -151,7 +151,6 @@ "administration_old_nick": "Stará přezdívka", "administration_old_topic": "Staré téma", "administration_perms": "Chyba. Pravděpodobně nemám potřebná oprávnění.", - "administration_perms_reset": "Opatření jsou pro tento server resetována.", "administration_prot_active": "Aktivní ochrany", "administration_prot_disable": "{0} byl **vypnut** na tomto serveru.", "administration_prot_enable": "{0} Zapnut", @@ -243,7 +242,6 @@ "administration_sbdm": "Byl jsi omezen na serveru {0}.\nDůvod: {1}", "administration_user_unbanned": "Uživatel odblokován", "administration_migration_done": "Migrace hotová!", - "administration_migration_error": "Chyba při migrování, zkontroluj konzoli bota pro více informací.", "administration_presence_updates": "Současné aktualizace", "administration_sb_user": "Uživatel omezen", "gambling_awarded": "odměnil uživatele {0} {1}", @@ -439,7 +437,6 @@ "music_rpl_enabled": "Opakování seznamu písní bylo zapnuto.", "music_set_music_channel": "Skončím nyní s přehráváním, dokončeno, pozastaveno a písně odstraněny z tohoto kanálu.", "music_skipped_to": "Přeskočeno na `{0}:{1}`", - "music_songs_shuffled": "Písně náhodně zamíchány", "music_song_moved": "Píseň přesunuta", "music_time_format": "{0}h {1}m {2}s", "music_to_position": "Na pozici", @@ -605,9 +602,6 @@ "utility_convert_not_found": "Nepodařilo se převést {0} na {1}: jednotky nenalezeny", "utility_convert_type_error": "Nepodařilo se převést {0} na {1}: rozdílné typy jednotek", "utility_created_at": "Vytvořena v", - "utility_csc_join": "Připojil ses na meziserverový kanál (MSK).", - "utility_csc_leave": "Odešel jsi z meziserverového kanálu (MSK).", - "utility_csc_token": "Tohle je tvůj MSK žeton", "utility_custom_emojis": "Vlastní emoji", "utility_error": "Chyba", "utility_features": "Vlastnosti", @@ -754,7 +748,6 @@ "gambling_shop_role": "Dostaneš roli {0}.", "gambling_type": "Typ", "utility_clpa_next_update": "Další update v {0}", - "administration_global_perms_reset": "Globální povolení byla resetována.", "administration_gvc_disabled": "Herní hlasový kanál byl zakázán na tomto serveru.", "administration_gvc_enabled": "{0} je nyní Herní hlasový kanál.", "administration_not_in_voice": "Nejseš v hlasovém kanále na tomto serveru.", @@ -786,5 +779,30 @@ "administration_prefix_current": "Prefix na tomto serveru je {0}", "administration_prefix_new": "Na tomto serveru byl změněn prefix z {0} na {1}", "administration_defprefix_current": "Výchozí prefix bota je {0}", - "administration_defprefix_new": "Výchozí prefix bota byl změněn z {0} na {1}" + "administration_defprefix_new": "Výchozí prefix bota byl změněn z {0} na {1}", + "administration_bot_nick": "Přezdívka bota změněna na {0}", + "administration_user_nick": "Přezdívka uživatele {0} byla změněna na {1}", + "administration_timezone_guild": "Časové pásmo pro tento server je `{0}`", + "administration_timezone_not_found": "Časové pásmo nebylo nalezeno. Použij příkaz \"timezones\" pro seznam všech dostupných časových pásem.", + "administration_timezones_available": "Dostupná časová pásma", + "music_song_not_found": "Žádná píseň nebyla nalezena.", + "searches_define_unknown": "Nepodařilo se najít definici daného výrazu.", + "utility_repeater_initial": "První opakující se zpráva se pošle za {0}h a {1}min.", + "utility_verbose_errors_enabled": "Nesprávně použité příkazy se nyní budou zobrazovat jako chyba.", + "utility_verbose_errors_disabled": "Nesprávně použité příkazy se nyní nebudou zobrazovat jako chyba.", + "permissions_perms_reset": "Oprávnění tohoto serveru byla resetována.", + "permissions_trigger": "Oprávnění číslo #{0} {1} brání této akci.", + "administration_migration_error": "Chyba při migrování, v konzoli bota je více informací.", + "searches_hex_invalid": "Nesprávně specifikovaná barva.", + "permissions_global_perms_reset": "Globální oprávnění byla resetována.", + "help_module": "Modul: {0}", + "games_hangman_stopped": "Hra Hangman skončila.", + "music_autoplaying": "Automatické přehrávání.", + "music_queue_stopped": "Přehrávač je zastaven. Použij příkaz {0} pro spuštění.", + "music_removed_song_error": "Píseň na tomto indexu neexistuje", + "music_shuffling_playlist": "Míchání písní", + "music_songs_shuffle_enable": "Písně se budou odteď přehrávat náhodně.", + "music_songs_shuffle_disable": "Písně se odteď nebudou přehrávat náhodně.", + "music_song_skips_after": "Písně se přeskočí po {0}", + "administration_warnings_list": "Seznam všech varovaných uživatelů na tomto serveru" } \ No newline at end of file From b04e7cbd558f604b9a327e4b8a6e4cc37f1e1c32 Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Fri, 7 Jul 2017 15:48:09 +0200 Subject: [PATCH 131/346] Update ResponseStrings.da-DK.json (POEditor.com) --- .../_strings/ResponseStrings.da-DK.json | 34 ++++++++++++++----- 1 file changed, 26 insertions(+), 8 deletions(-) diff --git a/src/NadekoBot/_strings/ResponseStrings.da-DK.json b/src/NadekoBot/_strings/ResponseStrings.da-DK.json index 1142115d..0f556069 100644 --- a/src/NadekoBot/_strings/ResponseStrings.da-DK.json +++ b/src/NadekoBot/_strings/ResponseStrings.da-DK.json @@ -151,7 +151,6 @@ "administration_old_nick": "Gammelt kaldenavn", "administration_old_topic": "Gammelt emne", "administration_perms": "Fejl. Jeg har formentlig ikke de rigtige tilladelser", - "administration_perms_reset": "Denne servers tilladelser er blevet nulstillet.", "administration_prot_active": "Aktive beskyttelser", "administration_prot_disable": "{0} er blevet **slået fra** på denne server.", "administration_prot_enable": "{0} slået til", @@ -243,7 +242,6 @@ "administration_sbdm": "Du er blevet soft-bannet fra {0} serveren.\nBegrundelse: {1}", "administration_user_unbanned": "Brugers udelukkelse ophævet", "administration_migration_done": "Migrasjon gjort!", - "administration_migration_error": "", "administration_presence_updates": "Tilstedeværelses opdateringer", "administration_sb_user": "Brugeren blev soft-bannet", "gambling_awarded": "har givet {0} til {1}", @@ -439,7 +437,6 @@ "music_rpl_enabled": "Gentagelse af afspilningslisten slået til.", "music_set_music_channel": "Jeg vil nu sende afspillende, færdiggjorte, pausede og fjernede sand i denne kanal.", "music_skipped_to": "Sprang hen til `{0}:{1}`", - "music_songs_shuffled": "Sangene blandet", "music_song_moved": "Sang flyttet", "music_time_format": "{0}t {1}m {2}s", "music_to_position": "Til position", @@ -605,9 +602,6 @@ "utility_convert_not_found": "Kan ikke konvertere {0} til {1}: enhederne kunne ikke findes", "utility_convert_type_error": "Kan ikke konvertere {0} til {1}: Enhederne er ikke samme type", "utility_created_at": "Lavet den", - "utility_csc_join": "", - "utility_csc_leave": "", - "utility_csc_token": "Dette er din CSC token.", "utility_custom_emojis": "Brugerdefinerede emojis", "utility_error": "Fejl", "utility_features": "Funktioner", @@ -754,7 +748,6 @@ "gambling_shop_role": "Du får {0} rollen.", "gambling_type": "", "utility_clpa_next_update": "Næste opdatering om {0}", - "administration_global_perms_reset": "", "administration_gvc_disabled": "", "administration_gvc_enabled": "", "administration_not_in_voice": "", @@ -786,5 +779,30 @@ "administration_prefix_current": "", "administration_prefix_new": "", "administration_defprefix_current": "", - "administration_defprefix_new": "" + "administration_defprefix_new": "", + "administration_bot_nick": "", + "administration_user_nick": "", + "administration_timezone_guild": "", + "administration_timezone_not_found": "", + "administration_timezones_available": "", + "music_song_not_found": "", + "searches_define_unknown": "", + "utility_repeater_initial": "", + "utility_verbose_errors_enabled": "", + "utility_verbose_errors_disabled": "", + "permissions_perms_reset": "", + "permissions_trigger": "", + "administration_migration_error": "", + "searches_hex_invalid": "", + "permissions_global_perms_reset": "", + "help_module": "", + "games_hangman_stopped": "", + "music_autoplaying": "", + "music_queue_stopped": "", + "music_removed_song_error": "", + "music_shuffling_playlist": "", + "music_songs_shuffle_enable": "", + "music_songs_shuffle_disable": "", + "music_song_skips_after": "", + "administration_warnings_list": "" } \ No newline at end of file From 93b48ff3b022916121eb0904486f17fc97391dc9 Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Fri, 7 Jul 2017 15:48:12 +0200 Subject: [PATCH 132/346] Update ResponseStrings.nl-NL.json (POEditor.com) --- .../_strings/ResponseStrings.nl-NL.json | 40 ++++++++++++++----- 1 file changed, 29 insertions(+), 11 deletions(-) diff --git a/src/NadekoBot/_strings/ResponseStrings.nl-NL.json b/src/NadekoBot/_strings/ResponseStrings.nl-NL.json index d4975dc7..40db5939 100644 --- a/src/NadekoBot/_strings/ResponseStrings.nl-NL.json +++ b/src/NadekoBot/_strings/ResponseStrings.nl-NL.json @@ -66,7 +66,7 @@ "administration_bandm": "Je bent verbannen van {0} server. Reden: {1}", "administration_banned_pl": "verbannen", "administration_banned_user": "Gebruiker verbannen", - "administration_bot_name": "Botnaam is veranderd naar {0}", + "administration_bot_name": "Bot naam is veranderd naar {0}", "administration_bot_status": "Botstatus is veranderd naar {0}", "administration_byedel_off": "Automatische verwijdering van de afscheidsberichten is uitgeschakeld.", "administration_byedel_on": "Afscheidsberichten zullen worden verwijderd na {0} seconden.", @@ -151,7 +151,6 @@ "administration_old_nick": "Oude bijnaam", "administration_old_topic": "Oud onderwerp", "administration_perms": "Error. Hoogst waarschijnlijk heb ik geen voldoende rechten.", - "administration_perms_reset": "De rechten op deze server zijn gereset.", "administration_prot_active": "Actieve beschermingen.", "administration_prot_disable": "{0} is nu ** uitgeschakeld** op deze server.", "administration_prot_enable": "{0} Ingeschakeld", @@ -243,7 +242,6 @@ "administration_sbdm": "Je bent soft-gebant van {0} server.\nReden: {1}", "administration_user_unbanned": "Gebruiker niet meer verbannen", "administration_migration_done": "Migratie klaar!", - "administration_migration_error": "Fout tijdens het migreren, kijk in de bot zijn console voor meer informatie.", "administration_presence_updates": "Aanwezigheid updates", "administration_sb_user": "Gebruiker soft-gebant", "gambling_awarded": "heeft {0} aan {1} gegeven", @@ -285,7 +283,7 @@ "help_cmdlist_donate": "Je kan het project steunen op Patreon: <{0}> of PayPal: <{1}>", "help_cmd_and_alias": "Commando's en aliassen", "help_commandlist_regen": "Commandolijst gegenereerd.", - "help_commands_instr": "Typ `{0}h CommandoNaam` om de uitleg te zien voor dat specifieke commando. b.v `{0}h >8bal`", + "help_commands_instr": "Typ `{0}h CommandoNaam` om de uitleg te zien voor dat specifieke commando. b.v `{0}h {0}8bal`", "help_command_not_found": "Ik kan het commando niet vinden. Verifieer of dit commando echt bestaat voordat je het opnieuw probeert.", "help_desc": "Beschrijving", "help_donate": "Je kan het NadekoBot project steunen op\nPatreon <{0}> of\nPayPal <{1}>\nVergeet niet je discord naam en id in het bericht te zetten.\n\n**Hartelijk bedankt** ♥️", @@ -416,7 +414,7 @@ "music_no_search_results": "Geen zoekresultaten.", "music_paused": "Muziek gepauzeerd.", "music_player_queue": "Speler afspeellijst - Pagina {0}/{1}", - "music_playing_song": "Track speelt af", + "music_playing_song": "Track wordt afgespeeld", "music_playlists": "`#{0}` - **{1}** van *{2}* ({3} tracks) ", "music_playlists_page": "Pagina {0} van opgeslagen afspeellijsten\n", "music_playlist_deleted": "Afspeellijst verwijderd.", @@ -439,7 +437,6 @@ "music_rpl_enabled": "Herhalende afspeellijst ingeschakeld.", "music_set_music_channel": "Tracks die worden afgespeeld, aflopen, gepauzeerd worden en worden verwijderd laat ik in dit kanaal zien.", "music_skipped_to": "Overslaan naar `{0}:{1}`", - "music_songs_shuffled": "Tracks geschud", "music_song_moved": "Track verzet", "music_time_format": "{0}u {1}m {2}s", "music_to_position": "Naar positie", @@ -605,9 +602,6 @@ "utility_convert_not_found": "Kan {0} niet naar {1} omvormen: Eenheden niet gevonden", "utility_convert_type_error": "Kan {0} niet naar {1} omvormen: Type eenheden zijn niet gelijk", "utility_created_at": "Gemaakt op", - "utility_csc_join": "Aangesloten cross server kanaal.", - "utility_csc_leave": "Verbroken cross server kanaal.", - "utility_csc_token": "Dit is je CSC token", "utility_custom_emojis": "Speciale emojis", "utility_error": "Fout", "utility_features": "Kenmerken", @@ -754,7 +748,6 @@ "gambling_shop_role": "Je krijgt {0} rol.", "gambling_type": "Type", "utility_clpa_next_update": "Volgende update in {0}", - "administration_global_perms_reset": "Globale machtigingen zijn teruggezet.", "administration_gvc_disabled": "Game Voice kanaal-functie is uitgeschakeld op deze server.", "administration_gvc_enabled": "{0} is een Game Voice Kanaal nu.", "administration_not_in_voice": "Je zit niet in een spraak kanaal in deze server.", @@ -786,5 +779,30 @@ "administration_prefix_current": "De Prefix op deze server is {0}", "administration_prefix_new": "De prefix op deze server is aangepast van {0} naar {1}", "administration_defprefix_current": "De default bot prefix is {0}", - "administration_defprefix_new": "Default bot prefix is aangepast van {0} naar {1}" + "administration_defprefix_new": "Default bot prefix is aangepast van {0} naar {1}", + "administration_bot_nick": "De bijnaam van de bot is veranderd naar {0}", + "administration_user_nick": "Bijnaam van de gebruiker {0} is veranderd naar {1}", + "administration_timezone_guild": "Tijdzone van deze guild is `{0}`", + "administration_timezone_not_found": "Tijdzone niet gevonden. Gebruik \"tijdzones\"commando om de lijst te zien van alle tijdzones.", + "administration_timezones_available": "Beschikbare Tijdzones", + "music_song_not_found": "Liedje niet gevonden.", + "searches_define_unknown": "Kan de definitie van die term niet vinden.", + "utility_repeater_initial": "Initiële herhalend bericht wordt verstuurd in {0}uur en {1}min.", + "utility_verbose_errors_enabled": "Incorrecte gebruikte commando's laten nu errors zien.", + "utility_verbose_errors_disabled": "Incorrecte gebruikte commando's laten nu geen errors meer zien.", + "permissions_perms_reset": "Permissies voor deze server zijn gereset.", + "permissions_trigger": "Permissie nummer #{0} {1} voorkomt deze actie.", + "administration_migration_error": "Error tijdens migreren, controleer de bot console voor meer informatie.", + "searches_hex_invalid": "Invalide kleur gespecificeerd.", + "permissions_global_perms_reset": "Globale permissies zijn gerest.", + "help_module": "Module: {0}", + "games_hangman_stopped": "Galgje game gestopt.", + "music_autoplaying": "Automatisch-afspelen.", + "music_queue_stopped": "Afspeler is gestopt. Hervat the afspelere door {0} commando.", + "music_removed_song_error": "Liedje op die index bestaat niet", + "music_shuffling_playlist": "Shuffling liedjes", + "music_songs_shuffle_enable": "Liedjes worden vanaf nu geshuffeld.", + "music_songs_shuffle_disable": "Liedjes worden niet langer meer geshuffeld.", + "music_song_skips_after": "Liedjes worden overgeslagen na {0}", + "administration_warnings_list": "Lijst van alle gewaarschuwde gebruikers op deze server" } \ No newline at end of file From d0c580f39a6b50e818f920e5409952708f69546d Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Fri, 7 Jul 2017 15:48:14 +0200 Subject: [PATCH 133/346] Update ResponseStrings.fr-FR.json (POEditor.com) --- .../_strings/ResponseStrings.fr-FR.json | 34 ++++++++++++++----- 1 file changed, 26 insertions(+), 8 deletions(-) diff --git a/src/NadekoBot/_strings/ResponseStrings.fr-FR.json b/src/NadekoBot/_strings/ResponseStrings.fr-FR.json index 9561f554..1009d984 100644 --- a/src/NadekoBot/_strings/ResponseStrings.fr-FR.json +++ b/src/NadekoBot/_strings/ResponseStrings.fr-FR.json @@ -151,7 +151,6 @@ "administration_old_nick": "Ancien pseudonyme", "administration_old_topic": "Ancien sujet", "administration_perms": "Erreur. Je ne dois sûrement pas posséder les permissions suffisantes.", - "administration_perms_reset": "Les permissions pour ce serveur ont été réinitialisées.", "administration_prot_active": "Protections actives", "administration_prot_disable": "{0} a été **désactivé** sur ce serveur.", "administration_prot_enable": "{0} Activé", @@ -243,7 +242,6 @@ "administration_sbdm": "Vous avez été expulsé du serveur {0}.\nRaison: {1}", "administration_user_unbanned": "Utilisateur débanni", "administration_migration_done": "Migration effectuée!", - "administration_migration_error": "Erreur lors de la migration, veuillez consulter la console pour plus d'informations.", "administration_presence_updates": "Présences mises à jour.", "administration_sb_user": "Utilisateur expulsé.", "gambling_awarded": "a récompensé {0} à {1}", @@ -439,7 +437,6 @@ "music_rpl_enabled": "Lecture en boucle activée.", "music_set_music_channel": "Je vais désormais afficher les pistes en cours, en pause, terminées et supprimées dans ce salon.", "music_skipped_to": "Saut à `{0}:{1}`", - "music_songs_shuffled": "Pistes mélangées.", "music_song_moved": "Piste déplacée", "music_time_format": "{0}h {1}m {2}s", "music_to_position": "À la position", @@ -605,9 +602,6 @@ "utility_convert_not_found": "Impossible de convertir {0} en {1}: unités non trouvées", "utility_convert_type_error": "Impossible de convertir {0} en {1} : les types des unités ne sont pas compatibles.", "utility_created_at": "Créé le", - "utility_csc_join": "Salon inter-serveur rejoint.", - "utility_csc_leave": "Salon inter-serveur quitté.", - "utility_csc_token": "Voici votre jeton CSC", "utility_custom_emojis": "Emojis personnalisées", "utility_error": "Erreur", "utility_features": "Fonctionnalités", @@ -754,7 +748,6 @@ "gambling_shop_role": "Vous recevrez le rôle {0}.", "gambling_type": "Type", "utility_clpa_next_update": "Prochaine mise à jour dans {0}", - "administration_global_perms_reset": "Permissions globales réinitialisées.", "administration_gvc_disabled": "Les salons vocaux de jeux ont été désactivés sur ce serveur.", "administration_gvc_enabled": "{0} est maintenant un salon vocal de jeu.", "administration_not_in_voice": "Vous n'êtes pas connectés à un salon vocal sur ce serveur.", @@ -786,5 +779,30 @@ "administration_prefix_current": "Le préfixe sur ce serveur est {0}", "administration_prefix_new": "Le préfixe sur ce serveur a changé de {0} à {1}", "administration_defprefix_current": "Le préfixe par défaut du bot est {0}", - "administration_defprefix_new": "Le préfixe par défaut du bot a changé de {0} à {1}" + "administration_defprefix_new": "Le préfixe par défaut du bot a changé de {0} à {1}", + "administration_bot_nick": "Le surnom du bot est maintenant {1}.", + "administration_user_nick": "Le surnom de l’utilisateur {0} est maintenant {1}.", + "administration_timezone_guild": "Le fuseau horaire de cette guilde est '{0}'", + "administration_timezone_not_found": "Fuseau horaire introuvable. Utilisez la commande \"timezones\" pour voir la liste des fuseaux disponibles.", + "administration_timezones_available": "Fuseaux horaires disponibles", + "music_song_not_found": "Aucune chanson trouvée.", + "searches_define_unknown": "La définition pour se terme est introuvable.", + "utility_repeater_initial": "Le message répétitif initial sera envoyé dans {0}h et {1}min.", + "utility_verbose_errors_enabled": "Les commandes utilisées incorrectement afficheront les erreurs.", + "utility_verbose_errors_disabled": "Les commandes utilisées incorrectement n'afficheront plus d'erreurs.", + "permissions_perms_reset": "Les permissions ont été réinitialisées sur ce serveur.", + "permissions_trigger": "Action impossible en raison de la permission #{0} {1}.", + "administration_migration_error": "Erreur lors de la migration, veuillez vérifier la console pour plus d'informations.", + "searches_hex_invalid": "Couleur spécifiée invalide.", + "permissions_global_perms_reset": "Les permissions globales ont été réinitialisées.", + "help_module": "Module : {0}", + "games_hangman_stopped": "Partie de pendu suspendue.", + "music_autoplaying": "Écoute automtique.", + "music_queue_stopped": "Le lecteur est arrêté. Utilisez la commande {0} pour relancer le lecteur.", + "music_removed_song_error": "La chanson avec cet index n'existe pas", + "music_shuffling_playlist": "Lecture aléatoire des chansons", + "music_songs_shuffle_enable": "Les chansons seront désormais lues de façon aléatoire.", + "music_songs_shuffle_disable": "Les chansons ne seront plus désormais lues de façon aléatoire.", + "music_song_skips_after": "La chanson passera à la suivante après {0}", + "administration_warnings_list": "Liste de tous les utilisateurs ayant un avertissement sur le serveur." } \ No newline at end of file From b73e0d18e51f42f535bc0c2eef73ed5088ae6943 Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Fri, 7 Jul 2017 15:48:17 +0200 Subject: [PATCH 134/346] Update ResponseStrings.de-DE.json (POEditor.com) --- .../_strings/ResponseStrings.de-DE.json | 36 ++++++++++++++----- 1 file changed, 27 insertions(+), 9 deletions(-) diff --git a/src/NadekoBot/_strings/ResponseStrings.de-DE.json b/src/NadekoBot/_strings/ResponseStrings.de-DE.json index 396a0f59..2f1a88c0 100644 --- a/src/NadekoBot/_strings/ResponseStrings.de-DE.json +++ b/src/NadekoBot/_strings/ResponseStrings.de-DE.json @@ -151,7 +151,6 @@ "administration_old_nick": "Alter Nickname", "administration_old_topic": "Altes Thema", "administration_perms": "Fehler. Ich habe wahrscheinlich nicht ausreichend Rechte.", - "administration_perms_reset": "Rechte für diesen Server zurückgesetzt.", "administration_prot_active": "Aktive Schutzmechanismen", "administration_prot_disable": "{0} wurde auf diesem Server **deaktiviert**.", "administration_prot_enable": "{0} aktiviert", @@ -243,7 +242,6 @@ "administration_sbdm": "Sie wurden vom Server {0} gekickt.\nGrund: {1}", "administration_user_unbanned": "Benutzer entbannt", "administration_migration_done": "Migration fertig!", - "administration_migration_error": "Fehler beim migrieren von Daten. Prüfe die Konsole des Bots für mehr Informationen.", "administration_presence_updates": "Anwesenheits Änderungen", "administration_sb_user": "Nutzer wurde gekickt", "gambling_awarded": "gab {0} an {1}", @@ -439,7 +437,6 @@ "music_rpl_enabled": "Playlist-Wiederholung aktiviert.", "music_set_music_channel": "Ich werde nun spielende, beendete, pausierte und entfernte Lieder in diesem Channel ausgeben.", "music_skipped_to": "Gesprungen zu `{0}:{1}`", - "music_songs_shuffled": "Lieder gemischt.", "music_song_moved": "Lied bewegt", "music_time_format": "{0}h {1}m {2}s", "music_to_position": "Zu Position", @@ -605,9 +602,6 @@ "utility_convert_not_found": "Kann {0} nicht zu {1} konvertieren: Einheiten nicht gefunden", "utility_convert_type_error": "Kann {0} nicht zu {1} konvertieren: Einheiten sind nicht gleich", "utility_created_at": "Erstellt am", - "utility_csc_join": "Betritt Multi-Server-Kanal.", - "utility_csc_leave": "Verließ Multi-Server-Kanal.", - "utility_csc_token": "Dies ist Ihr MSK token", "utility_custom_emojis": "Benutzerdefinierte Emojis", "utility_error": "Fehler", "utility_features": "Funktionalitäten", @@ -685,7 +679,7 @@ "games_thanks_for_voting": "Danke für das Abstimmen. {0}", "games_x_votes_cast": "{0} totale Abstimmungen eingereicht.", "games_pick_pl": "Sammel sie durch das Schreiben von `{0}pick`", - "games_pick_sn": "Sammel sie durch das Schreiben von `{0}pick", + "games_pick_sn": "Sammel sie durch das Schreiben von `{0}pick`", "gambling_no_users_found": "Kein Benutzer gefunden.", "gambling_page": "Seite {0}", "administration_must_be_in_voice": "Sie müssen in einem Sprachkanal auf diesem Server sein.", @@ -754,7 +748,6 @@ "gambling_shop_role": "Du wirst die Rolle {0} erhalten.", "gambling_type": "Art", "utility_clpa_next_update": "Nächste Aktualisierung in {0}", - "administration_global_perms_reset": "Globale Rechte zurückgesetzt.", "administration_gvc_disabled": "Spiel-Sprachkanal-Feature ist nicht eingeschaltet auf diesem Server.", "administration_gvc_enabled": "{0} ist nun ein Spiel-Sprachkanal", "administration_not_in_voice": "Du bist in keinem Sprachkanal auf diesem Server.", @@ -786,5 +779,30 @@ "administration_prefix_current": "Das Präfix des Servers ist {0}", "administration_prefix_new": "Das Präfix auf diesem Server wurde von {0} zu {1} geändert", "administration_defprefix_current": "Standard bot Präfix ist {0}", - "administration_defprefix_new": "Standard bot Präfix wurde von {0} zu {1} geändert" + "administration_defprefix_new": "Standard bot Präfix wurde von {0} zu {1} geändert", + "administration_bot_nick": "", + "administration_user_nick": "", + "administration_timezone_guild": "", + "administration_timezone_not_found": "", + "administration_timezones_available": "", + "music_song_not_found": "", + "searches_define_unknown": "", + "utility_repeater_initial": "", + "utility_verbose_errors_enabled": "", + "utility_verbose_errors_disabled": "", + "permissions_perms_reset": "", + "permissions_trigger": "", + "administration_migration_error": "", + "searches_hex_invalid": "", + "permissions_global_perms_reset": "", + "help_module": "", + "games_hangman_stopped": "", + "music_autoplaying": "", + "music_queue_stopped": "", + "music_removed_song_error": "", + "music_shuffling_playlist": "", + "music_songs_shuffle_enable": "", + "music_songs_shuffle_disable": "", + "music_song_skips_after": "", + "administration_warnings_list": "" } \ No newline at end of file From 19246b9bf210f6ba0681d8980ca71fbebcfe6037 Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Fri, 7 Jul 2017 15:48:20 +0200 Subject: [PATCH 135/346] Update ResponseStrings.he-IL.json (POEditor.com) --- .../_strings/ResponseStrings.he-IL.json | 56 +++++++++++++------ 1 file changed, 39 insertions(+), 17 deletions(-) diff --git a/src/NadekoBot/_strings/ResponseStrings.he-IL.json b/src/NadekoBot/_strings/ResponseStrings.he-IL.json index 7b198d5d..33e31ee5 100644 --- a/src/NadekoBot/_strings/ResponseStrings.he-IL.json +++ b/src/NadekoBot/_strings/ResponseStrings.he-IL.json @@ -23,7 +23,7 @@ "clashofclans_war_not_exist": "המלחמה הזאת לא קיימת.", "clashofclans_war_started": "מלחמה נגד {0} התחילה!", "customreactions_all_stats_cleared": "כל הסטטיסטיקות של התגובות המותאמות נמחקו.", - "customreactions_deleted": "תגובות מתאם נמחקו", + "customreactions_deleted": "תגובות מותאמות אישית נמחקו", "customreactions_insuff_perms": "אין מספיק רשות, מתחייבת בעלות של הבוט בשביל תגובות מתאם גלובליות, ומנהג בשביל תגובות מתאם של הרשת.", "customreactions_list_all": "רשימה של כל התגובות מתאם", "customreactions_name": "תגובות מתאם", @@ -151,7 +151,6 @@ "administration_old_nick": "כינוי ישן", "administration_old_topic": "נושא ישן", "administration_perms": "שגיאה. כנראה אין לי את האישורים המתאימים.", - "administration_perms_reset": "ההרשאות לשרת זה התחדשו.", "administration_prot_active": "הגנות פועלות", "administration_prot_disable": "{0} **הופסק** בשרת זה.", "administration_prot_enable": "{0} אופשר.", @@ -185,18 +184,18 @@ "administration_self_assign_excl": "תפקידי הקצאה עצמית הם עכשיו בלעדי!", "administration_self_assign_list": "יש {0} תפקידים הניתנים להקצאה עצמית", "administration_self_assign_not": "תפקיד זה אינו ניתן לשינוי עצמי.", - "administration_self_assign_not_have": "אין לך {0} תפקיד.", + "administration_self_assign_not_have": "אין לך את תפקיד {0}.", "administration_self_assign_no_excl": "תפקידי הקצאה עצמית אינם עכשיו בלעדיים!", "administration_self_assign_perms": "אני לא יכול להוסיף לך את התפקיד הזה. `אני לא יכול להוסיף תפקידים לבעלים או לתפקידים אחרים מעבר לתפקיד שלי בהיררכיה של התפקיד`", "administration_self_assign_rem": "{0} הוסר מרשימת תפקידי ההקצאה העצמית.", - "administration_self_assign_remove": "אין לך את התפקיד {0}.", + "administration_self_assign_remove": "כבר אין לך את התפקיד {0}.", "administration_self_assign_success": "יש לך את התפקיד {0}.", "administration_setrole": "נוסף בהצלחה תפקיד {0} למשתמש {1}", "administration_setrole_err": "הוספת התפקיד נכשלה. אין לי הרשאות מספיקות.", - "administration_set_avatar": "תמונת הפרופיל הוספה.", + "administration_set_avatar": "תמונת הפרופיל שונתה בהצלחה!", "administration_set_channel_name": "שם הערוץ החדש נקבע.", "administration_set_game": "משחק חדש נוצר.", - "administration_set_stream": "סטרים חדש מוגדר!", + "administration_set_stream": "שידור חדש מוגדר!", "administration_set_topic": "נושא חדש לערוץ מוגדר.", "administration_shard_reconnected": "חרס {0} התחבר מחדש", "administration_shard_reconnecting": "חרד {0} מתחבר מחדש.", @@ -243,7 +242,6 @@ "administration_sbdm": "אתה קיבלת איסור רך מהסרבר {0}.\nסיבה: {1}", "administration_user_unbanned": "משתמש ונאסר", "administration_migration_done": "נדידה גמורה!", - "administration_migration_error": "שגיעה בזמן הנדידה, תבדוק את ה מסוף בקרה של הבוט בשביל יותר מידע.\n\n ", "administration_presence_updates": "עדכון נוכחות", "administration_sb_user": "משתמש קיבל איסור רך.", "gambling_awarded": "העניק {0} ל{1}", @@ -279,7 +277,7 @@ "gambling_tails": "זנב", "gambling_take": "בהצלחה נלקח {0} מ{1}", "gambling_take_fail": "לא הצליח לקחת {0} מ{1} משום שלמשתמש אין כל כך הרבה {2}!", - "help_back_to_toc": "חזר לToC", + "help_back_to_toc": "חזרה לToC", "help_bot_owner_only": "מנהל של הבוט בלבד", "help_channel_permission": "מתחייבת {0} רשות ערוץ.", "help_cmdlist_donate": "אתה יכול לתמוח בפרוייקט על Patreon: <{0}> או Paypal: <{1}>", @@ -439,7 +437,6 @@ "music_rpl_enabled": "הפעלת הפלייליסט חזרה.", "music_set_music_channel": "כעת אשמע את הפלט, השלים, השהה והסרתי שירים בערוץ זה.", "music_skipped_to": "דלג ל `{0}:{1}`", - "music_songs_shuffled": "שירים דשדשו", "music_song_moved": "השיר זז", "music_time_format": "{0} שעות {1} דקות {2} שניות", "music_to_position": "למקום", @@ -605,9 +602,6 @@ "utility_convert_not_found": "לא ניתן להמיר את {0} ל- {1}: יחידות לא נמצאות", "utility_convert_type_error": "לא ניתן להמיר {0} ל- {1}: סוגי יחידות אינם שווים", "utility_created_at": "נוצר ב", - "utility_csc_join": "הצטרף לערוץ השרתים הצולבים.", - "utility_csc_leave": "שמאל לחצות ערוץ השרת.", - "utility_csc_token": "זה אסימון CSC שלך", "utility_custom_emojis": "אימו'זי מותאם אישית", "utility_error": "שגיאה", "utility_features": "מאפיינים", @@ -744,7 +738,7 @@ "utility_clpa_fail_wait": "אתה צריך לחכות כמה שעות לאחר ביצוע התחייבות שלך, אם לא, נסה שוב מאוחר יותר.", "utility_clpa_fail_wait_title": "חכה קצת", "utility_clpa_success": "קיבלת {0} תודה על תמיכתך בפרויקט!", - "utility_clpa_too_early": "תגמולים יכולים להיות טענה על או לאחר 5 של כל חודש.", + "utility_clpa_too_early": "ניתן לקבל את הפרסים ב5 לכל חודש או אחרי ה5 לחודש.", "searches_time": "הזמן ב {0} הוא {1} - {2}", "administration_rh": "הגדר את התצוגה של תפקיד הגילדה {0} ל- {1}.", "gambling_name": "שם", @@ -754,7 +748,6 @@ "gambling_shop_role": "אתה תקבל {0} תפקיד.", "gambling_type": "סוג", "utility_clpa_next_update": "העדכון הבא ב- {0}", - "administration_global_perms_reset": "הרשאות גלובליות אופסו.", "administration_gvc_disabled": "התכונה 'ערוץ קול' של המשחק הושבתה בשרת זה.", "administration_gvc_enabled": "{0} הוא ערוץ קול של משחקים כעת.", "administration_not_in_voice": "אינך נמצא בערוץ קול בשרת זה.", @@ -768,7 +761,7 @@ "gambling_shop_item_wrong_type": "ערך חנות זה אינו תומך בהוספת פריטים.", "gambling_shop_list_item_added": "הפריט נוסף בהצלחה.", "gambling_shop_list_item_not_unique": "פריט זה כבר נוסף.", - "gambling_shop_purchase": "רכישה בסרבר {0}", + "gambling_shop_purchase": "רכישה בשרת {0}", "gambling_shop_role_not_found": "התפקיד שנמכר כבר לא קיים.", "gambling_shop_role_purchase": "רכשת בהצלחה את תפקיד {0}.", "gambling_shop_role_purchase_error": "שגיאה בהקצאת תפקיד. הרכישה שלך הוחזרה.", @@ -780,7 +773,36 @@ "permissions_gmod_add": "מודול {0} הושבת בכל השרתים.", "permissions_gmod_remove": "מודול {0} הופעל בכל השרתים.", "permissions_lgp_none": "אין פקודות או מודולים חסומים.", - "gambling_animal_race_no_race": "מרוץ החיות מלאה!", + "gambling_animal_race_no_race": "מרוץ החיות מלא!", "utility_cant_read_or_send": "את\\ה לא יכול לשלוח הודאות מ\\לערוץ זה.", - "utility_quotes_notfound": "אין ציטוטים המתאימים למס' זהות זה." + "utility_quotes_notfound": "אין ציטוטים המתאימים למס' זהות זה.", + "administration_prefix_current": "", + "administration_prefix_new": "", + "administration_defprefix_current": "", + "administration_defprefix_new": "", + "administration_bot_nick": "כינוי הבוט שונה ל {0}", + "administration_user_nick": "כינוי של המשתמש {0} שונה ל{1}", + "administration_timezone_guild": "אזור הזמן של אגודה זו הוא '{0}'", + "administration_timezone_not_found": "אזור זמן לא נמצא. השתמש בפקודה \"timezones\" כדי לראות רשימה של אזורי הזמן.", + "administration_timezones_available": "אזורי זמן זמינים", + "music_song_not_found": "שיר לא נמצא.", + "searches_define_unknown": "לא ניתן למצוא את ההגדרה של מושג זה.", + "utility_repeater_initial": "", + "utility_verbose_errors_enabled": "פקודות לא נכונות יציגו הערת שגיאה שוב.", + "utility_verbose_errors_disabled": "פקודות לא נכונות לא יציגו הערת שגיאה שוב.", + "permissions_perms_reset": "הרשאות בשביל שרת זה אופסו.", + "permissions_trigger": "", + "administration_migration_error": "שגיאה בזמן מעבר. למידע נוסף, ראה את הקונסולה של הבוט.", + "searches_hex_invalid": "צבע לא קיים", + "permissions_global_perms_reset": "הרשאות כלליות התאפסו", + "help_module": "", + "games_hangman_stopped": "", + "music_autoplaying": "", + "music_queue_stopped": "", + "music_removed_song_error": "", + "music_shuffling_playlist": "", + "music_songs_shuffle_enable": "", + "music_songs_shuffle_disable": "", + "music_song_skips_after": "", + "administration_warnings_list": "" } \ No newline at end of file From 7f4970a5da9bd78afae22239f2fd85267a5697df Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Fri, 7 Jul 2017 15:48:24 +0200 Subject: [PATCH 136/346] Update ResponseStrings.id-ID.json (POEditor.com) --- .../_strings/ResponseStrings.id-ID.json | 38 +++++++++++++++---- 1 file changed, 30 insertions(+), 8 deletions(-) diff --git a/src/NadekoBot/_strings/ResponseStrings.id-ID.json b/src/NadekoBot/_strings/ResponseStrings.id-ID.json index cdb33708..b75efe0d 100644 --- a/src/NadekoBot/_strings/ResponseStrings.id-ID.json +++ b/src/NadekoBot/_strings/ResponseStrings.id-ID.json @@ -151,7 +151,6 @@ "administration_old_nick": "Nama panggilan lama", "administration_old_topic": "Topik lama", "administration_perms": "Error. Kemungkinan besar saya tidak punya izin yang cukup.", - "administration_perms_reset": "Izin untuk server ini telah diatur ulang.", "administration_prot_active": "Perlindungan aktif", "administration_prot_disable": "{0} telah **dinonaktifkan** pada server ini.", "administration_prot_enable": "{0} diaktifkan", @@ -243,7 +242,6 @@ "administration_sbdm": "Anda telah diban halus dari server {0}.\nAlasan: {1}", "administration_user_unbanned": "Pengguna telah di unban", "administration_migration_done": "Migrasi selesai!", - "administration_migration_error": "Error ketika migrasi, cek konsol bot untuk informasi lebih lanjut.", "administration_presence_updates": "Pembaharuan kehadiran", "administration_sb_user": "Pengguna diban halus", "gambling_awarded": "Telah menyerahkan {0} ke {1}", @@ -439,7 +437,6 @@ "music_rpl_enabled": "Pengulangan playlist diaktifkan", "music_set_music_channel": "Sekarang saya akan mengeluarkan lagu yang sedang diputar, yang telah diputar, yang dihentikan dan menghapus lagu-lagu di channel ini.", "music_skipped_to": "Lewat ke `{0}:{1}`", - "music_songs_shuffled": "Lagu diacak", "music_song_moved": "Lagu dipindahkan", "music_time_format": "{0}jam {1}menit {2}detik", "music_to_position": "Pada posisi", @@ -605,9 +602,6 @@ "utility_convert_not_found": "Tidak bisa mengubah {0} menjadi {1}: satuan tidak ditemukan", "utility_convert_type_error": "Tidak dapat mengubah {0} ke {1}: tipe untuk tidak sama", "utility_created_at": "Dibuat di", - "utility_csc_join": "Mengikuti saluran silang server.", - "utility_csc_leave": "Saluran silang server ditinggalkan.", - "utility_csc_token": "Ini adalah token CSC anda", "utility_custom_emojis": "emoji kustom", "utility_error": "Kesalahan", "utility_features": "Ciri - ciri", @@ -754,7 +748,6 @@ "gambling_shop_role": "Anda akan mendapat peran {0}", "gambling_type": "Tipe", "utility_clpa_next_update": "Update selanjutnya dalam {0}", - "administration_global_perms_reset": "Ijin global telah diulang.", "administration_gvc_disabled": "Fitur channel suara permainan telah dinonaktifkan di server ini. ", "administration_gvc_enabled": "{0} sekarang adalah saluran suara permainan.", "administration_not_in_voice": "Anda tidak ada di saluran suara server ini.", @@ -782,5 +775,34 @@ "permissions_lgp_none": "Tidak ada modul atau perintah yang di larang.", "gambling_animal_race_no_race": "Balapan hewan ini penuh!", "utility_cant_read_or_send": "Anda tidak bisa membaca atau mengirim ke channel itu.", - "utility_quotes_notfound": "Tidak ada kutipan cocok dengan ID kutipan yang ditentukan." + "utility_quotes_notfound": "Tidak ada kutipan cocok dengan ID kutipan yang ditentukan.", + "administration_prefix_current": "Awalan di server in adalah {0}", + "administration_prefix_new": "Awalan di server ini diubah dari {0} ke {1}", + "administration_defprefix_current": "Awalan default adalah {0}", + "administration_defprefix_new": "Awalan default bot diubah dari {0} ke {1}", + "administration_bot_nick": "Nama panggilan bot diubah menjadi {0}", + "administration_user_nick": "Nama panggilan pengguna diubah dari {0} menjadi {1}.", + "administration_timezone_guild": "Timezone untuk kelompok ini adalah '{0}'", + "administration_timezone_not_found": "Timezone tidak ditemukan. Gunakan perintah \"timezones\" untuk melihat daftar timezone yang tersedia.", + "administration_timezones_available": "Timezone yang tersedia", + "music_song_not_found": "Lagu tidak ditemukan.", + "searches_define_unknown": "Tidak dapat menemukan definisi dari istilah itu.", + "utility_repeater_initial": "Awal pesan mengulang akan dikirim dalam {0}h dan {1} menit lagi.", + "utility_verbose_errors_enabled": "Perintah salah akan sekarang menunjukkan error.", + "utility_verbose_errors_disabled": "Perintah salah tidak akan menunukkan error.", + "permissions_perms_reset": "Ijin untuk server telah direset.", + "permissions_trigger": "Ijin nomor #{0} {1} sedang memberhentikan aksi ini.", + "administration_migration_error": "Error saat memindah, cek konsol bot untuk informasi selanjutnya.", + "searches_hex_invalid": "Warna yang dipilih tidak sah.", + "permissions_global_perms_reset": "Ijin global telah direset.", + "help_module": "", + "games_hangman_stopped": "", + "music_autoplaying": "", + "music_queue_stopped": "", + "music_removed_song_error": "", + "music_shuffling_playlist": "", + "music_songs_shuffle_enable": "", + "music_songs_shuffle_disable": "", + "music_song_skips_after": "", + "administration_warnings_list": "" } \ No newline at end of file From cb04e987c30f250597aa44d9c81c25165c56642a Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Fri, 7 Jul 2017 15:48:27 +0200 Subject: [PATCH 137/346] Update ResponseStrings.it-IT.json (POEditor.com) --- .../_strings/ResponseStrings.it-IT.json | 38 +++++++++++++++---- 1 file changed, 30 insertions(+), 8 deletions(-) diff --git a/src/NadekoBot/_strings/ResponseStrings.it-IT.json b/src/NadekoBot/_strings/ResponseStrings.it-IT.json index d0d058f7..4531be81 100644 --- a/src/NadekoBot/_strings/ResponseStrings.it-IT.json +++ b/src/NadekoBot/_strings/ResponseStrings.it-IT.json @@ -151,7 +151,6 @@ "administration_old_nick": "Nickname precedente", "administration_old_topic": "Discussione precedente", "administration_perms": "Errore. Probabilmente non dispongo dei permessi necessari.", - "administration_perms_reset": "I permessi di questo server sono stati reimpostati.", "administration_prot_active": "Protezioni attive", "administration_prot_disable": "{0} è stato **disattivato** in questo server.", "administration_prot_enable": "{0} Attivato", @@ -243,7 +242,6 @@ "administration_sbdm": "Sei statto cacciato da {0} server.\nMotivo : {1}", "administration_user_unbanned": "Utente sbannato", "administration_migration_done": "Trasferimento completato!", - "administration_migration_error": "Errore durante il trasferimento, guarda la console del bot per più informazioni.", "administration_presence_updates": "Aggiornamenti sulla presenza", "administration_sb_user": "Utente ammonito", "gambling_awarded": "ha regalato {0} to {1}", @@ -439,7 +437,6 @@ "music_rpl_enabled": "Ripeti playlist abilitato.", "music_set_music_channel": "Ora mostrerò i brani in ascolto, finiti, messi in pausa e rimossi in questo canale.", "music_skipped_to": "Saltato a '{0}:{1}'", - "music_songs_shuffled": "Canzoni mischiate", "music_song_moved": "Canzone spostata", "music_time_format": "{0}o {1}m {2}s", "music_to_position": "Alla posizione", @@ -605,9 +602,6 @@ "utility_convert_not_found": "Non posso convertire {0} a {1}: unità non trovate.", "utility_convert_type_error": "Impossibile convertire {0} in {1}: i tipi di unitá sono diversi", "utility_created_at": "Creato a", - "utility_csc_join": "Entrato nel canale tra server.", - "utility_csc_leave": "Uscito dal canale tra server.", - "utility_csc_token": "Questo é il tuo token CSC", "utility_custom_emojis": "Emoji personalizzati", "utility_error": "Errore", "utility_features": "Funzioni", @@ -754,7 +748,6 @@ "gambling_shop_role": "Otterrai il ruolo {0}.", "gambling_type": "Tipo", "utility_clpa_next_update": "Prossimo update alle {0}", - "administration_global_perms_reset": "I permessi globali sono stati resettati.", "administration_gvc_disabled": "La funzione di canale vocale di gioco é stata disabilitata in questo server.", "administration_gvc_enabled": "{0} é un canale vocale di gioco ora.", "administration_not_in_voice": "Non sei in un canale vocale in questo server.", @@ -782,5 +775,34 @@ "permissions_lgp_none": "Nessun comando o modulo bloccato", "gambling_animal_race_no_race": "Questa Gara degli Animali è al completo!", "utility_cant_read_or_send": "Non puoi leggere o mandare messaggi al quel canale.", - "utility_quotes_notfound": "Non è stata trovata nessuna citazione corrispondente all'ID specificato." + "utility_quotes_notfound": "Non è stata trovata nessuna citazione corrispondente all'ID specificato.", + "administration_prefix_current": "Il prefisso in questo server é {0}", + "administration_prefix_new": "Prefisso in questo server cambiato da {0} a {1}", + "administration_defprefix_current": "", + "administration_defprefix_new": "", + "administration_bot_nick": "Il soprannome del Bot é cambiato in {0}", + "administration_user_nick": "Il soprannome dell'utente {0} é cambiato in {1}", + "administration_timezone_guild": "Il fuso orario di questa gilda é '{0}'", + "administration_timezone_not_found": "", + "administration_timezones_available": "", + "music_song_not_found": "Nessuna canzone trovata.", + "searches_define_unknown": "Impossibile trovare la definizione per quel termine.", + "utility_repeater_initial": "", + "utility_verbose_errors_enabled": "I comandi usati incorrettamente faranno ora vedere gli errori", + "utility_verbose_errors_disabled": "", + "permissions_perms_reset": "I permessi per questo server sono stati resettati.", + "permissions_trigger": "", + "administration_migration_error": "Errore nella migrazione, controlla la console del bot per maggiori informazioni.", + "searches_hex_invalid": "", + "permissions_global_perms_reset": "I permessi globali sono stati resettati.", + "help_module": "", + "games_hangman_stopped": "", + "music_autoplaying": "", + "music_queue_stopped": "", + "music_removed_song_error": "", + "music_shuffling_playlist": "", + "music_songs_shuffle_enable": "", + "music_songs_shuffle_disable": "", + "music_song_skips_after": "", + "administration_warnings_list": "" } \ No newline at end of file From fcbf3a0918e88704b5512307bd41f89bbb8dc007 Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Fri, 7 Jul 2017 15:48:29 +0200 Subject: [PATCH 138/346] Update ResponseStrings.ja-JP.json (POEditor.com) --- .../_strings/ResponseStrings.ja-JP.json | 38 +++++++++++++++---- 1 file changed, 30 insertions(+), 8 deletions(-) diff --git a/src/NadekoBot/_strings/ResponseStrings.ja-JP.json b/src/NadekoBot/_strings/ResponseStrings.ja-JP.json index 4655d076..934754ad 100644 --- a/src/NadekoBot/_strings/ResponseStrings.ja-JP.json +++ b/src/NadekoBot/_strings/ResponseStrings.ja-JP.json @@ -151,7 +151,6 @@ "administration_old_nick": "古いニックネーム", "administration_old_topic": "古いトピック", "administration_perms": "エラー。ほとんどの場合、十分な権限がありません。\n", - "administration_perms_reset": "このサーバーのアクセス許可はリセットされます。\n", "administration_prot_active": "アクティブな保護\n", "administration_prot_disable": "このサーバーで{0}は**無効**になっています。\n", "administration_prot_enable": "{0}を有効にしました\n", @@ -243,7 +242,6 @@ "administration_sbdm": "あなたは{0}サーバーからソフト禁止されました。\n理由:{1}", "administration_user_unbanned": "Since I'm using \"exiled\" for banned. need to rethink a new word.. so just leave this one alone\n", "administration_migration_done": "移行が完了しました!\n", - "administration_migration_error": "移行中にエラーが発生しました。詳細については、ボットのコンソールを確認してください。\n", "administration_presence_updates": "存在の更新", "administration_sb_user": "ユーザはソフトバン", "gambling_awarded": "{0}から{1}に授与されました\n", @@ -439,7 +437,6 @@ "music_rpl_enabled": "プレイリストの繰り返しは有効です。", "music_set_music_channel": "このチャンネルでは、再生、終了、一時停止、曲の削除ができます。", "music_skipped_to": "スキップしました。 {0}:{1} ", - "music_songs_shuffled": "シャッフルした曲", "music_song_moved": "削除した曲", "music_time_format": "{0}時 {1}分 {2}秒 ", "music_to_position": "位置に", @@ -605,9 +602,6 @@ "utility_convert_not_found": "{0}から{1}に変換できません:ユニットが見つかりません\n", "utility_convert_type_error": "{0}から{1}に変換できません:ユニットのタイプが等しくない\n", "utility_created_at": "作成日\n", - "utility_csc_join": "結合されたクロスサーバチャネル。\n", - "utility_csc_leave": "左クロスサーバチャネル。\n", - "utility_csc_token": "これはあなたのCSCトークンです\n", "utility_custom_emojis": "カスタム絵文字\n", "utility_error": "エラー", "utility_features": "特徴", @@ -754,7 +748,6 @@ "gambling_shop_role": "あなたは{0}ロールを取得します。\n", "gambling_type": "タイプ\n", "utility_clpa_next_update": "{0}の次の更新\n", - "administration_global_perms_reset": "グローバル権限がリセットされました。\n", "administration_gvc_disabled": "このサーバーでGame Voice Channel機能が無効になっています。\n", "administration_gvc_enabled": "{0}はゲーム音声チャンネルです。\n", "administration_not_in_voice": "あなたはこのサーバー上の音声チャネルにいません。\n", @@ -782,5 +775,34 @@ "permissions_lgp_none": "ブロックされたコマンドやモジュールはありません。\n", "gambling_animal_race_no_race": "この動物レースはいっぱいです!\n", "utility_cant_read_or_send": "そのチャンネルへのメッセージの読み込みや送信はできません。\n", - "utility_quotes_notfound": "指定された見積もりIDに一致する見積もりは見つかりませんでした。\n" + "utility_quotes_notfound": "指定された見積もりIDに一致する見積もりは見つかりませんでした。\n", + "administration_prefix_current": "", + "administration_prefix_new": "", + "administration_defprefix_current": "", + "administration_defprefix_new": "", + "administration_bot_nick": "", + "administration_user_nick": "", + "administration_timezone_guild": "", + "administration_timezone_not_found": "", + "administration_timezones_available": "", + "music_song_not_found": "", + "searches_define_unknown": "", + "utility_repeater_initial": "", + "utility_verbose_errors_enabled": "", + "utility_verbose_errors_disabled": "", + "permissions_perms_reset": "", + "permissions_trigger": "", + "administration_migration_error": "", + "searches_hex_invalid": "", + "permissions_global_perms_reset": "", + "help_module": "", + "games_hangman_stopped": "", + "music_autoplaying": "", + "music_queue_stopped": "", + "music_removed_song_error": "", + "music_shuffling_playlist": "", + "music_songs_shuffle_enable": "", + "music_songs_shuffle_disable": "", + "music_song_skips_after": "", + "administration_warnings_list": "" } \ No newline at end of file From 88e42e672b375500e5713f33df44fe675a1ee7b4 Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Fri, 7 Jul 2017 15:48:32 +0200 Subject: [PATCH 139/346] Update ResponseStrings.ko-KR.json (POEditor.com) --- .../_strings/ResponseStrings.ko-KR.json | 40 ++++++++++++++----- 1 file changed, 29 insertions(+), 11 deletions(-) diff --git a/src/NadekoBot/_strings/ResponseStrings.ko-KR.json b/src/NadekoBot/_strings/ResponseStrings.ko-KR.json index 535c2900..4f025452 100644 --- a/src/NadekoBot/_strings/ResponseStrings.ko-KR.json +++ b/src/NadekoBot/_strings/ResponseStrings.ko-KR.json @@ -1,5 +1,5 @@ { - "clashofclans_base_already_claimed": "기지가 이미 요청되었거나 파괴되었습니다.", + "clashofclans_base_already_claimed": " 기지가 이미 요청되었거나 파괴되었습니다.", "clashofclans_base_already_destroyed": "기지가 이미 파괴되었습니다.", "clashofclans_base_already_unclaimed": "기지가 요청당하지 않았습니다.", "clashofclans_base_destroyed": "{1}와의 전쟁에서 #{0}번 기지가 **파괴**되었습니다.", @@ -151,7 +151,6 @@ "administration_old_nick": "이전 닉네임", "administration_old_topic": "이전 주제", "administration_perms": "오류. 봇에게 충분한 권한이 없습니다.", - "administration_perms_reset": "이 서버의 모든 권한이 초기화되었습니다.", "administration_prot_active": "보호기능 활성화", "administration_prot_disable": "{0}은(는) 이 서버에서 **비활성화**되었습니다.", "administration_prot_enable": "{0}이(가) 활성화되었습니다.", @@ -243,7 +242,6 @@ "administration_sbdm": "당신은 {0} 서버에서 소프트밴을 당했습니다.\n사유: {1}", "administration_user_unbanned": "사용자 밴 해제", "administration_migration_done": "이전 완료!", - "administration_migration_error": "이전 과정에서 오류가 발생했습니다. 자세한 정보는 봇의 콘솔을 통해서 확인하세요.", "administration_presence_updates": "현재 상태 업데이트", "administration_sb_user": "사용자 소프트 밴", "gambling_awarded": "님이 {1}에게 {0}개를 지급했습니다.", @@ -439,7 +437,6 @@ "music_rpl_enabled": "재생목록 반복이 활성화되었습니다.", "music_set_music_channel": "앞으로 재생 중, 완료, 일시정지, 삭제된 곡들을 이 채널에 출력합니다.", "music_skipped_to": "`{0}:{1}`로 이동하였습니다.", - "music_songs_shuffled": "곡을 섞었습니다.", "music_song_moved": "곡이 이동되었습니다.", "music_time_format": "{0}시간 {1}분 {2}초", "music_to_position": "바뀐 위치", @@ -605,9 +602,6 @@ "utility_convert_not_found": "단위를 찾지 못해서 {0}을(를) {1}(으)로 변환 할 수 없습니다.", "utility_convert_type_error": "단위의 종류가 같지 않기때문에 {0}을(를) {1}(으)로 변환 할 수 없습니다.", "utility_created_at": "생성 시간", - "utility_csc_join": "크로스 서버 채널에 입장했습니다.", - "utility_csc_leave": "크로스 서버 채널에서 퇴장했습니다.", - "utility_csc_token": "이것이 당신의 CSC 토큰입니다.", "utility_custom_emojis": "커스텀 이모지", "utility_error": "오류", "utility_features": "기능", @@ -754,7 +748,6 @@ "gambling_shop_role": "당신은 {0} 역할을 얻게 될 것 입니다.", "gambling_type": "종류", "utility_clpa_next_update": "다음 업데이트는 {0}에 됩니다.", - "administration_global_perms_reset": "글로벌 권한이 리셋되었습니다.", "administration_gvc_disabled": "이 서버에서 게임 음성 채널 기능이 비활성화되었습니다.", "administration_gvc_enabled": "이제 {0}이(가) 게임 음성 채널입니다.", "administration_not_in_voice": "당신은 이 서버의 음성 채널에 있지 않습니다.", @@ -780,11 +773,36 @@ "permissions_gmod_add": "모든 서버에서 {0} 모듈이 비활성화되었습니다.", "permissions_gmod_remove": "모든 서버에서 {0} 모듈이 활성화되었습니다.", "permissions_lgp_none": "차단된 명령어나 모듈이 없습니다.", - "gambling_animal_race_no_race": "이 동물 레이스는 찼습니다!", + "gambling_animal_race_no_race": "이 동물 레이스는 꽉 찼습니다!", "utility_cant_read_or_send": "당신은 그 채널에서 메시지를 보내거나 볼 수 없습니다.", "utility_quotes_notfound": "해당 ID에 일치하는 인용구를 찾을 수 없습니다.", "administration_prefix_current": "이 서버의 봇 명령어 접두사는 {0} 입니다.", "administration_prefix_new": "이 서버의 봇 명령어 접두사가 {0}에서 {1}(으)로 변경되었습니다.", - "administration_defprefix_current": "표준의 봇 명령어 접두사는 {0} 입니다.", - "administration_defprefix_new": "표준의 봇 명령어 접두사가 {0}에서 {1}(으)로 변경되었습니다." + "administration_defprefix_current": "기본 봇 명령어 접두사는 {0} 입니다.", + "administration_defprefix_new": "기본 봇 명령어 접두사가 {0}에서 {1}(으)로 변경되었습니다.", + "administration_bot_nick": "봇의 닉네임이 {0}(으)로 변경되었습니다.", + "administration_user_nick": "사용자 {0}의 닉네임이 {1}(으)로 변경되었습니다.", + "administration_timezone_guild": "이 길드의 시간대는 `{0}`입니다.", + "administration_timezone_not_found": "시간대를 찾지 못했습니다. \"timezones\" 명령어를 통해서 가능한 시간대 목록을 볼 수 있습니다.", + "administration_timezones_available": "가능한 시간대", + "music_song_not_found": "음악을 찾지 못했습니다.", + "searches_define_unknown": "해당 용어에 대한 정의를 찾을 수 없습니다.", + "utility_repeater_initial": "이제 초기 반복 메시지는 {0}시간 {1}분 마다 전송됩니다.", + "utility_verbose_errors_enabled": "잘못 사용된 명령어는 이제 오류를 표시합니다.", + "utility_verbose_errors_disabled": "잘못 사용된 명령어는 이제 오류를 표시하지 않습니다.", + "permissions_perms_reset": "이 서버에서 권한을 초기화했습니다.", + "permissions_trigger": "권한 #{0} {1}이(가) 이 행동을 금지하고 있습니다.", + "administration_migration_error": "이주하는 동안 오류가 발생했습니다. 자세한 정보는 봇의 콘솔 확인하십시오.", + "searches_hex_invalid": "유효하지 않은 색입니다.", + "permissions_global_perms_reset": "모든 서버의 권한을 초기화했습니다.", + "help_module": "모듈: {0}", + "games_hangman_stopped": "행맨 게임이 정지되었습니다.", + "music_autoplaying": "자동재생", + "music_queue_stopped": "플레이어가 멈췄습니다. {0} 명령어를 사용하여 다시 시작하세요.", + "music_removed_song_error": "해당 색인에 노래가 없습니다.", + "music_shuffling_playlist": "노래들을 섞는 중...", + "music_songs_shuffle_enable": "지금부터 노래들을 섞습니다.", + "music_songs_shuffle_disable": "노래들을 더 이상 섞지 않습니다.", + "music_song_skips_after": "노래들을 {0}초 후에 스킵합니다.", + "administration_warnings_list": "이 서버의 경고받은 사용자 목록" } \ No newline at end of file From a49002a808cead690bafe00e503e05ba82031b3a Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Fri, 7 Jul 2017 15:48:35 +0200 Subject: [PATCH 140/346] Update ResponseStrings.nb-NO.json (POEditor.com) --- .../_strings/ResponseStrings.nb-NO.json | 46 +++++++++++++------ 1 file changed, 32 insertions(+), 14 deletions(-) diff --git a/src/NadekoBot/_strings/ResponseStrings.nb-NO.json b/src/NadekoBot/_strings/ResponseStrings.nb-NO.json index 4ff837fd..c28e563b 100644 --- a/src/NadekoBot/_strings/ResponseStrings.nb-NO.json +++ b/src/NadekoBot/_strings/ResponseStrings.nb-NO.json @@ -66,7 +66,7 @@ "administration_bandm": "Du har blitt utestengt fra {0}\nGrunn: {1}", "administration_banned_pl": "utestengt", "administration_banned_user": "Bruker ble utestengt", - "administration_bot_name": "Bot navn endret til {0}", + "administration_bot_name": "Bot'ens navn er endret til {0}", "administration_bot_status": "Bot status endret til {0}", "administration_byedel_off": "Automatisk sletting av 'farvel'-meldinger er aktivert", "administration_byedel_on": "Farvel-meldinger vil bli slettet etter {0} sekunder.", @@ -151,7 +151,6 @@ "administration_old_nick": "Gammelt kallenavn", "administration_old_topic": "Gammelt emne", "administration_perms": "Feil. Mest sansynlig har jeg ikke tilstrekkelige tillatelser.", - "administration_perms_reset": "Tillatelsene til denne serveren er nullstilt.", "administration_prot_active": "Aktive Beskyttelser", "administration_prot_disable": "{0} har blitt **deaktivert** på denne serveren.", "administration_prot_enable": "{0} aktivert", @@ -243,7 +242,6 @@ "administration_sbdm": "Du har blitt 'soft-banned' fra {0} server.\nGrunn: {1}", "administration_user_unbanned": "Fjernet bruker fra utestenging", "administration_migration_done": "Migrering fullført!", - "administration_migration_error": "Feil under migrering, sjekk botens konsoll for mer informasjon.", "administration_presence_updates": "Oppdatering av tilstedeværelse", "administration_sb_user": "Bruker soft-banned", "gambling_awarded": "har tildelt {0} til {1}", @@ -285,7 +283,7 @@ "help_cmdlist_donate": "Du kan støtte prosjektet på Patreon: <{0}> eller paypal: <{1}>", "help_cmd_and_alias": "Kommandoer og aliaser", "help_commandlist_regen": "Kommandoliste regenerert.", - "help_commands_instr": "Skriv `{0}h Kommandonavn` å se hjelpen for den oppgitte kommandoen. f.eks `{0}h >8ball`", + "help_commands_instr": "Skriv `{0}h Kommandonavn` å se hjelpen for den oppgitte kommandoen. f.eks `{0}h {0}8ball`", "help_command_not_found": "Jeg kan ikke finne den kommandoen. Kontroller at kommandoen finnes før du prøver igjen.", "help_desc": "Beskrivelse", "help_donate": "Du kan støtte NadekoBot prosjektet på\nPatreon <{0}> eller\nPaypal <{1}>\nIkke glem å skrive Discord navnet eller ID i meldingen.\n\n** Takk ** ♥ ️", @@ -398,7 +396,7 @@ "music_autoplay_enabled": "Automatisk avspilling aktivert.", "music_defvol_set": "Standard volum satt til {0}%", "music_dir_queue_complete": "Mappelisting ferdig.", - "music_fairplay": "fairplay", + "music_fairplay": "Fairplay", "music_finished_song": "Sang ferdig", "music_fp_disabled": "Rettferdig avspilling deaktivert", "music_fp_enabled": "Rettferdig avspilling aktivert", @@ -416,7 +414,7 @@ "music_no_search_results": "Ingen søkeresultater.", "music_paused": "Musikkavspilling pauset", "music_player_queue": "Spilleliste - Side {0}/{1}", - "music_playing_song": "Spiller sang", + "music_playing_song": "Spiller sang #{0}", "music_playlists": "#{0}` - **{1}** av *{2}* ({3} sanger)", "music_playlists_page": "Side {0} av lagrede spillelister", "music_playlist_deleted": "Spilleliste slettet.", @@ -439,7 +437,6 @@ "music_rpl_enabled": "Repetisjon av spilleliste startet.", "music_set_music_channel": "Meldinger om sanger som spilles av, er ferdige, pauset og fjernet vil bli vist i denne kanalen.", "music_skipped_to": "Skippet til '{0}:{1}'", - "music_songs_shuffled": "Sanger stokket", "music_song_moved": "Sang flyttet", "music_time_format": "{0}t {1}m {2}s", "music_to_position": "Til posisjon", @@ -605,9 +602,6 @@ "utility_convert_not_found": "Kan ikke konvertere {0} til {1}: enheter ikke funnet", "utility_convert_type_error": "Kan ikke konvertere {0} til {1}: typer enheter er ikke lik", "utility_created_at": "Laget på", - "utility_csc_join": "Ble med i kanalen for snakk på tvers av servere", - "utility_csc_leave": "Forlot kanalen for snakk på tvers av servere", - "utility_csc_token": "Dette er din CSC token", "utility_custom_emojis": "Spesiallagde emojier", "utility_error": "Feil", "utility_features": "Egenskaper", @@ -663,7 +657,7 @@ "utility_server_info": "Server info", "utility_shard": "Shard", "utility_shard_stats": "Shard status", - "utility_shard_stats_txt": "Shard **#{0}** sin status: {1} med {2} servere", + "utility_shard_stats_txt": "Shard **#{0}** sin status: {1} med {2} servere - for {3} siden", "utility_showemojis": "**Navn:** {0} **Link:** {1}", "utility_showemojis_none": "Ingen spesielle emojier funnet.", "utility_stats_songs": "Spiller {0} sanger, {1} i kø.", @@ -754,7 +748,6 @@ "gambling_shop_role": "Du får rollen {0}", "gambling_type": "Type", "utility_clpa_next_update": "Neste oppdatering om {0}", - "administration_global_perms_reset": "Globale tillatelser er nullstilt", "administration_gvc_disabled": "Funksjonen 'Talekanal for spill' er slått av.", "administration_gvc_enabled": "{0} er nå 'talekanal for spill'.", "administration_not_in_voice": "Du er ikke i en talekanal på denne serveren.", @@ -785,6 +778,31 @@ "utility_quotes_notfound": "Ingen sitater funnet med det ID nummeret.", "administration_prefix_current": "Denne serverens prefiks er {0}", "administration_prefix_new": "Endret prefiks fra {0} til {1}", - "administration_defprefix_current": "Standard prefiks er {1}", - "administration_defprefix_new": "Endret standard prefiks fra {0} til {1}" + "administration_defprefix_current": "Standard prefiks er {0}", + "administration_defprefix_new": "Endret standard prefiks fra {0} til {1}", + "administration_bot_nick": "Bot'ens kallenavn er nå {0}", + "administration_user_nick": "{0} sitt kallenavn er nå {1}", + "administration_timezone_guild": "Denne guilden sin tidssone er {0}", + "administration_timezone_not_found": "Kunne ikke finne tidssone. Bruk \"timezone\"-kommandoen for å se en liste over tilgjengelige tidssoner.", + "administration_timezones_available": "Tilgjengelige tidssoner", + "music_song_not_found": "Kunne ikke finne sang", + "searches_define_unknown": "Kunne ikke finne definisjonen for det ordet", + "utility_repeater_initial": "Første repeterende melding vil bli sendt om {0}t og {1}m", + "utility_verbose_errors_enabled": "Ukorrekt bruk av kommandoer vil nå vise feilmeldinger.", + "utility_verbose_errors_disabled": "Ukorrekt bruk av kommandoer vil ikke lenger vise feilmeldinger.", + "permissions_perms_reset": "Tillatelsene er tilbakestilt for denne serveren.", + "permissions_trigger": "Tillatelse #{0} {1} forhindrer kommandoen i å bli utført.", + "administration_migration_error": "En feil oppstod under migreringen. Sjekk konsollen for mer informasjon.", + "searches_hex_invalid": "Ugyldig fargekode.", + "permissions_global_perms_reset": "Globale tillatelser er tilbakestilt.", + "help_module": "Modul: {0}", + "games_hangman_stopped": "Hangman stoppet.", + "music_autoplaying": "Spiller:", + "music_queue_stopped": "Avspiller stoppet. Bruk kommandoen {0} for å starte avspilling.", + "music_removed_song_error": "Ingen sang med den indexen.", + "music_shuffling_playlist": "Stokker sanger", + "music_songs_shuffle_enable": "Sanger vil spilles av i tilfeldig rekkefølge fra nå.", + "music_songs_shuffle_disable": "Sanger vil ikke lenger spilles av i tilfeldig rekkefølge.", + "music_song_skips_after": "Sanger vil hoppe etter {0}", + "administration_warnings_list": "Liste over advarte brukere på denne serveren." } \ No newline at end of file From bf860f9aa862adb63d378618a040ad46c64b365a Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Fri, 7 Jul 2017 15:48:37 +0200 Subject: [PATCH 141/346] Update ResponseStrings.pl-PL.json (POEditor.com) --- .../_strings/ResponseStrings.pl-PL.json | 38 +++++++++++++++---- 1 file changed, 30 insertions(+), 8 deletions(-) diff --git a/src/NadekoBot/_strings/ResponseStrings.pl-PL.json b/src/NadekoBot/_strings/ResponseStrings.pl-PL.json index 4cc15df4..d1c79767 100644 --- a/src/NadekoBot/_strings/ResponseStrings.pl-PL.json +++ b/src/NadekoBot/_strings/ResponseStrings.pl-PL.json @@ -151,7 +151,6 @@ "administration_old_nick": "Stary pseudonim", "administration_old_topic": "Stary temat", "administration_perms": "Wystąpił błąd. Najprawdopodobniej nie mam wystarczających uprawnień.", - "administration_perms_reset": "Uprawnienia tego serwera zostały zresetowane.", "administration_prot_active": "Aktywne zabezpieczenia", "administration_prot_disable": "{0} został **wyłączony** na tym serwerze.", "administration_prot_enable": "{0} włączony", @@ -243,7 +242,6 @@ "administration_sbdm": "Zostałeś tymczasowo zablokowany na serwerze {0}\nPowód: {1}", "administration_user_unbanned": "Użytkownik został odbanowany", "administration_migration_done": "Migracja zakończona!", - "administration_migration_error": "Błąd podczas migracji, sprawdz konsole bota po więcej informacji.", "administration_presence_updates": "Aktualizacje obecności", "administration_sb_user": "Użytkownik tymczasowo zablokowany", "gambling_awarded": "nagrodził {0} {1}", @@ -439,7 +437,6 @@ "music_rpl_enabled": "Powtarzanie playlisty włączone.", "music_set_music_channel": "Teraz wypiszę odtwarzane, skończone, wstrzymane i usunięte piosenki na tym kanale", "music_skipped_to": "Przewinięto do `{0}:{1}`", - "music_songs_shuffled": "Utwory pomieszane", "music_song_moved": "Utwór przeniesiony", "music_time_format": "{0}g {1}m {2}s", "music_to_position": "Do pozycji", @@ -605,9 +602,6 @@ "utility_convert_not_found": "Nie można przekonwertować {0} na {1}: nie znaleziono jednostki", "utility_convert_type_error": "Nie mogę przeliczyć {0} na {1}: rodzaje jednostek są różne", "utility_created_at": "Stworzono w", - "utility_csc_join": "Dołączyłeś do międzyserwerowego kanału.", - "utility_csc_leave": "Opuściłeś międzyserwerowy kanał.", - "utility_csc_token": "To jest twój token CSC", "utility_custom_emojis": "Niestandardowe emotikony", "utility_error": "Błąd", "utility_features": "Funkcje", @@ -754,7 +748,6 @@ "gambling_shop_role": "Dostaniesz rolę {0}.", "gambling_type": "Typ", "utility_clpa_next_update": "Następna aktualizacja za {0}", - "administration_global_perms_reset": "Uprawnienia globalne zostały zresetowane", "administration_gvc_disabled": "Opcja Czatu Głosowego gry została wyłączona na tym serwerze.", "administration_gvc_enabled": "{0} jest teraz Grą w Czacie głosowym.", "administration_not_in_voice": "Nie jesteś w kanale głosowym na tym serwerze", @@ -782,5 +775,34 @@ "permissions_lgp_none": "Brak zablokowanych komend i modułów", "gambling_animal_race_no_race": "Ten wyścig zwierzaków jest pełny!", "utility_cant_read_or_send": "Nie możesz czytać ani wysyłać wiadomości na tym kanale.", - "utility_quotes_notfound": "Nie znaleziono cytatów pasujących do podanego ID." + "utility_quotes_notfound": "Nie znaleziono cytatów pasujących do podanego ID.", + "administration_prefix_current": "Prefix na tym serwerze to {0}", + "administration_prefix_new": "Zmieniono prefix na tym serwerze z {0} na {1}", + "administration_defprefix_current": "Domyślny prefix to {0}", + "administration_defprefix_new": "Zmieniono domyślny prefix z {0} na {1}", + "administration_bot_nick": "Nickname bota został zmieniony na {0}", + "administration_user_nick": "Nickname użytkowanika {0} został zmieniony na {1}", + "administration_timezone_guild": "Strefa czasowa tej gildii to {0}", + "administration_timezone_not_found": "Strefa czasowa nie znaleziona. Użyj komendy \"timezones\" aby zobaczyć listę dostepnych stref czasowych.", + "administration_timezones_available": "Dostępne strefy czasowe.", + "music_song_not_found": "Nie znaleziono piosenki.", + "searches_define_unknown": "Nie można znaleźć definicji tego terminu.", + "utility_repeater_initial": "", + "utility_verbose_errors_enabled": "Niewłaściwie użyte komendy będą teraz wyświetlać błędy.", + "utility_verbose_errors_disabled": "Niewłaściwie użyte komendy nie będą już pokazywać błędów.", + "permissions_perms_reset": "", + "permissions_trigger": "", + "administration_migration_error": "", + "searches_hex_invalid": "", + "permissions_global_perms_reset": "", + "help_module": "", + "games_hangman_stopped": "", + "music_autoplaying": "", + "music_queue_stopped": "", + "music_removed_song_error": "", + "music_shuffling_playlist": "", + "music_songs_shuffle_enable": "", + "music_songs_shuffle_disable": "", + "music_song_skips_after": "", + "administration_warnings_list": "" } \ No newline at end of file From f3f934e05fac646ef77d332b64a19cc7cda0579f Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Fri, 7 Jul 2017 15:48:40 +0200 Subject: [PATCH 142/346] Update ResponseStrings.pt-BR.json (POEditor.com) --- .../_strings/ResponseStrings.pt-BR.json | 34 ++++++++++++++----- 1 file changed, 26 insertions(+), 8 deletions(-) diff --git a/src/NadekoBot/_strings/ResponseStrings.pt-BR.json b/src/NadekoBot/_strings/ResponseStrings.pt-BR.json index 4e8d01b7..16a05bd4 100644 --- a/src/NadekoBot/_strings/ResponseStrings.pt-BR.json +++ b/src/NadekoBot/_strings/ResponseStrings.pt-BR.json @@ -151,7 +151,6 @@ "administration_old_nick": "Apelido Antigo", "administration_old_topic": "Tópico Antigo", "administration_perms": "Erro. Não tenho permissões suficientes.", - "administration_perms_reset": "As permissões para este servidor foram resetadas.", "administration_prot_active": "Proteções ativadas", "administration_prot_disable": "{0} foi **desativado** neste servidor.", "administration_prot_enable": "{0} Ativado", @@ -243,7 +242,6 @@ "administration_sbdm": "Você foi banido temporariamente do servidor {0}.\nMotivo: {1}", "administration_user_unbanned": "Usuário desbanido", "administration_migration_done": "Migração concluída!", - "administration_migration_error": "Erro enquanto migrava, verifique o console do bot para mais informações.", "administration_presence_updates": "Atualizações de Presença", "administration_sb_user": "Usuário Banido Temporariamente", "gambling_awarded": "concedeu {0} para {1}", @@ -439,7 +437,6 @@ "music_rpl_enabled": "Repetição de playlist habilitada.", "music_set_music_channel": "Eu irei mostrar as músicas em andamento, concluídas, pausadas e removidas neste canal.", "music_skipped_to": "Pulando para `{0}:{1}`", - "music_songs_shuffled": "Músicas embaralhadas.", "music_song_moved": "Música movida", "music_time_format": "{0}h {1}m {2}s", "music_to_position": "para a posição", @@ -605,9 +602,6 @@ "utility_convert_not_found": "Não foi possível converter {0} para {1}: unidades não encontradas", "utility_convert_type_error": "Não foi possível converter {0} para {1}: as unidades não são do mesmo tipo", "utility_created_at": "Criado em", - "utility_csc_join": "Juntou-se ao canal de servidor cruzado.", - "utility_csc_leave": "Deixou o canal de servidor cruzado.", - "utility_csc_token": "Este é seu token de Canal de Servidor Cruzado", "utility_custom_emojis": "Emojis Personalizados", "utility_error": "Erro", "utility_features": "Atributos", @@ -754,7 +748,6 @@ "gambling_shop_role": "Você receberá a role {0}.", "gambling_type": "Tipo", "utility_clpa_next_update": "Próximo update em {0}", - "administration_global_perms_reset": "Permissões globais foram resetadas.", "administration_gvc_disabled": "Canais de Voz de Jogos foram desabilitados neste server.", "administration_gvc_enabled": "{0} é um Canal de Voz de Jogos agora.", "administration_not_in_voice": "Você não está em um canal de voz neste server.", @@ -786,5 +779,30 @@ "administration_prefix_current": "Prefixo neste server é {0}", "administration_prefix_new": "Prefixo neste server modificado de {0} para {1}", "administration_defprefix_current": "Prefixo de bot padrão é {0}", - "administration_defprefix_new": "Prefixo de bot padrão modificado de {0} para {1}" + "administration_defprefix_new": "Prefixo de bot padrão modificado de {0} para {1}", + "administration_bot_nick": "Apelido do Bot mudou para {0}", + "administration_user_nick": "Apelido do usuário {0} mudou para {1}", + "administration_timezone_guild": "Fuso horário para esta guild é `{0}`", + "administration_timezone_not_found": "Fuso horário não encontrado. Use o comando \"timezones\" para ver a lista de fusos horários disponíveis.", + "administration_timezones_available": "Fusos Horários Disponíveis", + "music_song_not_found": "Nenhuma música encontrada.", + "searches_define_unknown": "Não foi possível encontrar a definição para este termo.", + "utility_repeater_initial": "Repetição de mensagem inicial será enviada em {0}h e {1}min.", + "utility_verbose_errors_enabled": "Comandos usados incorretamente agora irão exibir erros.", + "utility_verbose_errors_disabled": "Comandos usados incorretamente não irão mais exibir erros.", + "permissions_perms_reset": "Permissões para este server foram resetadas.", + "permissions_trigger": "Permissão número #{0} {1} está prevenindo esta ação.", + "administration_migration_error": "Erro ao migrar, cheque o console do bot para mais informações.", + "searches_hex_invalid": "Cor inválida especificada.", + "permissions_global_perms_reset": "Permissões globais foram resetadas.", + "help_module": "", + "games_hangman_stopped": "", + "music_autoplaying": "", + "music_queue_stopped": "", + "music_removed_song_error": "", + "music_shuffling_playlist": "", + "music_songs_shuffle_enable": "", + "music_songs_shuffle_disable": "", + "music_song_skips_after": "", + "administration_warnings_list": "" } \ No newline at end of file From 9e9f21d5258333591f57803251db73d728ae111a Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Fri, 7 Jul 2017 15:48:42 +0200 Subject: [PATCH 143/346] Update ResponseStrings.ro-RO.json (POEditor.com) --- .../_strings/ResponseStrings.ro-RO.json | 44 ++++++++++++++----- 1 file changed, 33 insertions(+), 11 deletions(-) diff --git a/src/NadekoBot/_strings/ResponseStrings.ro-RO.json b/src/NadekoBot/_strings/ResponseStrings.ro-RO.json index 864bb8f3..3c4720c8 100644 --- a/src/NadekoBot/_strings/ResponseStrings.ro-RO.json +++ b/src/NadekoBot/_strings/ResponseStrings.ro-RO.json @@ -151,7 +151,6 @@ "administration_old_nick": "Poreclă veche", "administration_old_topic": "Topic vechi", "administration_perms": "Eroare. Mai mult ca sigur că nu am destule permisiuni.", - "administration_perms_reset": "Permisiunile pentru acest server au fost resetate.", "administration_prot_active": "Protecții active.", "administration_prot_disable": "{0} a fost **dezactivat** pe acest server.", "administration_prot_enable": "{0} Activat", @@ -243,7 +242,6 @@ "administration_sbdm": "Ai fost banat-ușor din serverul {0}.\nMotivul: {1}", "administration_user_unbanned": "Utilizator bannat.", "administration_migration_done": "Migrare terminata!", - "administration_migration_error": "Eroare in timpul migrarii, verifica consola bot-ului pentru mai multe informatii.", "administration_presence_updates": "Actualizări de prezență.", "administration_sb_user": "Utilizator banat-ușor.", "gambling_awarded": "A acordat {0} la {1}.", @@ -288,7 +286,7 @@ "help_commands_instr": "Tasteaza `{0}h NumeleComenzii` pentru a vedea informatii legate de acea comanda. ex. `{0}h >8ball`", "help_command_not_found": "Nu gasesc acea comanda. Te rog asigura-te ca exista comanda inainte de a incerca din nou.", "help_desc": "Descriere", - "help_donate": "Poți susține proiectul NadekoBot pe\nPatreon <{0}> sau\nPaypal <{1}>\nNu uita să iți lași numele de Discord sau ID-ul în mesaj.\n\n**Mulțumesc** <3", + "help_donate": "Poți susține proiectul NadekoBot pe\nPatreon <{0}> sau\nPaypal <{1}>\nNu uita să iți lași numele de Discord sau ID-ul în mesaj.\n\n**Mulțumesc** ♥️", "help_guide": "** Listă de comenzi **: <{0}>\n** Ghiduri și documente de găzduire pot fi găsite aici **: <{1}>", "help_list_of_commands": "Lista comenzilor", "help_list_of_modules": "Lista modulelor", @@ -439,7 +437,6 @@ "music_rpl_enabled": "Repetarea listei de redare activată.", "music_set_music_channel": "Acum voi scoate melodiile cântate, terminate, pauzate și eliminate în acest canal.", "music_skipped_to": "S-a sărit la `{0}:{1}`", - "music_songs_shuffled": "Cântece amestecate", "music_song_moved": "Cântec mutat", "music_time_format": "{0}o {1}m {2}s", "music_to_position": "Către poziția", @@ -537,7 +534,7 @@ "searches_list_of_place_tags": "Lista {0}etichete plasate", "searches_location": "Locație", "searches_magicitems_not_loaded": "Articolele magice nu au fost încărcate", - "searches_mal_profile": "{0} profilul lui MAL", + "searches_mal_profile": "profilul MAL al lui {0}", "searches_mashape_api_missing": "Proprietarul botului nu a specificat MashapeApiKey. Nu puteți utiliza această funcție.", "searches_min_max": "Min/Max", "searches_no_channel_found": "Nici-un canal găsit.", @@ -605,9 +602,6 @@ "utility_convert_not_found": "Nu se poate converti {0} la {1}: unitățile nu pot fii găsite", "utility_convert_type_error": "Nu se poate converti {0} la {1}: tipurile de unități nu sunt egale.", "utility_created_at": "Creat la", - "utility_csc_join": "S-a alăturat canalului încrucișat al serverului.", - "utility_csc_leave": "A părăsit canalul încrucișat al serverului.", - "utility_csc_token": "Acesta este tokenul tău CSC", "utility_custom_emojis": "Emojis personalizate", "utility_error": "Eroare", "utility_features": "Caracteristici", @@ -706,7 +700,7 @@ "searches_compet_playtime": "Timp jucat competitiv", "administration_channel": "Canal", "administration_command_text": "Comandă de text", - "administration_kicked_pl": "Dat afară", + "administration_kicked_pl": "Dați afară", "administration_moderator": "Moderator", "administration_page": "pagina {0}", "administration_reason": "Motiv", @@ -754,7 +748,6 @@ "gambling_shop_role": "O sa primești rolul {0}.", "gambling_type": "Tip", "utility_clpa_next_update": "Urmatorul update în {0}", - "administration_global_perms_reset": "Permisiile globale au fost resetate.", "administration_gvc_disabled": "Caracteristica Canalul Vocal de Joc a fost dezactivat pe acest server.", "administration_gvc_enabled": "{0} este un Canal Vocal de Joc acum.", "administration_not_in_voice": "Nu ești intr-un canal vocal pe acest server.", @@ -782,5 +775,34 @@ "permissions_lgp_none": "Nu sunt comenzi blocate sau module.", "gambling_animal_race_no_race": "Cursa animalelor este plină!", "utility_cant_read_or_send": "Nu poți să citești sau să trimiți mesaje din acel canal.", - "utility_quotes_notfound": "Nici un citat găsit care se potrivește cu ID citatului specificat." + "utility_quotes_notfound": "Nici un citat găsit care se potrivește cu ID citatului specificat.", + "administration_prefix_current": "Prefixul pe acest server este {0}", + "administration_prefix_new": "Am schimbat prefixul pe acest server din {0} în {1}", + "administration_defprefix_current": "Prefixul implicit al bot-ului este {0}", + "administration_defprefix_new": "Am schimbat prefixul implicit at bot-ului din {0} în {1}", + "administration_bot_nick": "", + "administration_user_nick": "", + "administration_timezone_guild": "", + "administration_timezone_not_found": "", + "administration_timezones_available": "", + "music_song_not_found": "", + "searches_define_unknown": "", + "utility_repeater_initial": "", + "utility_verbose_errors_enabled": "", + "utility_verbose_errors_disabled": "", + "permissions_perms_reset": "", + "permissions_trigger": "", + "administration_migration_error": "", + "searches_hex_invalid": "", + "permissions_global_perms_reset": "", + "help_module": "", + "games_hangman_stopped": "", + "music_autoplaying": "", + "music_queue_stopped": "", + "music_removed_song_error": "", + "music_shuffling_playlist": "", + "music_songs_shuffle_enable": "", + "music_songs_shuffle_disable": "", + "music_song_skips_after": "", + "administration_warnings_list": "" } \ No newline at end of file From ab99801a37e8492781ff646ac5198cc50a13ffb3 Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Fri, 7 Jul 2017 16:07:57 +0200 Subject: [PATCH 144/346] version upped --- src/NadekoBot/Services/Impl/StatsService.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/NadekoBot/Services/Impl/StatsService.cs b/src/NadekoBot/Services/Impl/StatsService.cs index 60d4a1e2..b7490a93 100644 --- a/src/NadekoBot/Services/Impl/StatsService.cs +++ b/src/NadekoBot/Services/Impl/StatsService.cs @@ -17,7 +17,7 @@ namespace NadekoBot.Services.Impl private readonly IBotCredentials _creds; private readonly DateTime _started; - public const string BotVersion = "1.54"; + public const string BotVersion = "1.54a"; public string Author => "Kwoth#2560"; public string Library => "Discord.Net"; From 4de7e3827789d79245b407ebf6aba4d598b0f0a0 Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Fri, 7 Jul 2017 23:22:34 +0200 Subject: [PATCH 145/346] fixed trivia, and some other things --- .../Games/Commands/Hangman/HangmanGame.cs | 4 ++- .../Games/Commands/Trivia/TriviaGame.cs | 36 ++++++++++--------- .../Modules/Searches/Commands/LoLCommands.cs | 16 ++++----- .../Modules/Utility/Commands/InfoCommands.cs | 3 +- 4 files changed, 32 insertions(+), 27 deletions(-) diff --git a/src/NadekoBot/Modules/Games/Commands/Hangman/HangmanGame.cs b/src/NadekoBot/Modules/Games/Commands/Hangman/HangmanGame.cs index eddd00fc..8818a798 100644 --- a/src/NadekoBot/Modules/Games/Commands/Hangman/HangmanGame.cs +++ b/src/NadekoBot/Modules/Games/Commands/Hangman/HangmanGame.cs @@ -109,8 +109,10 @@ namespace NadekoBot.Modules.Games.Hangman var embed = new EmbedBuilder().WithTitle("Hangman Game") .WithDescription(toSend) .AddField(efb => efb.WithName("It was").WithValue(Term.Word)) - .WithImageUrl(Term.ImageUrl) .WithFooter(efb => efb.WithText(string.Join(" ", Guesses))); + if(Uri.IsWellFormedUriString(Term.ImageUrl, UriKind.Absolute)) + embed.WithImageUrl(Term.ImageUrl); + if (Errors >= MaxErrors) await GameChannel.EmbedAsync(embed.WithErrorColor()).ConfigureAwait(false); else diff --git a/src/NadekoBot/Modules/Games/Commands/Trivia/TriviaGame.cs b/src/NadekoBot/Modules/Games/Commands/Trivia/TriviaGame.cs index b1b7d477..cc8089a0 100644 --- a/src/NadekoBot/Modules/Games/Commands/Trivia/TriviaGame.cs +++ b/src/NadekoBot/Modules/Games/Commands/Trivia/TriviaGame.cs @@ -89,12 +89,13 @@ namespace NadekoBot.Modules.Games.Trivia questionEmbed = new EmbedBuilder().WithOkColor() .WithTitle(GetText("trivia_game")) .AddField(eab => eab.WithName(GetText("category")).WithValue(CurrentQuestion.Category)) - .AddField(eab => eab.WithName(GetText("question")).WithValue(CurrentQuestion.Question)) - .WithImageUrl(CurrentQuestion.ImageUrl); + .AddField(eab => eab.WithName(GetText("question")).WithValue(CurrentQuestion.Question)); + if (Uri.IsWellFormedUriString(CurrentQuestion.ImageUrl, UriKind.Absolute)) + questionEmbed.WithImageUrl(CurrentQuestion.ImageUrl); questionMessage = await Channel.EmbedAsync(questionEmbed).ConfigureAwait(false); } - catch (HttpException ex) when (ex.HttpCode == System.Net.HttpStatusCode.NotFound || + catch (HttpException ex) when (ex.HttpCode == System.Net.HttpStatusCode.NotFound || ex.HttpCode == System.Net.HttpStatusCode.Forbidden || ex.HttpCode == System.Net.HttpStatusCode.BadRequest) { @@ -145,11 +146,13 @@ namespace NadekoBot.Modules.Games.Trivia { try { - await Channel.EmbedAsync(new EmbedBuilder().WithErrorColor() + var embed = new EmbedBuilder().WithErrorColor() .WithTitle(GetText("trivia_game")) - .WithDescription(GetText("trivia_times_up", Format.Bold(CurrentQuestion.Answer))) - .WithImageUrl(CurrentQuestion.AnswerImageUrl)) - .ConfigureAwait(false); + .WithDescription(GetText("trivia_times_up", Format.Bold(CurrentQuestion.Answer))); + if (Uri.IsWellFormedUriString(CurrentQuestion.AnswerImageUrl, UriKind.Absolute)) + embed.WithImageUrl(CurrentQuestion.AnswerImageUrl); + + await Channel.EmbedAsync(embed).ConfigureAwait(false); } catch (Exception ex) { @@ -215,13 +218,14 @@ namespace NadekoBot.Modules.Games.Trivia ShouldStopGame = true; try { - await Channel.EmbedAsync(new EmbedBuilder().WithOkColor() + var embedS = new EmbedBuilder().WithOkColor() .WithTitle(GetText("trivia_game")) .WithDescription(GetText("trivia_win", guildUser.Mention, - Format.Bold(CurrentQuestion.Answer))) - .WithImageUrl(CurrentQuestion.AnswerImageUrl)) - .ConfigureAwait(false); + Format.Bold(CurrentQuestion.Answer))); + if (Uri.IsWellFormedUriString(CurrentQuestion.AnswerImageUrl, UriKind.Absolute)) + embedS.WithImageUrl(CurrentQuestion.AnswerImageUrl); + await Channel.EmbedAsync(embedS).ConfigureAwait(false); } catch { @@ -232,12 +236,12 @@ namespace NadekoBot.Modules.Games.Trivia await _cs.AddAsync(guildUser, "Won trivia", reward, true).ConfigureAwait(false); return; } - - await Channel.EmbedAsync(new EmbedBuilder().WithOkColor() + var embed = new EmbedBuilder().WithOkColor() .WithTitle(GetText("trivia_game")) - .WithDescription(GetText("trivia_guess", guildUser.Mention, Format.Bold(CurrentQuestion.Answer))) - .WithImageUrl(CurrentQuestion.AnswerImageUrl)) - .ConfigureAwait(false); + .WithDescription(GetText("trivia_guess", guildUser.Mention, Format.Bold(CurrentQuestion.Answer))); + if (Uri.IsWellFormedUriString(CurrentQuestion.AnswerImageUrl, UriKind.Absolute)) + embed.WithImageUrl(CurrentQuestion.AnswerImageUrl); + await Channel.EmbedAsync(embed).ConfigureAwait(false); } catch (Exception ex) { _log.Warn(ex); } }); diff --git a/src/NadekoBot/Modules/Searches/Commands/LoLCommands.cs b/src/NadekoBot/Modules/Searches/Commands/LoLCommands.cs index 0225725c..bf15651b 100644 --- a/src/NadekoBot/Modules/Searches/Commands/LoLCommands.cs +++ b/src/NadekoBot/Modules/Searches/Commands/LoLCommands.cs @@ -39,16 +39,14 @@ namespace NadekoBot.Modules.Searches { using (var http = new HttpClient()) { - var data = JObject.Parse(await http.GetStringAsync($"http://api.champion.gg/stats/champs/mostBanned?" + - $"api_key={_creds.LoLApiKey}&page=1&" + - $"limit={showCount}") - .ConfigureAwait(false))["data"] as JArray; - var dataList = data.Distinct(new ChampionNameComparer()).Take(showCount).ToList(); - var eb = new EmbedBuilder().WithOkColor().WithTitle(Format.Underline(GetText("x_most_banned_champs",dataList.Count))); - foreach (var champ in dataList) + var data = JArray.Parse(await http.GetStringAsync($"http://api.champion.gg/v2/champions?champData=normalized&limit=200&api_key={_creds.LoLApiKey}")); + + var champs = data.OrderByDescending(x => (float)x["banRate"])/*.Distinct(x => x["championId"])*/.Take(6); + var eb = new EmbedBuilder().WithOkColor().WithTitle(Format.Underline(GetText("x_most_banned_champs", champs.Count()))); + foreach (var champ in champs) { - var champ1 = champ; - eb.AddField(efb => efb.WithName(champ1["name"].ToString()).WithValue(champ1["general"]["banRate"] + "%").WithIsInline(true)); + var lChamp = champ; + eb.AddField(efb => efb.WithName(lChamp["championId"].ToString()).WithValue((float)lChamp["banRate"] * 100 + "%").WithIsInline(true)); } await Context.Channel.EmbedAsync(eb, Format.Italics(trashTalk[new NadekoRandom().Next(0, trashTalk.Length)])).ConfigureAwait(false); diff --git a/src/NadekoBot/Modules/Utility/Commands/InfoCommands.cs b/src/NadekoBot/Modules/Utility/Commands/InfoCommands.cs index e3c625c4..c59017ea 100644 --- a/src/NadekoBot/Modules/Utility/Commands/InfoCommands.cs +++ b/src/NadekoBot/Modules/Utility/Commands/InfoCommands.cs @@ -59,8 +59,9 @@ namespace NadekoBot.Modules.Utility .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)) - .WithImageUrl(guild.IconUrl) .WithColor(NadekoBot.OkColor); + if (Uri.IsWellFormedUriString(guild.IconUrl, UriKind.Absolute)) + embed.WithImageUrl(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.Name}:{e.Id}>")))); From 96ae60378bed80c987a9409fc76a9f0c314acb53 Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Fri, 7 Jul 2017 23:23:10 +0200 Subject: [PATCH 146/346] 1.54b --- src/NadekoBot/Services/Impl/StatsService.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/NadekoBot/Services/Impl/StatsService.cs b/src/NadekoBot/Services/Impl/StatsService.cs index b7490a93..5965bec5 100644 --- a/src/NadekoBot/Services/Impl/StatsService.cs +++ b/src/NadekoBot/Services/Impl/StatsService.cs @@ -17,7 +17,7 @@ namespace NadekoBot.Services.Impl private readonly IBotCredentials _creds; private readonly DateTime _started; - public const string BotVersion = "1.54a"; + public const string BotVersion = "1.54b"; public string Author => "Kwoth#2560"; public string Library => "Discord.Net"; From 413256594888fa484dfa7647a337a1b8155f674a Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Sat, 8 Jul 2017 00:05:11 +0200 Subject: [PATCH 147/346] Fixed footer icon urls --- src/NadekoBot/DataStructures/CREmbed.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/NadekoBot/DataStructures/CREmbed.cs b/src/NadekoBot/DataStructures/CREmbed.cs index b7519ebc..14426310 100644 --- a/src/NadekoBot/DataStructures/CREmbed.cs +++ b/src/NadekoBot/DataStructures/CREmbed.cs @@ -40,7 +40,12 @@ namespace NadekoBot.DataStructures embed.WithDescription(Description); embed.WithColor(new Discord.Color(Color)); if (Footer != null) - embed.WithFooter(efb => efb.WithIconUrl(Footer.IconUrl).WithText(Footer.Text)); + embed.WithFooter(efb => + { + efb.WithText(Footer.Text); + if (Uri.IsWellFormedUriString(Footer.IconUrl, UriKind.Absolute)) + efb.WithIconUrl(Footer.IconUrl); + }); if (Thumbnail != null && Uri.IsWellFormedUriString(Thumbnail, UriKind.Absolute)) embed.WithThumbnailUrl(Thumbnail); From 568cdfbd3ca51d075fc70ee7426850cffd67b500 Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Sat, 8 Jul 2017 13:27:53 +0200 Subject: [PATCH 148/346] Fixed .save --- src/NadekoBot/Modules/Music/Music.cs | 7 +++---- src/NadekoBot/Services/GreetSettingsService.cs | 5 ++++- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/NadekoBot/Modules/Music/Music.cs b/src/NadekoBot/Modules/Music/Music.cs index 3c1b06da..f137e6bf 100644 --- a/src/NadekoBot/Modules/Music/Music.cs +++ b/src/NadekoBot/Modules/Music/Music.cs @@ -466,15 +466,14 @@ namespace NadekoBot.Modules.Music { var mp = await _music.GetOrCreatePlayer(Context); - var songs = await Task.WhenAll(mp.QueueArray().Songs - .Select(async s => new PlaylistSong() + var songs = mp.QueueArray().Songs + .Select(s => new PlaylistSong() { Provider = s.Provider, ProviderType = s.ProviderType, Title = s.Title, - Uri = await s.Uri(), Query = s.Query, - }).ToList()); + }).ToList(); MusicPlaylist playlist; using (var uow = _db.UnitOfWork) diff --git a/src/NadekoBot/Services/GreetSettingsService.cs b/src/NadekoBot/Services/GreetSettingsService.cs index 7e4aaee6..ac3b84da 100644 --- a/src/NadekoBot/Services/GreetSettingsService.cs +++ b/src/NadekoBot/Services/GreetSettingsService.cs @@ -154,7 +154,10 @@ namespace NadekoBot.Services { await channel.EmbedAsync(embedData.ToEmbed(), embedData.PlainText?.SanitizeMentions() ?? "").ConfigureAwait(false); } - catch (Exception ex) { _log.Warn(ex); } + catch (Exception ex) + { + _log.Warn(ex); + } } else { From 72e7b04319b6f66979b9ce929d80c16ed88ff249 Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Sat, 8 Jul 2017 13:42:16 +0200 Subject: [PATCH 149/346] Fixed 100% cpu usage on single threaded systems. Totally my bad. --- src/NadekoBot/Services/Music/MusicPlayer.cs | 304 ++++++++++---------- 1 file changed, 152 insertions(+), 152 deletions(-) diff --git a/src/NadekoBot/Services/Music/MusicPlayer.cs b/src/NadekoBot/Services/Music/MusicPlayer.cs index 49f2c99d..fbb1a56d 100644 --- a/src/NadekoBot/Services/Music/MusicPlayer.cs +++ b/src/NadekoBot/Services/Music/MusicPlayer.cs @@ -160,173 +160,173 @@ namespace NadekoBot.Services.Music manualSkip = false; manualIndex = false; } - if (data.Song == null) - continue; - - _log.Info("Starting"); - using (var b = new SongBuffer(await data.Song.Uri(), "", data.Song.ProviderType == Database.Models.MusicType.Local)) + if (data.Song != null) { - _log.Info("Created buffer, buffering..."); - AudioOutStream pcm = null; + _log.Info("Starting"); + using (var b = new SongBuffer(await data.Song.Uri(), "", data.Song.ProviderType == Database.Models.MusicType.Local)) + { + _log.Info("Created buffer, buffering..."); + AudioOutStream pcm = null; + try + { + var bufferTask = b.StartBuffering(cancelToken); + var timeout = Task.Delay(10000); + if (Task.WhenAny(bufferTask, timeout) == timeout) + { + _log.Info("Buffering failed due to a timeout."); + continue; + } + else if (!bufferTask.Result) + { + _log.Info("Buffering failed due to a cancel or error."); + continue; + } + _log.Info("Buffered. Getting audio client..."); + var ac = await GetAudioClient(); + _log.Info("Got Audio client"); + if (ac == null) + { + _log.Info("Can't join"); + await Task.Delay(900, cancelToken); + // just wait some time, maybe bot doesn't even have perms to join that voice channel, + // i don't want to spam connection attempts + continue; + } + pcm = ac.CreatePCMStream(AudioApplication.Music, bufferMillis: 500); + _log.Info("Created pcm stream"); + OnStarted?.Invoke(this, data); + + byte[] buffer = new byte[3840]; + int bytesRead = 0; + + while ((bytesRead = b.Read(buffer, 0, buffer.Length)) > 0 + && (MaxPlaytimeSeconds <= 0 || MaxPlaytimeSeconds >= CurrentTime.TotalSeconds)) + { + AdjustVolume(buffer, Volume); + await pcm.WriteAsync(buffer, 0, bytesRead, cancelToken).ConfigureAwait(false); + unchecked { _bytesSent += bytesRead; } + + await (pauseTaskSource?.Task ?? Task.CompletedTask); + } + } + catch (OperationCanceledException) + { + _log.Info("Song Canceled"); + cancel = true; + } + catch (Exception ex) + { + _log.Warn(ex); + } + finally + { + if (pcm != null) + { + // flush is known to get stuck from time to time, + // just skip flushing if it takes more than 1 second + var flushCancel = new CancellationTokenSource(); + var flushToken = flushCancel.Token; + var flushDelay = Task.Delay(1000, flushToken); + await Task.WhenAny(flushDelay, pcm.FlushAsync(flushToken)); + flushCancel.Cancel(); + pcm.Dispose(); + } + + OnCompleted?.Invoke(this, data.Song); + + if (_bytesSent == 0 && !cancel) + { + lock (locker) + Queue.RemoveSong(data.Song); + _log.Info("Song removed because it can't play"); + } + } + } try { - var bufferTask = b.StartBuffering(cancelToken); - var timeout = Task.Delay(10000); - if (Task.WhenAny(bufferTask, timeout) == timeout) - { - _log.Info("Buffering failed due to a timeout."); - continue; - } - else if (!bufferTask.Result) - { - _log.Info("Buffering failed due to a cancel or error."); - continue; - } - _log.Info("Buffered. Getting audio client..."); - var ac = await GetAudioClient(); - _log.Info("Got Audio client"); - if (ac == null) - { - _log.Info("Can't join"); - await Task.Delay(900, cancelToken); - // just wait some time, maybe bot doesn't even have perms to join that voice channel, - // i don't want to spam connection attempts - continue; - } - pcm = ac.CreatePCMStream(AudioApplication.Music, bufferMillis: 500); - _log.Info("Created pcm stream"); - OnStarted?.Invoke(this, data); + //if repeating current song, just ignore other settings, + // and play this song again (don't change the index) + // ignore rcs if song is manually skipped - byte[] buffer = new byte[3840]; - int bytesRead = 0; + int queueCount; + lock (locker) + queueCount = Queue.Count; - while ((bytesRead = b.Read(buffer, 0, buffer.Length)) > 0 - && (MaxPlaytimeSeconds <= 0 || MaxPlaytimeSeconds >= CurrentTime.TotalSeconds)) + if (!manualIndex && (!RepeatCurrentSong || manualSkip)) { - AdjustVolume(buffer, Volume); - await pcm.WriteAsync(buffer, 0, bytesRead, cancelToken).ConfigureAwait(false); - unchecked { _bytesSent += bytesRead; } - - await (pauseTaskSource?.Task ?? Task.CompletedTask); - } - } - catch (OperationCanceledException) - { - _log.Info("Song Canceled"); - cancel = true; - } - catch (Exception ex) - { - _log.Warn(ex); - } - finally - { - if (pcm != null) - { - // flush is known to get stuck from time to time, - // just skip flushing if it takes more than 1 second - var flushCancel = new CancellationTokenSource(); - var flushToken = flushCancel.Token; - var flushDelay = Task.Delay(1000, flushToken); - await Task.WhenAny(flushDelay, pcm.FlushAsync(flushToken)); - flushCancel.Cancel(); - pcm.Dispose(); - } - - OnCompleted?.Invoke(this, data.Song); - - if (_bytesSent == 0 && !cancel) - { - lock (locker) - Queue.RemoveSong(data.Song); - _log.Info("Song removed because it can't play"); - } - } - } - try - { - //if repeating current song, just ignore other settings, - // and play this song again (don't change the index) - // ignore rcs if song is manually skipped - - int queueCount; - lock (locker) - queueCount = Queue.Count; - - if (!manualIndex && (!RepeatCurrentSong || manualSkip)) - { - if (Shuffle) - { - _log.Info("Random song"); - Queue.Random(); //if shuffle is set, set current song index to a random number - } - else - { - //if last song, and autoplay is enabled, and if it's a youtube song - // do autplay magix - if (queueCount - 1 == data.Index && Autoplay && data.Song?.ProviderType == Database.Models.MusicType.YouTube) + if (Shuffle) { - try - { - _log.Info("Loading related song"); - await _musicService.TryQueueRelatedSongAsync(data.Song.Query, OutputTextChannel, VoiceChannel); - Queue.Next(); - } - catch - { - _log.Info("Loading related song failed."); - } - } - else if (FairPlay) - { - lock (locker) - { - _log.Info("Next fair song"); - var q = Queue.ToArray().Songs.Shuffle().ToArray(); - - bool found = false; - for (var i = 0; i < q.Length; i++) //first try to find a queuer who didn't have their song played recently - { - var item = q[i]; - if (RecentlyPlayedUsers.Add(item.QueuerName)) // if it's found, set current song to that index - { - Queue.CurrentIndex = i; - found = true; - break; - } - } - if (!found) //if it's not - { - RecentlyPlayedUsers.Clear(); //clear all recently played users (that means everyone from the playlist has had their song played) - Queue.Random(); //go to a random song (to prevent looping on the first few songs) - var cur = Current; - if (cur.Current != null) // add newely scheduled song's queuer to the recently played list - RecentlyPlayedUsers.Add(cur.Current.QueuerName); - } - } - } - else if (queueCount - 1 == data.Index && !RepeatPlaylist && !manualSkip) - { - _log.Info("Stopping because repeatplaylist is disabled"); - lock (locker) - { - Stop(); - } + _log.Info("Random song"); + Queue.Random(); //if shuffle is set, set current song index to a random number } else { - _log.Info("Next song"); - lock (locker) + //if last song, and autoplay is enabled, and if it's a youtube song + // do autplay magix + if (queueCount - 1 == data.Index && Autoplay && data.Song?.ProviderType == Database.Models.MusicType.YouTube) { - Queue.Next(); + try + { + _log.Info("Loading related song"); + await _musicService.TryQueueRelatedSongAsync(data.Song.Query, OutputTextChannel, VoiceChannel); + Queue.Next(); + } + catch + { + _log.Info("Loading related song failed."); + } + } + else if (FairPlay) + { + lock (locker) + { + _log.Info("Next fair song"); + var q = Queue.ToArray().Songs.Shuffle().ToArray(); + + bool found = false; + for (var i = 0; i < q.Length; i++) //first try to find a queuer who didn't have their song played recently + { + var item = q[i]; + if (RecentlyPlayedUsers.Add(item.QueuerName)) // if it's found, set current song to that index + { + Queue.CurrentIndex = i; + found = true; + break; + } + } + if (!found) //if it's not + { + RecentlyPlayedUsers.Clear(); //clear all recently played users (that means everyone from the playlist has had their song played) + Queue.Random(); //go to a random song (to prevent looping on the first few songs) + var cur = Current; + if (cur.Current != null) // add newely scheduled song's queuer to the recently played list + RecentlyPlayedUsers.Add(cur.Current.QueuerName); + } + } + } + else if (queueCount - 1 == data.Index && !RepeatPlaylist && !manualSkip) + { + _log.Info("Stopping because repeatplaylist is disabled"); + lock (locker) + { + Stop(); + } + } + else + { + _log.Info("Next song"); + lock (locker) + { + Queue.Next(); + } } } } } - } - catch (Exception ex) - { - _log.Error(ex); + catch (Exception ex) + { + _log.Error(ex); + } } do { From aa01314b3a8abcfaab62f16d7e7b12e798185d08 Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Sat, 8 Jul 2017 14:20:06 +0200 Subject: [PATCH 150/346] Embed field names and values will be trimmed to their acceptable length to prevent errors. closes #1355 --- src/NadekoBot/DataStructures/CREmbed.cs | 9 ++++++++- src/NadekoBot/Modules/CustomReactions/CustomReactions.cs | 2 +- src/NadekoBot/Modules/Searches/Commands/LoLCommands.cs | 2 +- src/NadekoBot/_strings/ResponseStrings.en-US.json | 1 + 4 files changed, 11 insertions(+), 3 deletions(-) diff --git a/src/NadekoBot/DataStructures/CREmbed.cs b/src/NadekoBot/DataStructures/CREmbed.cs index 14426310..4fdf78e3 100644 --- a/src/NadekoBot/DataStructures/CREmbed.cs +++ b/src/NadekoBot/DataStructures/CREmbed.cs @@ -1,4 +1,5 @@ using Discord; +using NadekoBot.Extensions; using Newtonsoft.Json; using NLog; using System; @@ -71,7 +72,13 @@ namespace NadekoBot.DataStructures try { var crembed = JsonConvert.DeserializeObject(input); - + + if(crembed.Fields != null && crembed.Fields.Length > 0) + foreach (var f in crembed.Fields) + { + f.Name = f.Name.TrimTo(256); + f.Value = f.Value.TrimTo(1024); + } if (!crembed.IsValid) return false; diff --git a/src/NadekoBot/Modules/CustomReactions/CustomReactions.cs b/src/NadekoBot/Modules/CustomReactions/CustomReactions.cs index ae033cad..dd6b162d 100644 --- a/src/NadekoBot/Modules/CustomReactions/CustomReactions.cs +++ b/src/NadekoBot/Modules/CustomReactions/CustomReactions.cs @@ -79,7 +79,7 @@ namespace NadekoBot.Modules.CustomReactions .WithTitle(GetText("new_cust_react")) .WithDescription($"#{cr.Id}") .AddField(efb => efb.WithName(GetText("trigger")).WithValue(key)) - .AddField(efb => efb.WithName(GetText("response")).WithValue(message)) + .AddField(efb => efb.WithName(GetText("response")).WithValue(message.Length > 1024 ? GetText("redacted_too_long") : message)) ).ConfigureAwait(false); } diff --git a/src/NadekoBot/Modules/Searches/Commands/LoLCommands.cs b/src/NadekoBot/Modules/Searches/Commands/LoLCommands.cs index bf15651b..91282423 100644 --- a/src/NadekoBot/Modules/Searches/Commands/LoLCommands.cs +++ b/src/NadekoBot/Modules/Searches/Commands/LoLCommands.cs @@ -33,7 +33,7 @@ namespace NadekoBot.Modules.Searches [NadekoCommand, Usage, Description, Aliases] public async Task Lolban() { - const int showCount = 8; + //const int showCount = 8; //http://api.champion.gg/stats/champs/mostBanned?api_key=YOUR_API_TOKEN&page=1&limit=2 try { diff --git a/src/NadekoBot/_strings/ResponseStrings.en-US.json b/src/NadekoBot/_strings/ResponseStrings.en-US.json index 06d62f39..7c330d6d 100644 --- a/src/NadekoBot/_strings/ResponseStrings.en-US.json +++ b/src/NadekoBot/_strings/ResponseStrings.en-US.json @@ -35,6 +35,7 @@ "customreactions_stats_cleared": "Stats cleared for {0} custom reaction.", "customreactions_stats_not_found": "No stats for that trigger found, no action taken.", "customreactions_trigger": "Trigger", + "customreactions_redacted_too_long": "Redecated because it's too long.", "nsfw_autohentai_stopped": "Autohentai stopped.", "nsfw_not_found": "No results found.", "pokemon_already_fainted": "{0} has already fainted.", From 27f925fa63c661ec2953bbc63e1a3ee79ffde802 Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Sat, 8 Jul 2017 14:54:19 +0200 Subject: [PATCH 151/346] Fixed userpresence and game changes readded to userpresence --- .../Administration/LogCommandService.cs | 80 ++++++++++++------- 1 file changed, 50 insertions(+), 30 deletions(-) diff --git a/src/NadekoBot/Services/Administration/LogCommandService.cs b/src/NadekoBot/Services/Administration/LogCommandService.cs index dcd00a6f..a34ff7d2 100644 --- a/src/NadekoBot/Services/Administration/LogCommandService.cs +++ b/src/NadekoBot/Services/Administration/LogCommandService.cs @@ -66,14 +66,15 @@ namespace NadekoBot.Services.Administration { var keys = PresenceUpdates.Keys.ToList(); - await Task.WhenAll(keys.Select(async key => + await Task.WhenAll(keys.Select(key => { - if (PresenceUpdates.TryRemove(key, out List messages)) - try { await key.SendConfirmAsync(GetText(key.Guild, "presence_updates"), string.Join(Environment.NewLine, messages)); } - catch - { - // ignored - } + if (PresenceUpdates.TryRemove(key, out var msgs)) + { + var title = GetText(key.Guild, "presence_updates"); + var desc = string.Join(Environment.NewLine, msgs); + return key.SendConfirmAsync(title, desc.TrimTo(2048)); + } + return Task.CompletedTask; })); } catch (Exception ex) @@ -369,40 +370,59 @@ namespace NadekoBot.Services.Administration { try { - if (!GuildLogSettings.TryGetValue(before.Guild.Id, out LogSetting logSetting) - || (logSetting.UserUpdatedId == null)) + if (!GuildLogSettings.TryGetValue(before.Guild.Id, out LogSetting logSetting)) return; ITextChannel logChannel; - if ((logChannel = await TryGetLogChannel(before.Guild, logSetting, LogType.UserUpdated)) == null) - return; - var embed = new EmbedBuilder().WithOkColor().WithFooter(efb => efb.WithText(CurrentTime(before.Guild))) - .WithTitle($"{before.Username}#{before.Discriminator} | {before.Id}"); - if (before.Nickname != after.Nickname) + if (logSetting.UserUpdatedId != null && (logChannel = await TryGetLogChannel(before.Guild, logSetting, LogType.UserUpdated)) != null) { - embed.WithAuthor(eab => eab.WithName("👥 " + GetText(logChannel.Guild, "nick_change"))) + var embed = new EmbedBuilder().WithOkColor().WithFooter(efb => efb.WithText(CurrentTime(before.Guild))) + .WithTitle($"{before.Username}#{before.Discriminator} | {before.Id}"); + if (before.Nickname != after.Nickname) + { + embed.WithAuthor(eab => eab.WithName("👥 " + GetText(logChannel.Guild, "nick_change"))) + .AddField(efb => efb.WithName(GetText(logChannel.Guild, "old_nick")).WithValue($"{before.Nickname}#{before.Discriminator}")) + .AddField(efb => efb.WithName(GetText(logChannel.Guild, "new_nick")).WithValue($"{after.Nickname}#{after.Discriminator}")); - .AddField(efb => efb.WithName(GetText(logChannel.Guild, "old_nick")).WithValue($"{before.Nickname}#{before.Discriminator}")) - .AddField(efb => efb.WithName(GetText(logChannel.Guild, "new_nick")).WithValue($"{after.Nickname}#{after.Discriminator}")); + await logChannel.EmbedAsync(embed).ConfigureAwait(false); + } + else if (!before.Roles.SequenceEqual(after.Roles)) + { + if (before.Roles.Count < after.Roles.Count) + { + var diffRoles = after.Roles.Where(r => !before.Roles.Contains(r)).Select(r => r.Name); + embed.WithAuthor(eab => eab.WithName("⚔ " + GetText(logChannel.Guild, "user_role_add"))) + .WithDescription(string.Join(", ", diffRoles).SanitizeMentions()); + } + else if (before.Roles.Count > after.Roles.Count) + { + var diffRoles = before.Roles.Where(r => !after.Roles.Contains(r)).Select(r => r.Name); + embed.WithAuthor(eab => eab.WithName("⚔ " + GetText(logChannel.Guild, "user_role_rem"))) + .WithDescription(string.Join(", ", diffRoles).SanitizeMentions()); + } + await logChannel.EmbedAsync(embed).ConfigureAwait(false); + } } - else if (!before.Roles.SequenceEqual(after.Roles)) + + logChannel = null; + if (logSetting.LogUserPresenceId != null && (logChannel = await TryGetLogChannel(before.Guild, logSetting, LogType.UserPresence)) != null) { - if (before.Roles.Count < after.Roles.Count) + if (before.Status != after.Status) { - var diffRoles = after.Roles.Where(r => !before.Roles.Contains(r)).Select(r => r.Name); - embed.WithAuthor(eab => eab.WithName("⚔ " + GetText(logChannel.Guild, "user_role_add"))) - .WithDescription(string.Join(", ", diffRoles).SanitizeMentions()); + var str = "🎭" + Format.Code(PrettyCurrentTime(after.Guild)) + + GetText(logChannel.Guild, "user_status_change", + "👤" + Format.Bold(after.Username), + Format.Bold(after.Status.ToString())); + PresenceUpdates.AddOrUpdate(logChannel, + new List() { str }, (id, list) => { list.Add(str); return list; }); } - else if (before.Roles.Count > after.Roles.Count) + else if (before.Game?.Name != after.Game?.Name) { - var diffRoles = before.Roles.Where(r => !after.Roles.Contains(r)).Select(r => r.Name); - embed.WithAuthor(eab => eab.WithName("⚔ " + GetText(logChannel.Guild, "user_role_rem"))) - .WithDescription(string.Join(", ", diffRoles).SanitizeMentions()); + var str = $"👾`{PrettyCurrentTime(after.Guild)}`👤__**{after.Username}**__ is now playing **{after.Game?.Name ?? "-"}**."; + PresenceUpdates.AddOrUpdate(logChannel, + new List() { str }, (id, list) => { list.Add(str); return list; }); } } - else - return; - await logChannel.EmbedAsync(embed).ConfigureAwait(false); } catch { @@ -580,7 +600,7 @@ namespace NadekoBot.Services.Administration "👤" + Format.Bold(usr.Username + "#" + usr.Discriminator), Format.Bold(beforeVch.Name ?? "")); } - if (str != null) + if (!string.IsNullOrWhiteSpace(str)) PresenceUpdates.AddOrUpdate(logChannel, new List() { str }, (id, list) => { list.Add(str); return list; }); } catch From f58e7ed7acd88ebd566a4e92bcae0dc8c891e251 Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Sat, 8 Jul 2017 15:27:48 +0200 Subject: [PATCH 152/346] Fixed .warnlogall, updated commandlist --- .../Modules/Administration/Commands/UserPunishCommands.cs | 2 +- .../Services/Database/Repositories/IWarningsRepository.cs | 1 + .../Database/Repositories/Impl/WarningsRepository.cs | 6 ++++++ src/NadekoBot/Services/Impl/StatsService.cs | 2 +- 4 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/NadekoBot/Modules/Administration/Commands/UserPunishCommands.cs b/src/NadekoBot/Modules/Administration/Commands/UserPunishCommands.cs index 9e7df4ec..79786bcf 100644 --- a/src/NadekoBot/Modules/Administration/Commands/UserPunishCommands.cs +++ b/src/NadekoBot/Modules/Administration/Commands/UserPunishCommands.cs @@ -204,7 +204,7 @@ namespace NadekoBot.Modules.Administration IGrouping[] warnings; using (var uow = _db.UnitOfWork) { - warnings = uow.Warnings.GetAll().GroupBy(x => x.UserId).ToArray(); + warnings = uow.Warnings.GetForGuild(Context.Guild.Id).GroupBy(x => x.UserId).ToArray(); } await Context.Channel.SendPaginatedConfirmAsync((DiscordSocketClient)Context.Client, page, async (curPage) => diff --git a/src/NadekoBot/Services/Database/Repositories/IWarningsRepository.cs b/src/NadekoBot/Services/Database/Repositories/IWarningsRepository.cs index 2b2e5ac4..f8c8296e 100644 --- a/src/NadekoBot/Services/Database/Repositories/IWarningsRepository.cs +++ b/src/NadekoBot/Services/Database/Repositories/IWarningsRepository.cs @@ -7,5 +7,6 @@ namespace NadekoBot.Services.Database.Repositories { Warning[] For(ulong guildId, ulong userId); Task ForgiveAll(ulong guildId, ulong userId, string mod); + Warning[] GetForGuild(ulong id); } } diff --git a/src/NadekoBot/Services/Database/Repositories/Impl/WarningsRepository.cs b/src/NadekoBot/Services/Database/Repositories/Impl/WarningsRepository.cs index aa5a6d7e..ef27ebd9 100644 --- a/src/NadekoBot/Services/Database/Repositories/Impl/WarningsRepository.cs +++ b/src/NadekoBot/Services/Database/Repositories/Impl/WarningsRepository.cs @@ -2,6 +2,7 @@ using Microsoft.EntityFrameworkCore; using System.Linq; using System.Threading.Tasks; +using System; namespace NadekoBot.Services.Database.Repositories.Impl { @@ -32,5 +33,10 @@ namespace NadekoBot.Services.Database.Repositories.Impl }) .ConfigureAwait(false); } + + public Warning[] GetForGuild(ulong id) + { + return _set.Where(x => x.GuildId == id).ToArray(); + } } } diff --git a/src/NadekoBot/Services/Impl/StatsService.cs b/src/NadekoBot/Services/Impl/StatsService.cs index 5965bec5..c5ef6ad8 100644 --- a/src/NadekoBot/Services/Impl/StatsService.cs +++ b/src/NadekoBot/Services/Impl/StatsService.cs @@ -17,7 +17,7 @@ namespace NadekoBot.Services.Impl private readonly IBotCredentials _creds; private readonly DateTime _started; - public const string BotVersion = "1.54b"; + public const string BotVersion = "1.55"; public string Author => "Kwoth#2560"; public string Library => "Discord.Net"; From 9eab7d949f518c0b8d89eef7b8d51fd29b3db0a3 Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Sun, 9 Jul 2017 00:56:12 +0200 Subject: [PATCH 153/346] Possibly fix youtube-dl certificate error --- src/NadekoBot/Services/Impl/Ytdl.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/NadekoBot/Services/Impl/Ytdl.cs b/src/NadekoBot/Services/Impl/Ytdl.cs index 5fce2bba..c2c102d8 100644 --- a/src/NadekoBot/Services/Impl/Ytdl.cs +++ b/src/NadekoBot/Services/Impl/Ytdl.cs @@ -21,7 +21,7 @@ namespace NadekoBot.Services.Impl StartInfo = new ProcessStartInfo() { FileName = "youtube-dl", - Arguments = $"-f bestaudio -e --get-url --get-id --get-thumbnail --get-duration \"ytsearch:{url}\"", + Arguments = $"-f bestaudio -e --get-url --get-id --get-thumbnail --get-duration --no-check-certificate \"ytsearch:{url}\"", UseShellExecute = false, RedirectStandardError = true, RedirectStandardOutput = true, From fad0b908c825424afb1863b39468967d111a9b7f Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Sun, 9 Jul 2017 01:37:17 +0200 Subject: [PATCH 154/346] fixed .autoplay --- src/NadekoBot/Services/Music/MusicPlayer.cs | 2 +- src/NadekoBot/Services/Music/MusicService.cs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/NadekoBot/Services/Music/MusicPlayer.cs b/src/NadekoBot/Services/Music/MusicPlayer.cs index fbb1a56d..1c697e57 100644 --- a/src/NadekoBot/Services/Music/MusicPlayer.cs +++ b/src/NadekoBot/Services/Music/MusicPlayer.cs @@ -268,7 +268,7 @@ namespace NadekoBot.Services.Music try { _log.Info("Loading related song"); - await _musicService.TryQueueRelatedSongAsync(data.Song.Query, OutputTextChannel, VoiceChannel); + await _musicService.TryQueueRelatedSongAsync(data.Song, OutputTextChannel, VoiceChannel); Queue.Next(); } catch diff --git a/src/NadekoBot/Services/Music/MusicService.cs b/src/NadekoBot/Services/Music/MusicService.cs index 6a29dffe..cfae3e60 100644 --- a/src/NadekoBot/Services/Music/MusicService.cs +++ b/src/NadekoBot/Services/Music/MusicService.cs @@ -182,9 +182,9 @@ namespace NadekoBot.Services.Music return null; } - public async Task TryQueueRelatedSongAsync(string query, ITextChannel txtCh, IVoiceChannel vch) + public async Task TryQueueRelatedSongAsync(SongInfo song, ITextChannel txtCh, IVoiceChannel vch) { - var related = (await _google.GetRelatedVideosAsync(query, 4)).ToArray(); + var related = (await _google.GetRelatedVideosAsync(song.VideoId, 4)).ToArray(); if (!related.Any()) return; From f1a4a88730d3e941207bb05e7bc85b4e8dd06082 Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Sun, 9 Jul 2017 01:39:47 +0200 Subject: [PATCH 155/346] Fixed song time on songs shorter than 1 minute --- src/NadekoBot/Services/Music/MusicService.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/NadekoBot/Services/Music/MusicService.cs b/src/NadekoBot/Services/Music/MusicService.cs index cfae3e60..7db9de4e 100644 --- a/src/NadekoBot/Services/Music/MusicService.cs +++ b/src/NadekoBot/Services/Music/MusicService.cs @@ -341,7 +341,7 @@ namespace NadekoBot.Services.Music return null; } TimeSpan time; - if (!TimeSpan.TryParseExact(data[4], new[] { "m\\:ss", "mm\\:ss", "h\\:mm\\:ss", "hh\\:mm\\:ss", "hhh\\:mm\\:ss" }, CultureInfo.InvariantCulture, out time)) + if (!TimeSpan.TryParseExact(data[4], new[] { "ss", "m\\:ss", "mm\\:ss", "h\\:mm\\:ss", "hh\\:mm\\:ss", "hhh\\:mm\\:ss" }, CultureInfo.InvariantCulture, out time)) time = TimeSpan.FromHours(24); return new SongInfo() From 39666292631336db1a248117ca47aa27d84cd167 Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Sun, 9 Jul 2017 01:40:54 +0200 Subject: [PATCH 156/346] Upped version --- src/NadekoBot/Services/Impl/StatsService.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/NadekoBot/Services/Impl/StatsService.cs b/src/NadekoBot/Services/Impl/StatsService.cs index c5ef6ad8..4cd518c5 100644 --- a/src/NadekoBot/Services/Impl/StatsService.cs +++ b/src/NadekoBot/Services/Impl/StatsService.cs @@ -17,7 +17,7 @@ namespace NadekoBot.Services.Impl private readonly IBotCredentials _creds; private readonly DateTime _started; - public const string BotVersion = "1.55"; + public const string BotVersion = "1.55.3"; public string Author => "Kwoth#2560"; public string Library => "Discord.Net"; From 9d778de7f2804a8f3090ec515d63a79168ab9708 Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Sun, 9 Jul 2017 12:43:14 +0200 Subject: [PATCH 157/346] fixed .ropl with %queued% and %playing% --- src/NadekoBot/Services/Administration/PlayingRotateService.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/NadekoBot/Services/Administration/PlayingRotateService.cs b/src/NadekoBot/Services/Administration/PlayingRotateService.cs index aa9321c6..78e946c8 100644 --- a/src/NadekoBot/Services/Administration/PlayingRotateService.cs +++ b/src/NadekoBot/Services/Administration/PlayingRotateService.cs @@ -34,7 +34,7 @@ namespace NadekoBot.Services.Administration _rep = new ReplacementBuilder() .WithClient(client) .WithStats(client) - //.WithMusic(music) + .WithMusic(music) .Build(); _t = new Timer(async (objState) => From f396fc78dbf76911ef3b33fbd01cf6487ab70e4a Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Sun, 9 Jul 2017 19:26:17 +0200 Subject: [PATCH 158/346] Fixed shard number bold in unresponsive shard --- src/NadekoBot/Modules/Utility/Utility.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/NadekoBot/Modules/Utility/Utility.cs b/src/NadekoBot/Modules/Utility/Utility.cs index 02d93d2c..ea76c755 100644 --- a/src/NadekoBot/Modules/Utility/Utility.cs +++ b/src/NadekoBot/Modules/Utility/Utility.cs @@ -298,7 +298,7 @@ namespace NadekoBot.Modules.Utility { var timeDiff = DateTime.UtcNow - x.Time; if (timeDiff > TimeSpan.FromSeconds(20)) - return $"Shard #{x.ShardId.ToString()} **UNRESPONSIVE** for {timeDiff.ToString(@"hh\:mm\:ss")}"; + return $"Shard #{Format.Bold(x.ShardId.ToString())} **UNRESPONSIVE** for {timeDiff.ToString(@"hh\:mm\:ss")}"; return GetText("shard_stats_txt", x.ShardId.ToString(), Format.Bold(x.ConnectionState.ToString()), Format.Bold(x.Guilds.ToString()), timeDiff.ToString(@"hh\:mm\:ss")); }) From 5db254e8f3714bfd213e7c97bf0d82ef02f00774 Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Mon, 10 Jul 2017 19:40:34 +0200 Subject: [PATCH 159/346] Fixed .lolban, closes #1340 --- .../Modules/Searches/Commands/LoLCommands.cs | 16 +- src/NadekoBot/data/lolchamps.json | 818 ++++++++++++++++++ 2 files changed, 830 insertions(+), 4 deletions(-) create mode 100644 src/NadekoBot/data/lolchamps.json diff --git a/src/NadekoBot/Modules/Searches/Commands/LoLCommands.cs b/src/NadekoBot/Modules/Searches/Commands/LoLCommands.cs index 91282423..9e1e1c43 100644 --- a/src/NadekoBot/Modules/Searches/Commands/LoLCommands.cs +++ b/src/NadekoBot/Modules/Searches/Commands/LoLCommands.cs @@ -2,9 +2,11 @@ using NadekoBot.Attributes; using NadekoBot.Extensions; using NadekoBot.Services; +using Newtonsoft.Json; using Newtonsoft.Json.Linq; using System; using System.Collections.Generic; +using System.IO; using System.Linq; using System.Net.Http; using System.Threading.Tasks; @@ -29,24 +31,30 @@ namespace NadekoBot.Modules.Searches "If you consider playing teemo, do it. If you consider teemo, you deserve him.", "Doesn't matter what you ban really. Enemy will ban your main and you will lose." }; + private static readonly Lazy> champData = new Lazy>(() => + ((IDictionary)JObject.Parse(File.ReadAllText("data/lolchamps.json"))) + .ToDictionary(x => (int)x.Value["id"], x => x.Value["name"].ToString()), true); [NadekoCommand, Usage, Description, Aliases] public async Task Lolban() { //const int showCount = 8; - //http://api.champion.gg/stats/champs/mostBanned?api_key=YOUR_API_TOKEN&page=1&limit=2 try { using (var http = new HttpClient()) { - var data = JArray.Parse(await http.GetStringAsync($"http://api.champion.gg/v2/champions?champData=normalized&limit=200&api_key={_creds.LoLApiKey}")); + var data = JArray.Parse(await http.GetStringAsync($"http://api.champion.gg/v2/champions?champData=general&limit=200&api_key={_creds.LoLApiKey}")); - var champs = data.OrderByDescending(x => (float)x["banRate"])/*.Distinct(x => x["championId"])*/.Take(6); + var champs = data.OrderByDescending(x => (double)x["banRate"]).Distinct(x => x["championId"]).Take(6); + + //_log.Info(string.Join("\n", champs.Select(x => x["championId"] + " | " + x["banRate"] + " | " + x["normalized"]["banRate"]))); var eb = new EmbedBuilder().WithOkColor().WithTitle(Format.Underline(GetText("x_most_banned_champs", champs.Count()))); foreach (var champ in champs) { var lChamp = champ; - eb.AddField(efb => efb.WithName(lChamp["championId"].ToString()).WithValue((float)lChamp["banRate"] * 100 + "%").WithIsInline(true)); + if (!champData.Value.TryGetValue((int)champ["championId"], out var champName)) + champName = "UNKNOWN"; + eb.AddField(efb => efb.WithName(champName).WithValue(((double)lChamp["banRate"] * 100).ToString("F2") + "%").WithIsInline(true)); } await Context.Channel.EmbedAsync(eb, Format.Italics(trashTalk[new NadekoRandom().Next(0, trashTalk.Length)])).ConfigureAwait(false); diff --git a/src/NadekoBot/data/lolchamps.json b/src/NadekoBot/data/lolchamps.json new file mode 100644 index 00000000..df7ab4d2 --- /dev/null +++ b/src/NadekoBot/data/lolchamps.json @@ -0,0 +1,818 @@ +{ + "Aatrox": { + "id": 266, + "key": "Aatrox", + "name": "Aatrox", + "title": "the Darkin Blade" + }, + "Ahri": { + "id": 103, + "key": "Ahri", + "name": "Ahri", + "title": "the Nine-Tailed Fox" + }, + "Akali": { + "id": 84, + "key": "Akali", + "name": "Akali", + "title": "the Fist of Shadow" + }, + "Alistar": { + "id": 12, + "key": "Alistar", + "name": "Alistar", + "title": "the Minotaur" + }, + "Amumu": { + "id": 32, + "key": "Amumu", + "name": "Amumu", + "title": "the Sad Mummy" + }, + "Anivia": { + "id": 34, + "key": "Anivia", + "name": "Anivia", + "title": "the Cryophoenix" + }, + "Annie": { + "id": 1, + "key": "Annie", + "name": "Annie", + "title": "the Dark Child" + }, + "Ashe": { + "id": 22, + "key": "Ashe", + "name": "Ashe", + "title": "the Frost Archer" + }, + "AurelionSol": { + "id": 136, + "key": "AurelionSol", + "name": "Aurelion Sol", + "title": "The Star Forger" + }, + "Azir": { + "id": 268, + "key": "Azir", + "name": "Azir", + "title": "the Emperor of the Sands" + }, + "Bard": { + "id": 432, + "key": "Bard", + "name": "Bard", + "title": "the Wandering Caretaker" + }, + "Blitzcrank": { + "id": 53, + "key": "Blitzcrank", + "name": "Blitzcrank", + "title": "the Great Steam Golem" + }, + "Brand": { + "id": 63, + "key": "Brand", + "name": "Brand", + "title": "the Burning Vengeance" + }, + "Braum": { + "id": 201, + "key": "Braum", + "name": "Braum", + "title": "the Heart of the Freljord" + }, + "Caitlyn": { + "id": 51, + "key": "Caitlyn", + "name": "Caitlyn", + "title": "the Sheriff of Piltover" + }, + "Camille": { + "id": 164, + "key": "Camille", + "name": "Camille", + "title": "the Steel Shadow" + }, + "Cassiopeia": { + "id": 69, + "key": "Cassiopeia", + "name": "Cassiopeia", + "title": "the Serpent's Embrace" + }, + "Chogath": { + "id": 31, + "key": "Chogath", + "name": "Cho'Gath", + "title": "the Terror of the Void" + }, + "Corki": { + "id": 42, + "key": "Corki", + "name": "Corki", + "title": "the Daring Bombardier" + }, + "Darius": { + "id": 122, + "key": "Darius", + "name": "Darius", + "title": "the Hand of Noxus" + }, + "Diana": { + "id": 131, + "key": "Diana", + "name": "Diana", + "title": "Scorn of the Moon" + }, + "Draven": { + "id": 119, + "key": "Draven", + "name": "Draven", + "title": "the Glorious Executioner" + }, + "DrMundo": { + "id": 36, + "key": "DrMundo", + "name": "Dr. Mundo", + "title": "the Madman of Zaun" + }, + "Ekko": { + "id": 245, + "key": "Ekko", + "name": "Ekko", + "title": "the Boy Who Shattered Time" + }, + "Elise": { + "id": 60, + "key": "Elise", + "name": "Elise", + "title": "the Spider Queen" + }, + "Evelynn": { + "id": 28, + "key": "Evelynn", + "name": "Evelynn", + "title": "the Widowmaker" + }, + "Ezreal": { + "id": 81, + "key": "Ezreal", + "name": "Ezreal", + "title": "the Prodigal Explorer" + }, + "Fiddlesticks": { + "id": 9, + "key": "Fiddlesticks", + "name": "Fiddlesticks", + "title": "the Harbinger of Doom" + }, + "Fiora": { + "id": 114, + "key": "Fiora", + "name": "Fiora", + "title": "the Grand Duelist" + }, + "Fizz": { + "id": 105, + "key": "Fizz", + "name": "Fizz", + "title": "the Tidal Trickster" + }, + "Galio": { + "id": 3, + "key": "Galio", + "name": "Galio", + "title": "the Colossus" + }, + "Gangplank": { + "id": 41, + "key": "Gangplank", + "name": "Gangplank", + "title": "the Saltwater Scourge" + }, + "Garen": { + "id": 86, + "key": "Garen", + "name": "Garen", + "title": "The Might of Demacia" + }, + "Gnar": { + "id": 150, + "key": "Gnar", + "name": "Gnar", + "title": "the Missing Link" + }, + "Gragas": { + "id": 79, + "key": "Gragas", + "name": "Gragas", + "title": "the Rabble Rouser" + }, + "Graves": { + "id": 104, + "key": "Graves", + "name": "Graves", + "title": "the Outlaw" + }, + "Hecarim": { + "id": 120, + "key": "Hecarim", + "name": "Hecarim", + "title": "the Shadow of War" + }, + "Heimerdinger": { + "id": 74, + "key": "Heimerdinger", + "name": "Heimerdinger", + "title": "the Revered Inventor" + }, + "Illaoi": { + "id": 420, + "key": "Illaoi", + "name": "Illaoi", + "title": "the Kraken Priestess" + }, + "Irelia": { + "id": 39, + "key": "Irelia", + "name": "Irelia", + "title": "the Will of the Blades" + }, + "Ivern": { + "id": 427, + "key": "Ivern", + "name": "Ivern", + "title": "the Green Father" + }, + "Janna": { + "id": 40, + "key": "Janna", + "name": "Janna", + "title": "the Storm's Fury" + }, + "JarvanIV": { + "id": 59, + "key": "JarvanIV", + "name": "Jarvan IV", + "title": "the Exemplar of Demacia" + }, + "Jax": { + "id": 24, + "key": "Jax", + "name": "Jax", + "title": "Grandmaster at Arms" + }, + "Jayce": { + "id": 126, + "key": "Jayce", + "name": "Jayce", + "title": "the Defender of Tomorrow" + }, + "Jhin": { + "id": 202, + "key": "Jhin", + "name": "Jhin", + "title": "the Virtuoso" + }, + "Jinx": { + "id": 222, + "key": "Jinx", + "name": "Jinx", + "title": "the Loose Cannon" + }, + "Kalista": { + "id": 429, + "key": "Kalista", + "name": "Kalista", + "title": "the Spear of Vengeance" + }, + "Karma": { + "id": 43, + "key": "Karma", + "name": "Karma", + "title": "the Enlightened One" + }, + "Karthus": { + "id": 30, + "key": "Karthus", + "name": "Karthus", + "title": "the Deathsinger" + }, + "Kassadin": { + "id": 38, + "key": "Kassadin", + "name": "Kassadin", + "title": "the Void Walker" + }, + "Katarina": { + "id": 55, + "key": "Katarina", + "name": "Katarina", + "title": "the Sinister Blade" + }, + "Kayle": { + "id": 10, + "key": "Kayle", + "name": "Kayle", + "title": "The Judicator" + }, + "Kennen": { + "id": 85, + "key": "Kennen", + "name": "Kennen", + "title": "the Heart of the Tempest" + }, + "Khazix": { + "id": 121, + "key": "Khazix", + "name": "Kha'Zix", + "title": "the Voidreaver" + }, + "Kindred": { + "id": 203, + "key": "Kindred", + "name": "Kindred", + "title": "The Eternal Hunters" + }, + "Kled": { + "id": 240, + "key": "Kled", + "name": "Kled", + "title": "the Cantankerous Cavalier" + }, + "KogMaw": { + "id": 96, + "key": "KogMaw", + "name": "Kog'Maw", + "title": "the Mouth of the Abyss" + }, + "Leblanc": { + "id": 7, + "key": "Leblanc", + "name": "LeBlanc", + "title": "the Deceiver" + }, + "LeeSin": { + "id": 64, + "key": "LeeSin", + "name": "Lee Sin", + "title": "the Blind Monk" + }, + "Leona": { + "id": 89, + "key": "Leona", + "name": "Leona", + "title": "the Radiant Dawn" + }, + "Lissandra": { + "id": 127, + "key": "Lissandra", + "name": "Lissandra", + "title": "the Ice Witch" + }, + "Lucian": { + "id": 236, + "key": "Lucian", + "name": "Lucian", + "title": "the Purifier" + }, + "Lulu": { + "id": 117, + "key": "Lulu", + "name": "Lulu", + "title": "the Fae Sorceress" + }, + "Lux": { + "id": 99, + "key": "Lux", + "name": "Lux", + "title": "the Lady of Luminosity" + }, + "Malphite": { + "id": 54, + "key": "Malphite", + "name": "Malphite", + "title": "Shard of the Monolith" + }, + "Malzahar": { + "id": 90, + "key": "Malzahar", + "name": "Malzahar", + "title": "the Prophet of the Void" + }, + "Maokai": { + "id": 57, + "key": "Maokai", + "name": "Maokai", + "title": "the Twisted Treant" + }, + "MasterYi": { + "id": 11, + "key": "MasterYi", + "name": "Master Yi", + "title": "the Wuju Bladesman" + }, + "MissFortune": { + "id": 21, + "key": "MissFortune", + "name": "Miss Fortune", + "title": "the Bounty Hunter" + }, + "MonkeyKing": { + "id": 62, + "key": "MonkeyKing", + "name": "Wukong", + "title": "the Monkey King" + }, + "Mordekaiser": { + "id": 82, + "key": "Mordekaiser", + "name": "Mordekaiser", + "title": "the Iron Revenant" + }, + "Morgana": { + "id": 25, + "key": "Morgana", + "name": "Morgana", + "title": "Fallen Angel" + }, + "Nami": { + "id": 267, + "key": "Nami", + "name": "Nami", + "title": "the Tidecaller" + }, + "Nasus": { + "id": 75, + "key": "Nasus", + "name": "Nasus", + "title": "the Curator of the Sands" + }, + "Nautilus": { + "id": 111, + "key": "Nautilus", + "name": "Nautilus", + "title": "the Titan of the Depths" + }, + "Nidalee": { + "id": 76, + "key": "Nidalee", + "name": "Nidalee", + "title": "the Bestial Huntress" + }, + "Nocturne": { + "id": 56, + "key": "Nocturne", + "name": "Nocturne", + "title": "the Eternal Nightmare" + }, + "Nunu": { + "id": 20, + "key": "Nunu", + "name": "Nunu", + "title": "the Yeti Rider" + }, + "Olaf": { + "id": 2, + "key": "Olaf", + "name": "Olaf", + "title": "the Berserker" + }, + "Orianna": { + "id": 61, + "key": "Orianna", + "name": "Orianna", + "title": "the Lady of Clockwork" + }, + "Pantheon": { + "id": 80, + "key": "Pantheon", + "name": "Pantheon", + "title": "the Artisan of War" + }, + "Poppy": { + "id": 78, + "key": "Poppy", + "name": "Poppy", + "title": "Keeper of the Hammer" + }, + "Quinn": { + "id": 133, + "key": "Quinn", + "name": "Quinn", + "title": "Demacia's Wings" + }, + "Rakan": { + "id": 497, + "key": "Rakan", + "name": "Rakan", + "title": "The Charmer" + }, + "Rammus": { + "id": 33, + "key": "Rammus", + "name": "Rammus", + "title": "the Armordillo" + }, + "RekSai": { + "id": 421, + "key": "RekSai", + "name": "Rek'Sai", + "title": "the Void Burrower" + }, + "Renekton": { + "id": 58, + "key": "Renekton", + "name": "Renekton", + "title": "the Butcher of the Sands" + }, + "Rengar": { + "id": 107, + "key": "Rengar", + "name": "Rengar", + "title": "the Pridestalker" + }, + "Riven": { + "id": 92, + "key": "Riven", + "name": "Riven", + "title": "the Exile" + }, + "Rumble": { + "id": 68, + "key": "Rumble", + "name": "Rumble", + "title": "the Mechanized Menace" + }, + "Ryze": { + "id": 13, + "key": "Ryze", + "name": "Ryze", + "title": "the Rune Mage" + }, + "Sejuani": { + "id": 113, + "key": "Sejuani", + "name": "Sejuani", + "title": "Fury of the North" + }, + "Shaco": { + "id": 35, + "key": "Shaco", + "name": "Shaco", + "title": "the Demon Jester" + }, + "Shen": { + "id": 98, + "key": "Shen", + "name": "Shen", + "title": "the Eye of Twilight" + }, + "Shyvana": { + "id": 102, + "key": "Shyvana", + "name": "Shyvana", + "title": "the Half-Dragon" + }, + "Singed": { + "id": 27, + "key": "Singed", + "name": "Singed", + "title": "the Mad Chemist" + }, + "Sion": { + "id": 14, + "key": "Sion", + "name": "Sion", + "title": "The Undead Juggernaut" + }, + "Sivir": { + "id": 15, + "key": "Sivir", + "name": "Sivir", + "title": "the Battle Mistress" + }, + "Skarner": { + "id": 72, + "key": "Skarner", + "name": "Skarner", + "title": "the Crystal Vanguard" + }, + "Sona": { + "id": 37, + "key": "Sona", + "name": "Sona", + "title": "Maven of the Strings" + }, + "Soraka": { + "id": 16, + "key": "Soraka", + "name": "Soraka", + "title": "the Starchild" + }, + "Swain": { + "id": 50, + "key": "Swain", + "name": "Swain", + "title": "the Master Tactician" + }, + "Syndra": { + "id": 134, + "key": "Syndra", + "name": "Syndra", + "title": "the Dark Sovereign" + }, + "TahmKench": { + "id": 223, + "key": "TahmKench", + "name": "Tahm Kench", + "title": "the River King" + }, + "Taliyah": { + "id": 163, + "key": "Taliyah", + "name": "Taliyah", + "title": "the Stoneweaver" + }, + "Talon": { + "id": 91, + "key": "Talon", + "name": "Talon", + "title": "the Blade's Shadow" + }, + "Taric": { + "id": 44, + "key": "Taric", + "name": "Taric", + "title": "the Shield of Valoran" + }, + "Teemo": { + "id": 17, + "key": "Teemo", + "name": "Teemo", + "title": "the Swift Scout" + }, + "Thresh": { + "id": 412, + "key": "Thresh", + "name": "Thresh", + "title": "the Chain Warden" + }, + "Tristana": { + "id": 18, + "key": "Tristana", + "name": "Tristana", + "title": "the Yordle Gunner" + }, + "Trundle": { + "id": 48, + "key": "Trundle", + "name": "Trundle", + "title": "the Troll King" + }, + "Tryndamere": { + "id": 23, + "key": "Tryndamere", + "name": "Tryndamere", + "title": "the Barbarian King" + }, + "TwistedFate": { + "id": 4, + "key": "TwistedFate", + "name": "Twisted Fate", + "title": "the Card Master" + }, + "Twitch": { + "id": 29, + "key": "Twitch", + "name": "Twitch", + "title": "the Plague Rat" + }, + "Udyr": { + "id": 77, + "key": "Udyr", + "name": "Udyr", + "title": "the Spirit Walker" + }, + "Urgot": { + "id": 6, + "key": "Urgot", + "name": "Urgot", + "title": "the Headsman's Pride" + }, + "Varus": { + "id": 110, + "key": "Varus", + "name": "Varus", + "title": "the Arrow of Retribution" + }, + "Vayne": { + "id": 67, + "key": "Vayne", + "name": "Vayne", + "title": "the Night Hunter" + }, + "Veigar": { + "id": 45, + "key": "Veigar", + "name": "Veigar", + "title": "the Tiny Master of Evil" + }, + "Velkoz": { + "id": 161, + "key": "Velkoz", + "name": "Vel'Koz", + "title": "the Eye of the Void" + }, + "Vi": { + "id": 254, + "key": "Vi", + "name": "Vi", + "title": "the Piltover Enforcer" + }, + "Viktor": { + "id": 112, + "key": "Viktor", + "name": "Viktor", + "title": "the Machine Herald" + }, + "Vladimir": { + "id": 8, + "key": "Vladimir", + "name": "Vladimir", + "title": "the Crimson Reaper" + }, + "Volibear": { + "id": 106, + "key": "Volibear", + "name": "Volibear", + "title": "the Thunder's Roar" + }, + "Warwick": { + "id": 19, + "key": "Warwick", + "name": "Warwick", + "title": "the Uncaged Wrath of Zaun" + }, + "Xayah": { + "id": 498, + "key": "Xayah", + "name": "Xayah", + "title": "the Rebel" + }, + "Xerath": { + "id": 101, + "key": "Xerath", + "name": "Xerath", + "title": "the Magus Ascendant" + }, + "XinZhao": { + "id": 5, + "key": "XinZhao", + "name": "Xin Zhao", + "title": "the Seneschal of Demacia" + }, + "Yasuo": { + "id": 157, + "key": "Yasuo", + "name": "Yasuo", + "title": "the Unforgiven" + }, + "Yorick": { + "id": 83, + "key": "Yorick", + "name": "Yorick", + "title": "Shepherd of Souls" + }, + "Zac": { + "id": 154, + "key": "Zac", + "name": "Zac", + "title": "the Secret Weapon" + }, + "Zed": { + "id": 238, + "key": "Zed", + "name": "Zed", + "title": "the Master of Shadows" + }, + "Ziggs": { + "id": 115, + "key": "Ziggs", + "name": "Ziggs", + "title": "the Hexplosives Expert" + }, + "Zilean": { + "id": 26, + "key": "Zilean", + "name": "Zilean", + "title": "the Chronokeeper" + }, + "Zyra": { + "id": 143, + "key": "Zyra", + "name": "Zyra", + "title": "Rise of the Thorns" + } +} \ No newline at end of file From 3119a47007679cc932c8378480ec456c8268235a Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Mon, 10 Jul 2017 19:40:55 +0200 Subject: [PATCH 160/346] Upped version --- src/NadekoBot/Services/Impl/StatsService.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/NadekoBot/Services/Impl/StatsService.cs b/src/NadekoBot/Services/Impl/StatsService.cs index 4cd518c5..7745a2cd 100644 --- a/src/NadekoBot/Services/Impl/StatsService.cs +++ b/src/NadekoBot/Services/Impl/StatsService.cs @@ -17,7 +17,7 @@ namespace NadekoBot.Services.Impl private readonly IBotCredentials _creds; private readonly DateTime _started; - public const string BotVersion = "1.55.3"; + public const string BotVersion = "1.55.4"; public string Author => "Kwoth#2560"; public string Library => "Discord.Net"; From 70f0f6af4493b4d0d314e508ac0f013b88064125 Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Mon, 10 Jul 2017 21:08:11 +0200 Subject: [PATCH 161/346] Fixed some links queueing random songs. closes #1368 --- src/NadekoBot/Services/Impl/Ytdl.cs | 2 +- src/NadekoBot/Services/Music/SongBuffer.cs | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/NadekoBot/Services/Impl/Ytdl.cs b/src/NadekoBot/Services/Impl/Ytdl.cs index c2c102d8..0ddebe6b 100644 --- a/src/NadekoBot/Services/Impl/Ytdl.cs +++ b/src/NadekoBot/Services/Impl/Ytdl.cs @@ -21,7 +21,7 @@ namespace NadekoBot.Services.Impl StartInfo = new ProcessStartInfo() { FileName = "youtube-dl", - Arguments = $"-f bestaudio -e --get-url --get-id --get-thumbnail --get-duration --no-check-certificate \"ytsearch:{url}\"", + Arguments = $"-f bestaudio -e --get-url --get-id --get-thumbnail --get-duration --no-check-certificate --default-search \"ytsearch:\" \"{url}\"", UseShellExecute = false, RedirectStandardError = true, RedirectStandardOutput = true, diff --git a/src/NadekoBot/Services/Music/SongBuffer.cs b/src/NadekoBot/Services/Music/SongBuffer.cs index 6664577e..e7a33a64 100644 --- a/src/NadekoBot/Services/Music/SongBuffer.cs +++ b/src/NadekoBot/Services/Music/SongBuffer.cs @@ -1,5 +1,4 @@ -using NadekoBot.DataStructures; -using NLog; +using NLog; using System; using System.Diagnostics; using System.IO; From 9a744172a9c18b830454e8b2d78a88c5d4b02000 Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Mon, 10 Jul 2017 21:21:45 +0200 Subject: [PATCH 162/346] Fixed a bug where spamming .n would crash the music player, closes #1372 --- src/NadekoBot/Services/Music/MusicPlayer.cs | 147 ++++++++++---------- src/NadekoBot/Services/Music/SongBuffer.cs | 38 +++-- 2 files changed, 104 insertions(+), 81 deletions(-) diff --git a/src/NadekoBot/Services/Music/MusicPlayer.cs b/src/NadekoBot/Services/Music/MusicPlayer.cs index 1c697e57..5c698fb3 100644 --- a/src/NadekoBot/Services/Music/MusicPlayer.cs +++ b/src/NadekoBot/Services/Music/MusicPlayer.cs @@ -163,83 +163,86 @@ namespace NadekoBot.Services.Music if (data.Song != null) { _log.Info("Starting"); - using (var b = new SongBuffer(await data.Song.Uri(), "", data.Song.ProviderType == Database.Models.MusicType.Local)) + AudioOutStream pcm = null; + SongBuffer b = null; + try { - _log.Info("Created buffer, buffering..."); - AudioOutStream pcm = null; - try + b = new SongBuffer(await data.Song.Uri(), "", data.Song.ProviderType == Database.Models.MusicType.Local); + //_log.Info("Created buffer, buffering..."); + + //var bufferTask = b.StartBuffering(cancelToken); + //var timeout = Task.Delay(10000); + //if (Task.WhenAny(bufferTask, timeout) == timeout) + //{ + // _log.Info("Buffering failed due to a timeout."); + // continue; + //} + //else if (!bufferTask.Result) + //{ + // _log.Info("Buffering failed due to a cancel or error."); + // continue; + //} + //_log.Info("Buffered. Getting audio client..."); + var ac = await GetAudioClient(); + _log.Info("Got Audio client"); + if (ac == null) { - var bufferTask = b.StartBuffering(cancelToken); - var timeout = Task.Delay(10000); - if (Task.WhenAny(bufferTask, timeout) == timeout) - { - _log.Info("Buffering failed due to a timeout."); - continue; - } - else if (!bufferTask.Result) - { - _log.Info("Buffering failed due to a cancel or error."); - continue; - } - _log.Info("Buffered. Getting audio client..."); - var ac = await GetAudioClient(); - _log.Info("Got Audio client"); - if (ac == null) - { - _log.Info("Can't join"); - await Task.Delay(900, cancelToken); - // just wait some time, maybe bot doesn't even have perms to join that voice channel, - // i don't want to spam connection attempts - continue; - } - pcm = ac.CreatePCMStream(AudioApplication.Music, bufferMillis: 500); - _log.Info("Created pcm stream"); - OnStarted?.Invoke(this, data); - - byte[] buffer = new byte[3840]; - int bytesRead = 0; - - while ((bytesRead = b.Read(buffer, 0, buffer.Length)) > 0 - && (MaxPlaytimeSeconds <= 0 || MaxPlaytimeSeconds >= CurrentTime.TotalSeconds)) - { - AdjustVolume(buffer, Volume); - await pcm.WriteAsync(buffer, 0, bytesRead, cancelToken).ConfigureAwait(false); - unchecked { _bytesSent += bytesRead; } - - await (pauseTaskSource?.Task ?? Task.CompletedTask); - } + _log.Info("Can't join"); + await Task.Delay(900, cancelToken); + // just wait some time, maybe bot doesn't even have perms to join that voice channel, + // i don't want to spam connection attempts + continue; } - catch (OperationCanceledException) - { - _log.Info("Song Canceled"); - cancel = true; - } - catch (Exception ex) - { - _log.Warn(ex); - } - finally - { - if (pcm != null) - { - // flush is known to get stuck from time to time, - // just skip flushing if it takes more than 1 second - var flushCancel = new CancellationTokenSource(); - var flushToken = flushCancel.Token; - var flushDelay = Task.Delay(1000, flushToken); - await Task.WhenAny(flushDelay, pcm.FlushAsync(flushToken)); - flushCancel.Cancel(); - pcm.Dispose(); - } + pcm = ac.CreatePCMStream(AudioApplication.Music, bufferMillis: 500); + _log.Info("Created pcm stream"); + OnStarted?.Invoke(this, data); - OnCompleted?.Invoke(this, data.Song); + byte[] buffer = new byte[3840]; + int bytesRead = 0; - if (_bytesSent == 0 && !cancel) - { - lock (locker) - Queue.RemoveSong(data.Song); - _log.Info("Song removed because it can't play"); - } + while ((bytesRead = b.Read(buffer, 0, buffer.Length)) > 0 + && (MaxPlaytimeSeconds <= 0 || MaxPlaytimeSeconds >= CurrentTime.TotalSeconds)) + { + AdjustVolume(buffer, Volume); + await pcm.WriteAsync(buffer, 0, bytesRead, cancelToken).ConfigureAwait(false); + unchecked { _bytesSent += bytesRead; } + + await (pauseTaskSource?.Task ?? Task.CompletedTask); + } + } + catch (OperationCanceledException) + { + _log.Info("Song Canceled"); + cancel = true; + } + catch (Exception ex) + { + _log.Warn(ex); + } + finally + { + if (pcm != null) + { + // flush is known to get stuck from time to time, + // just skip flushing if it takes more than 1 second + var flushCancel = new CancellationTokenSource(); + var flushToken = flushCancel.Token; + var flushDelay = Task.Delay(1000, flushToken); + await Task.WhenAny(flushDelay, pcm.FlushAsync(flushToken)); + flushCancel.Cancel(); + pcm.Dispose(); + } + + if (b != null) + b.Dispose(); + + OnCompleted?.Invoke(this, data.Song); + + if (_bytesSent == 0 && !cancel) + { + lock (locker) + Queue.RemoveSong(data.Song); + _log.Info("Song removed because it can't play"); } } try diff --git a/src/NadekoBot/Services/Music/SongBuffer.cs b/src/NadekoBot/Services/Music/SongBuffer.cs index e7a33a64..a9d30261 100644 --- a/src/NadekoBot/Services/Music/SongBuffer.cs +++ b/src/NadekoBot/Services/Music/SongBuffer.cs @@ -26,6 +26,34 @@ namespace NadekoBot.Services.Music //_log.Warn(songUri); this.SongUri = songUri; this._isLocal = isLocal; + + try + { + this.p = StartFFmpegProcess(SongUri, 0); + var t = Task.Run(() => + { + this.p.BeginErrorReadLine(); + this.p.ErrorDataReceived += P_ErrorDataReceived; + this.p.WaitForExit(); + }); + + this._outStream = this.p.StandardOutput.BaseStream; + + } + catch (System.ComponentModel.Win32Exception) + { + _log.Error(@"You have not properly installed or configured FFMPEG. +Please install and configure FFMPEG to play music. +Check the guides for your platform on how to setup ffmpeg correctly: + Windows Guide: https://goo.gl/OjKk8F + Linux Guide: https://goo.gl/ShjCUo"); + } + catch (OperationCanceledException) { } + catch (InvalidOperationException) { } // when ffmpeg is disposed + catch (Exception ex) + { + _log.Info(ex); + } } private Process StartFFmpegProcess(string songUri, float skipTo = 0) @@ -65,16 +93,8 @@ namespace NadekoBot.Services.Music var toReturn = new TaskCompletionSource(); var _ = Task.Run(() => { - try { - this.p = StartFFmpegProcess(SongUri, 0); - var t = Task.Run(() => + try { - this.p.BeginErrorReadLine(); - this.p.ErrorDataReceived += P_ErrorDataReceived; - this.p.WaitForExit(); - }); - - this._outStream = this.p.StandardOutput.BaseStream; ////int maxLoopsPerSec = 25; //var sw = Stopwatch.StartNew(); From 34b56c635368ffcd6ed107658a8a2a27d5602a36 Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Mon, 10 Jul 2017 21:31:47 +0200 Subject: [PATCH 163/346] Fixed .srm, closes #1373 --- src/NadekoBot/Services/Music/MusicPlayer.cs | 3 ++- src/NadekoBot/Services/Music/MusicQueue.cs | 5 +++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/NadekoBot/Services/Music/MusicPlayer.cs b/src/NadekoBot/Services/Music/MusicPlayer.cs index 5c698fb3..a6b0cbfb 100644 --- a/src/NadekoBot/Services/Music/MusicPlayer.cs +++ b/src/NadekoBot/Services/Music/MusicPlayer.cs @@ -480,9 +480,10 @@ namespace NadekoBot.Services.Music lock (locker) { var cur = Queue.Current; + var toReturn = Queue.RemoveAt(index); if (cur.Index == index) Next(); - return Queue.RemoveAt(index); + return toReturn; } } diff --git a/src/NadekoBot/Services/Music/MusicQueue.cs b/src/NadekoBot/Services/Music/MusicQueue.cs index 50135b72..681f3936 100644 --- a/src/NadekoBot/Services/Music/MusicQueue.cs +++ b/src/NadekoBot/Services/Music/MusicQueue.cs @@ -95,11 +95,12 @@ namespace NadekoBot.Services.Music if (index < 0 || index >= Songs.Count) throw new ArgumentOutOfRangeException(nameof(index)); - var current = Songs.First; + var current = Songs.First.Value; for (int i = 0; i < Songs.Count; i++) { if (i == index) { + current = Songs.ElementAt(index); Songs.Remove(current); if (CurrentIndex != 0) { @@ -111,7 +112,7 @@ namespace NadekoBot.Services.Music break; } } - return current.Value; + return current; } } From dc0176365b7685e3dee63cf660ea1d7adfec963b Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Mon, 10 Jul 2017 21:35:45 +0200 Subject: [PATCH 164/346] Fixed .convert, closes #1377 --- src/NadekoBot/Services/Utility/ConverterService.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/NadekoBot/Services/Utility/ConverterService.cs b/src/NadekoBot/Services/Utility/ConverterService.cs index 64100955..45334b48 100644 --- a/src/NadekoBot/Services/Utility/ConverterService.cs +++ b/src/NadekoBot/Services/Utility/ConverterService.cs @@ -20,6 +20,7 @@ namespace NadekoBot.Services.Utility private readonly Timer _currencyUpdater; private readonly TimeSpan _updateInterval = new TimeSpan(12, 0, 0); private readonly DbService _db; + private readonly ConvertUnit[] fileData; public ConverterService(DiscordSocketClient client, DbService db) { @@ -30,7 +31,7 @@ namespace NadekoBot.Services.Utility { try { - var data = JsonConvert.DeserializeObject>( + fileData = JsonConvert.DeserializeObject>( File.ReadAllText("data/units.json")) .Select(u => new ConvertUnit() { @@ -43,7 +44,7 @@ namespace NadekoBot.Services.Utility { if (uow.ConverterUnits.Empty()) { - uow.ConverterUnits.AddRange(data); + uow.ConverterUnits.AddRange(fileData); Units = uow.ConverterUnits.GetAll().ToList(); uow.Complete(); @@ -104,6 +105,7 @@ namespace NadekoBot.Services.Utility Units.RemoveAll(u => u.UnitType == unitTypeString); Units.Add(baseType); Units.AddRange(range); + Units.AddRange(fileData); } else { From ab3ad4f2fb32e08f1369ae48bcf07ee6de9d8c3a Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Mon, 10 Jul 2017 21:56:53 +0200 Subject: [PATCH 165/346] Upped version --- src/NadekoBot/Services/Impl/StatsService.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/NadekoBot/Services/Impl/StatsService.cs b/src/NadekoBot/Services/Impl/StatsService.cs index 7745a2cd..379116d1 100644 --- a/src/NadekoBot/Services/Impl/StatsService.cs +++ b/src/NadekoBot/Services/Impl/StatsService.cs @@ -17,7 +17,7 @@ namespace NadekoBot.Services.Impl private readonly IBotCredentials _creds; private readonly DateTime _started; - public const string BotVersion = "1.55.4"; + public const string BotVersion = "1.55.5"; public string Author => "Kwoth#2560"; public string Library => "Discord.Net"; From ec944597228f4a6963ba2b737b77e21993fefbeb Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Tue, 11 Jul 2017 01:23:37 +0200 Subject: [PATCH 166/346] Fixed soundcloud song length, closes #1380 --- src/NadekoBot/Services/Music/MusicService.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/NadekoBot/Services/Music/MusicService.cs b/src/NadekoBot/Services/Music/MusicService.cs index 7db9de4e..dab5aad1 100644 --- a/src/NadekoBot/Services/Music/MusicService.cs +++ b/src/NadekoBot/Services/Music/MusicService.cs @@ -254,7 +254,8 @@ namespace NadekoBot.Services.Music ProviderType = MusicType.Soundcloud, Query = svideo.TrackLink, AlbumArt = svideo.artwork_url, - QueuerName = queuerName + QueuerName = queuerName, + TotalTime = TimeSpan.FromMilliseconds(svideo.Duration) }); public SongInfo ResolveLocalSong(string query, string queuerName) From 8f90410e2d80d56c67b7c025c61f8af5a09da294 Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Tue, 11 Jul 2017 03:16:56 +0200 Subject: [PATCH 167/346] Small refactor --- src/NadekoBot/Modules/Music/Music.cs | 30 +- .../{Music => Impl}/SoundCloudApiService.cs | 2 +- .../Services/Music/Extensions/Extensions.cs | 22 ++ src/NadekoBot/Services/Music/MusicService.cs | 348 +++--------------- .../Services/Music/OldSongResolver.cs | 111 ++++++ src/NadekoBot/Services/Music/SongInfo.cs | 21 +- src/NadekoBot/Services/Music/SongResolver.cs | 111 ------ .../SongResolver/ISongResolverFactory.cs | 15 + .../Music/SongResolver/SongResolverFactory.cs | 41 +++ .../Strategies/IResolverStrategy.cs | 9 + .../Strategies/LocalSongResolveStrategy.cs | 22 ++ .../Strategies/RadioResolveStrategy.cs | 138 +++++++ .../Strategies/SoundCloudResolveStrategy.cs | 27 ++ .../Strategies/YoutubeResolveStrategy.cs | 69 ++++ 14 files changed, 539 insertions(+), 427 deletions(-) rename src/NadekoBot/Services/{Music => Impl}/SoundCloudApiService.cs (99%) create mode 100644 src/NadekoBot/Services/Music/Extensions/Extensions.cs create mode 100644 src/NadekoBot/Services/Music/OldSongResolver.cs delete mode 100644 src/NadekoBot/Services/Music/SongResolver.cs create mode 100644 src/NadekoBot/Services/Music/SongResolver/ISongResolverFactory.cs create mode 100644 src/NadekoBot/Services/Music/SongResolver/SongResolverFactory.cs create mode 100644 src/NadekoBot/Services/Music/SongResolver/Strategies/IResolverStrategy.cs create mode 100644 src/NadekoBot/Services/Music/SongResolver/Strategies/LocalSongResolveStrategy.cs create mode 100644 src/NadekoBot/Services/Music/SongResolver/Strategies/RadioResolveStrategy.cs create mode 100644 src/NadekoBot/Services/Music/SongResolver/Strategies/SoundCloudResolveStrategy.cs create mode 100644 src/NadekoBot/Services/Music/SongResolver/Strategies/YoutubeResolveStrategy.cs diff --git a/src/NadekoBot/Modules/Music/Music.cs b/src/NadekoBot/Modules/Music/Music.cs index f137e6bf..e5605999 100644 --- a/src/NadekoBot/Modules/Music/Music.cs +++ b/src/NadekoBot/Modules/Music/Music.cs @@ -15,6 +15,8 @@ using System.Collections.Concurrent; using System.IO; using System.Net.Http; using Newtonsoft.Json.Linq; +using NadekoBot.Services.Music.Extensions; +using NadekoBot.Services.Impl; namespace NadekoBot.Modules.Music { @@ -122,12 +124,15 @@ namespace NadekoBot.Modules.Music { try { - var queuedMessage = await mp.OutputTextChannel.EmbedAsync(new EmbedBuilder().WithOkColor() - .WithAuthor(eab => eab.WithName(GetText("queued_song") + " #" + (index)).WithMusicIcon()) - .WithDescription($"{songInfo.PrettyName}\n{GetText("queue")} ") - .WithThumbnailUrl(songInfo.Thumbnail) - .WithFooter(ef => ef.WithText(songInfo.PrettyProvider))) - .ConfigureAwait(false); + var embed = new EmbedBuilder().WithOkColor() + .WithAuthor(eab => eab.WithName(GetText("queued_song") + " #" + (index)).WithMusicIcon()) + .WithDescription($"{songInfo.PrettyName}\n{GetText("queue")} ") + .WithFooter(ef => ef.WithText(songInfo.PrettyProvider)); + + if (Uri.IsWellFormedUriString(songInfo.Thumbnail, UriKind.Absolute)) + embed.WithThumbnailUrl(songInfo.Thumbnail); + + var queuedMessage = await mp.OutputTextChannel.EmbedAsync(embed).ConfigureAwait(false); if (mp.Stopped) { (await ReplyErrorLocalized("queue_stopped", Format.Code(Prefix + "play")).ConfigureAwait(false)).DeleteAfter(10); @@ -375,6 +380,11 @@ namespace NadekoBot.Modules.Music [Priority(0)] public async Task SongRemove(int index) { + if (index < 1) + { + await ReplyErrorLocalized("removed_song_error").ConfigureAwait(false); + return; + } var mp = await _music.GetOrCreatePlayer(Context); try { @@ -399,7 +409,9 @@ namespace NadekoBot.Modules.Music [Priority(1)] public async Task SongRemove(All all) { - var mp = await _music.GetOrCreatePlayer(Context); + var mp = _music.GetPlayerOrDefault(Context.Guild.Id); + if (mp == null) + return; mp.Stop(true); await ReplyConfirmLocalized("queue_cleared").ConfigureAwait(false); } @@ -586,7 +598,9 @@ namespace NadekoBot.Modules.Music try { await Task.Yield(); - await InternalQueue(mp, await _music.SongInfoFromSVideo(svideo, Context.User.ToString()), true); + var sinfo = await svideo.GetSongInfo(); + sinfo.QueuerName = Context.User.ToString(); + await InternalQueue(mp, sinfo, true); } catch (Exception ex) { diff --git a/src/NadekoBot/Services/Music/SoundCloudApiService.cs b/src/NadekoBot/Services/Impl/SoundCloudApiService.cs similarity index 99% rename from src/NadekoBot/Services/Music/SoundCloudApiService.cs rename to src/NadekoBot/Services/Impl/SoundCloudApiService.cs index 1b2ecac0..a3dc8cdd 100644 --- a/src/NadekoBot/Services/Music/SoundCloudApiService.cs +++ b/src/NadekoBot/Services/Impl/SoundCloudApiService.cs @@ -4,7 +4,7 @@ using System.Linq; using System.Net.Http; using System.Threading.Tasks; -namespace NadekoBot.Services.Music +namespace NadekoBot.Services.Impl { public class SoundCloudApiService { diff --git a/src/NadekoBot/Services/Music/Extensions/Extensions.cs b/src/NadekoBot/Services/Music/Extensions/Extensions.cs new file mode 100644 index 00000000..4403c2dc --- /dev/null +++ b/src/NadekoBot/Services/Music/Extensions/Extensions.cs @@ -0,0 +1,22 @@ +using NadekoBot.Services.Database.Models; +using NadekoBot.Services.Impl; +using System; +using System.Threading.Tasks; + +namespace NadekoBot.Services.Music.Extensions +{ + public static class Extensions + { + public static Task GetSongInfo(this SoundCloudVideo svideo) => + Task.FromResult(new SongInfo + { + Title = svideo.FullName, + Provider = "SoundCloud", + Uri = () => svideo.StreamLink(), + ProviderType = MusicType.Soundcloud, + Query = svideo.TrackLink, + Thumbnail = svideo.artwork_url, + TotalTime = TimeSpan.FromMilliseconds(svideo.Duration) + }); + } +} diff --git a/src/NadekoBot/Services/Music/MusicService.cs b/src/NadekoBot/Services/Music/MusicService.cs index dab5aad1..5d949618 100644 --- a/src/NadekoBot/Services/Music/MusicService.cs +++ b/src/NadekoBot/Services/Music/MusicService.cs @@ -7,15 +7,11 @@ using NadekoBot.Extensions; using NadekoBot.Services.Database.Models; using NLog; using System.IO; -using VideoLibrary; using System.Collections.Generic; using Discord.Commands; using Discord.WebSocket; -using System.Text.RegularExpressions; -using System.Net.Http; +using NadekoBot.Services.Music.SongResolver; using NadekoBot.Services.Impl; -using System.Globalization; -using System.Threading; namespace NadekoBot.Services.Music { @@ -47,7 +43,6 @@ namespace NadekoBot.Services.Music _sc = sc; _creds = creds; _log = LogManager.GetCurrentClassLogger(); - _yt = YouTube.Default; try { Directory.Delete(MusicDataPath, true); } catch { } @@ -199,91 +194,18 @@ namespace NadekoBot.Services.Music { query.ThrowIfNull(nameof(query)); - SongInfo sinfo = null; - switch (musicType) - { - case MusicType.YouTube: - sinfo = await ResolveYoutubeSong(query, queuerName); - break; - case MusicType.Radio: - try { sinfo = ResolveRadioSong(IsRadioLink(query) ? await HandleStreamContainers(query) : query, queuerName); } catch { }; - break; - case MusicType.Local: - sinfo = ResolveLocalSong(query, queuerName); - break; - case MusicType.Soundcloud: - sinfo = await ResolveSoundCloudSong(query, queuerName); - break; - case null: - if (_sc.IsSoundCloudLink(query)) - sinfo = await ResolveSoundCloudSong(query, queuerName); - else if (IsRadioLink(query)) - sinfo = ResolveRadioSong(await HandleStreamContainers(query), queuerName); - else - try - { - sinfo = await ResolveYoutubeSong(query, queuerName); - } - catch - { - sinfo = null; - } - break; - } + ISongResolverFactory resolverFactory = new SongResolverFactory(_sc); + var strategy = await resolverFactory.GetResolveStrategy(query, musicType); + var sinfo = await strategy.ResolveSong(query); + + if (sinfo == null) + return null; + + sinfo.QueuerName = queuerName; return sinfo; } - public async Task ResolveSoundCloudSong(string query, string queuerName) - { - var svideo = !_sc.IsSoundCloudLink(query) ? - await _sc.GetVideoByQueryAsync(query).ConfigureAwait(false) : - await _sc.ResolveVideoAsync(query).ConfigureAwait(false); - - if (svideo == null) - return null; - return await SongInfoFromSVideo(svideo, queuerName); - } - - public Task SongInfoFromSVideo(SoundCloudVideo svideo, string queuerName) => - Task.FromResult(new SongInfo - { - Title = svideo.FullName, - Provider = "SoundCloud", - Uri = () => svideo.StreamLink(), - ProviderType = MusicType.Soundcloud, - Query = svideo.TrackLink, - AlbumArt = svideo.artwork_url, - QueuerName = queuerName, - TotalTime = TimeSpan.FromMilliseconds(svideo.Duration) - }); - - public SongInfo ResolveLocalSong(string query, string queuerName) - { - return new SongInfo - { - Uri = () => Task.FromResult("\"" + Path.GetFullPath(query) + "\""), - Title = Path.GetFileNameWithoutExtension(query), - Provider = "Local File", - ProviderType = MusicType.Local, - Query = query, - QueuerName = queuerName - }; - } - - public SongInfo ResolveRadioSong(string query, string queuerName) - { - return new SongInfo - { - Uri = () => Task.FromResult(query), - Title = query, - Provider = "Radio Stream", - ProviderType = MusicType.Radio, - Query = query, - QueuerName = queuerName - }; - } - public async Task DestroyAllPlayers() { foreach (var key in MusicPlayers.Keys) @@ -292,214 +214,66 @@ namespace NadekoBot.Services.Music } } - public Task ResolveYoutubeSong(string query, string queuerName) - { - _log.Info("Getting video"); - //var (link, video) = await GetYoutubeVideo(query); - - //if (video == null) // do something with this error - //{ - // _log.Info("Could not load any video elements based on the query."); - // return null; - //} - ////var m = Regex.Match(query, @"\?t=(?\d*)"); - ////int gotoTime = 0; - ////if (m.Captures.Count > 0) - //// int.TryParse(m.Groups["t"].ToString(), out gotoTime); - - //_log.Info("Creating song info"); - //var song = new SongInfo - //{ - // Title = video.Title.Substring(0, video.Title.Length - 10), // removing trailing "- You Tube" - // Provider = "YouTube", - // Uri = async () => { - // var vid = await GetYoutubeVideo(query); - // if (vid.Item2 == null) - // throw new HttpRequestException(); - - // return await vid.Item2.GetUriAsync(); - // }, - // Query = link, - // ProviderType = MusicType.YouTube, - // QueuerName = queuerName - //}; - return GetYoutubeVideo(query, queuerName); - } - - private async Task GetYoutubeVideo(string query, string queuerName) - { - _log.Info("Getting link"); - string[] data; - try - { - using (var ytdl = new YtdlOperation()) - { - data = (await ytdl.GetDataAsync(query)).Split('\n'); - } - if (data.Length < 6) - { - _log.Info("No song found. Data less than 6"); - return null; - } - TimeSpan time; - if (!TimeSpan.TryParseExact(data[4], new[] { "ss", "m\\:ss", "mm\\:ss", "h\\:mm\\:ss", "hh\\:mm\\:ss", "hhh\\:mm\\:ss" }, CultureInfo.InvariantCulture, out time)) - time = TimeSpan.FromHours(24); - - return new SongInfo() - { - Title = data[0], - VideoId = data[1], - Uri = async () => { - using (var ytdl = new YtdlOperation()) - { - data = (await ytdl.GetDataAsync(query)).Split('\n'); - } - if (data.Length < 6) - { - _log.Info("No song found. Data less than 6"); - return null; - } - return data[2]; - }, - AlbumArt = data[3], - TotalTime = time, - QueuerName = queuerName, - Provider = "YouTube", - ProviderType = MusicType.YouTube, - Query = "https://youtube.com/watch?v=" + data[1], - }; - } - catch (Exception ex) - { - _log.Warn(ex); - return null; - } - - //if (string.IsNullOrWhiteSpace(link)) - //{ - // _log.Info("No song found."); - // return (null, null); - //} - //_log.Info("Getting all videos"); - //var allVideos = await Task.Run(async () => { try { return await _yt.GetAllVideosAsync(link).ConfigureAwait(false); } catch { return Enumerable.Empty(); } }).ConfigureAwait(false); - //var videos = allVideos.Where(v => v.AdaptiveKind == AdaptiveKind.Audio); - //var video = videos - // .Where(v => v.AudioBitrate < 256) - // .OrderByDescending(v => v.AudioBitrate) - // .FirstOrDefault(); - - //return (link, video); - } - - private bool IsRadioLink(string query) => - (query.StartsWith("http") || - query.StartsWith("ww")) - && - (query.Contains(".pls") || - query.Contains(".m3u") || - query.Contains(".asx") || - query.Contains(".xspf")); - public async Task DestroyPlayer(ulong id) { if (MusicPlayers.TryRemove(id, out var mp)) await mp.Destroy(); } - private readonly Regex plsRegex = new Regex("File1=(?.*?)\\n", RegexOptions.Compiled); - private readonly Regex m3uRegex = new Regex("(?^[^#].*)", RegexOptions.Compiled | RegexOptions.Multiline); - private readonly Regex asxRegex = new Regex(".*?)\"", RegexOptions.Compiled); - private readonly Regex xspfRegex = new Regex("(?.*?)", RegexOptions.Compiled); - private readonly YouTube _yt; - private readonly Timer _t; - private async Task HandleStreamContainers(string query) - { - string file = null; - try - { - using (var http = new HttpClient()) - { - file = await http.GetStringAsync(query).ConfigureAwait(false); - } - } - catch - { - return query; - } - if (query.Contains(".pls")) - { - //File1=http://armitunes.com:8000/ - //Regex.Match(query) - try - { - var m = plsRegex.Match(file); - var res = m.Groups["url"]?.ToString(); - return res?.Trim(); - } - catch - { - _log.Warn($"Failed reading .pls:\n{file}"); - return null; - } - } - if (query.Contains(".m3u")) - { - /* -# This is a comment - C:\xxx4xx\xxxxxx3x\xx2xxxx\xx.mp3 - C:\xxx5xx\x6xxxxxx\x7xxxxx\xx.mp3 - */ - try - { - var m = m3uRegex.Match(file); - var res = m.Groups["url"]?.ToString(); - return res?.Trim(); - } - catch - { - _log.Warn($"Failed reading .m3u:\n{file}"); - return null; - } - } - if (query.Contains(".asx")) - { - // - try - { - var m = asxRegex.Match(file); - var res = m.Groups["url"]?.ToString(); - return res?.Trim(); - } - catch - { - _log.Warn($"Failed reading .asx:\n{file}"); - return null; - } - } - if (query.Contains(".xspf")) - { - /* - - - - file:///mp3s/song_1.mp3 - */ - try - { - var m = xspfRegex.Match(file); - var res = m.Groups["url"]?.ToString(); - return res?.Trim(); - } - catch - { - _log.Warn($"Failed reading .xspf:\n{file}"); - return null; - } - } + //public Task ResolveYoutubeSong(string query, string queuerName) + //{ + // _log.Info("Getting video"); + // //var (link, video) = await GetYoutubeVideo(query); - return query; - } + // //if (video == null) // do something with this error + // //{ + // // _log.Info("Could not load any video elements based on the query."); + // // return null; + // //} + // ////var m = Regex.Match(query, @"\?t=(?\d*)"); + // ////int gotoTime = 0; + // ////if (m.Captures.Count > 0) + // //// int.TryParse(m.Groups["t"].ToString(), out gotoTime); + + // //_log.Info("Creating song info"); + // //var song = new SongInfo + // //{ + // // Title = video.Title.Substring(0, video.Title.Length - 10), // removing trailing "- You Tube" + // // Provider = "YouTube", + // // Uri = async () => { + // // var vid = await GetYoutubeVideo(query); + // // if (vid.Item2 == null) + // // throw new HttpRequestException(); + + // // return await vid.Item2.GetUriAsync(); + // // }, + // // Query = link, + // // ProviderType = MusicType.YouTube, + // // QueuerName = queuerName + // //}; + // return GetYoutubeVideo(query, queuerName); + //} + + //private async Task GetYoutubeVideo(string query, string queuerName) + //{ + + + // //if (string.IsNullOrWhiteSpace(link)) + // //{ + // // _log.Info("No song found."); + // // return (null, null); + // //} + // //_log.Info("Getting all videos"); + // //var allVideos = await Task.Run(async () => { try { return await _yt.GetAllVideosAsync(link).ConfigureAwait(false); } catch { return Enumerable.Empty(); } }).ConfigureAwait(false); + // //var videos = allVideos.Where(v => v.AdaptiveKind == AdaptiveKind.Audio); + // //var video = videos + // // .Where(v => v.AudioBitrate < 256) + // // .OrderByDescending(v => v.AudioBitrate) + // // .FirstOrDefault(); + + // //return (link, video); + //} } } \ No newline at end of file diff --git a/src/NadekoBot/Services/Music/OldSongResolver.cs b/src/NadekoBot/Services/Music/OldSongResolver.cs new file mode 100644 index 00000000..147bb6e5 --- /dev/null +++ b/src/NadekoBot/Services/Music/OldSongResolver.cs @@ -0,0 +1,111 @@ +//using System; +//using System.Collections.Generic; +//using System.Linq; +//using System.Text; +//using System.Threading.Tasks; + +//namespace NadekoBot.Services.Music +//{ +// public class OldSongResolver +// { +// // public async Task ResolveSong(string query, MusicType musicType = MusicType.Normal) +// // { +// // if (string.IsNullOrWhiteSpace(query)) +// // throw new ArgumentNullException(nameof(query)); + +// // if (musicType != MusicType.Local && IsRadioLink(query)) +// // { +// // musicType = MusicType.Radio; +// // query = await HandleStreamContainers(query).ConfigureAwait(false) ?? query; +// // } + +// // try +// // { +// // switch (musicType) +// // { +// // case MusicType.Local: +// // return new Song(new SongInfo +// // { +// // Uri = "\"" + Path.GetFullPath(query) + "\"", +// // Title = Path.GetFileNameWithoutExtension(query), +// // Provider = "Local File", +// // ProviderType = musicType, +// // Query = query, +// // }); +// // case MusicType.Radio: +// // return new Song(new SongInfo +// // { +// // Uri = query, +// // Title = $"{query}", +// // Provider = "Radio Stream", +// // ProviderType = musicType, +// // Query = query +// // }) +// // { TotalTime = TimeSpan.MaxValue }; +// // } +// // if (_sc.IsSoundCloudLink(query)) +// // { +// // var svideo = await _sc.ResolveVideoAsync(query).ConfigureAwait(false); +// // return new Song(new SongInfo +// // { +// // Title = svideo.FullName, +// // Provider = "SoundCloud", +// // Uri = await svideo.StreamLink(), +// // ProviderType = musicType, +// // Query = svideo.TrackLink, +// // AlbumArt = svideo.artwork_url, +// // }) +// // { TotalTime = TimeSpan.FromMilliseconds(svideo.Duration) }; +// // } + +// // if (musicType == MusicType.Soundcloud) +// // { +// // var svideo = await _sc.GetVideoByQueryAsync(query).ConfigureAwait(false); +// // return new Song(new SongInfo +// // { +// // Title = svideo.FullName, +// // Provider = "SoundCloud", +// // Uri = await svideo.StreamLink(), +// // ProviderType = MusicType.Soundcloud, +// // Query = svideo.TrackLink, +// // AlbumArt = svideo.artwork_url, +// // }) +// // { TotalTime = TimeSpan.FromMilliseconds(svideo.Duration) }; +// // } + +// // var link = (await _google.GetVideoLinksByKeywordAsync(query).ConfigureAwait(false)).FirstOrDefault(); +// // if (string.IsNullOrWhiteSpace(link)) +// // throw new OperationCanceledException("Not a valid youtube query."); +// // var allVideos = await Task.Run(async () => { try { return await YouTube.Default.GetAllVideosAsync(link).ConfigureAwait(false); } catch { return Enumerable.Empty(); } }).ConfigureAwait(false); +// // var videos = allVideos.Where(v => v.AdaptiveKind == AdaptiveKind.Audio); +// // var video = videos +// // .Where(v => v.AudioBitrate < 256) +// // .OrderByDescending(v => v.AudioBitrate) +// // .FirstOrDefault(); + +// // if (video == null) // do something with this error +// // throw new Exception("Could not load any video elements based on the query."); +// // var m = Regex.Match(query, @"\?t=(?\d*)"); +// // int gotoTime = 0; +// // if (m.Captures.Count > 0) +// // int.TryParse(m.Groups["t"].ToString(), out gotoTime); +// // var song = new Song(new SongInfo +// // { +// // Title = video.Title.Substring(0, video.Title.Length - 10), // removing trailing "- You Tube" +// // Provider = "YouTube", +// // Uri = await video.GetUriAsync().ConfigureAwait(false), +// // Query = link, +// // ProviderType = musicType, +// // }); +// // song.SkipTo = gotoTime; +// // return song; +// // } +// // catch (Exception ex) +// // { +// // _log.Warn($"Failed resolving the link.{ex.Message}"); +// // _log.Warn(ex); +// // return null; +// // } +// // } +// } +//} diff --git a/src/NadekoBot/Services/Music/SongInfo.cs b/src/NadekoBot/Services/Music/SongInfo.cs index cb3798d9..db4a65e7 100644 --- a/src/NadekoBot/Services/Music/SongInfo.cs +++ b/src/NadekoBot/Services/Music/SongInfo.cs @@ -15,7 +15,7 @@ namespace NadekoBot.Services.Music public string Query { get; set; } public string Title { get; set; } public Func> Uri { get; set; } - public string AlbumArt { get; set; } + public string Thumbnail { get; set; } public string QueuerName { get; set; } public TimeSpan TotalTime { get; set; } = TimeSpan.Zero; @@ -75,24 +75,5 @@ namespace NadekoBot.Services.Music } private readonly Regex videoIdRegex = new Regex("<=v=[a-zA-Z0-9-]+(?=&)|(?<=[0-9])[^&\n]+|(?<=v=)[^&\n]+", RegexOptions.Compiled); - public string Thumbnail - { - get - { - switch (ProviderType) - { - case MusicType.Radio: - return "https://cdn.discordapp.com/attachments/155726317222887425/261850925063340032/1482522097_radio.png"; //test links - case MusicType.YouTube: - return $"https://img.youtube.com/vi/{ VideoId }/0.jpg"; - case MusicType.Local: - return "https://cdn.discordapp.com/attachments/155726317222887425/261850914783100928/1482522077_music.png"; //test links - case MusicType.Soundcloud: - return AlbumArt; - default: - return ""; - } - } - } } } diff --git a/src/NadekoBot/Services/Music/SongResolver.cs b/src/NadekoBot/Services/Music/SongResolver.cs deleted file mode 100644 index 06fe5194..00000000 --- a/src/NadekoBot/Services/Music/SongResolver.cs +++ /dev/null @@ -1,111 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace NadekoBot.Services.Music -{ - public class SongResolver - { - // public async Task ResolveSong(string query, MusicType musicType = MusicType.Normal) - // { - // if (string.IsNullOrWhiteSpace(query)) - // throw new ArgumentNullException(nameof(query)); - - // if (musicType != MusicType.Local && IsRadioLink(query)) - // { - // musicType = MusicType.Radio; - // query = await HandleStreamContainers(query).ConfigureAwait(false) ?? query; - // } - - // try - // { - // switch (musicType) - // { - // case MusicType.Local: - // return new Song(new SongInfo - // { - // Uri = "\"" + Path.GetFullPath(query) + "\"", - // Title = Path.GetFileNameWithoutExtension(query), - // Provider = "Local File", - // ProviderType = musicType, - // Query = query, - // }); - // case MusicType.Radio: - // return new Song(new SongInfo - // { - // Uri = query, - // Title = $"{query}", - // Provider = "Radio Stream", - // ProviderType = musicType, - // Query = query - // }) - // { TotalTime = TimeSpan.MaxValue }; - // } - // if (_sc.IsSoundCloudLink(query)) - // { - // var svideo = await _sc.ResolveVideoAsync(query).ConfigureAwait(false); - // return new Song(new SongInfo - // { - // Title = svideo.FullName, - // Provider = "SoundCloud", - // Uri = await svideo.StreamLink(), - // ProviderType = musicType, - // Query = svideo.TrackLink, - // AlbumArt = svideo.artwork_url, - // }) - // { TotalTime = TimeSpan.FromMilliseconds(svideo.Duration) }; - // } - - // if (musicType == MusicType.Soundcloud) - // { - // var svideo = await _sc.GetVideoByQueryAsync(query).ConfigureAwait(false); - // return new Song(new SongInfo - // { - // Title = svideo.FullName, - // Provider = "SoundCloud", - // Uri = await svideo.StreamLink(), - // ProviderType = MusicType.Soundcloud, - // Query = svideo.TrackLink, - // AlbumArt = svideo.artwork_url, - // }) - // { TotalTime = TimeSpan.FromMilliseconds(svideo.Duration) }; - // } - - // var link = (await _google.GetVideoLinksByKeywordAsync(query).ConfigureAwait(false)).FirstOrDefault(); - // if (string.IsNullOrWhiteSpace(link)) - // throw new OperationCanceledException("Not a valid youtube query."); - // var allVideos = await Task.Run(async () => { try { return await YouTube.Default.GetAllVideosAsync(link).ConfigureAwait(false); } catch { return Enumerable.Empty(); } }).ConfigureAwait(false); - // var videos = allVideos.Where(v => v.AdaptiveKind == AdaptiveKind.Audio); - // var video = videos - // .Where(v => v.AudioBitrate < 256) - // .OrderByDescending(v => v.AudioBitrate) - // .FirstOrDefault(); - - // if (video == null) // do something with this error - // throw new Exception("Could not load any video elements based on the query."); - // var m = Regex.Match(query, @"\?t=(?\d*)"); - // int gotoTime = 0; - // if (m.Captures.Count > 0) - // int.TryParse(m.Groups["t"].ToString(), out gotoTime); - // var song = new Song(new SongInfo - // { - // Title = video.Title.Substring(0, video.Title.Length - 10), // removing trailing "- You Tube" - // Provider = "YouTube", - // Uri = await video.GetUriAsync().ConfigureAwait(false), - // Query = link, - // ProviderType = musicType, - // }); - // song.SkipTo = gotoTime; - // return song; - // } - // catch (Exception ex) - // { - // _log.Warn($"Failed resolving the link.{ex.Message}"); - // _log.Warn(ex); - // return null; - // } - // } - } -} diff --git a/src/NadekoBot/Services/Music/SongResolver/ISongResolverFactory.cs b/src/NadekoBot/Services/Music/SongResolver/ISongResolverFactory.cs new file mode 100644 index 00000000..b2ae8aa8 --- /dev/null +++ b/src/NadekoBot/Services/Music/SongResolver/ISongResolverFactory.cs @@ -0,0 +1,15 @@ +using NadekoBot.Services.Database.Models; +using NadekoBot.Services.Music.SongResolver.Strategies; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace NadekoBot.Services.Music.SongResolver +{ + public interface ISongResolverFactory + { + Task GetResolveStrategy(string query, MusicType? musicType); + } +} diff --git a/src/NadekoBot/Services/Music/SongResolver/SongResolverFactory.cs b/src/NadekoBot/Services/Music/SongResolver/SongResolverFactory.cs new file mode 100644 index 00000000..a70102f1 --- /dev/null +++ b/src/NadekoBot/Services/Music/SongResolver/SongResolverFactory.cs @@ -0,0 +1,41 @@ +using System.Threading.Tasks; +using NadekoBot.Services.Database.Models; +using NadekoBot.Services.Music.SongResolver.Strategies; +using NadekoBot.Services.Impl; + +namespace NadekoBot.Services.Music.SongResolver +{ + public class SongResolverFactory : ISongResolverFactory + { + private readonly SoundCloudApiService _sc; + + public SongResolverFactory(SoundCloudApiService sc) + { + _sc = sc; + } + + public async Task GetResolveStrategy(string query, MusicType? musicType) + { + await Task.Yield(); //for async warning + switch (musicType) + { + case MusicType.YouTube: + return new YoutubeResolveStrategy(); + case MusicType.Radio: + return new RadioResolveStrategy(); + case MusicType.Local: + return new LocalSongResolveStrategy(); + case MusicType.Soundcloud: + return new SoundcloudResolveStrategy(_sc); + default: + if (_sc.IsSoundCloudLink(query)) + return new SoundcloudResolveStrategy(_sc); + else if (RadioResolveStrategy.IsRadioLink(query)) + return new RadioResolveStrategy(); + // maybe add a check for local files in the future + else + return new YoutubeResolveStrategy(); + } + } + } +} diff --git a/src/NadekoBot/Services/Music/SongResolver/Strategies/IResolverStrategy.cs b/src/NadekoBot/Services/Music/SongResolver/Strategies/IResolverStrategy.cs new file mode 100644 index 00000000..42a77637 --- /dev/null +++ b/src/NadekoBot/Services/Music/SongResolver/Strategies/IResolverStrategy.cs @@ -0,0 +1,9 @@ +using System.Threading.Tasks; + +namespace NadekoBot.Services.Music.SongResolver.Strategies +{ + public interface IResolveStrategy + { + Task ResolveSong(string query); + } +} diff --git a/src/NadekoBot/Services/Music/SongResolver/Strategies/LocalSongResolveStrategy.cs b/src/NadekoBot/Services/Music/SongResolver/Strategies/LocalSongResolveStrategy.cs new file mode 100644 index 00000000..bac28fd8 --- /dev/null +++ b/src/NadekoBot/Services/Music/SongResolver/Strategies/LocalSongResolveStrategy.cs @@ -0,0 +1,22 @@ +using NadekoBot.Services.Database.Models; +using System.IO; +using System.Threading.Tasks; + +namespace NadekoBot.Services.Music.SongResolver.Strategies +{ + public class LocalSongResolveStrategy : IResolveStrategy + { + public Task ResolveSong(string query) + { + return Task.FromResult(new SongInfo + { + Uri = () => Task.FromResult("\"" + Path.GetFullPath(query) + "\""), + Title = Path.GetFileNameWithoutExtension(query), + Provider = "Local File", + ProviderType = MusicType.Local, + Query = query, + Thumbnail = "https://cdn.discordapp.com/attachments/155726317222887425/261850914783100928/1482522077_music.png", + }); + } + } +} diff --git a/src/NadekoBot/Services/Music/SongResolver/Strategies/RadioResolveStrategy.cs b/src/NadekoBot/Services/Music/SongResolver/Strategies/RadioResolveStrategy.cs new file mode 100644 index 00000000..212db609 --- /dev/null +++ b/src/NadekoBot/Services/Music/SongResolver/Strategies/RadioResolveStrategy.cs @@ -0,0 +1,138 @@ +using NadekoBot.Services.Database.Models; +using NLog; +using System; +using System.Net.Http; +using System.Text.RegularExpressions; +using System.Threading.Tasks; + +namespace NadekoBot.Services.Music.SongResolver.Strategies +{ + public class RadioResolveStrategy : IResolveStrategy + { + private readonly Regex plsRegex = new Regex("File1=(?.*?)\\n", RegexOptions.Compiled); + private readonly Regex m3uRegex = new Regex("(?^[^#].*)", RegexOptions.Compiled | RegexOptions.Multiline); + private readonly Regex asxRegex = new Regex(".*?)\"", RegexOptions.Compiled); + private readonly Regex xspfRegex = new Regex("(?.*?)", RegexOptions.Compiled); + private readonly Logger _log; + + public RadioResolveStrategy() + { + _log = LogManager.GetCurrentClassLogger(); + } + + public async Task ResolveSong(string query) + { + if (IsRadioLink(query)) + query = await HandleStreamContainers(query); + + return new SongInfo + { + Uri = () => Task.FromResult(query), + Title = query, + Provider = "Radio Stream", + ProviderType = MusicType.Radio, + Query = query, + TotalTime = TimeSpan.MaxValue, + Thumbnail = "https://cdn.discordapp.com/attachments/155726317222887425/261850925063340032/1482522097_radio.png", + }; + } + + public static bool IsRadioLink(string query) => + (query.StartsWith("http") || + query.StartsWith("ww")) + && + (query.Contains(".pls") || + query.Contains(".m3u") || + query.Contains(".asx") || + query.Contains(".xspf")); + + private async Task HandleStreamContainers(string query) + { + string file = null; + try + { + using (var http = new HttpClient()) + { + file = await http.GetStringAsync(query).ConfigureAwait(false); + } + } + catch + { + return query; + } + if (query.Contains(".pls")) + { + //File1=http://armitunes.com:8000/ + //Regex.Match(query) + try + { + var m = plsRegex.Match(file); + var res = m.Groups["url"]?.ToString(); + return res?.Trim(); + } + catch + { + _log.Warn($"Failed reading .pls:\n{file}"); + return null; + } + } + if (query.Contains(".m3u")) + { + /* +# This is a comment + C:\xxx4xx\xxxxxx3x\xx2xxxx\xx.mp3 + C:\xxx5xx\x6xxxxxx\x7xxxxx\xx.mp3 + */ + try + { + var m = m3uRegex.Match(file); + var res = m.Groups["url"]?.ToString(); + return res?.Trim(); + } + catch + { + _log.Warn($"Failed reading .m3u:\n{file}"); + return null; + } + + } + if (query.Contains(".asx")) + { + // + try + { + var m = asxRegex.Match(file); + var res = m.Groups["url"]?.ToString(); + return res?.Trim(); + } + catch + { + _log.Warn($"Failed reading .asx:\n{file}"); + return null; + } + } + if (query.Contains(".xspf")) + { + /* + + + + file:///mp3s/song_1.mp3 + */ + try + { + var m = xspfRegex.Match(file); + var res = m.Groups["url"]?.ToString(); + return res?.Trim(); + } + catch + { + _log.Warn($"Failed reading .xspf:\n{file}"); + return null; + } + } + + return query; + } + } +} \ No newline at end of file diff --git a/src/NadekoBot/Services/Music/SongResolver/Strategies/SoundCloudResolveStrategy.cs b/src/NadekoBot/Services/Music/SongResolver/Strategies/SoundCloudResolveStrategy.cs new file mode 100644 index 00000000..3e2b9269 --- /dev/null +++ b/src/NadekoBot/Services/Music/SongResolver/Strategies/SoundCloudResolveStrategy.cs @@ -0,0 +1,27 @@ +using NadekoBot.Services.Impl; +using NadekoBot.Services.Music.Extensions; +using System.Threading.Tasks; + +namespace NadekoBot.Services.Music.SongResolver.Strategies +{ + public class SoundcloudResolveStrategy : IResolveStrategy + { + private readonly SoundCloudApiService _sc; + + public SoundcloudResolveStrategy(SoundCloudApiService sc) + { + _sc = sc; + } + + public async Task ResolveSong(string query) + { + var svideo = !_sc.IsSoundCloudLink(query) ? + await _sc.GetVideoByQueryAsync(query).ConfigureAwait(false) : + await _sc.ResolveVideoAsync(query).ConfigureAwait(false); + + if (svideo == null) + return null; + return await svideo.GetSongInfo(); + } + } +} diff --git a/src/NadekoBot/Services/Music/SongResolver/Strategies/YoutubeResolveStrategy.cs b/src/NadekoBot/Services/Music/SongResolver/Strategies/YoutubeResolveStrategy.cs new file mode 100644 index 00000000..b183e9d4 --- /dev/null +++ b/src/NadekoBot/Services/Music/SongResolver/Strategies/YoutubeResolveStrategy.cs @@ -0,0 +1,69 @@ +using NadekoBot.Services.Database.Models; +using NadekoBot.Services.Impl; +using NLog; +using System; +using System.Globalization; +using System.Threading.Tasks; + +namespace NadekoBot.Services.Music.SongResolver.Strategies +{ + public class YoutubeResolveStrategy : IResolveStrategy + { + private readonly Logger _log; + + public YoutubeResolveStrategy() + { + _log = LogManager.GetCurrentClassLogger(); + } + + public async Task ResolveSong(string query) + { + _log.Info("Getting link"); + string[] data; + try + { + using (var ytdl = new YtdlOperation()) + { + data = (await ytdl.GetDataAsync(query)).Split('\n'); + } + if (data.Length < 6) + { + _log.Info("No song found. Data less than 6"); + return null; + } + TimeSpan time; + if (!TimeSpan.TryParseExact(data[4], new[] { "ss", "m\\:ss", "mm\\:ss", "h\\:mm\\:ss", "hh\\:mm\\:ss", "hhh\\:mm\\:ss" }, CultureInfo.InvariantCulture, out time)) + time = TimeSpan.FromHours(24); + + return new SongInfo() + { + Title = data[0], + VideoId = data[1], + Uri = async () => + { + using (var ytdl = new YtdlOperation()) + { + data = (await ytdl.GetDataAsync(query)).Split('\n'); + } + if (data.Length < 6) + { + _log.Info("No song found. Data less than 6"); + return null; + } + return data[2]; + }, + Thumbnail = data[3], + TotalTime = time, + Provider = "YouTube", + ProviderType = MusicType.YouTube, + Query = "https://youtube.com/watch?v=" + data[1], + }; + } + catch (Exception ex) + { + _log.Warn(ex); + return null; + } + } + } +} From 0e4728d9c9d7ebe1b7b30a934e3282b7a6f7270b Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Tue, 11 Jul 2017 19:52:10 +0200 Subject: [PATCH 168/346] Fixed .play when queue is stopped. .play X will now also unstop the player --- .../Commands/UserPunishCommands.cs | 1 + src/NadekoBot/Services/Music/MusicPlayer.cs | 31 +++++++++++-------- 2 files changed, 19 insertions(+), 13 deletions(-) diff --git a/src/NadekoBot/Modules/Administration/Commands/UserPunishCommands.cs b/src/NadekoBot/Modules/Administration/Commands/UserPunishCommands.cs index 79786bcf..8538c619 100644 --- a/src/NadekoBot/Modules/Administration/Commands/UserPunishCommands.cs +++ b/src/NadekoBot/Modules/Administration/Commands/UserPunishCommands.cs @@ -28,6 +28,7 @@ namespace NadekoBot.Modules.Administration _muteService = muteService; } + //todo move to service private async Task InternalWarn(IGuild guild, ulong userId, string modName, string reason) { if (string.IsNullOrWhiteSpace(reason)) diff --git a/src/NadekoBot/Services/Music/MusicPlayer.cs b/src/NadekoBot/Services/Music/MusicPlayer.cs index a6b0cbfb..56b47497 100644 --- a/src/NadekoBot/Services/Music/MusicPlayer.cs +++ b/src/NadekoBot/Services/Music/MusicPlayer.cs @@ -339,18 +339,6 @@ namespace NadekoBot.Services.Music } } - public void SetIndex(int index) - { - if (index < 0) - throw new ArgumentOutOfRangeException(nameof(index)); - lock (locker) - { - Queue.CurrentIndex = index; - manualIndex = true; - CancelCurrentSong(); - } - } - private async Task GetAudioClient(bool reconnect = false) { if (_audioClient == null || @@ -409,6 +397,21 @@ namespace NadekoBot.Services.Music } } + public void SetIndex(int index) + { + if (index < 0) + throw new ArgumentOutOfRangeException(nameof(index)); + lock (locker) + { + if (Exited) + return; + Queue.CurrentIndex = index; + manualIndex = true; + Stopped = false; + CancelCurrentSong(); + } + } + public void Next(int skipCount = 1) { lock (locker) @@ -420,9 +423,11 @@ namespace NadekoBot.Services.Music // It's a bit weird, but that's the least annoying solution if (!Stopped) Queue.Next(skipCount - 1); + else + Queue.CurrentIndex = 0; Stopped = false; - Unpause(); CancelCurrentSong(); + Unpause(); } } From 5e230fad223436c75505709a184c71693d17296f Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Tue, 11 Jul 2017 20:11:36 +0200 Subject: [PATCH 169/346] using .n when rpl is disabled will stop the queue --- src/NadekoBot/Services/Music/MusicPlayer.cs | 15 +++++++++++++-- src/NadekoBot/Services/Music/MusicQueue.cs | 6 ++++++ 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/src/NadekoBot/Services/Music/MusicPlayer.cs b/src/NadekoBot/Services/Music/MusicPlayer.cs index 56b47497..50779c85 100644 --- a/src/NadekoBot/Services/Music/MusicPlayer.cs +++ b/src/NadekoBot/Services/Music/MusicPlayer.cs @@ -252,8 +252,12 @@ namespace NadekoBot.Services.Music // ignore rcs if song is manually skipped int queueCount; + bool stopped; lock (locker) + { queueCount = Queue.Count; + stopped = Stopped; + } if (!manualIndex && (!RepeatCurrentSong || manualSkip)) { @@ -320,7 +324,8 @@ namespace NadekoBot.Services.Music _log.Info("Next song"); lock (locker) { - Queue.Next(); + if(!Stopped) + Queue.Next(); } } } @@ -422,7 +427,13 @@ namespace NadekoBot.Services.Music // if player is stopped, and user uses .n, it should play current song. // It's a bit weird, but that's the least annoying solution if (!Stopped) - Queue.Next(skipCount - 1); + if (!RepeatPlaylist && Queue.IsLast()) // if it's the last song in the queue, and repeat playlist is disabled + { //stop the queue + Stop(); + return; + } + else + Queue.Next(skipCount - 1); else Queue.CurrentIndex = 0; Stopped = false; diff --git a/src/NadekoBot/Services/Music/MusicQueue.cs b/src/NadekoBot/Services/Music/MusicQueue.cs index 681f3936..b22e62c9 100644 --- a/src/NadekoBot/Services/Music/MusicQueue.cs +++ b/src/NadekoBot/Services/Music/MusicQueue.cs @@ -172,5 +172,11 @@ namespace NadekoBot.Services.Music Songs.Remove(song); } } + + public bool IsLast() + { + lock (locker) + return CurrentIndex == Songs.Count - 1; + } } } From c1cf85b33882de7cebacdfcba3c8fc509a8ffdd3 Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Tue, 11 Jul 2017 20:22:06 +0200 Subject: [PATCH 170/346] Fixed error when logging something related to a user who has no avatar, closes #1381 --- .../Services/Administration/LogCommandService.cs | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/NadekoBot/Services/Administration/LogCommandService.cs b/src/NadekoBot/Services/Administration/LogCommandService.cs index a34ff7d2..f698c476 100644 --- a/src/NadekoBot/Services/Administration/LogCommandService.cs +++ b/src/NadekoBot/Services/Administration/LogCommandService.cs @@ -751,13 +751,19 @@ namespace NadekoBot.Services.Administration ITextChannel logChannel; if ((logChannel = await TryGetLogChannel(guild, logSetting, LogType.UserBanned)) == null) return; - await logChannel.EmbedAsync(new EmbedBuilder() + var embed = new EmbedBuilder() .WithOkColor() .WithTitle("🚫 " + GetText(logChannel.Guild, "user_banned")) - .WithThumbnailUrl(usr.GetAvatarUrl()) .WithDescription(usr.ToString()) .AddField(efb => efb.WithName("Id").WithValue(usr.Id.ToString())) - .WithFooter(efb => efb.WithText(CurrentTime(guild)))).ConfigureAwait(false); + .WithFooter(efb => efb.WithText(CurrentTime(guild))); + + var avatarUrl = usr.GetAvatarUrl(); + + if (Uri.IsWellFormedUriString(avatarUrl, UriKind.Absolute)) + embed.WithThumbnailUrl(usr.GetAvatarUrl()); + + await logChannel.EmbedAsync(embed).ConfigureAwait(false); } catch (Exception ex) { _log.Warn(ex); } }); From 4b9977e5d6b53784b761ce34d506c63cff121b0f Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Tue, 11 Jul 2017 20:22:24 +0200 Subject: [PATCH 171/346] Upped version --- .../Administration/LogCommandService.cs | 39 ++++++++++++------- src/NadekoBot/Services/Impl/StatsService.cs | 2 +- 2 files changed, 27 insertions(+), 14 deletions(-) diff --git a/src/NadekoBot/Services/Administration/LogCommandService.cs b/src/NadekoBot/Services/Administration/LogCommandService.cs index f698c476..9b3b432c 100644 --- a/src/NadekoBot/Services/Administration/LogCommandService.cs +++ b/src/NadekoBot/Services/Administration/LogCommandService.cs @@ -147,10 +147,13 @@ namespace NadekoBot.Services.Administration { embed.WithTitle("👥" + GetText(g, "avatar_changed")) .WithDescription($"{before.Username}#{before.Discriminator} | {before.Id}") - .WithThumbnailUrl(before.GetAvatarUrl()) - .WithImageUrl(after.GetAvatarUrl()) .WithFooter(fb => fb.WithText(CurrentTime(g))) .WithOkColor(); + + if (Uri.IsWellFormedUriString(before.GetAvatarUrl(), UriKind.Absolute)) + embed.WithThumbnailUrl(before.GetAvatarUrl()); + if (Uri.IsWellFormedUriString(after.GetAvatarUrl(), UriKind.Absolute)) + embed.WithImageUrl(after.GetAvatarUrl()); } else { @@ -667,14 +670,17 @@ namespace NadekoBot.Services.Administration ITextChannel logChannel; if ((logChannel = await TryGetLogChannel(usr.Guild, logSetting, LogType.UserLeft)) == null) return; - - await logChannel.EmbedAsync(new EmbedBuilder() + var embed = new EmbedBuilder() .WithOkColor() .WithTitle("❌ " + GetText(logChannel.Guild, "user_left")) - .WithThumbnailUrl(usr.GetAvatarUrl()) .WithDescription(usr.ToString()) .AddField(efb => efb.WithName("Id").WithValue(usr.Id.ToString())) - .WithFooter(efb => efb.WithText(CurrentTime(usr.Guild)))).ConfigureAwait(false); + .WithFooter(efb => efb.WithText(CurrentTime(usr.Guild))); + + if (Uri.IsWellFormedUriString(usr.GetAvatarUrl(), UriKind.Absolute)) + embed.WithThumbnailUrl(usr.GetAvatarUrl()); + + await logChannel.EmbedAsync(embed).ConfigureAwait(false); } catch { @@ -698,13 +704,17 @@ namespace NadekoBot.Services.Administration if ((logChannel = await TryGetLogChannel(usr.Guild, logSetting, LogType.UserJoined)) == null) return; - await logChannel.EmbedAsync(new EmbedBuilder() + var embed = new EmbedBuilder() .WithOkColor() .WithTitle("✅ " + GetText(logChannel.Guild, "user_joined")) - .WithThumbnailUrl(usr.GetAvatarUrl()) .WithDescription($"{usr}") .AddField(efb => efb.WithName("Id").WithValue(usr.Id.ToString())) - .WithFooter(efb => efb.WithText(CurrentTime(usr.Guild)))).ConfigureAwait(false); + .WithFooter(efb => efb.WithText(CurrentTime(usr.Guild))); + + if (Uri.IsWellFormedUriString(usr.GetAvatarUrl(), UriKind.Absolute)) + embed.WithThumbnailUrl(usr.GetAvatarUrl()); + + await logChannel.EmbedAsync(embed).ConfigureAwait(false); } catch (Exception ex) { _log.Warn(ex); } }); @@ -724,14 +734,17 @@ namespace NadekoBot.Services.Administration ITextChannel logChannel; if ((logChannel = await TryGetLogChannel(guild, logSetting, LogType.UserUnbanned)) == null) return; - - await logChannel.EmbedAsync(new EmbedBuilder() + var embed = new EmbedBuilder() .WithOkColor() .WithTitle("♻️ " + GetText(logChannel.Guild, "user_unbanned")) - .WithThumbnailUrl(usr.GetAvatarUrl()) .WithDescription(usr.ToString()) .AddField(efb => efb.WithName("Id").WithValue(usr.Id.ToString())) - .WithFooter(efb => efb.WithText(CurrentTime(guild)))).ConfigureAwait(false); + .WithFooter(efb => efb.WithText(CurrentTime(guild))); + + if (Uri.IsWellFormedUriString(usr.GetAvatarUrl(), UriKind.Absolute)) + embed.WithThumbnailUrl(usr.GetAvatarUrl()); + + await logChannel.EmbedAsync(embed).ConfigureAwait(false); } catch (Exception ex) { _log.Warn(ex); } }); diff --git a/src/NadekoBot/Services/Impl/StatsService.cs b/src/NadekoBot/Services/Impl/StatsService.cs index 379116d1..74bb561b 100644 --- a/src/NadekoBot/Services/Impl/StatsService.cs +++ b/src/NadekoBot/Services/Impl/StatsService.cs @@ -17,7 +17,7 @@ namespace NadekoBot.Services.Impl private readonly IBotCredentials _creds; private readonly DateTime _started; - public const string BotVersion = "1.55.5"; + public const string BotVersion = "1.55.6"; public string Author => "Kwoth#2560"; public string Library => "Discord.Net"; From 8a75c28d73695f65826174dbe5d94ef226e1f0a1 Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Wed, 12 Jul 2017 02:02:17 +0200 Subject: [PATCH 172/346] Small doc fixes --- docs/Commands List.md | 4 ++-- src/NadekoBot/Resources/CommandStrings.resx | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/Commands List.md b/docs/Commands List.md index 0deeaa6b..b5da3f22 100644 --- a/docs/Commands List.md +++ b/docs/Commands List.md @@ -236,7 +236,7 @@ Commands and aliases | Description | Usage `.play` `.start` | If no arguments are specified, acts as `.next 1` command. If you specify a song number, it will jump to that song. If you specify a search query, acts as a `.q` command | `.play` or `.play 5` or `.play Dream Of Venice` `.queue` `.q` `.yq` | Queue a song using keywords or a link. Bot will join your voice channel. **You must be in a voice channel**. | `.q Dream Of Venice` `.queuesearch` `.qs` `.yqs` | Search for top 5 youtube song result using keywords, and type the index of the song to play that song. Bot will join your voice channel. **You must be in a voice channel**. | `.qs Dream Of Venice` -`.listqueue` `.lq` | Lists 15 currently queued songs per page. Default page is 1. | `.lq` or `.lq 2` +`.listqueue` `.lq` | Lists 10 currently queued songs per page. Default page is 1. | `.lq` or `.lq 2` `.next` `.n` | Goes to the next song in the queue. You have to be in the same voice channel as the bot. You can skip multiple songs, but in that case songs will not be requeued if .rcs or .rpl is enabled. | `.n` or `.n 5` `.stop` `.s` | Stops the music and clears the playlist. Stays in the channel. | `.s` `.destroy` `.d` | Completely stops the music and unbinds the bot from the channel. (may cause weird behaviour) | `.d` @@ -421,7 +421,7 @@ Commands and aliases | Description | Usage `.calcops` | Shows all available operations in the `.calc` command | `.calcops` `.alias` `.cmdmap` | Create a custom alias for a certain Nadeko command. Provide no alias to remove the existing one. **Requires Administrator server permission.** | `.alias allin $bf 100 h` or `.alias "linux thingy" >loonix Spyware Windows` `.aliaslist` `.cmdmaplist` `.aliases` | Shows the list of currently set aliases. Paginated. | `.aliaslist` or `.aliaslist 3` -`.serverinfo` `.sinfo` | Shows info about the server the bot is on. If no channel is supplied, it defaults to current one. | `.sinfo Some Server` +`.serverinfo` `.sinfo` | Shows info about the server the bot is on. If no server is supplied, it defaults to current one. | `.sinfo Some Server` `.channelinfo` `.cinfo` | Shows info about the channel. If no channel is supplied, it defaults to current one. | `.cinfo #some-channel` `.userinfo` `.uinfo` | Shows info about the user. If no user is supplied, it defaults a user running the command. | `.uinfo @SomeUser` `.activity` | Checks for spammers. **Bot owner only** | `.activity` diff --git a/src/NadekoBot/Resources/CommandStrings.resx b/src/NadekoBot/Resources/CommandStrings.resx index e3504acb..a55bfb98 100644 --- a/src/NadekoBot/Resources/CommandStrings.resx +++ b/src/NadekoBot/Resources/CommandStrings.resx @@ -814,7 +814,7 @@ serverinfo sinfo - Shows info about the server the bot is on. If no channel is supplied, it defaults to current one. + Shows info about the server the bot is on. If no server is supplied, it defaults to current one. `{0}sinfo Some Server` @@ -1534,7 +1534,7 @@ listqueue lq - Lists 15 currently queued songs per page. Default page is 1. + Lists 10 currently queued songs per page. Default page is 1. `{0}lq` or `{0}lq 2` From c0012e296ec8478611095f3b4e74d97d5954725c Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Wed, 12 Jul 2017 03:39:44 +0200 Subject: [PATCH 173/346] Some stats stuff for science --- src/NadekoBot/Services/Impl/StatsService.cs | 28 +++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/src/NadekoBot/Services/Impl/StatsService.cs b/src/NadekoBot/Services/Impl/StatsService.cs index 74bb561b..3b0eaf8f 100644 --- a/src/NadekoBot/Services/Impl/StatsService.cs +++ b/src/NadekoBot/Services/Impl/StatsService.cs @@ -6,6 +6,8 @@ using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Net.Http; +using System.Security.Cryptography; +using System.Text; using System.Threading; using System.Threading.Tasks; @@ -35,6 +37,7 @@ namespace NadekoBot.Services.Impl public long CommandsRan => Interlocked.Read(ref _commandsRan); private readonly Timer _carbonitexTimer; + private readonly Timer _dataTimer; private readonly ShardsCoordinator _sc; public int GuildCount => @@ -153,6 +156,31 @@ namespace NadekoBot.Services.Impl // ignored } }, null, TimeSpan.FromHours(1), TimeSpan.FromHours(1)); + + _dataTimer = new Timer(async (state) => + { + try + { + using (var http = new HttpClient()) + { + using (var content = new FormUrlEncodedContent( + new Dictionary { + { "id", string.Concat(MD5.Create().ComputeHash(Encoding.ASCII.GetBytes(_creds.ClientId.ToString())).Select(x => x.ToString("X2"))) }, + { "guildCount", sc.GuildCount.ToString() }, + { "version", BotVersion } })) + { + content.Headers.Clear(); + content.Headers.Add("Content-Type", "application/x-www-form-urlencoded"); + + await http.PostAsync("https://selfstats.nadekobot.me/", content).ConfigureAwait(false); + } + } + } + catch + { + // ignored + } + }, null, TimeSpan.FromHours(1), TimeSpan.FromHours(1)); } } From 467b482ff939fac8a8eaf66313ff61d1fba7fb2f Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Wed, 12 Jul 2017 04:13:50 +0200 Subject: [PATCH 174/346] Added some stuff to stats sending --- src/NadekoBot/Services/Impl/StatsService.cs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/NadekoBot/Services/Impl/StatsService.cs b/src/NadekoBot/Services/Impl/StatsService.cs index 3b0eaf8f..31b5a047 100644 --- a/src/NadekoBot/Services/Impl/StatsService.cs +++ b/src/NadekoBot/Services/Impl/StatsService.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Net.Http; +using System.Runtime.InteropServices; using System.Security.Cryptography; using System.Text; using System.Threading; @@ -157,6 +158,14 @@ namespace NadekoBot.Services.Impl } }, null, TimeSpan.FromHours(1), TimeSpan.FromHours(1)); + var platform = "other"; + if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + platform = "linux"; + else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + platform = "osx"; + else if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + platform = "windows"; + _dataTimer = new Timer(async (state) => { try @@ -167,7 +176,8 @@ namespace NadekoBot.Services.Impl new Dictionary { { "id", string.Concat(MD5.Create().ComputeHash(Encoding.ASCII.GetBytes(_creds.ClientId.ToString())).Select(x => x.ToString("X2"))) }, { "guildCount", sc.GuildCount.ToString() }, - { "version", BotVersion } })) + { "version", BotVersion }, + { "platform", platform }})) { content.Headers.Clear(); content.Headers.Add("Content-Type", "application/x-www-form-urlencoded"); From b85cc023f254f9928b552b7a525143ce6d852a08 Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Wed, 12 Jul 2017 15:38:34 +0200 Subject: [PATCH 175/346] format --- src/NadekoBot/Services/Impl/StatsService.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/NadekoBot/Services/Impl/StatsService.cs b/src/NadekoBot/Services/Impl/StatsService.cs index 31b5a047..f9c432a6 100644 --- a/src/NadekoBot/Services/Impl/StatsService.cs +++ b/src/NadekoBot/Services/Impl/StatsService.cs @@ -188,9 +188,9 @@ namespace NadekoBot.Services.Impl } catch { - // ignored - } - }, null, TimeSpan.FromHours(1), TimeSpan.FromHours(1)); + // ignored + } + }, null, TimeSpan.FromSeconds(1), TimeSpan.FromHours(1)); } } From ed40bc99b25a34c05902bc75c0d681559ebf0ff9 Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Thu, 13 Jul 2017 04:09:21 +0200 Subject: [PATCH 176/346] .movesong works more intuitively now, and current song's index is now updated after songs are moved --- src/NadekoBot/Modules/Music/Music.cs | 6 +++--- src/NadekoBot/Services/Music/MusicQueue.cs | 21 ++++++++++++++++----- 2 files changed, 19 insertions(+), 8 deletions(-) diff --git a/src/NadekoBot/Modules/Music/Music.cs b/src/NadekoBot/Modules/Music/Music.cs index e5605999..b26095a3 100644 --- a/src/NadekoBot/Modules/Music/Music.cs +++ b/src/NadekoBot/Modules/Music/Music.cs @@ -774,7 +774,7 @@ namespace NadekoBot.Modules.Music SongInfo s; if (fromtoArr.Length != 2 || !int.TryParse(fromtoArr[0], out var n1) || !int.TryParse(fromtoArr[1], out var n2) || n1 < 1 || n2 < 1 || n1 == n2 - || (s = mp.MoveSong(n1, n2)) == null) + || (s = mp.MoveSong(--n1, --n2)) == null) { await ReplyConfirmLocalized("invalid_input").ConfigureAwait(false); return; @@ -784,8 +784,8 @@ namespace NadekoBot.Modules.Music .WithTitle(s.Title.TrimTo(65)) .WithUrl(s.SongUrl) .WithAuthor(eab => eab.WithName(GetText("song_moved")).WithIconUrl("https://cdn.discordapp.com/attachments/155726317222887425/258605269972549642/music1.png")) - .AddField(fb => fb.WithName(GetText("from_position")).WithValue($"#{n1}").WithIsInline(true)) - .AddField(fb => fb.WithName(GetText("to_position")).WithValue($"#{n2}").WithIsInline(true)) + .AddField(fb => fb.WithName(GetText("from_position")).WithValue($"#{n1 + 1}").WithIsInline(true)) + .AddField(fb => fb.WithName(GetText("to_position")).WithValue($"#{n2 + 1}").WithIsInline(true)) .WithColor(NadekoBot.OkColor); await Context.Channel.EmbedAsync(embed).ConfigureAwait(false); } diff --git a/src/NadekoBot/Services/Music/MusicQueue.cs b/src/NadekoBot/Services/Music/MusicQueue.cs index b22e62c9..861765b3 100644 --- a/src/NadekoBot/Services/Music/MusicQueue.cs +++ b/src/NadekoBot/Services/Music/MusicQueue.cs @@ -153,14 +153,22 @@ namespace NadekoBot.Services.Music { lock (locker) { + var currentSong = Current.Song; var playlist = Songs.ToList(); - if (n1 > playlist.Count || n2 > playlist.Count) + if (n1 >= playlist.Count || n2 >= playlist.Count || n1 == n2) return null; - var s = playlist[n1 - 1]; - playlist.Insert(n2 - 1, s); - var nn1 = n2 < n1 ? n1 : n1 - 1; - playlist.RemoveAt(nn1); + + var s = playlist[n1]; + + playlist.RemoveAt(n1); + playlist.Insert(n2, s); + Songs = new LinkedList(playlist); + + + if (currentSong != null) + CurrentIndex = playlist.IndexOf(currentSong); + return s; } } @@ -180,3 +188,6 @@ namespace NadekoBot.Services.Music } } } +//O O [O] O O O O +// +// 3 \ No newline at end of file From 67f0cfb717e5828265fa45af6e43e5d3f1884838 Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Thu, 13 Jul 2017 04:09:38 +0200 Subject: [PATCH 177/346] Version upped --- src/NadekoBot/Services/Impl/StatsService.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/NadekoBot/Services/Impl/StatsService.cs b/src/NadekoBot/Services/Impl/StatsService.cs index f9c432a6..92a68be7 100644 --- a/src/NadekoBot/Services/Impl/StatsService.cs +++ b/src/NadekoBot/Services/Impl/StatsService.cs @@ -20,7 +20,7 @@ namespace NadekoBot.Services.Impl private readonly IBotCredentials _creds; private readonly DateTime _started; - public const string BotVersion = "1.55.6"; + public const string BotVersion = "1.55.7"; public string Author => "Kwoth#2560"; public string Library => "Discord.Net"; From c328ec68d3aa4068f42294816ce6ac1bb3e6e0d3 Mon Sep 17 00:00:00 2001 From: samvaio Date: Thu, 13 Jul 2017 17:34:28 +0530 Subject: [PATCH 178/346] Documents updated macOS prerequisite info added --- docs/JSON Explanations.md | 3 ++- docs/guides/OSX Guide.md | 2 ++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/JSON Explanations.md b/docs/JSON Explanations.md index 028b1891..eb6fabe4 100644 --- a/docs/JSON Explanations.md +++ b/docs/JSON Explanations.md @@ -185,6 +185,8 @@ in order to open the database file you will need [DB Browser for SQLite](http:// - click on **Apply** - click on **Write Changes** +![nadekodb](https://cdn.discordapp.com/attachments/251504306010849280/254067055240806400/nadekodb.gif) + and that will save all the changes. ## Sharding your bot @@ -205,7 +207,6 @@ and that will save all the changes. - Bot uses a random UDP port in [5000, 6000) range for communication between shards -![nadekodb](https://cdn.discordapp.com/attachments/251504306010849280/254067055240806400/nadekodb.gif) [Google Console]: https://console.developers.google.com [DiscordApp]: https://discordapp.com/developers/applications/me diff --git a/docs/guides/OSX Guide.md b/docs/guides/OSX Guide.md index b9256356..a9d53736 100644 --- a/docs/guides/OSX Guide.md +++ b/docs/guides/OSX Guide.md @@ -24,6 +24,8 @@ brew install opusfile brew install libffi brew install libsodium brew install tmux +brew install python +brew install youtube-dl ``` #### Installing .NET Core SDK From fd410bc85600aff87e3d84fd0879e676cd02336b Mon Sep 17 00:00:00 2001 From: samvaio Date: Thu, 13 Jul 2017 17:36:49 +0530 Subject: [PATCH 179/346] Stop works as it used to before. Auto rpl disabled. Bot no longer repeats playlist on default. Bot now clears the queued songs list (playlist) - if `.stop` command is used. - if the last song of the queue is playing and `.next` command is used. - if the the last song of the queue finished playing. --- src/NadekoBot/Services/Music/MusicPlayer.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/NadekoBot/Services/Music/MusicPlayer.cs b/src/NadekoBot/Services/Music/MusicPlayer.cs index 50779c85..bae20574 100644 --- a/src/NadekoBot/Services/Music/MusicPlayer.cs +++ b/src/NadekoBot/Services/Music/MusicPlayer.cs @@ -62,7 +62,7 @@ namespace NadekoBot.Services.Music public bool RepeatCurrentSong { get; private set; } public bool Shuffle { get; private set; } public bool Autoplay { get; private set; } - public bool RepeatPlaylist { get; private set; } = true; + public bool RepeatPlaylist { get; private set; } = false; public uint MaxQueueSize { get => Queue.MaxQueueSize; @@ -442,12 +442,12 @@ namespace NadekoBot.Services.Music } } - public void Stop(bool clearQueue = false) + public void Stop(bool clearQueue = true) { lock (locker) { - Stopped = true; - Queue.ResetCurrent(); + //Stopped = true; + //Queue.ResetCurrent(); if (clearQueue) Queue.Clear(); Unpause(); From 202c5e98a5006833746d4c40264d13b9338a5627 Mon Sep 17 00:00:00 2001 From: samvaio Date: Thu, 13 Jul 2017 17:45:46 +0530 Subject: [PATCH 180/346] Makeover to highlight the information. Look is similar to early version of Nadeko. --- .../Modules/Administration/Commands/LogCommand.cs | 4 ++-- .../Modules/Administration/Commands/SelfCommands.cs | 8 ++++---- src/NadekoBot/Modules/Utility/Utility.cs | 7 ++++--- 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/src/NadekoBot/Modules/Administration/Commands/LogCommand.cs b/src/NadekoBot/Modules/Administration/Commands/LogCommand.cs index 9823d735..dbdb63dc 100644 --- a/src/NadekoBot/Modules/Administration/Commands/LogCommand.cs +++ b/src/NadekoBot/Modules/Administration/Commands/LogCommand.cs @@ -106,8 +106,8 @@ namespace NadekoBot.Modules.Administration [OwnerOnly] public async Task LogEvents() { - await Context.Channel.SendConfirmAsync(GetText("log_events") + "\n" + - string.Join(", ", Enum.GetNames(typeof(LogType)).Cast())) + await Context.Channel.SendConfirmAsync(Format.Bold(GetText("log_events")) + "\n" + + $"```fix\n{string.Join(", ", Enum.GetNames(typeof(LogType)).Cast())}```") .ConfigureAwait(false); } diff --git a/src/NadekoBot/Modules/Administration/Commands/SelfCommands.cs b/src/NadekoBot/Modules/Administration/Commands/SelfCommands.cs index bd1a8eb6..14a9e196 100644 --- a/src/NadekoBot/Modules/Administration/Commands/SelfCommands.cs +++ b/src/NadekoBot/Modules/Administration/Commands/SelfCommands.cs @@ -100,13 +100,13 @@ namespace NadekoBot.Modules.Administration } else { - await Context.Channel.SendConfirmAsync("", string.Join("\n--\n", scmds.Select(x => + await Context.Channel.SendConfirmAsync("", string.Join("\n", scmds.Select(x => { - string str = Format.Code(GetText("server")) + ": " + (x.GuildId == null ? "-" : x.GuildName + "/" + x.GuildId); + string str = $"```css\n[{GetText("server") + "]: " + (x.GuildId == null ? "-" : x.GuildName + " #" + x.GuildId)}"; str += $@" -{Format.Code(GetText("channel"))}: {x.ChannelName}/{x.ChannelId} -{Format.Code(GetText("command_text"))}: {x.CommandText}"; +[{GetText("channel")}]: {x.ChannelName} #{x.ChannelId} +[{GetText("command_text")}]: {x.CommandText}```"; return str; })), footer: GetText("page", page + 1)) .ConfigureAwait(false); diff --git a/src/NadekoBot/Modules/Utility/Utility.cs b/src/NadekoBot/Modules/Utility/Utility.cs index ea76c755..e1cf1226 100644 --- a/src/NadekoBot/Modules/Utility/Utility.cs +++ b/src/NadekoBot/Modules/Utility/Utility.cs @@ -161,11 +161,12 @@ namespace NadekoBot.Modules.Utility var usrs = (await Context.Guild.GetUsersAsync()).ToArray(); var roleUsers = usrs.Where(u => u.RoleIds.Contains(role.Id)).Select(u => u.ToString()) .ToArray(); + var inroleusers = string.Join(", ", roleUsers + .OrderBy(x => rng.Next()) + .Take(50)); var embed = new EmbedBuilder().WithOkColor() .WithTitle("ℹ️ " + Format.Bold(GetText("inrole_list", Format.Bold(role.Name))) + $" - {roleUsers.Length}") - .WithDescription(string.Join(", ", roleUsers - .OrderBy(x => rng.Next()) - .Take(50))); + .WithDescription($"```css\n[{role.Name}]\n{inroleusers}```"); await Context.Channel.EmbedAsync(embed).ConfigureAwait(false); } From 390a637c08bda4a5b990bf1c4e3380b896a73380 Mon Sep 17 00:00:00 2001 From: hakufu Date: Thu, 13 Jul 2017 14:08:51 -0400 Subject: [PATCH 181/346] Add deny perm for addReactions on mute Added the permission for the channel override when muting to also include the denial of "addReactions." --- src/NadekoBot/Services/Administration/MuteService.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/NadekoBot/Services/Administration/MuteService.cs b/src/NadekoBot/Services/Administration/MuteService.cs index 87bde743..2b8c3fb9 100644 --- a/src/NadekoBot/Services/Administration/MuteService.cs +++ b/src/NadekoBot/Services/Administration/MuteService.cs @@ -30,7 +30,7 @@ namespace NadekoBot.Services.Administration public event Action UserMuted = delegate { }; public event Action UserUnmuted = delegate { }; - private static readonly OverwritePermissions denyOverwrite = new OverwritePermissions(sendMessages: PermValue.Deny, attachFiles: PermValue.Deny); + private static readonly OverwritePermissions denyOverwrite = new OverwritePermissions(addReactions: PermValue.Deny, sendMessages: PermValue.Deny, attachFiles: PermValue.Deny); private readonly Logger _log = LogManager.GetCurrentClassLogger(); private readonly DiscordSocketClient _client; From f26a7de704ddd92e6321eba04fc5909bac2a0392 Mon Sep 17 00:00:00 2001 From: samvaio Date: Fri, 14 Jul 2017 00:54:23 +0530 Subject: [PATCH 182/346] Revert "Stop works as it used to before. Auto rpl disabled." This reverts commit fd410bc85600aff87e3d84fd0879e676cd02336b. --- src/NadekoBot/Services/Music/MusicPlayer.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/NadekoBot/Services/Music/MusicPlayer.cs b/src/NadekoBot/Services/Music/MusicPlayer.cs index bae20574..50779c85 100644 --- a/src/NadekoBot/Services/Music/MusicPlayer.cs +++ b/src/NadekoBot/Services/Music/MusicPlayer.cs @@ -62,7 +62,7 @@ namespace NadekoBot.Services.Music public bool RepeatCurrentSong { get; private set; } public bool Shuffle { get; private set; } public bool Autoplay { get; private set; } - public bool RepeatPlaylist { get; private set; } = false; + public bool RepeatPlaylist { get; private set; } = true; public uint MaxQueueSize { get => Queue.MaxQueueSize; @@ -442,12 +442,12 @@ namespace NadekoBot.Services.Music } } - public void Stop(bool clearQueue = true) + public void Stop(bool clearQueue = false) { lock (locker) { - //Stopped = true; - //Queue.ResetCurrent(); + Stopped = true; + Queue.ResetCurrent(); if (clearQueue) Queue.Clear(); Unpause(); From b8573f11b53457d525707a01d24a9ed1d5011280 Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Fri, 14 Jul 2017 05:00:30 +0200 Subject: [PATCH 183/346] Added .streamrole, needs testing. --- .../20170714021615_stream-role.Designer.cs | 1597 +++++++++++++++++ .../Migrations/20170714021615_stream-role.cs | 46 + .../NadekoSqliteContextModelSnapshot.cs | 29 + .../Utility/Commands/StreamRoleCommands.cs | 39 + src/NadekoBot/NadekoBot.csproj | 1 - src/NadekoBot/Resources/CommandStrings.resx | 9 + .../Services/Database/Models/GuildConfig.cs | 2 + .../Database/Models/StreamRoleSettings.cs | 23 + .../Services/Database/NadekoContext.cs | 8 +- .../Impl/GuildConfigRepository.cs | 1 + .../Services/Utility/StreamRoleService.cs | 150 ++ .../_strings/ResponseStrings.en-US.json | 2 + 12 files changed, 1905 insertions(+), 2 deletions(-) create mode 100644 src/NadekoBot/Migrations/20170714021615_stream-role.Designer.cs create mode 100644 src/NadekoBot/Migrations/20170714021615_stream-role.cs create mode 100644 src/NadekoBot/Modules/Utility/Commands/StreamRoleCommands.cs create mode 100644 src/NadekoBot/Services/Database/Models/StreamRoleSettings.cs create mode 100644 src/NadekoBot/Services/Utility/StreamRoleService.cs diff --git a/src/NadekoBot/Migrations/20170714021615_stream-role.Designer.cs b/src/NadekoBot/Migrations/20170714021615_stream-role.Designer.cs new file mode 100644 index 00000000..a1621e42 --- /dev/null +++ b/src/NadekoBot/Migrations/20170714021615_stream-role.Designer.cs @@ -0,0 +1,1597 @@ +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using NadekoBot.Services.Database; +using NadekoBot.Services.Database.Models; + +namespace NadekoBot.Migrations +{ + [DbContext(typeof(NadekoContext))] + [Migration("20170714021615_stream-role")] + partial class streamrole + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { + modelBuilder + .HasAnnotation("ProductVersion", "1.1.1"); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.AntiRaidSetting", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("Action"); + + b.Property("DateAdded"); + + b.Property("GuildConfigId"); + + b.Property("Seconds"); + + b.Property("UserThreshold"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId") + .IsUnique(); + + b.ToTable("AntiRaidSetting"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.AntiSpamIgnore", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AntiSpamSettingId"); + + b.Property("ChannelId"); + + b.Property("DateAdded"); + + b.HasKey("Id"); + + b.HasIndex("AntiSpamSettingId"); + + b.ToTable("AntiSpamIgnore"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.AntiSpamSetting", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("Action"); + + b.Property("DateAdded"); + + b.Property("GuildConfigId"); + + b.Property("MessageThreshold"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId") + .IsUnique(); + + b.ToTable("AntiSpamSetting"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.BlacklistItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("BotConfigId"); + + b.Property("DateAdded"); + + b.Property("ItemId"); + + b.Property("Type"); + + b.HasKey("Id"); + + b.HasIndex("BotConfigId"); + + b.ToTable("BlacklistItem"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.BlockedCmdOrMdl", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("BotConfigId"); + + b.Property("BotConfigId1"); + + b.Property("DateAdded"); + + b.Property("Name"); + + b.HasKey("Id"); + + b.HasIndex("BotConfigId"); + + b.HasIndex("BotConfigId1"); + + b.ToTable("BlockedCmdOrMdl"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.BotConfig", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("BetflipMultiplier"); + + b.Property("Betroll100Multiplier"); + + b.Property("Betroll67Multiplier"); + + b.Property("Betroll91Multiplier"); + + b.Property("BufferSize"); + + b.Property("CurrencyDropAmount"); + + b.Property("CurrencyDropAmountMax"); + + b.Property("CurrencyGenerationChance"); + + b.Property("CurrencyGenerationCooldown"); + + b.Property("CurrencyName"); + + b.Property("CurrencyPluralName"); + + b.Property("CurrencySign"); + + b.Property("CustomReactionsStartWith"); + + b.Property("DMHelpString"); + + b.Property("DateAdded"); + + b.Property("DefaultPrefix"); + + b.Property("ErrorColor"); + + b.Property("ForwardMessages"); + + b.Property("ForwardToAllOwners"); + + b.Property("HelpString"); + + b.Property("Locale"); + + b.Property("MigrationVersion"); + + b.Property("MinimumBetAmount"); + + b.Property("OkColor"); + + b.Property("PermissionVersion"); + + b.Property("RemindMessageFormat"); + + b.Property("RotatingStatuses"); + + b.Property("TriviaCurrencyReward"); + + b.HasKey("Id"); + + b.ToTable("BotConfig"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.ClashCaller", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("BaseDestroyed"); + + b.Property("CallUser"); + + b.Property("ClashWarId"); + + b.Property("DateAdded"); + + b.Property("SequenceNumber"); + + b.Property("Stars"); + + b.Property("TimeAdded"); + + b.HasKey("Id"); + + b.HasIndex("ClashWarId"); + + b.ToTable("ClashCallers"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.ClashWar", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ChannelId"); + + b.Property("DateAdded"); + + b.Property("EnemyClan"); + + b.Property("GuildId"); + + b.Property("Size"); + + b.Property("StartedAt"); + + b.Property("WarState"); + + b.HasKey("Id"); + + b.ToTable("ClashOfClans"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.CommandAlias", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("GuildConfigId"); + + b.Property("Mapping"); + + b.Property("Trigger"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.ToTable("CommandAlias"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.CommandCooldown", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("CommandName"); + + b.Property("DateAdded"); + + b.Property("GuildConfigId"); + + b.Property("Seconds"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.ToTable("CommandCooldown"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.CommandPrice", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("BotConfigId"); + + b.Property("CommandName"); + + b.Property("DateAdded"); + + b.Property("Price"); + + b.HasKey("Id"); + + b.HasIndex("BotConfigId"); + + b.HasIndex("Price") + .IsUnique(); + + b.ToTable("CommandPrice"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.ConvertUnit", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("InternalTrigger"); + + b.Property("Modifier"); + + b.Property("UnitType"); + + b.HasKey("Id"); + + b.ToTable("ConversionUnits"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.Currency", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("Amount"); + + b.Property("DateAdded"); + + b.Property("UserId"); + + b.HasKey("Id"); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("Currency"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.CurrencyTransaction", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("Amount"); + + b.Property("DateAdded"); + + b.Property("Reason"); + + b.Property("UserId"); + + b.HasKey("Id"); + + b.ToTable("CurrencyTransactions"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.CustomReaction", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AutoDeleteTrigger"); + + b.Property("DateAdded"); + + b.Property("DmResponse"); + + b.Property("GuildId"); + + b.Property("IsRegex"); + + b.Property("OwnerOnly"); + + b.Property("Response"); + + b.Property("Trigger"); + + b.HasKey("Id"); + + b.ToTable("CustomReactions"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.DiscordUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AvatarId"); + + b.Property("DateAdded"); + + b.Property("Discriminator"); + + b.Property("UserId"); + + b.Property("Username"); + + b.HasKey("Id"); + + b.HasAlternateKey("UserId"); + + b.ToTable("DiscordUser"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.Donator", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("Amount"); + + b.Property("DateAdded"); + + b.Property("Name"); + + b.Property("UserId"); + + b.HasKey("Id"); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("Donators"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.EightBallResponse", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("BotConfigId"); + + b.Property("DateAdded"); + + b.Property("Text"); + + b.HasKey("Id"); + + b.HasIndex("BotConfigId"); + + b.ToTable("EightBallResponses"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.FilterChannelId", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ChannelId"); + + b.Property("DateAdded"); + + b.Property("GuildConfigId"); + + b.Property("GuildConfigId1"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.HasIndex("GuildConfigId1"); + + b.ToTable("FilterChannelId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.FilteredWord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("GuildConfigId"); + + b.Property("Word"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.ToTable("FilteredWord"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.FollowedStream", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ChannelId"); + + b.Property("DateAdded"); + + b.Property("GuildConfigId"); + + b.Property("GuildId"); + + b.Property("Type"); + + b.Property("Username"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.ToTable("FollowedStream"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.GCChannelId", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ChannelId"); + + b.Property("DateAdded"); + + b.Property("GuildConfigId"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.ToTable("GCChannelId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.GuildConfig", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AutoAssignRoleId"); + + b.Property("AutoDeleteByeMessages"); + + b.Property("AutoDeleteByeMessagesTimer"); + + b.Property("AutoDeleteGreetMessages"); + + b.Property("AutoDeleteGreetMessagesTimer"); + + b.Property("AutoDeleteSelfAssignedRoleMessages"); + + b.Property("ByeMessageChannelId"); + + b.Property("ChannelByeMessageText"); + + b.Property("ChannelGreetMessageText"); + + b.Property("CleverbotEnabled"); + + b.Property("DateAdded"); + + b.Property("DefaultMusicVolume"); + + b.Property("DeleteMessageOnCommand"); + + b.Property("DmGreetMessageText"); + + b.Property("ExclusiveSelfAssignedRoles"); + + b.Property("FilterInvites"); + + b.Property("FilterWords"); + + b.Property("GameVoiceChannel"); + + b.Property("GreetMessageChannelId"); + + b.Property("GuildId"); + + b.Property("Locale"); + + b.Property("LogSettingId"); + + b.Property("MuteRoleName"); + + b.Property("PermissionRole"); + + b.Property("Prefix"); + + b.Property("RootPermissionId"); + + b.Property("SendChannelByeMessage"); + + b.Property("SendChannelGreetMessage"); + + b.Property("SendDmGreetMessage"); + + b.Property("TimeZoneId"); + + b.Property("VerboseErrors"); + + b.Property("VerbosePermissions"); + + b.Property("VoicePlusTextEnabled"); + + b.Property("WarningsInitialized"); + + b.HasKey("Id"); + + b.HasIndex("GuildId") + .IsUnique(); + + b.HasIndex("LogSettingId"); + + b.HasIndex("RootPermissionId"); + + b.ToTable("GuildConfigs"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.GuildRepeater", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ChannelId"); + + b.Property("DateAdded"); + + b.Property("GuildConfigId"); + + b.Property("GuildId"); + + b.Property("Interval"); + + b.Property("Message"); + + b.Property("StartTimeOfDay"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.ToTable("GuildRepeater"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.IgnoredLogChannel", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ChannelId"); + + b.Property("DateAdded"); + + b.Property("LogSettingId"); + + b.HasKey("Id"); + + b.HasIndex("LogSettingId"); + + b.ToTable("IgnoredLogChannels"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.IgnoredVoicePresenceChannel", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ChannelId"); + + b.Property("DateAdded"); + + b.Property("LogSettingId"); + + b.HasKey("Id"); + + b.HasIndex("LogSettingId"); + + b.ToTable("IgnoredVoicePresenceCHannels"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.LogSetting", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ChannelCreated"); + + b.Property("ChannelCreatedId"); + + b.Property("ChannelDestroyed"); + + b.Property("ChannelDestroyedId"); + + b.Property("ChannelId"); + + b.Property("ChannelUpdated"); + + b.Property("ChannelUpdatedId"); + + b.Property("DateAdded"); + + b.Property("IsLogging"); + + b.Property("LogOtherId"); + + b.Property("LogUserPresence"); + + b.Property("LogUserPresenceId"); + + b.Property("LogVoicePresence"); + + b.Property("LogVoicePresenceId"); + + b.Property("LogVoicePresenceTTSId"); + + b.Property("MessageDeleted"); + + b.Property("MessageDeletedId"); + + b.Property("MessageUpdated"); + + b.Property("MessageUpdatedId"); + + b.Property("UserBanned"); + + b.Property("UserBannedId"); + + b.Property("UserJoined"); + + b.Property("UserJoinedId"); + + b.Property("UserLeft"); + + b.Property("UserLeftId"); + + b.Property("UserMutedId"); + + b.Property("UserPresenceChannelId"); + + b.Property("UserUnbanned"); + + b.Property("UserUnbannedId"); + + b.Property("UserUpdated"); + + b.Property("UserUpdatedId"); + + b.Property("VoicePresenceChannelId"); + + b.HasKey("Id"); + + b.ToTable("LogSettings"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.ModulePrefix", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("BotConfigId"); + + b.Property("DateAdded"); + + b.Property("ModuleName"); + + b.Property("Prefix"); + + b.HasKey("Id"); + + b.HasIndex("BotConfigId"); + + b.ToTable("ModulePrefixes"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.MusicPlaylist", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("Author"); + + b.Property("AuthorId"); + + b.Property("DateAdded"); + + b.Property("Name"); + + b.HasKey("Id"); + + b.ToTable("MusicPlaylists"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.MutedUserId", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("GuildConfigId"); + + b.Property("UserId"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.ToTable("MutedUserId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.Permission", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("NextId"); + + b.Property("PrimaryTarget"); + + b.Property("PrimaryTargetId"); + + b.Property("SecondaryTarget"); + + b.Property("SecondaryTargetName"); + + b.Property("State"); + + b.HasKey("Id"); + + b.HasIndex("NextId") + .IsUnique(); + + b.ToTable("Permission"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.Permissionv2", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("GuildConfigId"); + + b.Property("Index"); + + b.Property("PrimaryTarget"); + + b.Property("PrimaryTargetId"); + + b.Property("SecondaryTarget"); + + b.Property("SecondaryTargetName"); + + b.Property("State"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.ToTable("Permissionv2"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.PlayingStatus", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("BotConfigId"); + + b.Property("DateAdded"); + + b.Property("Status"); + + b.HasKey("Id"); + + b.HasIndex("BotConfigId"); + + b.ToTable("PlayingStatus"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.PlaylistSong", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("MusicPlaylistId"); + + b.Property("Provider"); + + b.Property("ProviderType"); + + b.Property("Query"); + + b.Property("Title"); + + b.Property("Uri"); + + b.HasKey("Id"); + + b.HasIndex("MusicPlaylistId"); + + b.ToTable("PlaylistSong"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.Quote", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AuthorId"); + + b.Property("AuthorName") + .IsRequired(); + + b.Property("DateAdded"); + + b.Property("GuildId"); + + b.Property("Keyword") + .IsRequired(); + + b.Property("Text") + .IsRequired(); + + b.HasKey("Id"); + + b.ToTable("Quotes"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.RaceAnimal", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("BotConfigId"); + + b.Property("DateAdded"); + + b.Property("Icon"); + + b.Property("Name"); + + b.HasKey("Id"); + + b.HasIndex("BotConfigId"); + + b.ToTable("RaceAnimals"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.Reminder", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ChannelId"); + + b.Property("DateAdded"); + + b.Property("IsPrivate"); + + b.Property("Message"); + + b.Property("ServerId"); + + b.Property("UserId"); + + b.Property("When"); + + b.HasKey("Id"); + + b.ToTable("Reminders"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.RewardedUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AmountRewardedThisMonth"); + + b.Property("DateAdded"); + + b.Property("LastReward"); + + b.Property("PatreonUserId"); + + b.Property("UserId"); + + b.HasKey("Id"); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("RewardedUsers"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.SelfAssignedRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("GuildId"); + + b.Property("RoleId"); + + b.HasKey("Id"); + + b.HasIndex("GuildId", "RoleId") + .IsUnique(); + + b.ToTable("SelfAssignableRoles"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.ShopEntry", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AuthorId"); + + b.Property("DateAdded"); + + b.Property("GuildConfigId"); + + b.Property("Index"); + + b.Property("Name"); + + b.Property("Price"); + + b.Property("RoleId"); + + b.Property("RoleName"); + + b.Property("Type"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.ToTable("ShopEntry"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.ShopEntryItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("ShopEntryId"); + + b.Property("Text"); + + b.HasKey("Id"); + + b.HasIndex("ShopEntryId"); + + b.ToTable("ShopEntryItem"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.SlowmodeIgnoredRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("GuildConfigId"); + + b.Property("RoleId"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.ToTable("SlowmodeIgnoredRole"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.SlowmodeIgnoredUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("GuildConfigId"); + + b.Property("UserId"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.ToTable("SlowmodeIgnoredUser"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.StartupCommand", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("BotConfigId"); + + b.Property("ChannelId"); + + b.Property("ChannelName"); + + b.Property("CommandText"); + + b.Property("DateAdded"); + + b.Property("GuildId"); + + b.Property("GuildName"); + + b.Property("Index"); + + b.Property("VoiceChannelId"); + + b.Property("VoiceChannelName"); + + b.HasKey("Id"); + + b.HasIndex("BotConfigId"); + + b.ToTable("StartupCommand"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.StreamRoleSettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AddRoleId"); + + b.Property("DateAdded"); + + b.Property("FromRoleId"); + + b.Property("GuildConfigId"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId") + .IsUnique(); + + b.ToTable("StreamRoleSettings"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.UnmuteTimer", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("GuildConfigId"); + + b.Property("UnmuteAt"); + + b.Property("UserId"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.ToTable("UnmuteTimer"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.UserPokeTypes", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("UserId"); + + b.Property("type"); + + b.HasKey("Id"); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("PokeGame"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.VcRoleInfo", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("GuildConfigId"); + + b.Property("RoleId"); + + b.Property("VoiceChannelId"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.ToTable("VcRoleInfo"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.WaifuInfo", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AffinityId"); + + b.Property("ClaimerId"); + + b.Property("DateAdded"); + + b.Property("Price"); + + b.Property("WaifuId"); + + b.HasKey("Id"); + + b.HasIndex("AffinityId"); + + b.HasIndex("ClaimerId"); + + b.HasIndex("WaifuId") + .IsUnique(); + + b.ToTable("WaifuInfo"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.WaifuUpdate", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("NewId"); + + b.Property("OldId"); + + b.Property("UpdateType"); + + b.Property("UserId"); + + b.HasKey("Id"); + + b.HasIndex("NewId"); + + b.HasIndex("OldId"); + + b.HasIndex("UserId"); + + b.ToTable("WaifuUpdates"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.Warning", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("Forgiven"); + + b.Property("ForgivenBy"); + + b.Property("GuildId"); + + b.Property("Moderator"); + + b.Property("Reason"); + + b.Property("UserId"); + + b.HasKey("Id"); + + b.ToTable("Warnings"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.WarningPunishment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("Count"); + + b.Property("DateAdded"); + + b.Property("GuildConfigId"); + + b.Property("Punishment"); + + b.Property("Time"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.ToTable("WarningPunishment"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.AntiRaidSetting", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig", "GuildConfig") + .WithOne("AntiRaidSetting") + .HasForeignKey("NadekoBot.Services.Database.Models.AntiRaidSetting", "GuildConfigId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.AntiSpamIgnore", b => + { + b.HasOne("NadekoBot.Services.Database.Models.AntiSpamSetting") + .WithMany("IgnoredChannels") + .HasForeignKey("AntiSpamSettingId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.AntiSpamSetting", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig", "GuildConfig") + .WithOne("AntiSpamSetting") + .HasForeignKey("NadekoBot.Services.Database.Models.AntiSpamSetting", "GuildConfigId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.BlacklistItem", b => + { + b.HasOne("NadekoBot.Services.Database.Models.BotConfig") + .WithMany("Blacklist") + .HasForeignKey("BotConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.BlockedCmdOrMdl", b => + { + b.HasOne("NadekoBot.Services.Database.Models.BotConfig") + .WithMany("BlockedCommands") + .HasForeignKey("BotConfigId"); + + b.HasOne("NadekoBot.Services.Database.Models.BotConfig") + .WithMany("BlockedModules") + .HasForeignKey("BotConfigId1"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.ClashCaller", b => + { + b.HasOne("NadekoBot.Services.Database.Models.ClashWar", "ClashWar") + .WithMany("Bases") + .HasForeignKey("ClashWarId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.CommandAlias", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig") + .WithMany("CommandAliases") + .HasForeignKey("GuildConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.CommandCooldown", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig") + .WithMany("CommandCooldowns") + .HasForeignKey("GuildConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.CommandPrice", b => + { + b.HasOne("NadekoBot.Services.Database.Models.BotConfig") + .WithMany("CommandPrices") + .HasForeignKey("BotConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.EightBallResponse", b => + { + b.HasOne("NadekoBot.Services.Database.Models.BotConfig") + .WithMany("EightBallResponses") + .HasForeignKey("BotConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.FilterChannelId", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig") + .WithMany("FilterInvitesChannelIds") + .HasForeignKey("GuildConfigId"); + + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig") + .WithMany("FilterWordsChannelIds") + .HasForeignKey("GuildConfigId1"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.FilteredWord", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig") + .WithMany("FilteredWords") + .HasForeignKey("GuildConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.FollowedStream", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig") + .WithMany("FollowedStreams") + .HasForeignKey("GuildConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.GCChannelId", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig") + .WithMany("GenerateCurrencyChannelIds") + .HasForeignKey("GuildConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.GuildConfig", b => + { + b.HasOne("NadekoBot.Services.Database.Models.LogSetting", "LogSetting") + .WithMany() + .HasForeignKey("LogSettingId"); + + b.HasOne("NadekoBot.Services.Database.Models.Permission", "RootPermission") + .WithMany() + .HasForeignKey("RootPermissionId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.GuildRepeater", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig") + .WithMany("GuildRepeaters") + .HasForeignKey("GuildConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.IgnoredLogChannel", b => + { + b.HasOne("NadekoBot.Services.Database.Models.LogSetting", "LogSetting") + .WithMany("IgnoredChannels") + .HasForeignKey("LogSettingId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.IgnoredVoicePresenceChannel", b => + { + b.HasOne("NadekoBot.Services.Database.Models.LogSetting", "LogSetting") + .WithMany("IgnoredVoicePresenceChannelIds") + .HasForeignKey("LogSettingId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.ModulePrefix", b => + { + b.HasOne("NadekoBot.Services.Database.Models.BotConfig") + .WithMany("ModulePrefixes") + .HasForeignKey("BotConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.MutedUserId", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig") + .WithMany("MutedUsers") + .HasForeignKey("GuildConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.Permission", b => + { + b.HasOne("NadekoBot.Services.Database.Models.Permission", "Next") + .WithOne("Previous") + .HasForeignKey("NadekoBot.Services.Database.Models.Permission", "NextId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.Permissionv2", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig") + .WithMany("Permissions") + .HasForeignKey("GuildConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.PlayingStatus", b => + { + b.HasOne("NadekoBot.Services.Database.Models.BotConfig") + .WithMany("RotatingStatusMessages") + .HasForeignKey("BotConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.PlaylistSong", b => + { + b.HasOne("NadekoBot.Services.Database.Models.MusicPlaylist") + .WithMany("Songs") + .HasForeignKey("MusicPlaylistId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.RaceAnimal", b => + { + b.HasOne("NadekoBot.Services.Database.Models.BotConfig") + .WithMany("RaceAnimals") + .HasForeignKey("BotConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.ShopEntry", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig") + .WithMany("ShopEntries") + .HasForeignKey("GuildConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.ShopEntryItem", b => + { + b.HasOne("NadekoBot.Services.Database.Models.ShopEntry") + .WithMany("Items") + .HasForeignKey("ShopEntryId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.SlowmodeIgnoredRole", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig") + .WithMany("SlowmodeIgnoredRoles") + .HasForeignKey("GuildConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.SlowmodeIgnoredUser", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig") + .WithMany("SlowmodeIgnoredUsers") + .HasForeignKey("GuildConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.StartupCommand", b => + { + b.HasOne("NadekoBot.Services.Database.Models.BotConfig") + .WithMany("StartupCommands") + .HasForeignKey("BotConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.StreamRoleSettings", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig", "GuildConfig") + .WithOne("StreamRole") + .HasForeignKey("NadekoBot.Services.Database.Models.StreamRoleSettings", "GuildConfigId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.UnmuteTimer", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig") + .WithMany("UnmuteTimers") + .HasForeignKey("GuildConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.VcRoleInfo", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig") + .WithMany("VcRoleInfos") + .HasForeignKey("GuildConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.WaifuInfo", b => + { + b.HasOne("NadekoBot.Services.Database.Models.DiscordUser", "Affinity") + .WithMany() + .HasForeignKey("AffinityId"); + + b.HasOne("NadekoBot.Services.Database.Models.DiscordUser", "Claimer") + .WithMany() + .HasForeignKey("ClaimerId"); + + b.HasOne("NadekoBot.Services.Database.Models.DiscordUser", "Waifu") + .WithOne() + .HasForeignKey("NadekoBot.Services.Database.Models.WaifuInfo", "WaifuId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.WaifuUpdate", b => + { + b.HasOne("NadekoBot.Services.Database.Models.DiscordUser", "New") + .WithMany() + .HasForeignKey("NewId"); + + b.HasOne("NadekoBot.Services.Database.Models.DiscordUser", "Old") + .WithMany() + .HasForeignKey("OldId"); + + b.HasOne("NadekoBot.Services.Database.Models.DiscordUser", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.WarningPunishment", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig") + .WithMany("WarnPunishments") + .HasForeignKey("GuildConfigId"); + }); + } + } +} diff --git a/src/NadekoBot/Migrations/20170714021615_stream-role.cs b/src/NadekoBot/Migrations/20170714021615_stream-role.cs new file mode 100644 index 00000000..22f6abec --- /dev/null +++ b/src/NadekoBot/Migrations/20170714021615_stream-role.cs @@ -0,0 +1,46 @@ +using System; +using System.Collections.Generic; +using Microsoft.EntityFrameworkCore.Migrations; + +namespace NadekoBot.Migrations +{ + public partial class streamrole : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "StreamRoleSettings", + columns: table => new + { + Id = table.Column(nullable: false) + .Annotation("Sqlite:Autoincrement", true), + AddRoleId = table.Column(nullable: false), + DateAdded = table.Column(nullable: true), + FromRoleId = table.Column(nullable: false), + GuildConfigId = table.Column(nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_StreamRoleSettings", x => x.Id); + table.ForeignKey( + name: "FK_StreamRoleSettings_GuildConfigs_GuildConfigId", + column: x => x.GuildConfigId, + principalTable: "GuildConfigs", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_StreamRoleSettings_GuildConfigId", + table: "StreamRoleSettings", + column: "GuildConfigId", + unique: true); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "StreamRoleSettings"); + } + } +} diff --git a/src/NadekoBot/Migrations/NadekoSqliteContextModelSnapshot.cs b/src/NadekoBot/Migrations/NadekoSqliteContextModelSnapshot.cs index 44ce853f..42fa008a 100644 --- a/src/NadekoBot/Migrations/NadekoSqliteContextModelSnapshot.cs +++ b/src/NadekoBot/Migrations/NadekoSqliteContextModelSnapshot.cs @@ -1126,6 +1126,27 @@ namespace NadekoBot.Migrations b.ToTable("StartupCommand"); }); + modelBuilder.Entity("NadekoBot.Services.Database.Models.StreamRoleSettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AddRoleId"); + + b.Property("DateAdded"); + + b.Property("FromRoleId"); + + b.Property("GuildConfigId"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId") + .IsUnique(); + + b.ToTable("StreamRoleSettings"); + }); + modelBuilder.Entity("NadekoBot.Services.Database.Models.UnmuteTimer", b => { b.Property("Id") @@ -1510,6 +1531,14 @@ namespace NadekoBot.Migrations .HasForeignKey("BotConfigId"); }); + modelBuilder.Entity("NadekoBot.Services.Database.Models.StreamRoleSettings", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig", "GuildConfig") + .WithOne("StreamRole") + .HasForeignKey("NadekoBot.Services.Database.Models.StreamRoleSettings", "GuildConfigId") + .OnDelete(DeleteBehavior.Cascade); + }); + modelBuilder.Entity("NadekoBot.Services.Database.Models.UnmuteTimer", b => { b.HasOne("NadekoBot.Services.Database.Models.GuildConfig") diff --git a/src/NadekoBot/Modules/Utility/Commands/StreamRoleCommands.cs b/src/NadekoBot/Modules/Utility/Commands/StreamRoleCommands.cs new file mode 100644 index 00000000..769f04fa --- /dev/null +++ b/src/NadekoBot/Modules/Utility/Commands/StreamRoleCommands.cs @@ -0,0 +1,39 @@ +using Discord; +using Discord.Commands; +using NadekoBot.Attributes; +using NadekoBot.Services.Utility; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace NadekoBot.Modules.Utility.Commands +{ + public class StreamRoleCommands : NadekoSubmodule + { + private readonly StreamRoleService service; + + public StreamRoleCommands(StreamRoleService service) + { + this.service = service; + } + + [NadekoCommand, Usage, Description, Aliases] + [RequireContext(ContextType.Guild)] + public async Task StreamRole(IRole fromRole, IRole addRole) + { + this.service.SetStreamRole(fromRole, addRole); + + await ReplyConfirmLocalized("stream_role_enabled", Format.Bold(fromRole.ToString()), Format.Bold(addRole.ToString())).ConfigureAwait(false); + } + + [NadekoCommand, Usage, Description, Aliases] + [RequireContext(ContextType.Guild)] + public async Task StreamRole() + { + this.service.StopStreamRole(Context.Guild.Id); + await ReplyConfirmLocalized("stream_role_disabled").ConfigureAwait(false); + } + } +} diff --git a/src/NadekoBot/NadekoBot.csproj b/src/NadekoBot/NadekoBot.csproj index 84884614..22771b48 100644 --- a/src/NadekoBot/NadekoBot.csproj +++ b/src/NadekoBot/NadekoBot.csproj @@ -91,6 +91,5 @@ - diff --git a/src/NadekoBot/Resources/CommandStrings.resx b/src/NadekoBot/Resources/CommandStrings.resx index a55bfb98..b7625645 100644 --- a/src/NadekoBot/Resources/CommandStrings.resx +++ b/src/NadekoBot/Resources/CommandStrings.resx @@ -1700,6 +1700,15 @@ `{0}save classical1` + + + streamrole + + + Sets a role which is monitored for streamers (FromRole), and a role to add if a user from 'FromRole' is streaming (AddRole). When a user from 'FromRole' starts streaming, they will receive an 'AddRole'. Provide no arguments to disable + + + `{0}streamrole "Eligible Streamers" "Featured Streams"` load diff --git a/src/NadekoBot/Services/Database/Models/GuildConfig.cs b/src/NadekoBot/Services/Database/Models/GuildConfig.cs index 9b13294e..6222712b 100644 --- a/src/NadekoBot/Services/Database/Models/GuildConfig.cs +++ b/src/NadekoBot/Services/Database/Models/GuildConfig.cs @@ -83,6 +83,8 @@ namespace NadekoBot.Services.Database.Models public ulong? GameVoiceChannel { get; set; } = null; public bool VerboseErrors { get; set; } = false; + public StreamRoleSettings StreamRole { get; set; } = new StreamRoleSettings(); + //public List ProtectionIgnoredChannels { get; set; } = new List(); } diff --git a/src/NadekoBot/Services/Database/Models/StreamRoleSettings.cs b/src/NadekoBot/Services/Database/Models/StreamRoleSettings.cs new file mode 100644 index 00000000..f73abeac --- /dev/null +++ b/src/NadekoBot/Services/Database/Models/StreamRoleSettings.cs @@ -0,0 +1,23 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace NadekoBot.Services.Database.Models +{ + public class StreamRoleSettings : DbEntity + { + public int GuildConfigId { get; set; } + public GuildConfig GuildConfig { get; set; } + + /// + /// Id of the role to give to the users in the role 'FromRole' when they start streaming + /// + public ulong AddRoleId { get; set; } + /// + /// Id of the role whose users are eligible to get the 'AddRole' + /// + public ulong FromRoleId { get; set; } + } +} diff --git a/src/NadekoBot/Services/Database/NadekoContext.cs b/src/NadekoBot/Services/Database/NadekoContext.cs index 238130ed..7c00e1f3 100644 --- a/src/NadekoBot/Services/Database/NadekoContext.cs +++ b/src/NadekoBot/Services/Database/NadekoContext.cs @@ -169,13 +169,19 @@ namespace NadekoBot.Services.Database #endregion + #region streamrole + modelBuilder.Entity() + .HasOne(x => x.GuildConfig) + .WithOne(x => x.StreamRole); + #endregion + #region BotConfig //var botConfigEntity = modelBuilder.Entity(); //botConfigEntity // .HasMany(c => c.ModulePrefixes) // .WithOne(mp => mp.BotConfig) // .HasForeignKey(mp => mp.BotConfigId); - + #endregion #region ClashOfClans diff --git a/src/NadekoBot/Services/Database/Repositories/Impl/GuildConfigRepository.cs b/src/NadekoBot/Services/Database/Repositories/Impl/GuildConfigRepository.cs index 3628a4e9..9c0931d5 100644 --- a/src/NadekoBot/Services/Database/Repositories/Impl/GuildConfigRepository.cs +++ b/src/NadekoBot/Services/Database/Repositories/Impl/GuildConfigRepository.cs @@ -45,6 +45,7 @@ namespace NadekoBot.Services.Database.Repositories.Impl .Include(gc => gc.AntiSpamSetting) .ThenInclude(x => x.IgnoredChannels) .Include(gc => gc.FollowedStreams) + .Include(gc => gc.StreamRole) .ToList(); /// diff --git a/src/NadekoBot/Services/Utility/StreamRoleService.cs b/src/NadekoBot/Services/Utility/StreamRoleService.cs new file mode 100644 index 00000000..2d3ac97f --- /dev/null +++ b/src/NadekoBot/Services/Utility/StreamRoleService.cs @@ -0,0 +1,150 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Discord; +using NadekoBot.Services.Database.Models; +using System.Collections.Concurrent; +using NadekoBot.Extensions; +using Discord.WebSocket; +using Microsoft.EntityFrameworkCore; +using NLog; + +namespace NadekoBot.Services.Utility +{ + public class StreamRoleService + { + private readonly DbService _db; + private readonly ConcurrentDictionary guildSettings; + //(guildId, userId), roleId + private readonly ConcurrentDictionary<(ulong GuildId, ulong UserId), ulong> toRemove = new ConcurrentDictionary<(ulong GuildId, ulong UserId), ulong>(); + private readonly Logger _log; + + public StreamRoleService(DiscordSocketClient client, DbService db, IEnumerable gcs) + { + this._db = db; + this._log = LogManager.GetCurrentClassLogger(); + + guildSettings = gcs.ToDictionary(x => x.GuildId, x => x.StreamRole) + .ToConcurrent(); + + client.GuildMemberUpdated += Client_GuildMemberUpdated; + } + + 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 + // and has a game status now + // and that status is a streaming status + // and we are supposed to give him a role + if ((!before.Game.HasValue || before.Game.Value.StreamType == StreamType.NotStreaming) && + after.Game.HasValue && + after.Game.Value.StreamType != StreamType.NotStreaming + && guildSettings.TryGetValue(after.Guild.Id, out var setting)) + { + IRole fromRole; + IRole addRole; + try + { + //get needed roles + fromRole = after.Guild.GetRole(setting.FromRoleId); + addRole = after.Guild.GetRole(setting.AddRoleId); + } + catch (Exception ex) + { + StopStreamRole(before.Guild.Id); + _log.Warn("Error getting Stream Role(s). Disabling stream role feature."); + _log.Error(ex); + return; + } + + try + { + //check if user is in the fromrole + if (after.Roles.Contains(fromRole)) + { + //check if he doesn't have addrole already, to avoid errors + if(!after.Roles.Contains(addRole)) + await after.AddRoleAsync(addRole).ConfigureAwait(false); + //schedule him for the role removal when he stops streaming + toRemove.TryAdd((addRole.Guild.Id, after.Id), addRole.Id); + } + } + catch (Exception ex) + { + _log.Warn("Failed adding stream role."); + _log.Error(ex); + } + } + + // try removing a role that was given to the user + // if user had a game status + // and he was streaming + // and he no longer has a game status, or has a game status which is not a stream + // and if he's scheduled for role removal, get the roleid to remove + else if (before.Game.HasValue && + before.Game.Value.StreamType != StreamType.NotStreaming && + (!after.Game.HasValue || after.Game.Value.StreamType == StreamType.NotStreaming) && + toRemove.TryRemove((after.Guild.Id, after.Id), out var roleId)) + { + try + { + //get the role to remove from the role id + var role = after.Guild.GetRole(roleId); + if (role == null) + return; + //check if user has the role which needs to be removed to avoid errors + if (after.Roles.Contains(role)) + await after.RemoveRoleAsync(role).ConfigureAwait(false); + } + catch (Exception ex) + { + _log.Warn("Failed removing the stream role from the user who stopped streaming."); + _log.Error(ex); + } + } + }); + + return Task.CompletedTask; + } + + public void SetStreamRole(IRole fromRole, IRole addRole) + { + StreamRoleSettings setting; + using (var uow = _db.UnitOfWork) + { + var gc = uow.GuildConfigs.For(fromRole.Guild.Id, x => x.Include(y => y.StreamRole)); + + if (gc.StreamRole == null) + gc.StreamRole = new StreamRoleSettings() + { + AddRoleId = addRole.Id, + FromRoleId = fromRole.Id + }; + else + { + gc.StreamRole.AddRoleId = addRole.Id; + gc.StreamRole.FromRoleId = fromRole.Id; + } + setting = gc.StreamRole; + uow.Complete(); + } + + guildSettings.AddOrUpdate(fromRole.Guild.Id, (key) => setting, (key, old) => setting); + } + + public void StopStreamRole(ulong guildId) + { + using (var uow = _db.UnitOfWork) + { + var gc = uow.GuildConfigs.For(guildId, x => x.Include(y => y.StreamRole)); + gc.StreamRole = null; + uow.Complete(); + } + + guildSettings.TryRemove(guildId, out _); + } + } +} diff --git a/src/NadekoBot/_strings/ResponseStrings.en-US.json b/src/NadekoBot/_strings/ResponseStrings.en-US.json index 7c330d6d..bfaa9248 100644 --- a/src/NadekoBot/_strings/ResponseStrings.en-US.json +++ b/src/NadekoBot/_strings/ResponseStrings.en-US.json @@ -608,6 +608,8 @@ "searches_wind_speed": "Wind speed", "searches_x_most_banned_champs": "The {0} most banned champions", "searches_yodify_error": "Failed to yodify your sentence.", + "utility_stream_role_enabled": "When a user from {0} role starts streaming, I will give them {1} role.", + "utility_stream_role_disabled": "Stream role feature has been disabled.", "utiliity_joined": "Joined", "utility_activity_line": "`{0}.` {1} [{2:F2}/s] - {3} total", "utility_activity_page": "Activity page #{0}", From 11b533c3b7e596947a811da8acc29f3e422a813e Mon Sep 17 00:00:00 2001 From: samvaio Date: Fri, 14 Jul 2017 09:45:31 +0530 Subject: [PATCH 184/346] Done as per Kwoth. --- src/NadekoBot/Services/Music/MusicPlayer.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/NadekoBot/Services/Music/MusicPlayer.cs b/src/NadekoBot/Services/Music/MusicPlayer.cs index 50779c85..8e47e984 100644 --- a/src/NadekoBot/Services/Music/MusicPlayer.cs +++ b/src/NadekoBot/Services/Music/MusicPlayer.cs @@ -62,7 +62,7 @@ namespace NadekoBot.Services.Music public bool RepeatCurrentSong { get; private set; } public bool Shuffle { get; private set; } public bool Autoplay { get; private set; } - public bool RepeatPlaylist { get; private set; } = true; + public bool RepeatPlaylist { get; private set; } = false; public uint MaxQueueSize { get => Queue.MaxQueueSize; @@ -447,7 +447,7 @@ namespace NadekoBot.Services.Music lock (locker) { Stopped = true; - Queue.ResetCurrent(); + //Queue.ResetCurrent(); if (clearQueue) Queue.Clear(); Unpause(); From 17719dd8e2182aa900baffceee0381974ed63cba Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Fri, 14 Jul 2017 17:09:06 +0200 Subject: [PATCH 185/346] finished .streamrole --- .../Utility/Commands/StreamRoleCommands.cs | 53 ++++++++++--------- src/NadekoBot/NadekoBot.cs | 2 + .../Services/Utility/StreamRoleService.cs | 4 ++ 3 files changed, 34 insertions(+), 25 deletions(-) diff --git a/src/NadekoBot/Modules/Utility/Commands/StreamRoleCommands.cs b/src/NadekoBot/Modules/Utility/Commands/StreamRoleCommands.cs index 769f04fa..5bfa393b 100644 --- a/src/NadekoBot/Modules/Utility/Commands/StreamRoleCommands.cs +++ b/src/NadekoBot/Modules/Utility/Commands/StreamRoleCommands.cs @@ -2,38 +2,41 @@ using Discord.Commands; using NadekoBot.Attributes; using NadekoBot.Services.Utility; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; using System.Threading.Tasks; -namespace NadekoBot.Modules.Utility.Commands +namespace NadekoBot.Modules.Utility { - public class StreamRoleCommands : NadekoSubmodule + public partial class Utility { - private readonly StreamRoleService service; - - public StreamRoleCommands(StreamRoleService service) + public class StreamRoleCommands : NadekoSubmodule { - this.service = service; - } + private readonly StreamRoleService service; - [NadekoCommand, Usage, Description, Aliases] - [RequireContext(ContextType.Guild)] - public async Task StreamRole(IRole fromRole, IRole addRole) - { - this.service.SetStreamRole(fromRole, addRole); + public StreamRoleCommands(StreamRoleService service) + { + this.service = service; + } - await ReplyConfirmLocalized("stream_role_enabled", Format.Bold(fromRole.ToString()), Format.Bold(addRole.ToString())).ConfigureAwait(false); - } + [NadekoCommand, Usage, Description, Aliases] + [RequireBotPermission(GuildPermission.ManageRoles)] + [RequireUserPermission(GuildPermission.ManageRoles)] + [RequireContext(ContextType.Guild)] + public async Task StreamRole(IRole fromRole, IRole addRole) + { + this.service.SetStreamRole(fromRole, addRole); - [NadekoCommand, Usage, Description, Aliases] - [RequireContext(ContextType.Guild)] - public async Task StreamRole() - { - this.service.StopStreamRole(Context.Guild.Id); - await ReplyConfirmLocalized("stream_role_disabled").ConfigureAwait(false); + await ReplyConfirmLocalized("stream_role_enabled", Format.Bold(fromRole.ToString()), Format.Bold(addRole.ToString())).ConfigureAwait(false); + } + + [NadekoCommand, Usage, Description, Aliases] + [RequireBotPermission(GuildPermission.ManageRoles)] + [RequireUserPermission(GuildPermission.ManageRoles)] + [RequireContext(ContextType.Guild)] + public async Task StreamRole() + { + this.service.StopStreamRole(Context.Guild.Id); + await ReplyConfirmLocalized("stream_role_disabled").ConfigureAwait(false); + } } } -} +} \ No newline at end of file diff --git a/src/NadekoBot/NadekoBot.cs b/src/NadekoBot/NadekoBot.cs index 1c983731..e5b59a29 100644 --- a/src/NadekoBot/NadekoBot.cs +++ b/src/NadekoBot/NadekoBot.cs @@ -167,6 +167,7 @@ namespace NadekoBot var patreonRewardsService = new PatreonRewardsService(Credentials, Db, Currency, Client); var verboseErrorsService = new VerboseErrorsService(AllGuildConfigs, Db, CommandHandler, helpService); var pruneService = new PruneService(); + var streamRoleService = new StreamRoleService(Client, Db, AllGuildConfigs); #endregion #region permissions @@ -236,6 +237,7 @@ namespace NadekoBot .Add(verboseErrorsService) .Add(patreonRewardsService) .Add(pruneService) + .Add(streamRoleService) .Add(searchesService) .Add(streamNotificationService) .Add(animeSearchService) diff --git a/src/NadekoBot/Services/Utility/StreamRoleService.cs b/src/NadekoBot/Services/Utility/StreamRoleService.cs index 2d3ac97f..2bd8a156 100644 --- a/src/NadekoBot/Services/Utility/StreamRoleService.cs +++ b/src/NadekoBot/Services/Utility/StreamRoleService.cs @@ -50,7 +50,11 @@ namespace NadekoBot.Services.Utility { //get needed roles fromRole = after.Guild.GetRole(setting.FromRoleId); + if (fromRole == null) + throw new InvalidOperationException(); addRole = after.Guild.GetRole(setting.AddRoleId); + if (addRole == null) + throw new InvalidOperationException(); } catch (Exception ex) { From f239c46e20ac987873f7f53f9ea74656f8d75392 Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Fri, 14 Jul 2017 18:39:21 +0200 Subject: [PATCH 186/346] Words should get filtered in edited messages too. --- src/NadekoBot/Services/Permissions/FilterService.cs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/NadekoBot/Services/Permissions/FilterService.cs b/src/NadekoBot/Services/Permissions/FilterService.cs index a6d97d0d..9ac67d4d 100644 --- a/src/NadekoBot/Services/Permissions/FilterService.cs +++ b/src/NadekoBot/Services/Permissions/FilterService.cs @@ -59,7 +59,13 @@ namespace NadekoBot.Services.Permissions _client.MessageUpdated += (oldData, newMsg, channel) => { - var _ = Task.Run(() => FilterInvites((channel as ITextChannel)?.Guild, newMsg as IUserMessage)); + var _ = Task.Run(async () => + { + var guild = (channel as ITextChannel)?.Guild; + var usrMsg = newMsg as IUserMessage; + + return (await FilterInvites(guild, usrMsg)) || (await FilterWords(guild, usrMsg)); + }); return Task.CompletedTask; }; } From 4eca5be1d43088bc40da2182dacf3f25e45bb4be Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Sat, 15 Jul 2017 05:04:16 +0200 Subject: [PATCH 187/346] Huge cleanup, rewrite of the NadekoBot.cs, way services are loaded has changed. Updated discord.net. --- .../TypeReaders/BotCommandTypeReader.cs | 26 +- .../TypeReaders/GuildDateTimeTypeReader.cs | 11 +- .../Administration/Commands/MuteCommands.cs | 8 +- .../Administration/Commands/PrefixCommands.cs | 4 +- .../Administration/Commands/PruneCommands.cs | 4 +- .../Commands/RatelimitCommand.cs | 4 +- .../Administration/Commands/SelfCommands.cs | 4 +- .../Commands/UserPunishCommands.cs | 8 +- .../Modules/ClashOfClans/ClashOfClans.cs | 239 ---------------- .../CustomReactions/CustomReactions.cs | 4 +- .../Gambling/Commands/DiceRollCommand.cs | 8 +- .../Gambling/Commands/WaifuClaimCommands.cs | 4 +- src/NadekoBot/Modules/Gambling/Gambling.cs | 8 +- src/NadekoBot/Modules/Help/Help.cs | 4 +- src/NadekoBot/Modules/Music/Music.cs | 39 +-- src/NadekoBot/Modules/NSFW/NSFW.cs | 14 +- .../Searches/Commands/AnimeSearchCommands.cs | 4 +- .../Modules/Searches/Commands/XkcdCommands.cs | 4 +- .../Modules/Utility/Commands/Remind.cs | 4 +- .../Utility/Commands/RepeatCommands.cs | 5 +- src/NadekoBot/NadekoBot.cs | 210 +++----------- src/NadekoBot/NadekoBot.csproj | 2 +- .../Administration/AdministrationService.cs | 2 +- .../Administration/AutoAssignRoleService.cs | 2 +- .../Administration/GameVoiceChannelService.cs | 2 +- .../Administration/GuildTimezoneService.cs | 2 +- .../Administration/LogCommandService.cs | 2 +- .../Services/Administration/MuteService.cs | 2 +- .../Administration/PlayingRotateService.cs | 2 +- .../Administration/ProtectionService.cs | 2 +- .../Services/Administration/PruneService.cs | 2 +- .../Administration/RatelimitService.cs | 2 +- .../Services/Administration/SelfService.cs | 8 +- .../Services/Administration/VcRoleService.cs | 2 +- .../Services/Administration/VplusTService.cs | 2 +- .../ClashOfClans/ClashOfClansService.cs | 262 ------------------ .../Services/ClashOfClans/Extensions.cs | 40 --- src/NadekoBot/Services/CommandHandler.cs | 182 +++++++----- src/NadekoBot/Services/CurrencyService.cs | 2 +- .../CustomReactions/CustomReactionsService.cs | 2 +- .../Services/Database/Models/BotConfig.cs | 2 +- .../Repositories/IReminderRepository.cs | 2 +- .../Repositories/Impl/ReminderRepository.cs | 2 +- .../Services/Games/ChatterbotService.cs | 2 +- src/NadekoBot/Services/Games/GamesService.cs | 2 +- src/NadekoBot/Services/Games/PollService.cs | 2 +- .../Services/GreetSettingsService.cs | 2 +- src/NadekoBot/Services/Help/HelpService.cs | 2 +- src/NadekoBot/Services/IGoogleApiService.cs | 2 +- src/NadekoBot/Services/IImagesService.cs | 2 +- src/NadekoBot/Services/INService.cs | 16 ++ src/NadekoBot/Services/IStatsService.cs | 2 +- src/NadekoBot/Services/Impl/NadekoStrings.cs | 2 +- .../Services/Impl/SoundCloudApiService.cs | 2 +- .../Impl/StartingGuildsListService.cs | 24 ++ src/NadekoBot/Services/Music/MusicService.cs | 2 +- .../Services/Permissions/BlacklistService.cs | 2 +- .../Services/Permissions/CmdCdService.cs | 2 +- .../Services/Permissions/FilterService.cs | 2 +- .../Permissions/GlobalPermissionService.cs | 2 +- .../Permissions/PermissionsService.cs | 40 ++- .../Services/Pokemon/PokemonService.cs | 2 +- .../Services/Searches/AnimeSearchService.cs | 2 +- .../Services/Searches/SearchesService.cs | 2 +- .../Searches/StreamNotificationService.cs | 2 +- src/NadekoBot/Services/ServiceProvider.cs | 72 ++++- .../Services/Utility/CommandMapService.cs | 2 +- .../Services/Utility/ConverterService.cs | 2 +- .../Utility/MessageRepeaterService.cs | 5 +- .../Services/Utility/PatreonRewardsService.cs | 3 +- .../Services/Utility/RemindService.cs | 8 +- .../Services/Utility/StreamRoleService.cs | 3 +- .../Services/Utility/VerboseErrorsService.cs | 2 +- src/NadekoBot/ShardsCoordinator.cs | 1 - 74 files changed, 395 insertions(+), 965 deletions(-) delete mode 100644 src/NadekoBot/Modules/ClashOfClans/ClashOfClans.cs delete mode 100644 src/NadekoBot/Services/ClashOfClans/ClashOfClansService.cs delete mode 100644 src/NadekoBot/Services/ClashOfClans/Extensions.cs create mode 100644 src/NadekoBot/Services/INService.cs create mode 100644 src/NadekoBot/Services/Impl/StartingGuildsListService.cs diff --git a/src/NadekoBot/DataStructures/TypeReaders/BotCommandTypeReader.cs b/src/NadekoBot/DataStructures/TypeReaders/BotCommandTypeReader.cs index 8a415eae..d3e7b2c3 100644 --- a/src/NadekoBot/DataStructures/TypeReaders/BotCommandTypeReader.cs +++ b/src/NadekoBot/DataStructures/TypeReaders/BotCommandTypeReader.cs @@ -9,17 +9,10 @@ namespace NadekoBot.TypeReaders { public class CommandTypeReader : TypeReader { - private readonly CommandService _cmds; - private readonly CommandHandler _cmdHandler; - - public CommandTypeReader(CommandService cmds, CommandHandler cmdHandler) - { - _cmds = cmds; - _cmdHandler = cmdHandler; - } - - public override Task Read(ICommandContext context, string input, IServiceProvider _) + public override Task Read(ICommandContext context, string input, IServiceProvider services) { + var _cmds = ((INServiceProvider)services).GetService(); + var _cmdHandler = ((INServiceProvider)services).GetService(); input = input.ToUpperInvariant(); var prefix = _cmdHandler.GetPrefix(context.Guild); if (!input.StartsWith(prefix.ToUpperInvariant())) @@ -38,17 +31,12 @@ namespace NadekoBot.TypeReaders public class CommandOrCrTypeReader : CommandTypeReader { - private readonly CustomReactionsService _crs; - - public CommandOrCrTypeReader(CustomReactionsService crs, CommandService cmds, CommandHandler cmdHandler) : base(cmds, cmdHandler) - { - _crs = crs; - } - - public override async Task Read(ICommandContext context, string input, IServiceProvider _) + public override async Task Read(ICommandContext context, string input, IServiceProvider services) { input = input.ToUpperInvariant(); + var _crs = ((INServiceProvider)services).GetService(); + if (_crs.GlobalReactions.Any(x => x.Trigger.ToUpperInvariant() == input)) { return TypeReaderResult.FromSuccess(new CommandOrCrInfo(input)); @@ -65,7 +53,7 @@ namespace NadekoBot.TypeReaders } } - var cmd = await base.Read(context, input, _); + var cmd = await base.Read(context, input, services); if (cmd.IsSuccess) { return TypeReaderResult.FromSuccess(new CommandOrCrInfo(((CommandInfo)cmd.Values.First().Value).Name)); diff --git a/src/NadekoBot/DataStructures/TypeReaders/GuildDateTimeTypeReader.cs b/src/NadekoBot/DataStructures/TypeReaders/GuildDateTimeTypeReader.cs index 7e394a21..8b6ea976 100644 --- a/src/NadekoBot/DataStructures/TypeReaders/GuildDateTimeTypeReader.cs +++ b/src/NadekoBot/DataStructures/TypeReaders/GuildDateTimeTypeReader.cs @@ -1,4 +1,5 @@ using Discord.Commands; +using NadekoBot.Services; using NadekoBot.Services.Administration; using System; using System.Threading.Tasks; @@ -7,15 +8,9 @@ namespace NadekoBot.TypeReaders { public class GuildDateTimeTypeReader : TypeReader { - private readonly GuildTimezoneService _gts; - - public GuildDateTimeTypeReader(GuildTimezoneService gts) - { - _gts = gts; - } - - public override Task Read(ICommandContext context, string input, IServiceProvider _) + public override Task Read(ICommandContext context, string input, IServiceProvider services) { + var _gts = (GuildTimezoneService)services.GetService(typeof(GuildTimezoneService)); if (!DateTime.TryParse(input, out var dt)) return Task.FromResult(TypeReaderResult.FromError(CommandError.ParseFailed, "Input string is in an incorrect format.")); diff --git a/src/NadekoBot/Modules/Administration/Commands/MuteCommands.cs b/src/NadekoBot/Modules/Administration/Commands/MuteCommands.cs index 85f075fa..e3ca82b9 100644 --- a/src/NadekoBot/Modules/Administration/Commands/MuteCommands.cs +++ b/src/NadekoBot/Modules/Administration/Commands/MuteCommands.cs @@ -25,7 +25,7 @@ namespace NadekoBot.Modules.Administration [NadekoCommand, Usage, Description, Aliases] [RequireContext(ContextType.Guild)] [RequireUserPermission(GuildPermission.ManageRoles)] - [Priority(1)] + [Priority(0)] public async Task SetMuteRole([Remainder] string name) { name = name.Trim(); @@ -45,7 +45,7 @@ namespace NadekoBot.Modules.Administration [NadekoCommand, Usage, Description, Aliases] [RequireContext(ContextType.Guild)] [RequireUserPermission(GuildPermission.ManageRoles)] - [Priority(0)] + [Priority(1)] public Task SetMuteRole([Remainder] IRole role) => SetMuteRole(role.Name); @@ -53,7 +53,7 @@ namespace NadekoBot.Modules.Administration [RequireContext(ContextType.Guild)] [RequireUserPermission(GuildPermission.ManageRoles)] [RequireUserPermission(GuildPermission.MuteMembers)] - [Priority(1)] + [Priority(0)] public async Task Mute(IGuildUser user) { try @@ -71,7 +71,7 @@ namespace NadekoBot.Modules.Administration [RequireContext(ContextType.Guild)] [RequireUserPermission(GuildPermission.ManageRoles)] [RequireUserPermission(GuildPermission.MuteMembers)] - [Priority(0)] + [Priority(1)] public async Task Mute(int minutes, IGuildUser user) { if (minutes < 1 || minutes > 1440) diff --git a/src/NadekoBot/Modules/Administration/Commands/PrefixCommands.cs b/src/NadekoBot/Modules/Administration/Commands/PrefixCommands.cs index 8f8d4cec..012b26c5 100644 --- a/src/NadekoBot/Modules/Administration/Commands/PrefixCommands.cs +++ b/src/NadekoBot/Modules/Administration/Commands/PrefixCommands.cs @@ -11,7 +11,7 @@ namespace NadekoBot.Modules.Administration public class PrefixCommands : NadekoSubmodule { [NadekoCommand, Usage, Description, Aliases] - [Priority(0)] + [Priority(1)] public new async Task Prefix() { await ReplyConfirmLocalized("prefix_current", Format.Code(_cmdHandler.GetPrefix(Context.Guild))).ConfigureAwait(false); @@ -21,7 +21,7 @@ namespace NadekoBot.Modules.Administration [NadekoCommand, Usage, Description, Aliases] [RequireContext(ContextType.Guild)] [RequireUserPermission(GuildPermission.Administrator)] - [Priority(1)] + [Priority(0)] public new async Task Prefix([Remainder]string prefix) { if (string.IsNullOrWhiteSpace(prefix)) diff --git a/src/NadekoBot/Modules/Administration/Commands/PruneCommands.cs b/src/NadekoBot/Modules/Administration/Commands/PruneCommands.cs index a4086f82..44407c19 100644 --- a/src/NadekoBot/Modules/Administration/Commands/PruneCommands.cs +++ b/src/NadekoBot/Modules/Administration/Commands/PruneCommands.cs @@ -39,7 +39,7 @@ namespace NadekoBot.Modules.Administration [RequireContext(ContextType.Guild)] [RequireUserPermission(ChannelPermission.ManageMessages)] [RequireBotPermission(GuildPermission.ManageMessages)] - [Priority(0)] + [Priority(1)] public async Task Prune(int count) { count++; @@ -55,7 +55,7 @@ namespace NadekoBot.Modules.Administration [RequireContext(ContextType.Guild)] [RequireUserPermission(ChannelPermission.ManageMessages)] [RequireBotPermission(GuildPermission.ManageMessages)] - [Priority(1)] + [Priority(0)] public async Task Prune(IGuildUser user, int count = 100) { if (user.Id == Context.User.Id) diff --git a/src/NadekoBot/Modules/Administration/Commands/RatelimitCommand.cs b/src/NadekoBot/Modules/Administration/Commands/RatelimitCommand.cs index 878b4a77..bf23d99b 100644 --- a/src/NadekoBot/Modules/Administration/Commands/RatelimitCommand.cs +++ b/src/NadekoBot/Modules/Administration/Commands/RatelimitCommand.cs @@ -67,7 +67,7 @@ namespace NadekoBot.Modules.Administration [NadekoCommand, Usage, Description, Aliases] [RequireContext(ContextType.Guild)] [RequireUserPermission(GuildPermission.ManageMessages)] - [Priority(1)] + [Priority(0)] public async Task SlowmodeWhitelist(IGuildUser user) { var siu = new SlowmodeIgnoredUser @@ -99,7 +99,7 @@ namespace NadekoBot.Modules.Administration [NadekoCommand, Usage, Description, Aliases] [RequireContext(ContextType.Guild)] [RequireUserPermission(GuildPermission.ManageMessages)] - [Priority(0)] + [Priority(1)] public async Task SlowmodeWhitelist(IRole role) { var sir = new SlowmodeIgnoredRole diff --git a/src/NadekoBot/Modules/Administration/Commands/SelfCommands.cs b/src/NadekoBot/Modules/Administration/Commands/SelfCommands.cs index 14a9e196..2205a1c8 100644 --- a/src/NadekoBot/Modules/Administration/Commands/SelfCommands.cs +++ b/src/NadekoBot/Modules/Administration/Commands/SelfCommands.cs @@ -289,7 +289,7 @@ namespace NadekoBot.Modules.Administration [NadekoCommand, Usage, Description, Aliases] [RequireUserPermission(GuildPermission.ManageNicknames)] - [Priority(1)] + [Priority(0)] public async Task SetNick([Remainder] string newNick = null) { if (string.IsNullOrWhiteSpace(newNick)) @@ -303,7 +303,7 @@ namespace NadekoBot.Modules.Administration [NadekoCommand, Usage, Description, Aliases] [RequireBotPermission(GuildPermission.ManageNicknames)] [RequireUserPermission(GuildPermission.ManageNicknames)] - [Priority(0)] + [Priority(1)] public async Task SetNick(IGuildUser gu, [Remainder] string newNick = null) { await gu.ModifyAsync(u => u.Nickname = newNick).ConfigureAwait(false); diff --git a/src/NadekoBot/Modules/Administration/Commands/UserPunishCommands.cs b/src/NadekoBot/Modules/Administration/Commands/UserPunishCommands.cs index 8538c619..692c8502 100644 --- a/src/NadekoBot/Modules/Administration/Commands/UserPunishCommands.cs +++ b/src/NadekoBot/Modules/Administration/Commands/UserPunishCommands.cs @@ -132,27 +132,27 @@ namespace NadekoBot.Modules.Administration [NadekoCommand, Usage, Description, Aliases] [RequireContext(ContextType.Guild)] [RequireUserPermission(GuildPermission.BanMembers)] - [Priority(1)] + [Priority(2)] public Task Warnlog(int page, IGuildUser user) => Warnlog(page, user.Id); [NadekoCommand, Usage, Description, Aliases] [RequireContext(ContextType.Guild)] - [Priority(0)] + [Priority(3)] public Task Warnlog(IGuildUser user) => Context.User.Id == user.Id || ((IGuildUser)Context.User).GuildPermissions.BanMembers ? Warnlog(user.Id) : Task.CompletedTask; [NadekoCommand, Usage, Description, Aliases] [RequireContext(ContextType.Guild)] [RequireUserPermission(GuildPermission.BanMembers)] - [Priority(3)] + [Priority(0)] public Task Warnlog(int page, ulong userId) => InternalWarnlog(userId, page - 1); [NadekoCommand, Usage, Description, Aliases] [RequireContext(ContextType.Guild)] [RequireUserPermission(GuildPermission.BanMembers)] - [Priority(2)] + [Priority(1)] public Task Warnlog(ulong userId) => InternalWarnlog(userId, 0); diff --git a/src/NadekoBot/Modules/ClashOfClans/ClashOfClans.cs b/src/NadekoBot/Modules/ClashOfClans/ClashOfClans.cs deleted file mode 100644 index cb11dd29..00000000 --- a/src/NadekoBot/Modules/ClashOfClans/ClashOfClans.cs +++ /dev/null @@ -1,239 +0,0 @@ -using Discord.Commands; -using System; -using System.Collections.Generic; -using System.Text; -using System.Threading.Tasks; -using Discord; -using NadekoBot.Attributes; -using NadekoBot.Services.Database.Models; -using NadekoBot.Extensions; -using NadekoBot.Services.ClashOfClans; - -namespace NadekoBot.Modules.ClashOfClans -{ - public class ClashOfClans : NadekoTopLevelModule - { - private readonly ClashOfClansService _service; - - public ClashOfClans(ClashOfClansService service) - { - _service = service; - } - - [NadekoCommand, Usage, Description, Aliases] - [RequireContext(ContextType.Guild)] - [RequireUserPermission(GuildPermission.ManageMessages)] - public async Task CreateWar(int size, [Remainder] string enemyClan = null) - { - if (string.IsNullOrWhiteSpace(enemyClan)) - return; - - if (size < 10 || size > 50 || size % 5 != 0) - { - await ReplyErrorLocalized("invalid_size").ConfigureAwait(false); - return; - } - List wars; - if (!_service.ClashWars.TryGetValue(Context.Guild.Id, out wars)) - { - wars = new List(); - if (!_service.ClashWars.TryAdd(Context.Guild.Id, wars)) - return; - } - - - var cw = await _service.CreateWar(enemyClan, size, Context.Guild.Id, Context.Channel.Id); - - wars.Add(cw); - await ReplyErrorLocalized("war_created", _service.ShortPrint(cw)).ConfigureAwait(false); - } - - [NadekoCommand, Usage, Description, Aliases] - [RequireContext(ContextType.Guild)] - public async Task StartWar([Remainder] string number = null) - { - int num = 0; - int.TryParse(number, out num); - - var warsInfo = _service.GetWarInfo(Context.Guild, num); - if (warsInfo == null) - { - await ReplyErrorLocalized("war_not_exist").ConfigureAwait(false); - return; - } - var war = warsInfo.Item1[warsInfo.Item2]; - try - { - war.Start(); - await ReplyConfirmLocalized("war_started", _service.ShortPrint(war)).ConfigureAwait(false); - } - catch - { - await ReplyErrorLocalized("war_already_started", _service.ShortPrint(war)).ConfigureAwait(false); - } - _service.SaveWar(war); - } - - [NadekoCommand, Usage, Description, Aliases] - [RequireContext(ContextType.Guild)] - public async Task ListWar([Remainder] string number = null) - { - - // if number is null, print all wars in a short way - if (string.IsNullOrWhiteSpace(number)) - { - //check if there are any wars - List wars = null; - _service.ClashWars.TryGetValue(Context.Guild.Id, out wars); - if (wars == null || wars.Count == 0) - { - await ReplyErrorLocalized("no_active_wars").ConfigureAwait(false); - return; - } - - var sb = new StringBuilder(); - sb.AppendLine("**-------------------------**"); - for (var i = 0; i < wars.Count; i++) - { - sb.AppendLine($"**#{i + 1}.** `{GetText("enemy")}:` **{wars[i].EnemyClan}**"); - sb.AppendLine($"\t\t`{GetText("size")}:` **{wars[i].Size} v {wars[i].Size}**"); - sb.AppendLine("**-------------------------**"); - } - await Context.Channel.SendConfirmAsync(GetText("list_active_wars"), sb.ToString()).ConfigureAwait(false); - return; - } - var num = 0; - int.TryParse(number, out num); - //if number is not null, print the war needed - var warsInfo = _service.GetWarInfo(Context.Guild, num); - if (warsInfo == null) - { - await ReplyErrorLocalized("war_not_exist").ConfigureAwait(false); - return; - } - var war = warsInfo.Item1[warsInfo.Item2]; - await Context.Channel.SendConfirmAsync(_service.Localize(war, "info_about_war", $"`{war.EnemyClan}` ({war.Size} v {war.Size})"), _service.ToPrettyString(war)).ConfigureAwait(false); - } - - [NadekoCommand, Usage, Description, Aliases] - [RequireContext(ContextType.Guild)] - public async Task BaseCall(int number, int baseNumber, [Remainder] string other_name = null) - { - var warsInfo = _service.GetWarInfo(Context.Guild, number); - if (warsInfo == null || warsInfo.Item1.Count == 0) - { - await ReplyErrorLocalized("war_not_exist").ConfigureAwait(false); - return; - } - var usr = - string.IsNullOrWhiteSpace(other_name) ? - Context.User.Username : - other_name; - try - { - var war = warsInfo.Item1[warsInfo.Item2]; - _service.Call(war, usr, baseNumber - 1); - _service.SaveWar(war); - await ConfirmLocalized("claimed_base", Format.Bold(usr.ToString()), baseNumber, _service.ShortPrint(war)).ConfigureAwait(false); - } - catch (Exception ex) - { - await Context.Channel.SendErrorAsync(ex.Message).ConfigureAwait(false); - } - } - - [NadekoCommand, Usage, Description, Aliases] - [RequireContext(ContextType.Guild)] - public async Task CallFinish1(int number, int baseNumber = 0) - { - await FinishClaim(number, baseNumber - 1, 1); - } - - [NadekoCommand, Usage, Description, Aliases] - [RequireContext(ContextType.Guild)] - public async Task CallFinish2(int number, int baseNumber = 0) - { - await FinishClaim(number, baseNumber - 1, 2); - } - - [NadekoCommand, Usage, Description, Aliases] - [RequireContext(ContextType.Guild)] - public async Task CallFinish(int number, int baseNumber = 0) - { - await FinishClaim(number, baseNumber - 1); - } - - [NadekoCommand, Usage, Description, Aliases] - [RequireContext(ContextType.Guild)] - public async Task EndWar(int number) - { - var warsInfo = _service.GetWarInfo(Context.Guild, number); - if (warsInfo == null) - { - await ReplyErrorLocalized("war_not_exist").ConfigureAwait(false); - return; - } - var war = warsInfo.Item1[warsInfo.Item2]; - war.End(); - _service.SaveWar(war); - await ReplyConfirmLocalized("war_ended", _service.ShortPrint(warsInfo.Item1[warsInfo.Item2])).ConfigureAwait(false); - - warsInfo.Item1.RemoveAt(warsInfo.Item2); - } - - [NadekoCommand, Usage, Description, Aliases] - [RequireContext(ContextType.Guild)] - public async Task Unclaim(int number, [Remainder] string otherName = null) - { - var warsInfo = _service.GetWarInfo(Context.Guild, number); - if (warsInfo == null || warsInfo.Item1.Count == 0) - { - await ReplyErrorLocalized("war_not_exist").ConfigureAwait(false); - return; - } - var usr = - string.IsNullOrWhiteSpace(otherName) ? - Context.User.Username : - otherName; - try - { - var war = warsInfo.Item1[warsInfo.Item2]; - var baseNumber = _service.Uncall(war, usr); - _service.SaveWar(war); - await ReplyConfirmLocalized("base_unclaimed", usr, baseNumber + 1, _service.ShortPrint(war)).ConfigureAwait(false); - } - catch (Exception ex) - { - await Context.Channel.SendErrorAsync(ex.Message).ConfigureAwait(false); - } - } - - private async Task FinishClaim(int number, int baseNumber, int stars = 3) - { - var warInfo = _service.GetWarInfo(Context.Guild, number); - if (warInfo == null || warInfo.Item1.Count == 0) - { - await ReplyErrorLocalized("war_not_exist").ConfigureAwait(false); - return; - } - var war = warInfo.Item1[warInfo.Item2]; - try - { - if (baseNumber == -1) - { - baseNumber = _service.FinishClaim(war, Context.User.Username, stars); - _service.SaveWar(war); - } - else - { - _service.FinishClaim(war, baseNumber, stars); - } - await ReplyConfirmLocalized("base_destroyed", baseNumber + 1, _service.ShortPrint(war)).ConfigureAwait(false); - } - catch (Exception ex) - { - await Context.Channel.SendErrorAsync($"🔰 {ex.Message}").ConfigureAwait(false); - } - } - } -} \ No newline at end of file diff --git a/src/NadekoBot/Modules/CustomReactions/CustomReactions.cs b/src/NadekoBot/Modules/CustomReactions/CustomReactions.cs index dd6b162d..b80a5921 100644 --- a/src/NadekoBot/Modules/CustomReactions/CustomReactions.cs +++ b/src/NadekoBot/Modules/CustomReactions/CustomReactions.cs @@ -84,7 +84,7 @@ namespace NadekoBot.Modules.CustomReactions } [NadekoCommand, Usage, Description, Aliases] - [Priority(0)] + [Priority(1)] public async Task ListCustReact(int page = 1) { if (--page < 0 || page > 999) @@ -130,7 +130,7 @@ namespace NadekoBot.Modules.CustomReactions } [NadekoCommand, Usage, Description, Aliases] - [Priority(1)] + [Priority(0)] public async Task ListCustReact(All x) { CustomReaction[] customReactions; diff --git a/src/NadekoBot/Modules/Gambling/Commands/DiceRollCommand.cs b/src/NadekoBot/Modules/Gambling/Commands/DiceRollCommand.cs index 2b15c79d..0a81a943 100644 --- a/src/NadekoBot/Modules/Gambling/Commands/DiceRollCommand.cs +++ b/src/NadekoBot/Modules/Gambling/Commands/DiceRollCommand.cs @@ -58,7 +58,7 @@ namespace NadekoBot.Modules.Gambling } [NadekoCommand, Usage, Description, Aliases] - [Priority(0)] + [Priority(1)] public async Task Roll(int num) { await InternalRoll(num, true).ConfigureAwait(false); @@ -66,21 +66,21 @@ namespace NadekoBot.Modules.Gambling [NadekoCommand, Usage, Description, Aliases] - [Priority(0)] + [Priority(1)] public async Task Rolluo(int num = 1) { await InternalRoll(num, false).ConfigureAwait(false); } [NadekoCommand, Usage, Description, Aliases] - [Priority(1)] + [Priority(0)] public async Task Roll(string arg) { await InternallDndRoll(arg, true).ConfigureAwait(false); } [NadekoCommand, Usage, Description, Aliases] - [Priority(1)] + [Priority(0)] public async Task Rolluo(string arg) { await InternallDndRoll(arg, false).ConfigureAwait(false); diff --git a/src/NadekoBot/Modules/Gambling/Commands/WaifuClaimCommands.cs b/src/NadekoBot/Modules/Gambling/Commands/WaifuClaimCommands.cs index 02025444..962e24dc 100644 --- a/src/NadekoBot/Modules/Gambling/Commands/WaifuClaimCommands.cs +++ b/src/NadekoBot/Modules/Gambling/Commands/WaifuClaimCommands.cs @@ -197,12 +197,12 @@ namespace NadekoBot.Modules.Gambling private static readonly TimeSpan _divorceLimit = TimeSpan.FromHours(6); [NadekoCommand, Usage, Description, Aliases] [RequireContext(ContextType.Guild)] - [Priority(1)] + [Priority(0)] public Task Divorce([Remainder]IGuildUser target) => Divorce(target.Id); [NadekoCommand, Usage, Description, Aliases] [RequireContext(ContextType.Guild)] - [Priority(0)] + [Priority(1)] public async Task Divorce([Remainder]ulong targetId) { if (targetId == Context.User.Id) diff --git a/src/NadekoBot/Modules/Gambling/Gambling.cs b/src/NadekoBot/Modules/Gambling/Gambling.cs index 500319ca..c8013f0d 100644 --- a/src/NadekoBot/Modules/Gambling/Gambling.cs +++ b/src/NadekoBot/Modules/Gambling/Gambling.cs @@ -52,7 +52,7 @@ namespace NadekoBot.Modules.Gambling } [NadekoCommand, Usage, Description, Aliases] - [Priority(0)] + [Priority(1)] public async Task Cash([Remainder] IUser user = null) { if(user == null) @@ -62,7 +62,7 @@ namespace NadekoBot.Modules.Gambling } [NadekoCommand, Usage, Description, Aliases] - [Priority(1)] + [Priority(0)] public async Task Cash(ulong userId) { await ReplyConfirmLocalized("has", Format.Code(userId.ToString()), $"{GetCurrency(userId)} {CurrencySign}").ConfigureAwait(false); @@ -88,7 +88,7 @@ namespace NadekoBot.Modules.Gambling [NadekoCommand, Usage, Description, Aliases] [RequireContext(ContextType.Guild)] [OwnerOnly] - [Priority(2)] + [Priority(0)] public Task Award(int amount, [Remainder] IGuildUser usr) => Award(amount, usr.Id); @@ -107,7 +107,7 @@ namespace NadekoBot.Modules.Gambling [NadekoCommand, Usage, Description, Aliases] [RequireContext(ContextType.Guild)] [OwnerOnly] - [Priority(0)] + [Priority(2)] public async Task Award(int amount, [Remainder] IRole role) { var users = (await Context.Guild.GetUsersAsync()) diff --git a/src/NadekoBot/Modules/Help/Help.cs b/src/NadekoBot/Modules/Help/Help.cs index 4b15b72e..7d3162cc 100644 --- a/src/NadekoBot/Modules/Help/Help.cs +++ b/src/NadekoBot/Modules/Help/Help.cs @@ -82,14 +82,14 @@ namespace NadekoBot.Modules.Help await ConfirmLocalized("commands_instr", Prefix).ConfigureAwait(false); } [NadekoCommand, Usage, Description, Aliases] - [Priority(1)] + [Priority(0)] public async Task H([Remainder] string fail) { await ReplyErrorLocalized("command_not_found").ConfigureAwait(false); } [NadekoCommand, Usage, Description, Aliases] - [Priority(0)] + [Priority(1)] public async Task H([Remainder] CommandInfo com = null) { var channel = Context.Channel; diff --git a/src/NadekoBot/Modules/Music/Music.cs b/src/NadekoBot/Modules/Music/Music.cs index b26095a3..1d84dcee 100644 --- a/src/NadekoBot/Modules/Music/Music.cs +++ b/src/NadekoBot/Modules/Music/Music.cs @@ -377,7 +377,7 @@ namespace NadekoBot.Modules.Music [NadekoCommand, Usage, Description, Aliases] [RequireContext(ContextType.Guild)] - [Priority(0)] + [Priority(1)] public async Task SongRemove(int index) { if (index < 1) @@ -406,7 +406,7 @@ namespace NadekoBot.Modules.Music public enum All { All } [NadekoCommand, Usage, Description, Aliases] [RequireContext(ContextType.Guild)] - [Priority(1)] + [Priority(0)] public async Task SongRemove(All all) { var mp = _music.GetPlayerOrDefault(Context.Guild.Id); @@ -853,41 +853,6 @@ namespace NadekoBot.Modules.Music else await ReplyConfirmLocalized("rpl_disabled").ConfigureAwait(false); } - //todo readd goto - //[NadekoCommand, Usage, Description, Aliases] - //[RequireContext(ContextType.Guild)] - //public async Task Goto(int time) - //{ - // MusicPlayer musicPlayer; - // if ((musicPlayer = _music.GetPlayer(Context.Guild.Id)) == null) - // return; - // if (((IGuildUser)Context.User).VoiceChannel != musicPlayer.PlaybackVoiceChannel) - // return; - - // if (time < 0) - // return; - - // var currentSong = musicPlayer.CurrentSong; - - // if (currentSong == null) - // return; - - // //currentSong.PrintStatusMessage = false; - // var gotoSong = currentSong.Clone(); - // gotoSong.SkipTo = time; - // musicPlayer.AddSong(gotoSong, 0); - // musicPlayer.Next(); - - // var minutes = (time / 60).ToString(); - // var seconds = (time % 60).ToString(); - - // if (minutes.Length == 1) - // minutes = "0" + minutes; - // if (seconds.Length == 1) - // seconds = "0" + seconds; - - // await ReplyConfirmLocalized("skipped_to", minutes, seconds).ConfigureAwait(false); - //} [NadekoCommand, Usage, Description, Aliases] [RequireContext(ContextType.Guild)] diff --git a/src/NadekoBot/Modules/NSFW/NSFW.cs b/src/NadekoBot/Modules/NSFW/NSFW.cs index 7ce29178..a5815bc6 100644 --- a/src/NadekoBot/Modules/NSFW/NSFW.cs +++ b/src/NadekoBot/Modules/NSFW/NSFW.cs @@ -192,10 +192,18 @@ namespace NadekoBot.Modules.NSFW if (imgObj == null) await ReplyErrorLocalized("not_found").ConfigureAwait(false); else - await Context.Channel.EmbedAsync(new EmbedBuilder().WithOkColor() + { + var embed = new EmbedBuilder().WithOkColor() .WithDescription($"{Context.User} [{tag ?? "url"}]({imgObj}) ") - .WithImageUrl(imgObj.FileUrl) - .WithFooter(efb => efb.WithText(type.ToString()))).ConfigureAwait(false); + .WithFooter(efb => efb.WithText(type.ToString())); + + if (Uri.IsWellFormedUriString(imgObj.FileUrl, UriKind.Absolute)) + embed.WithImageUrl(imgObj.FileUrl); + else + _log.Error($"Image link from {type} is not a proper Url: {imgObj.FileUrl}"); + + await Context.Channel.EmbedAsync(embed).ConfigureAwait(false); + } } } } \ No newline at end of file diff --git a/src/NadekoBot/Modules/Searches/Commands/AnimeSearchCommands.cs b/src/NadekoBot/Modules/Searches/Commands/AnimeSearchCommands.cs index c80bd2c1..1c32b091 100644 --- a/src/NadekoBot/Modules/Searches/Commands/AnimeSearchCommands.cs +++ b/src/NadekoBot/Modules/Searches/Commands/AnimeSearchCommands.cs @@ -25,7 +25,7 @@ namespace NadekoBot.Modules.Searches } [NadekoCommand, Usage, Description, Aliases] - [Priority(1)] + [Priority(0)] public async Task Mal([Remainder] string name) { if (string.IsNullOrWhiteSpace(name)) @@ -132,7 +132,7 @@ namespace NadekoBot.Modules.Searches [NadekoCommand, Usage, Description, Aliases] [RequireContext(ContextType.Guild)] - [Priority(0)] + [Priority(1)] public Task Mal(IGuildUser usr) => Mal(usr.Username); [NadekoCommand, Usage, Description, Aliases] diff --git a/src/NadekoBot/Modules/Searches/Commands/XkcdCommands.cs b/src/NadekoBot/Modules/Searches/Commands/XkcdCommands.cs index a4b031a3..a3f37f60 100644 --- a/src/NadekoBot/Modules/Searches/Commands/XkcdCommands.cs +++ b/src/NadekoBot/Modules/Searches/Commands/XkcdCommands.cs @@ -17,7 +17,7 @@ namespace NadekoBot.Modules.Searches private const string _xkcdUrl = "https://xkcd.com"; [NadekoCommand, Usage, Description, Aliases] - [Priority(1)] + [Priority(0)] public async Task Xkcd(string arg = null) { if (arg?.ToLowerInvariant().Trim() == "latest") @@ -44,7 +44,7 @@ namespace NadekoBot.Modules.Searches } [NadekoCommand, Usage, Description, Aliases] - [Priority(0)] + [Priority(1)] public async Task Xkcd(int num) { if (num < 1) diff --git a/src/NadekoBot/Modules/Utility/Commands/Remind.cs b/src/NadekoBot/Modules/Utility/Commands/Remind.cs index 8957d1a8..e16a0486 100644 --- a/src/NadekoBot/Modules/Utility/Commands/Remind.cs +++ b/src/NadekoBot/Modules/Utility/Commands/Remind.cs @@ -36,7 +36,7 @@ namespace NadekoBot.Modules.Utility [NadekoCommand, Usage, Description, Aliases] [RequireContext(ContextType.Guild)] - [Priority(0)] + [Priority(1)] public async Task Remind(MeOrHere meorhere, string timeStr, [Remainder] string message) { ulong target; @@ -47,7 +47,7 @@ namespace NadekoBot.Modules.Utility [NadekoCommand, Usage, Description, Aliases] [RequireContext(ContextType.Guild)] [RequireUserPermission(GuildPermission.ManageMessages)] - [Priority(1)] + [Priority(0)] public async Task Remind(ITextChannel channel, string timeStr, [Remainder] string message) { var perms = ((IGuildUser)Context.User).GetPermissions((ITextChannel)channel); diff --git a/src/NadekoBot/Modules/Utility/Commands/RepeatCommands.cs b/src/NadekoBot/Modules/Utility/Commands/RepeatCommands.cs index 89619c1c..457e7b67 100644 --- a/src/NadekoBot/Modules/Utility/Commands/RepeatCommands.cs +++ b/src/NadekoBot/Modules/Utility/Commands/RepeatCommands.cs @@ -63,7 +63,6 @@ namespace NadekoBot.Modules.Utility [NadekoCommand, Usage, Description, Aliases] [RequireContext(ContextType.Guild)] [RequireUserPermission(GuildPermission.ManageMessages)] - [Priority(0)] public async Task RepeatRemove(int index) { if (!_service.RepeaterReady) @@ -103,7 +102,7 @@ namespace NadekoBot.Modules.Utility [NadekoCommand, Usage, Description, Aliases] [RequireContext(ContextType.Guild)] [RequireUserPermission(GuildPermission.ManageMessages)] - [Priority(1)] + [Priority(0)] public async Task Repeat(int minutes, [Remainder] string message) { if (!_service.RepeaterReady) @@ -152,7 +151,7 @@ namespace NadekoBot.Modules.Utility [NadekoCommand, Usage, Description, Aliases] [RequireContext(ContextType.Guild)] [RequireUserPermission(GuildPermission.ManageMessages)] - [Priority(0)] + [Priority(1)] public async Task Repeat(GuildDateTime gt, [Remainder] string message) { if (!_service.RepeaterReady) diff --git a/src/NadekoBot/NadekoBot.cs b/src/NadekoBot/NadekoBot.cs index e5b59a29..71a5a504 100644 --- a/src/NadekoBot/NadekoBot.cs +++ b/src/NadekoBot/NadekoBot.cs @@ -14,20 +14,12 @@ using System.Collections.Immutable; using System.Diagnostics; using NadekoBot.Services.Database.Models; using System.Threading; -using NadekoBot.Services.Searches; -using NadekoBot.Services.ClashOfClans; -using NadekoBot.Services.Music; -using NadekoBot.Services.CustomReactions; -using NadekoBot.Services.Games; -using NadekoBot.Services.Administration; -using NadekoBot.Services.Permissions; -using NadekoBot.Services.Utility; -using NadekoBot.Services.Help; using System.IO; -using NadekoBot.Services.Pokemon; using NadekoBot.DataStructures.ShardCom; using NadekoBot.DataStructures; using NadekoBot.Extensions; +using System.Collections.Generic; +using NadekoBot.Services.Database; namespace NadekoBot { @@ -35,6 +27,15 @@ namespace NadekoBot { private Logger _log; + public BotCredentials Credentials { get; } + + public DiscordSocketClient Client { get; } + public CommandService CommandService { get; } + + public DbService Db { get; } + public BotConfig BotConfig { get; } + public ImmutableArray AllGuildConfigs { get; private set; } + /* I don't know how to make this not be static * and keep the convenience of .WithOkColor * and .WithErrorColor extensions methods. @@ -44,23 +45,9 @@ namespace NadekoBot public static Color OkColor { get; private set; } public static Color ErrorColor { get; private set; } - public ImmutableArray AllGuildConfigs { get; private set; } - public BotConfig BotConfig { get; } - public DbService Db { get; } - public CommandService CommandService { get; } - public CommandHandler CommandHandler { get; private set; } - public Localization Localization { get; private set; } - public NadekoStrings Strings { get; private set; } - public StatsService Stats { get; private set; } - public ImagesService Images { get; } - public CurrencyService Currency { get; } - public GoogleApiService GoogleApi { get; } - - public DiscordSocketClient Client { get; } - public bool Ready { get; private set; } + public TaskCompletionSource Ready { get; private set; } = new TaskCompletionSource(); public INServiceProvider Services { get; private set; } - public BotCredentials Credentials { get; } public int ShardId { get; } public ShardsCoordinator ShardCoord { get; private set; } @@ -72,26 +59,14 @@ namespace NadekoBot if (shardId < 0) throw new ArgumentOutOfRangeException(nameof(shardId)); - ShardId = shardId; - LogSetup.SetupLogger(); _log = LogManager.GetCurrentClassLogger(); TerribleElevatedPermissionCheck(); + ShardId = shardId; + Credentials = new BotCredentials(); - - port = port ?? Credentials.ShardRunPort; - _comClient = new ShardComClient(port.Value); - Db = new DbService(Credentials); - - using (var uow = Db.UnitOfWork) - { - BotConfig = uow.BotConfig.GetOrCreate(); - OkColor = new Color(Convert.ToUInt32(BotConfig.OkColor, 16)); - ErrorColor = new Color(Convert.ToUInt32(BotConfig.ErrorColor, 16)); - } - Client = new DiscordSocketClient(new DiscordSocketConfig { MessageCacheSize = 10, @@ -101,16 +76,21 @@ namespace NadekoBot ShardId = shardId, AlwaysDownloadUsers = false, }); - CommandService = new CommandService(new CommandServiceConfig() { CaseSensitiveCommands = false, DefaultRunMode = RunMode.Sync, }); - - Images = new ImagesService(); - Currency = new CurrencyService(BotConfig, Db); - GoogleApi = new GoogleApiService(Credentials); + + port = port ?? Credentials.ShardRunPort; + _comClient = new ShardComClient(port.Value); + + using (var uow = Db.UnitOfWork) + { + BotConfig = uow.BotConfig.GetOrCreate(); + OkColor = new Color(Convert.ToUInt32(BotConfig.OkColor, 16)); + ErrorColor = new Color(Convert.ToUInt32(BotConfig.ErrorColor, 16)); + } SetupShard(shardId, parentProcessId, port.Value); @@ -145,141 +125,35 @@ namespace NadekoBot using (var uow = Db.UnitOfWork) { AllGuildConfigs = uow.GuildConfigs.GetAllGuildConfigs(startingGuildIdList).ToImmutableArray(); - - Localization = new Localization(BotConfig.Locale, AllGuildConfigs.ToDictionary(x => x.GuildId, x => x.Locale), Db); - Strings = new NadekoStrings(Localization); - CommandHandler = new CommandHandler(Client, Db, BotConfig, AllGuildConfigs, CommandService, Credentials, this); - Stats = new StatsService(Client, CommandHandler, Credentials, ShardCoord); - var soundcloudApiService = new SoundCloudApiService(Credentials); - - #region help - var helpService = new HelpService(BotConfig, CommandHandler, Strings); - #endregion - - //module services - //todo 90 - autodiscover, DI, and add instead of manual like this - #region utility - var remindService = new RemindService(Client, BotConfig, Db, startingGuildIdList, uow); - var repeaterService = new MessageRepeaterService(this, Client, AllGuildConfigs); - var converterService = new ConverterService(Client, Db); - var commandMapService = new CommandMapService(AllGuildConfigs); - var patreonRewardsService = new PatreonRewardsService(Credentials, Db, Currency, Client); - var verboseErrorsService = new VerboseErrorsService(AllGuildConfigs, Db, CommandHandler, helpService); - var pruneService = new PruneService(); - var streamRoleService = new StreamRoleService(Client, Db, AllGuildConfigs); - #endregion - - #region permissions - var permissionsService = new PermissionService(Client, Db, BotConfig, CommandHandler, Strings); - var blacklistService = new BlacklistService(BotConfig); - var cmdcdsService = new CmdCdService(AllGuildConfigs); - var filterService = new FilterService(Client, AllGuildConfigs); - var globalPermsService = new GlobalPermissionService(BotConfig); - #endregion - - #region Searches - var searchesService = new SearchesService(Client, GoogleApi, Db); - var streamNotificationService = new StreamNotificationService(Db, Client, Strings); - var animeSearchService = new AnimeSearchService(); - #endregion - - var clashService = new ClashOfClansService(Client, Db, Localization, Strings, uow, startingGuildIdList); - var musicService = new MusicService(Client, GoogleApi, Strings, Localization, Db, soundcloudApiService, Credentials, AllGuildConfigs); - var crService = new CustomReactionsService(permissionsService, Db, Strings, Client, CommandHandler, BotConfig, uow); - - #region Games - var gamesService = new GamesService(Client, BotConfig, AllGuildConfigs, Strings, Images, CommandHandler); - var chatterBotService = new ChatterBotService(Client, permissionsService, AllGuildConfigs, CommandHandler, Strings); - var pollService = new PollService(Client, Strings); - #endregion - - #region administration - var administrationService = new AdministrationService(AllGuildConfigs, CommandHandler); - var greetSettingsService = new GreetSettingsService(Client, AllGuildConfigs, Db); - var selfService = new SelfService(Client, this, CommandHandler, Db, BotConfig, Localization, Strings, Credentials); - var vcRoleService = new VcRoleService(Client, AllGuildConfigs, Db); - var vPlusTService = new VplusTService(Client, AllGuildConfigs, Strings, Db); - var muteService = new MuteService(Client, AllGuildConfigs, Db); - var ratelimitService = new SlowmodeService(Client, AllGuildConfigs); - var protectionService = new ProtectionService(Client, AllGuildConfigs, muteService); - var playingRotateService = new PlayingRotateService(Client, BotConfig, musicService, Db); - var gameVcService = new GameVoiceChannelService(Client, Db, AllGuildConfigs); - var autoAssignRoleService = new AutoAssignRoleService(Client, AllGuildConfigs); - var guildTimezoneService = new GuildTimezoneService(Client, AllGuildConfigs, Db); - var logCommandService = new LogCommandService(Client, Strings, AllGuildConfigs, Db, muteService, protectionService, guildTimezoneService); - #endregion - - #region pokemon - var pokemonService = new PokemonService(); - #endregion + var localization = new Localization(BotConfig.Locale, AllGuildConfigs.ToDictionary(x => x.GuildId, x => x.Locale), Db); //initialize Services Services = new NServiceProvider.ServiceProviderBuilder() - .Add(Localization) - .Add(Stats) - .Add(Images) - .Add(GoogleApi) - .Add(Stats) - .Add(Credentials) - .Add(CommandService) - .Add(Strings) - .Add(Client) - .Add(BotConfig) - .Add(Currency) - .Add(CommandHandler) - .Add(Db) - //modules - .Add(commandMapService) - .Add(remindService) - .Add(repeaterService) - .Add(converterService) - .Add(verboseErrorsService) - .Add(patreonRewardsService) - .Add(pruneService) - .Add(streamRoleService) - .Add(searchesService) - .Add(streamNotificationService) - .Add(animeSearchService) - .Add(clashService) - .Add(musicService) - .Add(greetSettingsService) - .Add(crService) - .Add(helpService) - .Add(gamesService) - .Add(chatterBotService) - .Add(pollService) - .Add(administrationService) - .Add(selfService) - .Add(vcRoleService) - .Add(vPlusTService) - .Add(muteService) - .Add(ratelimitService) - .Add(playingRotateService) - .Add(gameVcService) - .Add(autoAssignRoleService) - .Add(protectionService) - .Add(logCommandService) - .Add(guildTimezoneService) - .Add(permissionsService) - .Add(blacklistService) - .Add(cmdcdsService) - .Add(filterService) - .Add(globalPermsService) - .Add(pokemonService) - .Add(this) + .AddManual(Credentials) + .AddManual(Db) + .AddManual(BotConfig) + .AddManual(Client) + .AddManual(CommandService) + .AddManual(localization) + .AddManual>(AllGuildConfigs) //todo wrap this + .AddManual(this) + .AddManual(uow) + .AddManual(ShardCoord) + .LoadFrom(Assembly.GetEntryAssembly()) .Build(); - CommandHandler.AddServices(Services); + var commandHandler = Services.GetService(); + commandHandler.AddServices(Services); //setup typereaders CommandService.AddTypeReader(new PermissionActionTypeReader()); - CommandService.AddTypeReader(new CommandTypeReader(CommandService, CommandHandler)); - CommandService.AddTypeReader(new CommandOrCrTypeReader(crService, CommandService, CommandHandler)); + CommandService.AddTypeReader(new CommandTypeReader()); + CommandService.AddTypeReader(new CommandOrCrTypeReader()); CommandService.AddTypeReader(new ModuleTypeReader(CommandService)); CommandService.AddTypeReader(new ModuleOrCrTypeReader(CommandService)); CommandService.AddTypeReader(new GuildTypeReader(Client)); - CommandService.AddTypeReader(new GuildDateTimeTypeReader(guildTimezoneService)); + CommandService.AddTypeReader(new GuildDateTimeTypeReader()); } } @@ -382,7 +256,7 @@ namespace NadekoBot .Where(x => x.Preconditions.Any(y => y.GetType() == typeof(NoPublicBot))) .ForEach(x => CommandService.RemoveModuleAsync(x)); - Ready = true; + Ready.TrySetResult(true); _log.Info($"Shard {ShardId} ready."); //_log.Info(await stats.Print().ConfigureAwait(false)); } diff --git a/src/NadekoBot/NadekoBot.csproj b/src/NadekoBot/NadekoBot.csproj index 22771b48..b0d9d8b0 100644 --- a/src/NadekoBot/NadekoBot.csproj +++ b/src/NadekoBot/NadekoBot.csproj @@ -55,7 +55,7 @@ - + diff --git a/src/NadekoBot/Services/Administration/AdministrationService.cs b/src/NadekoBot/Services/Administration/AdministrationService.cs index 3ead9cc3..3fe34164 100644 --- a/src/NadekoBot/Services/Administration/AdministrationService.cs +++ b/src/NadekoBot/Services/Administration/AdministrationService.cs @@ -11,7 +11,7 @@ using System.Threading.Tasks; namespace NadekoBot.Services.Administration { - public class AdministrationService + public class AdministrationService : INService { public readonly ConcurrentHashSet DeleteMessagesOnCommand; private readonly Logger _log; diff --git a/src/NadekoBot/Services/Administration/AutoAssignRoleService.cs b/src/NadekoBot/Services/Administration/AutoAssignRoleService.cs index 7c8b8711..bba16d44 100644 --- a/src/NadekoBot/Services/Administration/AutoAssignRoleService.cs +++ b/src/NadekoBot/Services/Administration/AutoAssignRoleService.cs @@ -9,7 +9,7 @@ using System.Threading.Tasks; namespace NadekoBot.Services.Administration { - public class AutoAssignRoleService + public class AutoAssignRoleService : INService { private readonly Logger _log; private readonly DiscordSocketClient _client; diff --git a/src/NadekoBot/Services/Administration/GameVoiceChannelService.cs b/src/NadekoBot/Services/Administration/GameVoiceChannelService.cs index 52ea08f7..b7389bed 100644 --- a/src/NadekoBot/Services/Administration/GameVoiceChannelService.cs +++ b/src/NadekoBot/Services/Administration/GameVoiceChannelService.cs @@ -10,7 +10,7 @@ using System.Threading.Tasks; namespace NadekoBot.Services.Administration { - public class GameVoiceChannelService + public class GameVoiceChannelService : INService { public readonly ConcurrentHashSet GameVoiceChannels = new ConcurrentHashSet(); diff --git a/src/NadekoBot/Services/Administration/GuildTimezoneService.cs b/src/NadekoBot/Services/Administration/GuildTimezoneService.cs index 66736679..c15eb534 100644 --- a/src/NadekoBot/Services/Administration/GuildTimezoneService.cs +++ b/src/NadekoBot/Services/Administration/GuildTimezoneService.cs @@ -9,7 +9,7 @@ using Discord.WebSocket; namespace NadekoBot.Services.Administration { - public class GuildTimezoneService + public class GuildTimezoneService : INService { //hack >.> public static ConcurrentDictionary AllServices { get; } = new ConcurrentDictionary(); diff --git a/src/NadekoBot/Services/Administration/LogCommandService.cs b/src/NadekoBot/Services/Administration/LogCommandService.cs index 9b3b432c..d5221eb2 100644 --- a/src/NadekoBot/Services/Administration/LogCommandService.cs +++ b/src/NadekoBot/Services/Administration/LogCommandService.cs @@ -13,7 +13,7 @@ using System.Threading.Tasks; namespace NadekoBot.Services.Administration { - public class LogCommandService + public class LogCommandService : INService { private readonly DiscordSocketClient _client; diff --git a/src/NadekoBot/Services/Administration/MuteService.cs b/src/NadekoBot/Services/Administration/MuteService.cs index 2b8c3fb9..04042e5f 100644 --- a/src/NadekoBot/Services/Administration/MuteService.cs +++ b/src/NadekoBot/Services/Administration/MuteService.cs @@ -20,7 +20,7 @@ namespace NadekoBot.Services.Administration All } - public class MuteService + public class MuteService : INService { public ConcurrentDictionary GuildMuteRoles { get; } public ConcurrentDictionary> MutedUsers { get; } diff --git a/src/NadekoBot/Services/Administration/PlayingRotateService.cs b/src/NadekoBot/Services/Administration/PlayingRotateService.cs index 78e946c8..45603150 100644 --- a/src/NadekoBot/Services/Administration/PlayingRotateService.cs +++ b/src/NadekoBot/Services/Administration/PlayingRotateService.cs @@ -9,7 +9,7 @@ using System.Threading; namespace NadekoBot.Services.Administration { - public class PlayingRotateService + public class PlayingRotateService : INService { private readonly Timer _t; private readonly DiscordSocketClient _client; diff --git a/src/NadekoBot/Services/Administration/ProtectionService.cs b/src/NadekoBot/Services/Administration/ProtectionService.cs index 6a2ae99b..6996b3a1 100644 --- a/src/NadekoBot/Services/Administration/ProtectionService.cs +++ b/src/NadekoBot/Services/Administration/ProtectionService.cs @@ -10,7 +10,7 @@ using System.Threading.Tasks; namespace NadekoBot.Services.Administration { - public class ProtectionService + public class ProtectionService : INService { public readonly ConcurrentDictionary AntiRaidGuilds = new ConcurrentDictionary(); diff --git a/src/NadekoBot/Services/Administration/PruneService.cs b/src/NadekoBot/Services/Administration/PruneService.cs index e1078102..59da41b5 100644 --- a/src/NadekoBot/Services/Administration/PruneService.cs +++ b/src/NadekoBot/Services/Administration/PruneService.cs @@ -9,7 +9,7 @@ using System.Threading.Tasks; namespace NadekoBot.Services.Administration { - public class PruneService + public class PruneService : INService { //channelids where prunes are currently occuring private ConcurrentHashSet _pruningGuilds = new ConcurrentHashSet(); diff --git a/src/NadekoBot/Services/Administration/RatelimitService.cs b/src/NadekoBot/Services/Administration/RatelimitService.cs index a0f4171f..0c68accd 100644 --- a/src/NadekoBot/Services/Administration/RatelimitService.cs +++ b/src/NadekoBot/Services/Administration/RatelimitService.cs @@ -12,7 +12,7 @@ using System.Threading.Tasks; namespace NadekoBot.Services.Administration { - public class SlowmodeService : IEarlyBlocker + public class SlowmodeService : IEarlyBlocker, INService { public ConcurrentDictionary RatelimitingChannels = new ConcurrentDictionary(); public ConcurrentDictionary> IgnoredRoles = new ConcurrentDictionary>(); diff --git a/src/NadekoBot/Services/Administration/SelfService.cs b/src/NadekoBot/Services/Administration/SelfService.cs index 4134393b..8b567c42 100644 --- a/src/NadekoBot/Services/Administration/SelfService.cs +++ b/src/NadekoBot/Services/Administration/SelfService.cs @@ -12,7 +12,7 @@ using System.Collections.Generic; namespace NadekoBot.Services.Administration { - public class SelfService : ILateExecutor + public class SelfService : ILateExecutor, INService { public volatile bool ForwardDMs; public volatile bool ForwardDMsToAllOwners; @@ -44,8 +44,7 @@ namespace NadekoBot.Services.Administration var _ = Task.Run(async () => { - while (!bot.Ready) - await Task.Delay(1000); + await bot.Ready.Task.ConfigureAwait(false); foreach (var cmd in bc.StartupCommands) { @@ -56,8 +55,7 @@ namespace NadekoBot.Services.Administration var ___ = Task.Run(async () => { - while (!bot.Ready) - await Task.Delay(1000); + await bot.Ready.Task.ConfigureAwait(false); await Task.Delay(5000); diff --git a/src/NadekoBot/Services/Administration/VcRoleService.cs b/src/NadekoBot/Services/Administration/VcRoleService.cs index 10252833..e1d8e5d4 100644 --- a/src/NadekoBot/Services/Administration/VcRoleService.cs +++ b/src/NadekoBot/Services/Administration/VcRoleService.cs @@ -10,7 +10,7 @@ using System.Threading.Tasks; namespace NadekoBot.Services.Administration { - public class VcRoleService + public class VcRoleService : INService { private readonly Logger _log; private readonly DbService _db; diff --git a/src/NadekoBot/Services/Administration/VplusTService.cs b/src/NadekoBot/Services/Administration/VplusTService.cs index b4e3122d..d61bb9f8 100644 --- a/src/NadekoBot/Services/Administration/VplusTService.cs +++ b/src/NadekoBot/Services/Administration/VplusTService.cs @@ -13,7 +13,7 @@ using System.Threading.Tasks; namespace NadekoBot.Services.Administration { - public class VplusTService + public class VplusTService : INService { private readonly Regex _channelNameRegex = new Regex(@"[^a-zA-Z0-9 -]", RegexOptions.Compiled); diff --git a/src/NadekoBot/Services/ClashOfClans/ClashOfClansService.cs b/src/NadekoBot/Services/ClashOfClans/ClashOfClansService.cs deleted file mode 100644 index dcdd6376..00000000 --- a/src/NadekoBot/Services/ClashOfClans/ClashOfClansService.cs +++ /dev/null @@ -1,262 +0,0 @@ -using Discord; -using Discord.WebSocket; -using NadekoBot.Extensions; -using NadekoBot.Services.Database; -using NadekoBot.Services.Database.Models; -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading; -using System.Threading.Tasks; - -namespace NadekoBot.Services.ClashOfClans -{ - // todo 99 rewrite, just made this compile, it's a complete mess. A lot of the things here should actually be in the actual module. - // service should just handle the state, module should print out what happened, so anything that has to do with strings - // shouldn't be here - public class ClashOfClansService - { - private readonly DiscordSocketClient _client; - private readonly DbService _db; - private readonly ILocalization _localization; - private readonly NadekoStrings _strings; - private readonly Timer checkWarTimer; - - public ConcurrentDictionary> ClashWars { get; set; } - - public ClashOfClansService(DiscordSocketClient client, DbService db, - ILocalization localization, NadekoStrings strings, IUnitOfWork uow, - List guilds) - { - _client = client; - _db = db; - _localization = localization; - _strings = strings; - - ClashWars = new ConcurrentDictionary>( - uow.ClashOfClans - .GetAllWars(guilds) - .Select(cw => - { - cw.Channel = _client.GetGuild(cw.GuildId)? - .GetTextChannel(cw.ChannelId); - return cw; - }) - .Where(cw => cw.Channel != null) - .GroupBy(cw => cw.GuildId) - .ToDictionary(g => g.Key, g => g.ToList())); - - checkWarTimer = new Timer(async _ => - { - foreach (var kvp in ClashWars) - { - foreach (var war in kvp.Value) - { - try { await CheckWar(TimeSpan.FromHours(2), war).ConfigureAwait(false); } catch { } - } - } - }, null, TimeSpan.FromMinutes(1), TimeSpan.FromMinutes(1)); - } - - private async Task CheckWar(TimeSpan callExpire, ClashWar war) - { - var Bases = war.Bases; - for (var i = 0; i < Bases.Count; i++) - { - var callUser = Bases[i].CallUser; - if (callUser == null) continue; - if ((!Bases[i].BaseDestroyed) && DateTime.UtcNow - Bases[i].TimeAdded >= callExpire) - { - if (Bases[i].Stars != 3) - Bases[i].BaseDestroyed = true; - else - Bases[i] = null; - try - { - SaveWar(war); - await war.Channel.SendErrorAsync(_strings.GetText("claim_expired", - _localization.GetCultureInfo(war.Channel.GuildId), - typeof(ClashOfClansService).Name.ToLowerInvariant(), - Format.Bold(Bases[i].CallUser), - ShortPrint(war))); - } - catch { } - } - } - } - - public Tuple, int> GetWarInfo(IGuild guild, int num) - { - List wars = null; - ClashWars.TryGetValue(guild.Id, out wars); - if (wars == null || wars.Count == 0) - { - return null; - } - // get the number of the war - else if (num < 1 || num > wars.Count) - { - return null; - } - num -= 1; - //get the actual war - return new Tuple, int>(wars, num); - } - - public async Task CreateWar(string enemyClan, int size, ulong serverId, ulong channelId) - { - var channel = _client.GetGuild(serverId)?.GetTextChannel(channelId); - using (var uow = _db.UnitOfWork) - { - var cw = new ClashWar - { - EnemyClan = enemyClan, - Size = size, - Bases = new List(size), - GuildId = serverId, - ChannelId = channelId, - Channel = channel, - }; - cw.Bases.Capacity = size; - for (int i = 0; i < size; i++) - { - cw.Bases.Add(new ClashCaller() - { - CallUser = null, - SequenceNumber = i, - }); - } - uow.ClashOfClans.Add(cw); - await uow.CompleteAsync(); - return cw; - } - } - - public void SaveWar(ClashWar cw) - { - if (cw.WarState == StateOfWar.Ended) - { - using (var uow = _db.UnitOfWork) - { - uow.ClashOfClans.Remove(cw); - uow.CompleteAsync(); - } - return; - } - - using (var uow = _db.UnitOfWork) - { - uow.ClashOfClans.Update(cw); - uow.CompleteAsync(); - } - } - - public void Call(ClashWar cw, string u, int baseNumber) - { - if (baseNumber < 0 || baseNumber >= cw.Bases.Count) - throw new ArgumentException(Localize(cw, "invalid_base_number")); - if (cw.Bases[baseNumber].CallUser != null && cw.Bases[baseNumber].Stars == 3) - throw new ArgumentException(Localize(cw, "base_already_claimed")); - for (var i = 0; i < cw.Bases.Count; i++) - { - if (cw.Bases[i]?.BaseDestroyed == false && cw.Bases[i]?.CallUser == u) - throw new ArgumentException(Localize(cw, "claimed_other", u, i + 1)); - } - - var cc = cw.Bases[baseNumber]; - cc.CallUser = u.Trim(); - cc.TimeAdded = DateTime.UtcNow; - cc.BaseDestroyed = false; - } - - public int FinishClaim(ClashWar cw, string user, int stars = 3) - { - user = user.Trim(); - for (var i = 0; i < cw.Bases.Count; i++) - { - if (cw.Bases[i]?.BaseDestroyed != false || cw.Bases[i]?.CallUser != user) continue; - cw.Bases[i].BaseDestroyed = true; - cw.Bases[i].Stars = stars; - return i; - } - throw new InvalidOperationException(Localize(cw, "not_partic_or_destroyed", user)); - } - - public void FinishClaim(ClashWar cw, int index, int stars = 3) - { - if (index < 0 || index > cw.Bases.Count) - throw new ArgumentOutOfRangeException(nameof(index)); - var toFinish = cw.Bases[index]; - if (toFinish.BaseDestroyed != false) throw new InvalidOperationException(Localize(cw, "base_already_destroyed")); - if (toFinish.CallUser == null) throw new InvalidOperationException(Localize(cw, "base_already_unclaimed")); - toFinish.BaseDestroyed = true; - toFinish.Stars = stars; - } - - public int Uncall(ClashWar cw, string user) - { - user = user.Trim(); - for (var i = 0; i < cw.Bases.Count; i++) - { - if (cw.Bases[i]?.CallUser != user) continue; - cw.Bases[i].CallUser = null; - return i; - } - throw new InvalidOperationException(Localize(cw, "not_partic")); - } - - public string ShortPrint(ClashWar cw) => - $"`{cw.EnemyClan}` ({cw.Size} v {cw.Size})"; - - public string ToPrettyString(ClashWar cw) - { - var sb = new StringBuilder(); - - if (cw.WarState == StateOfWar.Created) - sb.AppendLine("`not started`"); - var twoHours = new TimeSpan(2, 0, 0); - for (var i = 0; i < cw.Bases.Count; i++) - { - if (cw.Bases[i].CallUser == null) - { - sb.AppendLine($"`{i + 1}.` ❌*{Localize(cw, "not_claimed")}*"); - } - else - { - if (cw.Bases[i].BaseDestroyed) - { - sb.AppendLine($"`{i + 1}.` ✅ `{cw.Bases[i].CallUser}` {new string('⭐', cw.Bases[i].Stars)}"); - } - else - { - var left = (cw.WarState == StateOfWar.Started) ? twoHours - (DateTime.UtcNow - cw.Bases[i].TimeAdded) : twoHours; - if (cw.Bases[i].Stars == 3) - { - sb.AppendLine($"`{i + 1}.` ✅ `{cw.Bases[i].CallUser}` {left.Hours}h {left.Minutes}m {left.Seconds}s left"); - } - else - { - sb.AppendLine($"`{i + 1}.` ✅ `{cw.Bases[i].CallUser}` {left.Hours}h {left.Minutes}m {left.Seconds}s left {new string('⭐', cw.Bases[i].Stars)} {string.Concat(Enumerable.Repeat("🔸", 3 - cw.Bases[i].Stars))}"); - } - } - } - - } - return sb.ToString(); - } - - public string Localize(ClashWar cw, string key, params object[] replacements) - { - return string.Format(Localize(cw, key), replacements); - } - - public string Localize(ClashWar cw, string key) - { - return _strings.GetText(key, - _localization.GetCultureInfo(cw.Channel?.GuildId), - "ClashOfClans".ToLowerInvariant()); - } - } -} diff --git a/src/NadekoBot/Services/ClashOfClans/Extensions.cs b/src/NadekoBot/Services/ClashOfClans/Extensions.cs deleted file mode 100644 index 635557cf..00000000 --- a/src/NadekoBot/Services/ClashOfClans/Extensions.cs +++ /dev/null @@ -1,40 +0,0 @@ -using NadekoBot.Services.Database.Models; -using System; -using System.Linq; - -namespace NadekoBot.Services.ClashOfClans -{ - public static class Extensions - { - public static void ResetTime(this ClashCaller c) - { - c.TimeAdded = DateTime.UtcNow; - } - - public static void Destroy(this ClashCaller c) - { - c.BaseDestroyed = true; - } - - public static void End(this ClashWar cw) - { - //Ended = true; - cw.WarState = StateOfWar.Ended; - } - - public static void Start(this ClashWar cw) - { - if (cw.WarState == StateOfWar.Started) - throw new InvalidOperationException("war_already_started"); - //if (Started) - // throw new InvalidOperationException(); - //Started = true; - cw.WarState = StateOfWar.Started; - cw.StartedAt = DateTime.UtcNow; - foreach (var b in cw.Bases.Where(b => b.CallUser != null)) - { - b.ResetTime(); - } - } - } -} \ No newline at end of file diff --git a/src/NadekoBot/Services/CommandHandler.cs b/src/NadekoBot/Services/CommandHandler.cs index 58acff4d..81ecb073 100644 --- a/src/NadekoBot/Services/CommandHandler.cs +++ b/src/NadekoBot/Services/CommandHandler.cs @@ -24,7 +24,8 @@ namespace NadekoBot.Services public int GetHashCode(IGuildUser obj) => obj.Id.GetHashCode(); } - public class CommandHandler + + public class CommandHandler : INService { public const int GlobalCommandsCooldown = 750; @@ -189,7 +190,7 @@ namespace NadekoBot.Services { try { - if (msg.Author.IsBot || !_bot.Ready) //no bots, wait until bot connected and initialized + if (msg.Author.IsBot || !_bot.Ready.Task.IsCompleted) //no bots, wait until bot connected and initialized return; if (!(msg is SocketUserMessage usrMsg)) @@ -296,83 +297,124 @@ namespace NadekoBot.Services => ExecuteCommand(context, input.Substring(argPos), serviceProvider, multiMatchHandling); - public async Task<(bool Success, string Error, CommandInfo Info)> ExecuteCommand(CommandContext context, string input, IServiceProvider serviceProvider, MultiMatchHandling multiMatchHandling = MultiMatchHandling.Exception) + public async Task<(bool Success, string Error, CommandInfo Info)> ExecuteCommand(CommandContext context, string input, IServiceProvider services, MultiMatchHandling multiMatchHandling = MultiMatchHandling.Exception) { var searchResult = _commandService.Search(context, input); if (!searchResult.IsSuccess) return (false, null, null); var commands = searchResult.Commands; - for (int i = commands.Count - 1; i >= 0; i--) + var preconditionResults = new Dictionary(); + + foreach (var match in commands) { - var preconditionResult = await commands[i].CheckPreconditionsAsync(context, serviceProvider).ConfigureAwait(false); - if (!preconditionResult.IsSuccess) - { - return (false, preconditionResult.ErrorReason, commands[i].Command); - } - - var parseResult = await commands[i].ParseAsync(context, searchResult, preconditionResult).ConfigureAwait(false); - if (!parseResult.IsSuccess) - { - if (parseResult.Error == CommandError.MultipleMatches) - { - TypeReaderValue[] argList, paramList; - switch (multiMatchHandling) - { - case MultiMatchHandling.Best: - argList = parseResult.ArgValues.Select(x => x.Values.OrderByDescending(y => y.Score).First()).ToArray(); - paramList = parseResult.ParamValues.Select(x => x.Values.OrderByDescending(y => y.Score).First()).ToArray(); - parseResult = ParseResult.FromSuccess(argList, paramList); - break; - } - } - - if (!parseResult.IsSuccess) - { - if (commands.Count == 1) - return (false, parseResult.ErrorReason, commands[i].Command); - else - continue; - } - } - - var cmd = commands[i].Command; - - // Bot will ignore commands which are ran more often than what specified by - // GlobalCommandsCooldown constant (miliseconds) - if (!UsersOnShortCooldown.Add(context.Message.Author.Id)) - return (false, null, commands[i].Command); - //return SearchResult.FromError(CommandError.Exception, "You are on a global cooldown."); - - var commandName = cmd.Aliases.First(); - foreach (var svc in _services) - { - if (svc is ILateBlocker exec && - await exec.TryBlockLate(_client, context.Message, context.Guild, context.Channel, context.User, cmd.Module.GetTopLevelModule().Name, commandName).ConfigureAwait(false)) - { - _log.Info("Late blocking User [{0}] Command: [{1}] in [{2}]", context.User, commandName, svc.GetType().Name); - return (false, null, commands[i].Command); - } - } - - var execResult = (ExecuteResult)(await commands[i].ExecuteAsync(context, parseResult, serviceProvider)); - if (execResult.Exception != null && (!(execResult.Exception is HttpException he) || he.DiscordCode != 50013)) - { - lock (errorLogLock) - { - var now = DateTime.Now; - File.AppendAllText($"./command_errors_{now:yyyy-MM-dd}.txt", - $"[{now:HH:mm-yyyy-MM-dd}]" + Environment.NewLine - + execResult.Exception.ToString() + Environment.NewLine - + "------" + Environment.NewLine); - _log.Warn(execResult.Exception); - } - } - return (true, null, commands[i].Command); + preconditionResults[match] = await match.Command.CheckPreconditionsAsync(context, services).ConfigureAwait(false); } - return (false, null, null); - //return new ExecuteCommandResult(null, null, SearchResult.FromError(CommandError.UnknownCommand, "This input does not match any overload.")); + var successfulPreconditions = preconditionResults + .Where(x => x.Value.IsSuccess) + .ToArray(); + + if (successfulPreconditions.Length == 0) + { + //All preconditions failed, return the one from the highest priority command + var bestCandidate = preconditionResults + .OrderByDescending(x => x.Key.Command.Priority) + .FirstOrDefault(x => !x.Value.IsSuccess); + return (false, bestCandidate.Value.ErrorReason, commands[0].Command); + } + + var parseResultsDict = new Dictionary(); + foreach (var pair in successfulPreconditions) + { + var parseResult = await pair.Key.ParseAsync(context, searchResult, pair.Value, services).ConfigureAwait(false); + + if (parseResult.Error == CommandError.MultipleMatches) + { + IReadOnlyList argList, paramList; + switch (multiMatchHandling) + { + case MultiMatchHandling.Best: + argList = parseResult.ArgValues.Select(x => x.Values.OrderByDescending(y => y.Score).First()).ToImmutableArray(); + paramList = parseResult.ParamValues.Select(x => x.Values.OrderByDescending(y => y.Score).First()).ToImmutableArray(); + parseResult = ParseResult.FromSuccess(argList, paramList); + break; + } + } + + parseResultsDict[pair.Key] = parseResult; + } + // Calculates the 'score' of a command given a parse result + float CalculateScore(CommandMatch match, ParseResult parseResult) + { + float argValuesScore = 0, paramValuesScore = 0; + + if (match.Command.Parameters.Count > 0) + { + var argValuesSum = parseResult.ArgValues?.Sum(x => x.Values.OrderByDescending(y => y.Score).FirstOrDefault().Score) ?? 0; + var paramValuesSum = parseResult.ParamValues?.Sum(x => x.Values.OrderByDescending(y => y.Score).FirstOrDefault().Score) ?? 0; + + argValuesScore = argValuesSum / match.Command.Parameters.Count; + paramValuesScore = paramValuesSum / match.Command.Parameters.Count; + } + + var totalArgsScore = (argValuesScore + paramValuesScore) / 2; + return match.Command.Priority + totalArgsScore * 0.99f; + } + + //Order the parse results by their score so that we choose the most likely result to execute + var parseResults = parseResultsDict + .OrderByDescending(x => CalculateScore(x.Key, x.Value)); + + var successfulParses = parseResults + .Where(x => x.Value.IsSuccess) + .ToArray(); + + if (successfulParses.Length == 0) + { + //All parses failed, return the one from the highest priority command, using score as a tie breaker + var bestMatch = parseResults + .FirstOrDefault(x => !x.Value.IsSuccess); + return (false, bestMatch.Value.ErrorReason, commands[0].Command); + } + + var cmd = successfulParses[0].Key.Command; + + // Bot will ignore commands which are ran more often than what specified by + // GlobalCommandsCooldown constant (miliseconds) + if (!UsersOnShortCooldown.Add(context.Message.Author.Id)) + return (false, null, cmd); + //return SearchResult.FromError(CommandError.Exception, "You are on a global cooldown."); + + var commandName = cmd.Aliases.First(); + foreach (var svc in _services) + { + if (svc is ILateBlocker exec && + await exec.TryBlockLate(_client, context.Message, context.Guild, context.Channel, context.User, cmd.Module.GetTopLevelModule().Name, commandName).ConfigureAwait(false)) + { + _log.Info("Late blocking User [{0}] Command: [{1}] in [{2}]", context.User, commandName, svc.GetType().Name); + return (false, null, cmd); + } + } + + //If we get this far, at least one parse was successful. Execute the most likely overload. + var chosenOverload = successfulParses[0]; + var execResult = (ExecuteResult)await chosenOverload.Key.ExecuteAsync(context, chosenOverload.Value, services).ConfigureAwait(false); + + if (execResult.Exception != null && (!(execResult.Exception is HttpException he) || he.DiscordCode != 50013)) + { + lock (errorLogLock) + { + var now = DateTime.Now; + File.AppendAllText($"./command_errors_{now:yyyy-MM-dd}.txt", + $"[{now:HH:mm-yyyy-MM-dd}]" + Environment.NewLine + + execResult.Exception.ToString() + Environment.NewLine + + "------" + Environment.NewLine); + _log.Warn(execResult.Exception); + } + } + + return (true, null, cmd); } private readonly object errorLogLock = new object(); diff --git a/src/NadekoBot/Services/CurrencyService.cs b/src/NadekoBot/Services/CurrencyService.cs index 99f1644a..fb664d62 100644 --- a/src/NadekoBot/Services/CurrencyService.cs +++ b/src/NadekoBot/Services/CurrencyService.cs @@ -7,7 +7,7 @@ using NadekoBot.Services.Database; namespace NadekoBot.Services { - public class CurrencyService + public class CurrencyService : INService { private readonly BotConfig _config; private readonly DbService _db; diff --git a/src/NadekoBot/Services/CustomReactions/CustomReactionsService.cs b/src/NadekoBot/Services/CustomReactions/CustomReactionsService.cs index c64f5c81..2e068cde 100644 --- a/src/NadekoBot/Services/CustomReactions/CustomReactionsService.cs +++ b/src/NadekoBot/Services/CustomReactions/CustomReactionsService.cs @@ -14,7 +14,7 @@ using NadekoBot.Services.Database; namespace NadekoBot.Services.CustomReactions { - public class CustomReactionsService : IEarlyBlockingExecutor + public class CustomReactionsService : IEarlyBlockingExecutor, INService { public CustomReaction[] GlobalReactions = new CustomReaction[] { }; public ConcurrentDictionary GuildReactions { get; } = new ConcurrentDictionary(); diff --git a/src/NadekoBot/Services/Database/Models/BotConfig.cs b/src/NadekoBot/Services/Database/Models/BotConfig.cs index 0fa46b84..d765b813 100644 --- a/src/NadekoBot/Services/Database/Models/BotConfig.cs +++ b/src/NadekoBot/Services/Database/Models/BotConfig.cs @@ -65,7 +65,7 @@ Nadeko Support Server: https://discord.gg/nadekobot"; public List StartupCommands { get; set; } public HashSet BlockedCommands { get; set; } public HashSet BlockedModules { get; set; } - public int PermissionVersion { get; set; } = 1; + public int PermissionVersion { get; set; } public string DefaultPrefix { get; set; } = "."; public bool CustomReactionsStartWith { get; set; } = false; } diff --git a/src/NadekoBot/Services/Database/Repositories/IReminderRepository.cs b/src/NadekoBot/Services/Database/Repositories/IReminderRepository.cs index 07eec33f..a4812775 100644 --- a/src/NadekoBot/Services/Database/Repositories/IReminderRepository.cs +++ b/src/NadekoBot/Services/Database/Repositories/IReminderRepository.cs @@ -6,6 +6,6 @@ namespace NadekoBot.Services.Database.Repositories { public interface IReminderRepository : IRepository { - IEnumerable GetIncludedReminders(List guildIds); + IEnumerable GetIncludedReminders(IEnumerable guildIds); } } diff --git a/src/NadekoBot/Services/Database/Repositories/Impl/ReminderRepository.cs b/src/NadekoBot/Services/Database/Repositories/Impl/ReminderRepository.cs index b1a0e2a0..b2b13a2b 100644 --- a/src/NadekoBot/Services/Database/Repositories/Impl/ReminderRepository.cs +++ b/src/NadekoBot/Services/Database/Repositories/Impl/ReminderRepository.cs @@ -12,7 +12,7 @@ namespace NadekoBot.Services.Database.Repositories.Impl { } - public IEnumerable GetIncludedReminders(List guildIds) + public IEnumerable GetIncludedReminders(IEnumerable guildIds) { return _set.Where(x => guildIds.Contains((long)x.ServerId)).ToList(); } diff --git a/src/NadekoBot/Services/Games/ChatterbotService.cs b/src/NadekoBot/Services/Games/ChatterbotService.cs index 7f9c8251..0f067beb 100644 --- a/src/NadekoBot/Services/Games/ChatterbotService.cs +++ b/src/NadekoBot/Services/Games/ChatterbotService.cs @@ -13,7 +13,7 @@ using System.Threading.Tasks; namespace NadekoBot.Services.Games { - public class ChatterBotService : IEarlyBlockingExecutor + public class ChatterBotService : IEarlyBlockingExecutor, INService { private readonly DiscordSocketClient _client; private readonly Logger _log; diff --git a/src/NadekoBot/Services/Games/GamesService.cs b/src/NadekoBot/Services/Games/GamesService.cs index e677990b..d5822833 100644 --- a/src/NadekoBot/Services/Games/GamesService.cs +++ b/src/NadekoBot/Services/Games/GamesService.cs @@ -15,7 +15,7 @@ using System.Threading.Tasks; namespace NadekoBot.Services.Games { - public class GamesService + public class GamesService : INService { private readonly BotConfig _bc; diff --git a/src/NadekoBot/Services/Games/PollService.cs b/src/NadekoBot/Services/Games/PollService.cs index ba6fee19..c2d77066 100644 --- a/src/NadekoBot/Services/Games/PollService.cs +++ b/src/NadekoBot/Services/Games/PollService.cs @@ -9,7 +9,7 @@ using NLog; namespace NadekoBot.Services.Games { - public class PollService : IEarlyBlockingExecutor + public class PollService : IEarlyBlockingExecutor, INService { public ConcurrentDictionary ActivePolls = new ConcurrentDictionary(); private readonly Logger _log; diff --git a/src/NadekoBot/Services/GreetSettingsService.cs b/src/NadekoBot/Services/GreetSettingsService.cs index ac3b84da..10b0899c 100644 --- a/src/NadekoBot/Services/GreetSettingsService.cs +++ b/src/NadekoBot/Services/GreetSettingsService.cs @@ -13,7 +13,7 @@ using System.Threading.Tasks; namespace NadekoBot.Services { - public class GreetSettingsService + public class GreetSettingsService : INService { private readonly DbService _db; diff --git a/src/NadekoBot/Services/Help/HelpService.cs b/src/NadekoBot/Services/Help/HelpService.cs index cdc6adbe..b685a50b 100644 --- a/src/NadekoBot/Services/Help/HelpService.cs +++ b/src/NadekoBot/Services/Help/HelpService.cs @@ -11,7 +11,7 @@ using NadekoBot.Attributes; namespace NadekoBot.Services.Help { - public class HelpService : ILateExecutor + public class HelpService : ILateExecutor, INService { private readonly BotConfig _bc; private readonly CommandHandler _ch; diff --git a/src/NadekoBot/Services/IGoogleApiService.cs b/src/NadekoBot/Services/IGoogleApiService.cs index f2e26f11..758a7d0f 100644 --- a/src/NadekoBot/Services/IGoogleApiService.cs +++ b/src/NadekoBot/Services/IGoogleApiService.cs @@ -5,7 +5,7 @@ using System.Threading.Tasks; namespace NadekoBot.Services { - public interface IGoogleApiService + public interface IGoogleApiService : INService { IEnumerable Languages { get; } diff --git a/src/NadekoBot/Services/IImagesService.cs b/src/NadekoBot/Services/IImagesService.cs index 31e6c32a..5678191c 100644 --- a/src/NadekoBot/Services/IImagesService.cs +++ b/src/NadekoBot/Services/IImagesService.cs @@ -3,7 +3,7 @@ using System.Collections.Immutable; namespace NadekoBot.Services { - public interface IImagesService + public interface IImagesService : INService { ImmutableArray Heads { get; } ImmutableArray Tails { get; } diff --git a/src/NadekoBot/Services/INService.cs b/src/NadekoBot/Services/INService.cs new file mode 100644 index 00000000..76c30abb --- /dev/null +++ b/src/NadekoBot/Services/INService.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace NadekoBot.Services +{ + /// + /// All services must implement this interface in order to be auto-discovered by the DI system + /// + public interface INService + { + + } +} diff --git a/src/NadekoBot/Services/IStatsService.cs b/src/NadekoBot/Services/IStatsService.cs index d187c413..800b8fba 100644 --- a/src/NadekoBot/Services/IStatsService.cs +++ b/src/NadekoBot/Services/IStatsService.cs @@ -3,7 +3,7 @@ using System.Threading.Tasks; namespace NadekoBot.Services { - public interface IStatsService + public interface IStatsService : INService { string Author { get; } long CommandsRan { get; } diff --git a/src/NadekoBot/Services/Impl/NadekoStrings.cs b/src/NadekoBot/Services/Impl/NadekoStrings.cs index fb0d6f17..f6273770 100644 --- a/src/NadekoBot/Services/Impl/NadekoStrings.cs +++ b/src/NadekoBot/Services/Impl/NadekoStrings.cs @@ -11,7 +11,7 @@ using System.Text.RegularExpressions; namespace NadekoBot.Services { - public class NadekoStrings + public class NadekoStrings : INService { public const string stringsPath = @"_strings/"; diff --git a/src/NadekoBot/Services/Impl/SoundCloudApiService.cs b/src/NadekoBot/Services/Impl/SoundCloudApiService.cs index a3dc8cdd..acdba5b7 100644 --- a/src/NadekoBot/Services/Impl/SoundCloudApiService.cs +++ b/src/NadekoBot/Services/Impl/SoundCloudApiService.cs @@ -6,7 +6,7 @@ using System.Threading.Tasks; namespace NadekoBot.Services.Impl { - public class SoundCloudApiService + public class SoundCloudApiService : INService { private readonly IBotCredentials _creds; diff --git a/src/NadekoBot/Services/Impl/StartingGuildsListService.cs b/src/NadekoBot/Services/Impl/StartingGuildsListService.cs new file mode 100644 index 00000000..e7f413cc --- /dev/null +++ b/src/NadekoBot/Services/Impl/StartingGuildsListService.cs @@ -0,0 +1,24 @@ +using Discord.WebSocket; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Collections; + +namespace NadekoBot.Services.Impl +{ + public class StartingGuildsService : IEnumerable, INService + { + private readonly ImmutableList _guilds; + + public StartingGuildsService(DiscordSocketClient client) + { + this._guilds = client.Guilds.Select(x => (long)x.Id).ToImmutableList(); + } + + public IEnumerator GetEnumerator() => + _guilds.GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => + _guilds.GetEnumerator(); + } +} diff --git a/src/NadekoBot/Services/Music/MusicService.cs b/src/NadekoBot/Services/Music/MusicService.cs index 5d949618..4b498b42 100644 --- a/src/NadekoBot/Services/Music/MusicService.cs +++ b/src/NadekoBot/Services/Music/MusicService.cs @@ -15,7 +15,7 @@ using NadekoBot.Services.Impl; namespace NadekoBot.Services.Music { - public class MusicService + public class MusicService : INService { public const string MusicDataPath = "data/musicdata"; diff --git a/src/NadekoBot/Services/Permissions/BlacklistService.cs b/src/NadekoBot/Services/Permissions/BlacklistService.cs index 4f7a691d..cad2f356 100644 --- a/src/NadekoBot/Services/Permissions/BlacklistService.cs +++ b/src/NadekoBot/Services/Permissions/BlacklistService.cs @@ -7,7 +7,7 @@ using System.Threading.Tasks; namespace NadekoBot.Services.Permissions { - public class BlacklistService : IEarlyBlocker + public class BlacklistService : IEarlyBlocker, INService { public ConcurrentHashSet BlacklistedUsers { get; } public ConcurrentHashSet BlacklistedGuilds { get; } diff --git a/src/NadekoBot/Services/Permissions/CmdCdService.cs b/src/NadekoBot/Services/Permissions/CmdCdService.cs index 7a44a292..27a24ea5 100644 --- a/src/NadekoBot/Services/Permissions/CmdCdService.cs +++ b/src/NadekoBot/Services/Permissions/CmdCdService.cs @@ -9,7 +9,7 @@ using System.Threading.Tasks; namespace NadekoBot.Services.Permissions { - public class CmdCdService : ILateBlocker + public class CmdCdService : ILateBlocker, INService { public ConcurrentDictionary> CommandCooldowns { get; } public ConcurrentDictionary> ActiveCooldowns { get; } = new ConcurrentDictionary>(); diff --git a/src/NadekoBot/Services/Permissions/FilterService.cs b/src/NadekoBot/Services/Permissions/FilterService.cs index 9ac67d4d..38885646 100644 --- a/src/NadekoBot/Services/Permissions/FilterService.cs +++ b/src/NadekoBot/Services/Permissions/FilterService.cs @@ -12,7 +12,7 @@ using NLog; namespace NadekoBot.Services.Permissions { - public class FilterService : IEarlyBlocker + public class FilterService : IEarlyBlocker, INService { private readonly Logger _log; diff --git a/src/NadekoBot/Services/Permissions/GlobalPermissionService.cs b/src/NadekoBot/Services/Permissions/GlobalPermissionService.cs index 419f77f3..734dd9a8 100644 --- a/src/NadekoBot/Services/Permissions/GlobalPermissionService.cs +++ b/src/NadekoBot/Services/Permissions/GlobalPermissionService.cs @@ -8,7 +8,7 @@ using System.Threading.Tasks; namespace NadekoBot.Services.Permissions { - public class GlobalPermissionService : ILateBlocker + public class GlobalPermissionService : ILateBlocker, INService { public readonly ConcurrentHashSet BlockedModules; public readonly ConcurrentHashSet BlockedCommands; diff --git a/src/NadekoBot/Services/Permissions/PermissionsService.cs b/src/NadekoBot/Services/Permissions/PermissionsService.cs index c4c586a9..597ab2ba 100644 --- a/src/NadekoBot/Services/Permissions/PermissionsService.cs +++ b/src/NadekoBot/Services/Permissions/PermissionsService.cs @@ -11,14 +11,14 @@ using System.Threading.Tasks; using Discord; using Discord.WebSocket; using NadekoBot.Extensions; +using NadekoBot.Services.Database; using NadekoBot.Services; namespace NadekoBot.Services.Permissions { - public class PermissionService : ILateBlocker + public class PermissionService : ILateBlocker, INService { private readonly DbService _db; - private readonly Logger _log; private readonly CommandHandler _cmd; private readonly NadekoStrings _strings; @@ -26,16 +26,15 @@ namespace NadekoBot.Services.Permissions public ConcurrentDictionary Cache { get; } = new ConcurrentDictionary(); - public PermissionService(DiscordSocketClient client, DbService db, BotConfig bc, CommandHandler cmd, NadekoStrings strings) + public PermissionService(DiscordSocketClient client, DbService db, CommandHandler cmd, NadekoStrings strings) { - _log = LogManager.GetCurrentClassLogger(); _db = db; _cmd = cmd; _strings = strings; var sw = Stopwatch.StartNew(); if (client.ShardId == 0) - TryMigratePermissions(bc); + TryMigratePermissions(); using (var uow = _db.UnitOfWork) { @@ -49,9 +48,6 @@ namespace NadekoBot.Services.Permissions }); } } - - sw.Stop(); - _log.Debug($"Loaded in {sw.Elapsed.TotalSeconds:F2}s"); } public PermissionCache GetCache(ulong guildId) @@ -71,12 +67,13 @@ namespace NadekoBot.Services.Permissions return pc; } - private void TryMigratePermissions(BotConfig bc) + private void TryMigratePermissions() { - var log = LogManager.GetCurrentClassLogger(); - if (bc.PermissionVersion <= 1) + using (var uow = _db.UnitOfWork) { - using (var uow = _db.UnitOfWork) + var bc = uow.BotConfig.GetOrCreate(); + var log = LogManager.GetCurrentClassLogger(); + if (bc.PermissionVersion <= 1) { log.Info("Permission version is 1, upgrading to 2."); var oldCache = new ConcurrentDictionary(uow.GuildConfigs @@ -134,25 +131,22 @@ namespace NadekoBot.Services.Permissions bc.PermissionVersion = 2; uow.Complete(); } - } - if (bc.PermissionVersion <= 2) - { - using (var uow = _db.UnitOfWork) + if (bc.PermissionVersion <= 2) { var oldPrefixes = new[] { ".", ";", "!!", "!m", "!", "+", "-", "$", ">" }; uow._context.Database.ExecuteSqlCommand( -$@"UPDATE {nameof(Permissionv2)} + $@"UPDATE {nameof(Permissionv2)} SET secondaryTargetName=trim(substr(secondaryTargetName, 3)) WHERE secondaryTargetName LIKE '!!%' OR secondaryTargetName LIKE '!m%'; UPDATE {nameof(Permissionv2)} SET secondaryTargetName=substr(secondaryTargetName, 2) WHERE secondaryTargetName LIKE '.%' OR - secondaryTargetName LIKE '~%' OR - secondaryTargetName LIKE ';%' OR - secondaryTargetName LIKE '>%' OR - secondaryTargetName LIKE '-%' OR - secondaryTargetName LIKE '!%';"); +secondaryTargetName LIKE '~%' OR +secondaryTargetName LIKE ';%' OR +secondaryTargetName LIKE '>%' OR +secondaryTargetName LIKE '-%' OR +secondaryTargetName LIKE '!%';"); bc.PermissionVersion = 3; uow.Complete(); } @@ -229,4 +223,4 @@ WHERE secondaryTargetName LIKE '.%' OR return false; } } -} +} \ No newline at end of file diff --git a/src/NadekoBot/Services/Pokemon/PokemonService.cs b/src/NadekoBot/Services/Pokemon/PokemonService.cs index d2160865..9ac73bf1 100644 --- a/src/NadekoBot/Services/Pokemon/PokemonService.cs +++ b/src/NadekoBot/Services/Pokemon/PokemonService.cs @@ -6,7 +6,7 @@ using System.IO; namespace NadekoBot.Services.Pokemon { - public class PokemonService + public class PokemonService : INService { public readonly List PokemonTypes = new List(); public readonly ConcurrentDictionary Stats = new ConcurrentDictionary(); diff --git a/src/NadekoBot/Services/Searches/AnimeSearchService.cs b/src/NadekoBot/Services/Searches/AnimeSearchService.cs index a4187da3..bcf9a3a3 100644 --- a/src/NadekoBot/Services/Searches/AnimeSearchService.cs +++ b/src/NadekoBot/Services/Searches/AnimeSearchService.cs @@ -6,7 +6,7 @@ using System.Threading.Tasks; namespace NadekoBot.Services.Searches { - public class AnimeSearchService + public class AnimeSearchService : INService { private readonly Logger _log; diff --git a/src/NadekoBot/Services/Searches/SearchesService.cs b/src/NadekoBot/Services/Searches/SearchesService.cs index 54a268c8..43cc2a52 100644 --- a/src/NadekoBot/Services/Searches/SearchesService.cs +++ b/src/NadekoBot/Services/Searches/SearchesService.cs @@ -15,7 +15,7 @@ using System.Xml; namespace NadekoBot.Services.Searches { - public class SearchesService + public class SearchesService : INService { private readonly DiscordSocketClient _client; private readonly IGoogleApiService _google; diff --git a/src/NadekoBot/Services/Searches/StreamNotificationService.cs b/src/NadekoBot/Services/Searches/StreamNotificationService.cs index c077e4e1..3b4039a1 100644 --- a/src/NadekoBot/Services/Searches/StreamNotificationService.cs +++ b/src/NadekoBot/Services/Searches/StreamNotificationService.cs @@ -15,7 +15,7 @@ using System.Threading.Tasks; namespace NadekoBot.Services.Searches { - public class StreamNotificationService + public class StreamNotificationService : INService { private readonly Timer _streamCheckTimer; private bool firstStreamNotifPass { get; set; } = true; diff --git a/src/NadekoBot/Services/ServiceProvider.cs b/src/NadekoBot/Services/ServiceProvider.cs index c3b46ba3..03eebec6 100644 --- a/src/NadekoBot/Services/ServiceProvider.cs +++ b/src/NadekoBot/Services/ServiceProvider.cs @@ -3,6 +3,15 @@ using System.Collections; using System.Collections.Concurrent; using System.Collections.Generic; using System.Collections.Immutable; +using System.Reflection; +using Discord.Commands; +using Discord.WebSocket; +using NadekoBot.Services.Database.Models; +using NadekoBot.Services.Impl; +using System.Linq; +using NadekoBot.Extensions; +using System.Diagnostics; +using NLog; namespace NadekoBot.Services { @@ -16,8 +25,14 @@ namespace NadekoBot.Services public class ServiceProviderBuilder { private ConcurrentDictionary _dict = new ConcurrentDictionary(); + private readonly Logger _log; - public ServiceProviderBuilder Add(T obj) + public ServiceProviderBuilder() + { + _log = LogManager.GetCurrentClassLogger(); + } + + public ServiceProviderBuilder AddManual(T obj) { _dict.TryAdd(typeof(T), obj); return this; @@ -27,6 +42,61 @@ namespace NadekoBot.Services { return new NServiceProvider(_dict); } + + public ServiceProviderBuilder LoadFrom(Assembly assembly) + { + var allTypes = assembly.GetTypes(); + var services = new Queue(allTypes + .Where(x => x.GetInterfaces().Contains(typeof(INService)) && !x.GetTypeInfo().IsInterface && !x.GetTypeInfo().IsAbstract) + .ToArray()); + + var interfaces = new HashSet(allTypes + .Where(x => x.GetInterfaces().Contains(typeof(INService)) && x.GetTypeInfo().IsInterface)); + + var sw = Stopwatch.StartNew(); + var swInstance = new Stopwatch(); + while (services.Count > 0) + { + var type = services.Dequeue(); //get a type i need to make an instance of + + if (_dict.TryGetValue(type, out _)) // if that type is already instantiated, skip + continue; + + var ctor = type.GetConstructors()[0]; + var argTypes = ctor + .GetParameters() + .Select(x => x.ParameterType) + .ToArray(); // get constructor argument types i need to pass in + + var args = new List(argTypes.Length); + foreach (var arg in argTypes) //get constructor arguments from the dictionary of already instantiated types + { + if (_dict.TryGetValue(arg, out var argObj)) //if i got current one, add it to the list of instances and move on + args.Add(argObj); + else //if i failed getting it, add it to the end, and break + { + services.Enqueue(type); + break; + } + } + if (args.Count != argTypes.Length) + continue; + swInstance.Restart(); + var instance = ctor.Invoke(args.ToArray()); + swInstance.Stop(); + if (swInstance.Elapsed.TotalSeconds > 5) + _log.Info($"{type.Name} took {swInstance.Elapsed.TotalSeconds:F2}s to load."); + var interfaceType = interfaces.FirstOrDefault(x => instance.GetType().GetInterfaces().Contains(x)); + if (interfaceType != null) + _dict.TryAdd(interfaceType, instance); + + _dict.TryAdd(type, instance); + } + sw.Stop(); + _log.Info($"All services loaded in {sw.Elapsed.TotalSeconds:F2}s"); + + return this; + } } private readonly ImmutableDictionary _services; diff --git a/src/NadekoBot/Services/Utility/CommandMapService.cs b/src/NadekoBot/Services/Utility/CommandMapService.cs index 9fe6e546..5c07688e 100644 --- a/src/NadekoBot/Services/Utility/CommandMapService.cs +++ b/src/NadekoBot/Services/Utility/CommandMapService.cs @@ -10,7 +10,7 @@ using NadekoBot.Extensions; namespace NadekoBot.Services.Utility { - public class CommandMapService : IInputTransformer + public class CommandMapService : IInputTransformer, INService { private readonly Logger _log; diff --git a/src/NadekoBot/Services/Utility/ConverterService.cs b/src/NadekoBot/Services/Utility/ConverterService.cs index 45334b48..9d4dfcff 100644 --- a/src/NadekoBot/Services/Utility/ConverterService.cs +++ b/src/NadekoBot/Services/Utility/ConverterService.cs @@ -13,7 +13,7 @@ using System.Threading.Tasks; namespace NadekoBot.Services.Utility { - public class ConverterService + public class ConverterService : INService { public List Units { get; } = new List(); private readonly Logger _log; diff --git a/src/NadekoBot/Services/Utility/MessageRepeaterService.cs b/src/NadekoBot/Services/Utility/MessageRepeaterService.cs index cc402aa8..eeaecf37 100644 --- a/src/NadekoBot/Services/Utility/MessageRepeaterService.cs +++ b/src/NadekoBot/Services/Utility/MessageRepeaterService.cs @@ -8,7 +8,7 @@ using System.Threading.Tasks; namespace NadekoBot.Services.Utility { //todo 50 rewrite - public class MessageRepeaterService + public class MessageRepeaterService : INService { //messagerepeater //guildid/RepeatRunners @@ -19,8 +19,7 @@ namespace NadekoBot.Services.Utility { var _ = Task.Run(async () => { - while (!bot.Ready) - await Task.Delay(1000); + await bot.Ready.Task.ConfigureAwait(false); Repeaters = new ConcurrentDictionary>(gcs .ToDictionary(gc => gc.GuildId, diff --git a/src/NadekoBot/Services/Utility/PatreonRewardsService.cs b/src/NadekoBot/Services/Utility/PatreonRewardsService.cs index 7c08f747..b7fae1f4 100644 --- a/src/NadekoBot/Services/Utility/PatreonRewardsService.cs +++ b/src/NadekoBot/Services/Utility/PatreonRewardsService.cs @@ -9,13 +9,12 @@ using System.Collections.Immutable; using System.IO; using System.Linq; using System.Net.Http; -using System.Text; using System.Threading; using System.Threading.Tasks; namespace NadekoBot.Services.Utility { - public class PatreonRewardsService + public class PatreonRewardsService : INService { private readonly SemaphoreSlim getPledgesLocker = new SemaphoreSlim(1, 1); diff --git a/src/NadekoBot/Services/Utility/RemindService.cs b/src/NadekoBot/Services/Utility/RemindService.cs index 48f3f869..89bc20a6 100644 --- a/src/NadekoBot/Services/Utility/RemindService.cs +++ b/src/NadekoBot/Services/Utility/RemindService.cs @@ -4,9 +4,9 @@ using NadekoBot.DataStructures.Replacements; using NadekoBot.Extensions; using NadekoBot.Services.Database; using NadekoBot.Services.Database.Models; +using NadekoBot.Services.Impl; using NLog; using System; -using System.Collections.Generic; using System.Linq; using System.Text.RegularExpressions; using System.Threading; @@ -14,7 +14,7 @@ using System.Threading.Tasks; namespace NadekoBot.Services.Utility { - public class RemindService + public class RemindService : INService { public readonly Regex Regex = new Regex(@"^(?:(?\d)mo)?(?:(?\d)w)?(?:(?\d{1,2})d)?(?:(?\d{1,2})h)?(?:(?\d{1,2})m)?$", RegexOptions.Compiled | RegexOptions.Multiline); @@ -28,8 +28,8 @@ namespace NadekoBot.Services.Utility private readonly DiscordSocketClient _client; private readonly DbService _db; - public RemindService(DiscordSocketClient client, BotConfig config, DbService db, - List guilds, IUnitOfWork uow) + public RemindService(DiscordSocketClient client, BotConfig config, DbService db, + StartingGuildsService guilds, IUnitOfWork uow) { _config = config; _client = client; diff --git a/src/NadekoBot/Services/Utility/StreamRoleService.cs b/src/NadekoBot/Services/Utility/StreamRoleService.cs index 2bd8a156..25863135 100644 --- a/src/NadekoBot/Services/Utility/StreamRoleService.cs +++ b/src/NadekoBot/Services/Utility/StreamRoleService.cs @@ -12,7 +12,7 @@ using NLog; namespace NadekoBot.Services.Utility { - public class StreamRoleService + public class StreamRoleService : INService { private readonly DbService _db; private readonly ConcurrentDictionary guildSettings; @@ -26,6 +26,7 @@ namespace NadekoBot.Services.Utility this._log = LogManager.GetCurrentClassLogger(); guildSettings = gcs.ToDictionary(x => x.GuildId, x => x.StreamRole) + .Where(x => x.Value.FromRoleId != 0 && x.Value.AddRoleId != 0) .ToConcurrent(); client.GuildMemberUpdated += Client_GuildMemberUpdated; diff --git a/src/NadekoBot/Services/Utility/VerboseErrorsService.cs b/src/NadekoBot/Services/Utility/VerboseErrorsService.cs index 7658ea1c..8ed89d46 100644 --- a/src/NadekoBot/Services/Utility/VerboseErrorsService.cs +++ b/src/NadekoBot/Services/Utility/VerboseErrorsService.cs @@ -10,7 +10,7 @@ using System.Linq; namespace NadekoBot.Services.Utility { - public class VerboseErrorsService + public class VerboseErrorsService : INService { private readonly ConcurrentHashSet guildsEnabled; private readonly DbService _db; diff --git a/src/NadekoBot/ShardsCoordinator.cs b/src/NadekoBot/ShardsCoordinator.cs index 6846e4d2..9e4851bd 100644 --- a/src/NadekoBot/ShardsCoordinator.cs +++ b/src/NadekoBot/ShardsCoordinator.cs @@ -3,7 +3,6 @@ using NadekoBot.Services; using NadekoBot.Services.Impl; using NLog; using System; -using System.Collections.Concurrent; using System.Diagnostics; using System.Linq; using System.Threading.Tasks; From 30bafa8a89ed719eb0f55e3b26ab648e02932f7d Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Sat, 15 Jul 2017 05:23:46 +0200 Subject: [PATCH 188/346] More cleanup --- src/NadekoBot/Modules/Utility/Utility.cs | 10 ++-- src/NadekoBot/NadekoBot.cs | 53 +++++++++----------- src/NadekoBot/Services/Impl/NadekoStrings.cs | 4 +- 3 files changed, 31 insertions(+), 36 deletions(-) diff --git a/src/NadekoBot/Modules/Utility/Utility.cs b/src/NadekoBot/Modules/Utility/Utility.cs index e1cf1226..b4c8b003 100644 --- a/src/NadekoBot/Modules/Utility/Utility.cs +++ b/src/NadekoBot/Modules/Utility/Utility.cs @@ -28,14 +28,14 @@ namespace NadekoBot.Modules.Utility private readonly DiscordSocketClient _client; private readonly IStatsService _stats; private readonly IBotCredentials _creds; - private readonly NadekoBot _bot; + private readonly ShardsCoordinator _shardCoord; - public Utility(NadekoBot bot, DiscordSocketClient client, IStatsService stats, IBotCredentials creds) + public Utility(ShardsCoordinator shardCoord, DiscordSocketClient client, IStatsService stats, IBotCredentials creds) { _client = client; _stats = stats; _creds = creds; - _bot = bot; + _shardCoord = shardCoord; } [NadekoCommand, Usage, Description, Aliases] @@ -286,7 +286,7 @@ namespace NadekoBot.Modules.Utility { if (--page < 0) return; - var statuses = _bot.ShardCoord.Statuses.ToArray() + var statuses = _shardCoord.Statuses.ToArray() .Where(x => x != null); var status = string.Join(", ", statuses @@ -331,7 +331,7 @@ namespace NadekoBot.Modules.Utility .WithIconUrl("https://cdn.discordapp.com/avatars/116275390695079945/b21045e778ef21c96d175400e779f0fb.jpg")) .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($"#{_bot.ShardId} / {_creds.TotalShards}").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)) diff --git a/src/NadekoBot/NadekoBot.cs b/src/NadekoBot/NadekoBot.cs index 71a5a504..41bdda06 100644 --- a/src/NadekoBot/NadekoBot.cs +++ b/src/NadekoBot/NadekoBot.cs @@ -49,7 +49,6 @@ namespace NadekoBot public INServiceProvider Services { get; private set; } - public int ShardId { get; } public ShardsCoordinator ShardCoord { get; private set; } private readonly ShardComClient _comClient; @@ -63,8 +62,6 @@ namespace NadekoBot _log = LogManager.GetCurrentClassLogger(); TerribleElevatedPermissionCheck(); - ShardId = shardId; - Credentials = new BotCredentials(); Db = new DbService(Credentials); Client = new DiscordSocketClient(new DiscordSocketConfig @@ -92,7 +89,7 @@ namespace NadekoBot ErrorColor = new Color(Convert.ToUInt32(BotConfig.ErrorColor, 16)); } - SetupShard(shardId, parentProcessId, port.Value); + SetupShard(parentProcessId, port.Value); #if GLOBAL_NADEKO Client.Log += Client_Log; @@ -187,7 +184,7 @@ namespace NadekoBot } //connect - _log.Info("Shard {0} logging in ...", ShardId); + _log.Info("Shard {0} logging in ...", Client.ShardId); await Client.LoginAsync(TokenType.Bot, token).ConfigureAwait(false); await Client.StartAsync().ConfigureAwait(false); Client.Ready += SetClientReady; @@ -195,7 +192,7 @@ namespace NadekoBot Client.Ready -= SetClientReady; Client.JoinedGuild += Client_JoinedGuild; Client.LeftGuild += Client_LeftGuild; - _log.Info("Shard {0} logged in.", ShardId); + _log.Info("Shard {0} logged in.", Client.ShardId); } private Task Client_LeftGuild(SocketGuild arg) @@ -212,18 +209,18 @@ namespace NadekoBot public async Task RunAsync(params string[] args) { - if(ShardId == 0) + if(Client.ShardId == 0) _log.Info("Starting NadekoBot v" + StatsService.BotVersion); var sw = Stopwatch.StartNew(); await LoginAsync(Credentials.Token).ConfigureAwait(false); - _log.Info($"Shard {ShardId} loading services..."); + _log.Info($"Shard {Client.ShardId} loading services..."); AddServices(); sw.Stop(); - _log.Info($"Shard {ShardId} connected in {sw.Elapsed.TotalSeconds:F2}s"); + _log.Info($"Shard {Client.ShardId} connected in {sw.Elapsed.TotalSeconds:F2}s"); var stats = Services.GetService(); stats.Initialize(); @@ -257,7 +254,7 @@ namespace NadekoBot .ForEach(x => CommandService.RemoveModuleAsync(x)); Ready.TrySetResult(true); - _log.Info($"Shard {ShardId} ready."); + _log.Info($"Shard {Client.ShardId} ready."); //_log.Info(await stats.Print().ConfigureAwait(false)); } @@ -297,29 +294,27 @@ namespace NadekoBot } } - private void SetupShard(int shardId, int parentProcessId, int port) + private void SetupShard(int parentProcessId, int port) { - if (shardId != 0) - { - new Thread(new ThreadStart(() => - { - try - { - var p = Process.GetProcessById(parentProcessId); - if (p == null) - return; - p.WaitForExit(); - } - finally - { - Environment.Exit(10); - } - })).Start(); - } - else + if (Client.ShardId == 0) { ShardCoord = new ShardsCoordinator(port); + return; } + new Thread(new ThreadStart(() => + { + try + { + var p = Process.GetProcessById(parentProcessId); + if (p == null) + return; + p.WaitForExit(); + } + finally + { + Environment.Exit(10); + } + })).Start(); } } } diff --git a/src/NadekoBot/Services/Impl/NadekoStrings.cs b/src/NadekoBot/Services/Impl/NadekoStrings.cs index f6273770..651805b8 100644 --- a/src/NadekoBot/Services/Impl/NadekoStrings.cs +++ b/src/NadekoBot/Services/Impl/NadekoStrings.cs @@ -42,9 +42,9 @@ namespace NadekoBot.Services responseStrings = allLangsDict.ToImmutableDictionary(); sw.Stop(); - _log.Info("Loaded {0} languages ({1}) in {2:F2}s", + _log.Info("Loaded {0} languages in {1:F2}s", responseStrings.Count, - string.Join(",", responseStrings.Keys), + //string.Join(",", responseStrings.Keys), sw.Elapsed.TotalSeconds); ////improper string format checks From ac5e4e7540d60de5ef975e5aad2fa0983307feb3 Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Sat, 15 Jul 2017 05:33:23 +0200 Subject: [PATCH 189/346] Version upped --- src/NadekoBot/Services/Impl/StatsService.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/NadekoBot/Services/Impl/StatsService.cs b/src/NadekoBot/Services/Impl/StatsService.cs index 92a68be7..1065cf2a 100644 --- a/src/NadekoBot/Services/Impl/StatsService.cs +++ b/src/NadekoBot/Services/Impl/StatsService.cs @@ -20,7 +20,7 @@ namespace NadekoBot.Services.Impl private readonly IBotCredentials _creds; private readonly DateTime _started; - public const string BotVersion = "1.55.7"; + public const string BotVersion = "1.6"; public string Author => "Kwoth#2560"; public string Library => "Discord.Net"; From 028606b08064668db53454971564d461acda8eac Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Sat, 15 Jul 2017 05:54:36 +0200 Subject: [PATCH 190/346] More cleanup --- .../Commands/UserPunishCommands.cs | 85 +--------------- .../Administration/UserPunishService.cs | 96 +++++++++++++++++++ 2 files changed, 100 insertions(+), 81 deletions(-) create mode 100644 src/NadekoBot/Services/Administration/UserPunishService.cs diff --git a/src/NadekoBot/Modules/Administration/Commands/UserPunishCommands.cs b/src/NadekoBot/Modules/Administration/Commands/UserPunishCommands.cs index 692c8502..363a9222 100644 --- a/src/NadekoBot/Modules/Administration/Commands/UserPunishCommands.cs +++ b/src/NadekoBot/Modules/Administration/Commands/UserPunishCommands.cs @@ -7,8 +7,6 @@ using NadekoBot.Extensions; using NadekoBot.Services; using NadekoBot.Services.Administration; using NadekoBot.Services.Database.Models; -using System; -using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; @@ -20,87 +18,12 @@ namespace NadekoBot.Modules.Administration public class UserPunishCommands : NadekoSubmodule { private readonly DbService _db; - private readonly MuteService _muteService; + private readonly UserPunishService _service; - public UserPunishCommands(DbService db, MuteService muteService) + public UserPunishCommands(UserPunishService service, DbService db, MuteService muteService) { _db = db; - _muteService = muteService; - } - - //todo move to service - private async Task InternalWarn(IGuild guild, ulong userId, string modName, string reason) - { - if (string.IsNullOrWhiteSpace(reason)) - reason = "-"; - - var guildId = guild.Id; - - var warn = new Warning() - { - UserId = userId, - GuildId = guildId, - Forgiven = false, - Reason = reason, - Moderator = modName, - }; - - int warnings = 1; - List ps; - using (var uow = _db.UnitOfWork) - { - ps = uow.GuildConfigs.For(guildId, set => set.Include(x => x.WarnPunishments)) - .WarnPunishments; - - warnings += uow.Warnings - .For(guildId, userId) - .Where(w => !w.Forgiven && w.UserId == userId) - .Count(); - - uow.Warnings.Add(warn); - - uow.Complete(); - } - - var p = ps.FirstOrDefault(x => x.Count == warnings); - - if (p != null) - { - var user = await guild.GetUserAsync(userId); - if (user == null) - return null; - switch (p.Punishment) - { - case PunishmentAction.Mute: - if (p.Time == 0) - await _muteService.MuteUser(user).ConfigureAwait(false); - else - await _muteService.TimedMute(user, TimeSpan.FromMinutes(p.Time)).ConfigureAwait(false); - break; - case PunishmentAction.Kick: - await user.KickAsync().ConfigureAwait(false); - break; - case PunishmentAction.Ban: - await guild.AddBanAsync(user).ConfigureAwait(false); - break; - case PunishmentAction.Softban: - await guild.AddBanAsync(user, 7).ConfigureAwait(false); - try - { - await guild.RemoveBanAsync(user).ConfigureAwait(false); - } - catch - { - await guild.RemoveBanAsync(user).ConfigureAwait(false); - } - break; - default: - break; - } - return p.Punishment; - } - - return null; + _service = service; } [NadekoCommand, Usage, Description, Aliases] @@ -117,7 +40,7 @@ namespace NadekoBot.Modules.Administration .ConfigureAwait(false); } catch { } - var punishment = await InternalWarn(Context.Guild, user.Id, Context.User.ToString(), reason).ConfigureAwait(false); + var punishment = await _service.Warn(Context.Guild, user.Id, Context.User.ToString(), reason).ConfigureAwait(false); if (punishment == null) { diff --git a/src/NadekoBot/Services/Administration/UserPunishService.cs b/src/NadekoBot/Services/Administration/UserPunishService.cs new file mode 100644 index 00000000..84ec319c --- /dev/null +++ b/src/NadekoBot/Services/Administration/UserPunishService.cs @@ -0,0 +1,96 @@ +using Discord; +using Microsoft.EntityFrameworkCore; +using NadekoBot.Services.Database.Models; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace NadekoBot.Services.Administration +{ + public class UserPunishService : INService + { + private readonly MuteService _mute; + private readonly DbService _db; + + public UserPunishService(MuteService mute, DbService db) + { + _mute = mute; + _db = db; + } + + public async Task Warn(IGuild guild, ulong userId, string modName, string reason) + { + if (string.IsNullOrWhiteSpace(reason)) + reason = "-"; + + var guildId = guild.Id; + + var warn = new Warning() + { + UserId = userId, + GuildId = guildId, + Forgiven = false, + Reason = reason, + Moderator = modName, + }; + + int warnings = 1; + List ps; + using (var uow = _db.UnitOfWork) + { + ps = uow.GuildConfigs.For(guildId, set => set.Include(x => x.WarnPunishments)) + .WarnPunishments; + + warnings += uow.Warnings + .For(guildId, userId) + .Where(w => !w.Forgiven && w.UserId == userId) + .Count(); + + uow.Warnings.Add(warn); + + uow.Complete(); + } + + var p = ps.FirstOrDefault(x => x.Count == warnings); + + if (p != null) + { + var user = await guild.GetUserAsync(userId); + if (user == null) + return null; + switch (p.Punishment) + { + case PunishmentAction.Mute: + if (p.Time == 0) + await _mute.MuteUser(user).ConfigureAwait(false); + else + await _mute.TimedMute(user, TimeSpan.FromMinutes(p.Time)).ConfigureAwait(false); + break; + case PunishmentAction.Kick: + await user.KickAsync().ConfigureAwait(false); + break; + case PunishmentAction.Ban: + await guild.AddBanAsync(user).ConfigureAwait(false); + break; + case PunishmentAction.Softban: + await guild.AddBanAsync(user, 7).ConfigureAwait(false); + try + { + await guild.RemoveBanAsync(user).ConfigureAwait(false); + } + catch + { + await guild.RemoveBanAsync(user).ConfigureAwait(false); + } + break; + default: + break; + } + return p.Punishment; + } + + return null; + } + } +} From b3243eb0e912f13df458c7fb8ea49a00a427dfa1 Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Sat, 15 Jul 2017 15:08:34 +0200 Subject: [PATCH 191/346] More cleanup --- .../Commands/ResetPermissionsCommands.cs | 29 +- .../Searches/Commands/AnimeSearchCommands.cs | 17 +- .../Administration/GuildTimezoneService.cs | 2 +- src/NadekoBot/Services/CommandHandler.cs | 2 - src/NadekoBot/Services/Music/Song.cs | 245 ------------ .../Permissions/ResetPermissionsService.cs | 44 +++ src/NadekoBot/_Extensions/Extensions.cs | 356 ++---------------- .../_Extensions/IMessageChannelExtensions.cs | 119 ++++++ src/NadekoBot/_Extensions/IUserExtensions.cs | 43 +++ src/NadekoBot/_Extensions/NumberExtensions.cs | 29 ++ src/NadekoBot/_Extensions/StringExtensions.cs | 136 +++++++ 11 files changed, 412 insertions(+), 610 deletions(-) delete mode 100644 src/NadekoBot/Services/Music/Song.cs create mode 100644 src/NadekoBot/Services/Permissions/ResetPermissionsService.cs create mode 100644 src/NadekoBot/_Extensions/IMessageChannelExtensions.cs create mode 100644 src/NadekoBot/_Extensions/IUserExtensions.cs create mode 100644 src/NadekoBot/_Extensions/NumberExtensions.cs create mode 100644 src/NadekoBot/_Extensions/StringExtensions.cs diff --git a/src/NadekoBot/Modules/Permissions/Commands/ResetPermissionsCommands.cs b/src/NadekoBot/Modules/Permissions/Commands/ResetPermissionsCommands.cs index 049b4782..2e6ea132 100644 --- a/src/NadekoBot/Modules/Permissions/Commands/ResetPermissionsCommands.cs +++ b/src/NadekoBot/Modules/Permissions/Commands/ResetPermissionsCommands.cs @@ -13,15 +13,11 @@ namespace NadekoBot.Modules.Permissions [Group] public class ResetPermissionsCommands : NadekoSubmodule { - private readonly PermissionService _service; - private readonly DbService _db; - private readonly GlobalPermissionService _globalPerms; + private readonly ResetPermissionsService _service; - public ResetPermissionsCommands(PermissionService service, GlobalPermissionService globalPerms, DbService db) + public ResetPermissionsCommands(ResetPermissionsService service) { _service = service; - _db = db; - _globalPerms = globalPerms; } [NadekoCommand, Usage, Description, Aliases] @@ -29,14 +25,7 @@ namespace NadekoBot.Modules.Permissions [RequireUserPermission(GuildPermission.Administrator)] public async Task ResetPermissions() { - //todo 50 move to service - using (var uow = _db.UnitOfWork) - { - var config = uow.GuildConfigs.GcWithPermissionsv2For(Context.Guild.Id); - config.Permissions = Permissionv2.GetDefaultPermlist; - await uow.CompleteAsync(); - _service.UpdateCache(config); - } + await _service.ResetPermissions(Context.Guild.Id).ConfigureAwait(false); await ReplyConfirmLocalized("perms_reset").ConfigureAwait(false); } @@ -44,17 +33,7 @@ namespace NadekoBot.Modules.Permissions [OwnerOnly] public async Task ResetGlobalPermissions() { - //todo 50 move to service - using (var uow = _db.UnitOfWork) - { - var gc = uow.BotConfig.GetOrCreate(); - gc.BlockedCommands.Clear(); - gc.BlockedModules.Clear(); - - _globalPerms.BlockedCommands.Clear(); - _globalPerms.BlockedModules.Clear(); - await uow.CompleteAsync(); - } + await _service.ResetGlobalPermissions().ConfigureAwait(false); await ReplyConfirmLocalized("global_perms_reset").ConfigureAwait(false); } } diff --git a/src/NadekoBot/Modules/Searches/Commands/AnimeSearchCommands.cs b/src/NadekoBot/Modules/Searches/Commands/AnimeSearchCommands.cs index 1c32b091..ff41e0ca 100644 --- a/src/NadekoBot/Modules/Searches/Commands/AnimeSearchCommands.cs +++ b/src/NadekoBot/Modules/Searches/Commands/AnimeSearchCommands.cs @@ -54,16 +54,6 @@ namespace NadekoBot.Modules.Searches return $"[{elem.InnerHtml}]({elem.Href})"; })); - //var favManga = "No favorite manga yet."; - //if (favorites[1].QuerySelector("p") == null) - // favManga = string.Join("\n", favorites[1].QuerySelectorAll("ul > li > div.di-tc.va-t > a") - // .Take(3) - // .Select(x => - // { - // var elem = (IHtmlAnchorElement)x; - // return $"[{elem.InnerHtml}]({elem.Href})"; - // })); - var info = document.QuerySelectorAll("ul.user-status:nth-child(3) > li.clearfix") .Select(x => Tuple.Create(x.Children[0].InnerHtml, x.Children[1].InnerHtml)) .ToList(); @@ -113,7 +103,8 @@ namespace NadekoBot.Modules.Searches await Context.Channel.EmbedAsync(embed).ConfigureAwait(false); } - private static string MalInfoToEmoji(string info) { + private static string MalInfoToEmoji(string info) + { info = info.Trim().ToLowerInvariant(); switch (info) { @@ -156,7 +147,7 @@ namespace NadekoBot.Modules.Searches .WithImageUrl(animeData.image_url_lge) .AddField(efb => efb.WithName(GetText("episodes")).WithValue(animeData.total_episodes.ToString()).WithIsInline(true)) .AddField(efb => efb.WithName(GetText("status")).WithValue(animeData.AiringStatus.ToString()).WithIsInline(true)) - .AddField(efb => efb.WithName(GetText("genres")).WithValue(String.Join(",\n", animeData.Genres)).WithIsInline(true)) + .AddField(efb => efb.WithName(GetText("genres")).WithValue(String.Join(",\n", animeData.Genres.Any() ? animeData.Genres : new[] { "none" })).WithIsInline(true)) .WithFooter(efb => efb.WithText(GetText("score") + " " + animeData.average_score + " / 100")); await Context.Channel.EmbedAsync(embed).ConfigureAwait(false); } @@ -183,7 +174,7 @@ namespace NadekoBot.Modules.Searches .WithImageUrl(mangaData.image_url_lge) .AddField(efb => efb.WithName(GetText("chapters")).WithValue(mangaData.total_chapters.ToString()).WithIsInline(true)) .AddField(efb => efb.WithName(GetText("status")).WithValue(mangaData.publishing_status.ToString()).WithIsInline(true)) - .AddField(efb => efb.WithName(GetText("genres")).WithValue(String.Join(",\n", mangaData.Genres)).WithIsInline(true)) + .AddField(efb => efb.WithName(GetText("genres")).WithValue(String.Join(",\n", mangaData.Genres.Any() ? mangaData.Genres : new[] { "none" })).WithIsInline(true)) .WithFooter(efb => efb.WithText(GetText("score") + " " + mangaData.average_score + " / 100")); await Context.Channel.EmbedAsync(embed).ConfigureAwait(false); diff --git a/src/NadekoBot/Services/Administration/GuildTimezoneService.cs b/src/NadekoBot/Services/Administration/GuildTimezoneService.cs index c15eb534..caa9340d 100644 --- a/src/NadekoBot/Services/Administration/GuildTimezoneService.cs +++ b/src/NadekoBot/Services/Administration/GuildTimezoneService.cs @@ -11,7 +11,7 @@ namespace NadekoBot.Services.Administration { public class GuildTimezoneService : INService { - //hack >.> + // todo 70 this is a hack >.< public static ConcurrentDictionary AllServices { get; } = new ConcurrentDictionary(); private ConcurrentDictionary _timezones; private readonly DbService _db; diff --git a/src/NadekoBot/Services/CommandHandler.cs b/src/NadekoBot/Services/CommandHandler.cs index 81ecb073..12740fa0 100644 --- a/src/NadekoBot/Services/CommandHandler.cs +++ b/src/NadekoBot/Services/CommandHandler.cs @@ -274,8 +274,6 @@ namespace NadekoBot.Services } else if (result.Error != null) { - //todo 80 should have log levels and it should return some kind of result, - // instead of tuple with the type of thing that went wrong, like before LogErroredExecution(result.Error, usrMsg, channel as ITextChannel, exec2, exec3, execTime); if (guild != null) await CommandErrored(result.Info, channel as ITextChannel, result.Error); diff --git a/src/NadekoBot/Services/Music/Song.cs b/src/NadekoBot/Services/Music/Song.cs deleted file mode 100644 index a9567f9c..00000000 --- a/src/NadekoBot/Services/Music/Song.cs +++ /dev/null @@ -1,245 +0,0 @@ -using NadekoBot.Extensions; -using System.Net; -using Discord; -using NadekoBot.Services.Database.Models; -using System; - -namespace NadekoBot.Services.Music -{ - //public class Song - //{ - // public SongInfo SongInfo { get; } - // public MusicPlayer MusicPlayer { get; set; } - - // private string _queuerName; - // public string QueuerName { get{ - // return Format.Sanitize(_queuerName); - // } set { _queuerName = value; } } - - // public TimeSpan TotalTime { get; set; } = TimeSpan.Zero; - // public TimeSpan CurrentTime => TimeSpan.FromSeconds(BytesSent / (float)_frameBytes / (1000 / (float)_milliseconds)); - - // private const int _milliseconds = 20; - // private const int _samplesPerFrame = (48000 / 1000) * _milliseconds; - // private const int _frameBytes = 3840; //16-bit, 2 channels - - // private ulong BytesSent { get; set; } - - // //pwetty - - // public string PrettyProvider => - // $"{(SongInfo.Provider ?? "???")}"; - - // public string PrettyFullTime => PrettyCurrentTime + " / " + PrettyTotalTime; - - // public string PrettyName => $"**[{SongInfo.Title.TrimTo(65)}]({SongUrl})**"; - - // public string PrettyInfo => $"{MusicPlayer.PrettyVolume} | {PrettyTotalTime} | {PrettyProvider} | {QueuerName}"; - - // public string PrettyFullName => $"{PrettyName}\n\t\t`{PrettyTotalTime} | {PrettyProvider} | {QueuerName}`"; - - // public string PrettyCurrentTime { - // get { - // var time = CurrentTime.ToString(@"mm\:ss"); - // var hrs = (int)CurrentTime.TotalHours; - - // if (hrs > 0) - // return hrs + ":" + time; - // else - // return time; - // } - // } - - // public string PrettyTotalTime { - // get - // { - // if (TotalTime == TimeSpan.Zero) - // return "(?)"; - // if (TotalTime == TimeSpan.MaxValue) - // return "∞"; - // var time = TotalTime.ToString(@"mm\:ss"); - // var hrs = (int)TotalTime.TotalHours; - - // if (hrs > 0) - // return hrs + ":" + time; - // return time; - // } - // } - - // public string Thumbnail { - // get { - // switch (SongInfo.ProviderType) - // { - // case MusicType.Radio: - // return "https://cdn.discordapp.com/attachments/155726317222887425/261850925063340032/1482522097_radio.png"; //test links - // case MusicType.Normal: - // //todo 50 have videoid in songinfo from the start - // var videoId = Regex.Match(SongInfo.Query, "<=v=[a-zA-Z0-9-]+(?=&)|(?<=[0-9])[^&\n]+|(?<=v=)[^&\n]+"); - // return $"https://img.youtube.com/vi/{ videoId }/0.jpg"; - // case MusicType.Local: - // return "https://cdn.discordapp.com/attachments/155726317222887425/261850914783100928/1482522077_music.png"; //test links - // case MusicType.Soundcloud: - // return SongInfo.AlbumArt; - // default: - // return ""; - // } - // } - // } - - // public string SongUrl { - // get { - // switch (SongInfo.ProviderType) - // { - // case MusicType.Normal: - // return SongInfo.Query; - // case MusicType.Soundcloud: - // return SongInfo.Query; - // case MusicType.Local: - // return $"https://google.com/search?q={ WebUtility.UrlEncode(SongInfo.Title).Replace(' ', '+') }"; - // case MusicType.Radio: - // return $"https://google.com/search?q={SongInfo.Title}"; - // default: - // return ""; - // } - // } - // } - - // private readonly Logger _log; - - // public Song(SongInfo songInfo) - // { - // SongInfo = songInfo; - // _log = LogManager.GetCurrentClassLogger(); - // } - - // public async Task Play(IAudioClient voiceClient, CancellationToken cancelToken) - // { - // BytesSent = (ulong) SkipTo * 3840 * 50; - // var filename = Path.Combine(MusicService.MusicDataPath, DateTime.UtcNow.UnixTimestamp().ToString()); - - // var inStream = new SongBuffer(MusicPlayer, filename, SongInfo, SkipTo, _frameBytes * 100); - // var bufferTask = inStream.BufferSong(cancelToken).ConfigureAwait(false); - - // try - // { - // var attempt = 0; - - // var prebufferingTask = CheckPrebufferingAsync(inStream, cancelToken, 1.MiB()); //Fast connection can do this easy - // var finished = false; - // var count = 0; - // var sw = new Stopwatch(); - // var slowconnection = false; - // sw.Start(); - // while (!finished) - // { - // var t = await Task.WhenAny(prebufferingTask, Task.Delay(2000, cancelToken)); - // if (t != prebufferingTask) - // { - // count++; - // if (count == 10) - // { - // slowconnection = true; - // prebufferingTask = CheckPrebufferingAsync(inStream, cancelToken, 20.MiB()); - // _log.Warn("Slow connection buffering more to ensure no disruption, consider hosting in cloud"); - // continue; - // } - - // if (inStream.BufferingCompleted && count == 1) - // { - // _log.Debug("Prebuffering canceled. Cannot get any data from the stream."); - // return; - // } - // else - // { - // continue; - // } - // } - // else if (prebufferingTask.IsCanceled) - // { - // _log.Debug("Prebuffering canceled. Cannot get any data from the stream."); - // return; - // } - // finished = true; - // } - // sw.Stop(); - // _log.Debug("Prebuffering successfully completed in " + sw.Elapsed); - - // var outStream = voiceClient.CreatePCMStream(AudioApplication.Music); - - // int nextTime = Environment.TickCount + _milliseconds; - - // byte[] buffer = new byte[_frameBytes]; - // while (!cancelToken.IsCancellationRequested && //song canceled for whatever reason - // !(MusicPlayer.MaxPlaytimeSeconds != 0 && CurrentTime.TotalSeconds >= MusicPlayer.MaxPlaytimeSeconds)) // or exceedded max playtime - // { - // var read = await inStream.ReadAsync(buffer, 0, buffer.Length).ConfigureAwait(false); - // //await inStream.CopyToAsync(voiceClient.OutputStream); - // if (read < _frameBytes) - // _log.Debug("read {0}", read); - // unchecked - // { - // BytesSent += (ulong)read; - // } - // if (read < _frameBytes) - // { - // if (read == 0) - // { - // if (inStream.BufferingCompleted) - // break; - // if (attempt++ == 20) - // { - // MusicPlayer.SongCancelSource.Cancel(); - // break; - // } - // if (slowconnection) - // { - // _log.Warn("Slow connection has disrupted music, waiting a bit for buffer"); - - // await Task.Delay(1000, cancelToken).ConfigureAwait(false); - // nextTime = Environment.TickCount + _milliseconds; - // } - // else - // { - // await Task.Delay(100, cancelToken).ConfigureAwait(false); - // nextTime = Environment.TickCount + _milliseconds; - // } - // } - // else - // attempt = 0; - // } - // else - // attempt = 0; - - // while (MusicPlayer.Paused) - // { - // await Task.Delay(200, cancelToken).ConfigureAwait(false); - // nextTime = Environment.TickCount + _milliseconds; - // } - - - // buffer = AdjustVolume(buffer, MusicPlayer.Volume); - // if (read != _frameBytes) continue; - // nextTime = unchecked(nextTime + _milliseconds); - // int delayMillis = unchecked(nextTime - Environment.TickCount); - // if (delayMillis > 0) - // await Task.Delay(delayMillis, cancelToken).ConfigureAwait(false); - // await outStream.WriteAsync(buffer, 0, read).ConfigureAwait(false); - // } - // } - // finally - // { - // await bufferTask; - // inStream.Dispose(); - // } - // } - - // private async Task CheckPrebufferingAsync(SongBuffer inStream, CancellationToken cancelToken, long size) - // { - // while (!inStream.BufferingCompleted && inStream.Length < size) - // { - // await Task.Delay(100, cancelToken); - // } - // _log.Debug("Buffering successfull"); - // } - //} -} \ No newline at end of file diff --git a/src/NadekoBot/Services/Permissions/ResetPermissionsService.cs b/src/NadekoBot/Services/Permissions/ResetPermissionsService.cs new file mode 100644 index 00000000..7816aab3 --- /dev/null +++ b/src/NadekoBot/Services/Permissions/ResetPermissionsService.cs @@ -0,0 +1,44 @@ +using NadekoBot.Services.Database.Models; +using System.Threading.Tasks; + +namespace NadekoBot.Services.Permissions +{ + public class ResetPermissionsService : INService + { + private readonly PermissionService _perms; + private readonly GlobalPermissionService _globalPerms; + private readonly DbService _db; + + public ResetPermissionsService(PermissionService perms, GlobalPermissionService globalPerms, DbService db) + { + _perms = perms; + _globalPerms = globalPerms; + _db = db; + } + + public async Task ResetPermissions(ulong guildId) + { + using (var uow = _db.UnitOfWork) + { + var config = uow.GuildConfigs.GcWithPermissionsv2For(guildId); + config.Permissions = Permissionv2.GetDefaultPermlist; + await uow.CompleteAsync().ConfigureAwait(false); + _perms.UpdateCache(config); + } + } + + public async Task ResetGlobalPermissions() + { + using (var uow = _db.UnitOfWork) + { + var gc = uow.BotConfig.GetOrCreate(); + gc.BlockedCommands.Clear(); + gc.BlockedModules.Clear(); + + _globalPerms.BlockedCommands.Clear(); + _globalPerms.BlockedModules.Clear(); + await uow.CompleteAsync().ConfigureAwait(false); + } + } + } +} diff --git a/src/NadekoBot/_Extensions/Extensions.cs b/src/NadekoBot/_Extensions/Extensions.cs index 5622b936..e2d26adc 100644 --- a/src/NadekoBot/_Extensions/Extensions.cs +++ b/src/NadekoBot/_Extensions/Extensions.cs @@ -48,86 +48,10 @@ namespace NadekoBot.Extensions public static bool IsAuthor(this IMessage msg, IDiscordClient client) => msg.Author?.Id == client.CurrentUser.Id; - private static readonly IEmote arrow_left = new Emoji("⬅"); - private static readonly IEmote arrow_right = new Emoji("➡"); - - public static string ToBase64(this string plainText) - { - var plainTextBytes = System.Text.Encoding.UTF8.GetBytes(plainText); - return Convert.ToBase64String(plainTextBytes); - } - public static string RealSummary(this CommandInfo cmd, string prefix) => string.Format(cmd.Summary, prefix); public static string RealRemarks(this CommandInfo cmd, string prefix) => string.Format(cmd.Remarks, prefix); - public static Stream ToStream(this IEnumerable bytes, bool canWrite = false) - { - var ms = new MemoryStream(bytes as byte[] ?? bytes.ToArray(), canWrite); - ms.Seek(0, SeekOrigin.Begin); - return ms; - } - public static Task SendPaginatedConfirmAsync(this IMessageChannel channel, DiscordSocketClient client, int currentPage, Func pageFunc, int? lastPage = null, bool addPaginatedFooter = true) => - channel.SendPaginatedConfirmAsync(client, currentPage, (x) => Task.FromResult(pageFunc(x)), lastPage, addPaginatedFooter); - /// - /// danny kamisama - /// - public static async Task SendPaginatedConfirmAsync(this IMessageChannel channel, DiscordSocketClient client, int currentPage, Func> pageFunc, int? lastPage = null, bool addPaginatedFooter = true) - { - var embed = await pageFunc(currentPage).ConfigureAwait(false); - - if (addPaginatedFooter) - embed.AddPaginatedFooter(currentPage, lastPage); - - var msg = await channel.EmbedAsync(embed) as IUserMessage; - - if (lastPage == 0) - return; - - - await msg.AddReactionAsync(arrow_left).ConfigureAwait(false); - await msg.AddReactionAsync(arrow_right).ConfigureAwait(false); - - await Task.Delay(2000).ConfigureAwait(false); - - Action changePage = async r => - { - try - { - if (r.Emote.Name == arrow_left.Name) - { - if (currentPage == 0) - return; - var toSend = await pageFunc(--currentPage).ConfigureAwait(false); - if (addPaginatedFooter) - toSend.AddPaginatedFooter(currentPage, lastPage); - await msg.ModifyAsync(x => x.Embed = toSend.Build()).ConfigureAwait(false); - } - else if (r.Emote.Name == arrow_right.Name) - { - if (lastPage == null || lastPage > currentPage) - { - var toSend = await pageFunc(++currentPage).ConfigureAwait(false); - if (addPaginatedFooter) - toSend.AddPaginatedFooter(currentPage, lastPage); - await msg.ModifyAsync(x => x.Embed = toSend.Build()).ConfigureAwait(false); - } - } - } - catch (Exception) - { - //ignored - } - }; - - using (msg.OnReaction(client, changePage, changePage)) - { - await Task.Delay(30000).ConfigureAwait(false); - } - - await msg.RemoveAllReactionsAsync().ConfigureAwait(false); - } - - private static EmbedBuilder AddPaginatedFooter(this EmbedBuilder embed, int curPage, int? lastPage) + public static EmbedBuilder AddPaginatedFooter(this EmbedBuilder embed, int curPage, int? lastPage) { if (lastPage != null) return embed.WithFooter(efb => efb.WithText($"{curPage + 1} / {lastPage + 1}")); @@ -135,6 +59,12 @@ namespace NadekoBot.Extensions return embed.WithFooter(efb => efb.WithText(curPage.ToString())); } + public static EmbedBuilder WithOkColor(this EmbedBuilder eb) => + eb.WithColor(NadekoBot.OkColor); + + public static EmbedBuilder WithErrorColor(this EmbedBuilder eb) => + eb.WithColor(NadekoBot.ErrorColor); + public static ReactionEventWrapper OnReaction(this IUserMessage msg, DiscordSocketClient client, Action reactionAdded, Action reactionRemoved = null) { if (reactionRemoved == null) @@ -153,17 +83,6 @@ namespace NadekoBot.Extensions http.DefaultRequestHeaders.Add("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"); } - public static string GetInitials(this string txt, string glue = "") => - string.Join(glue, txt.Split(' ').Select(x => x.FirstOrDefault())); - - public static DateTime ToUnixTimestamp(this double number) => new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc).AddSeconds(number); - - public static EmbedBuilder WithOkColor(this EmbedBuilder eb) => - eb.WithColor(NadekoBot.OkColor); - - public static EmbedBuilder WithErrorColor(this EmbedBuilder eb) => - eb.WithColor(NadekoBot.ErrorColor); - public static IMessage DeleteAfter(this IUserMessage msg, int seconds) { Task.Run(async () => @@ -184,29 +103,9 @@ namespace NadekoBot.Extensions return module; } - public static async Task SendMessageToOwnerAsync(this IGuild guild, string message) - { - var ownerPrivate = await (await guild.GetOwnerAsync().ConfigureAwait(false)).GetOrCreateDMChannelAsync() - .ConfigureAwait(false); - - return await ownerPrivate.SendMessageAsync(message).ConfigureAwait(false); - } - //public static async Task> MentionedUsers(this IUserMessage msg) => - public static IEnumerable GetRoles(this IGuildUser user) => - user.RoleIds.Select(r => user.Guild.GetRole(r)).Where(r => r != null); - - public static IEnumerable ForEach(this IEnumerable elems, Action exec) - { - foreach (var elem in elems) - { - exec(elem); - } - return elems; - } - public static void AddRange(this HashSet target, IEnumerable elements) where T : class { foreach (var item in elements) @@ -223,90 +122,22 @@ namespace NadekoBot.Extensions } } - public static bool IsInteger(this decimal number) => number == Math.Truncate(number); - - public static string SanitizeMentions(this string str) => - str.Replace("@everyone", "@everyοne").Replace("@here", "@һere"); - public static double UnixTimestamp(this DateTime dt) => dt.ToUniversalTime().Subtract(new DateTime(1970, 1, 1, 0, 0, 0)).TotalSeconds; - //public static async Task SendMessageAsync(this IUser user, string message, bool isTTS = false) => - // await (await user.CreateDMChannelAsync().ConfigureAwait(false)).SendMessageAsync(message, isTTS).ConfigureAwait(false); - - public static async Task SendConfirmAsync(this IUser user, string text) - => await (await user.GetOrCreateDMChannelAsync()).SendMessageAsync("", embed: new EmbedBuilder().WithOkColor().WithDescription(text)); - - public static async Task SendConfirmAsync(this IUser user, string title, string text, string url = null) - { - var eb = new EmbedBuilder().WithOkColor().WithDescription(text); - if (url != null && Uri.IsWellFormedUriString(url, UriKind.Absolute)) - eb.WithUrl(url); - return await (await user.GetOrCreateDMChannelAsync()).SendMessageAsync("", embed: eb); - } - - public static async Task SendErrorAsync(this IUser user, string title, string error, string url = null) - { - var eb = new EmbedBuilder().WithErrorColor().WithDescription(error); - if (url != null && Uri.IsWellFormedUriString(url, UriKind.Absolute)) - eb.WithUrl(url); - return await (await user.GetOrCreateDMChannelAsync()).SendMessageAsync("", embed: eb); - } - - public static async Task SendErrorAsync(this IUser user, string error) - => await (await user.GetOrCreateDMChannelAsync()).SendMessageAsync("", embed: new EmbedBuilder().WithErrorColor().WithDescription(error)); - - public static async Task SendFileAsync(this IUser user, string filePath, string caption = null, string text = null, bool isTTS = false) => - await (await user.GetOrCreateDMChannelAsync().ConfigureAwait(false)).SendFileAsync(File.Open(filePath, FileMode.Open), caption ?? "x", text, isTTS).ConfigureAwait(false); - - public static async Task SendFileAsync(this IUser user, Stream fileStream, string fileName, string caption = null, bool isTTS = false) => - await (await user.GetOrCreateDMChannelAsync().ConfigureAwait(false)).SendFileAsync(fileStream, fileName, caption, isTTS).ConfigureAwait(false); - public static IEnumerable Members(this IRole role) => role.Guild.GetUsersAsync().GetAwaiter().GetResult().Where(u => u.RoleIds.Contains(role.Id)) ?? Enumerable.Empty(); + + public static string ToJson(this T any, Formatting formatting = Formatting.Indented) => + JsonConvert.SerializeObject(any, formatting); - public static Task EmbedAsync(this IMessageChannel ch, EmbedBuilder embed, string msg = "") - => ch.SendMessageAsync(msg, embed: embed); - - public static Task SendErrorAsync(this IMessageChannel ch, string title, string error, string url = null, string footer = null) + public static Stream ToStream(this ImageSharp.Image img) { - var eb = new EmbedBuilder().WithErrorColor().WithDescription(error) - .WithTitle(title); - if (url != null && Uri.IsWellFormedUriString(url, UriKind.Absolute)) - eb.WithUrl(url); - if (!string.IsNullOrWhiteSpace(footer)) - eb.WithFooter(efb => efb.WithText(footer)); - return ch.SendMessageAsync("", embed: eb); + var imageStream = new MemoryStream(); + img.Save(imageStream); + imageStream.Position = 0; + return imageStream; } - public static Task SendErrorAsync(this IMessageChannel ch, string error) - => ch.SendMessageAsync("", embed: new EmbedBuilder().WithErrorColor().WithDescription(error)); - - public static Task SendConfirmAsync(this IMessageChannel ch, string title, string text, string url = null, string footer = null) - { - var eb = new EmbedBuilder().WithOkColor().WithDescription(text) - .WithTitle(title); - if (url != null && Uri.IsWellFormedUriString(url, UriKind.Absolute)) - eb.WithUrl(url); - if (!string.IsNullOrWhiteSpace(footer)) - eb.WithFooter(efb => efb.WithText(footer)); - return ch.SendMessageAsync("", embed: eb); - } - - public static Task SendConfirmAsync(this IMessageChannel ch, string text) - => ch.SendMessageAsync("", embed: new EmbedBuilder().WithOkColor().WithDescription(text)); - - public static Task SendTableAsync(this IMessageChannel ch, string seed, IEnumerable items, Func howToPrint, int columns = 3) - { - var i = 0; - return ch.SendMessageAsync($@"{seed}```css -{string.Join("\n", items.GroupBy(item => (i++) / columns) - .Select(ig => string.Concat(ig.Select(el => howToPrint(el)))))} -```"); - } - - public static Task SendTableAsync(this IMessageChannel ch, IEnumerable items, Func howToPrint, int columns = 3) => - ch.SendTableAsync("", items, howToPrint, columns); - /// /// returns an IEnumerable with randomized element order /// @@ -339,135 +170,32 @@ namespace NadekoBot.Extensions } } - /// - /// Easy use of fast, efficient case-insensitive Contains check with StringComparison Member Types - /// CurrentCulture, CurrentCultureIgnoreCase, InvariantCulture, InvariantCultureIgnoreCase, Ordinal, OrdinalIgnoreCase - /// - public static bool ContainsNoCase(this string str, string contains, StringComparison compare) + public static IEnumerable ForEach(this IEnumerable elems, Action exec) { - return str.IndexOf(contains, compare) >= 0; + foreach (var elem in elems) + { + exec(elem); + } + return elems; } - public static string TrimTo(this string str, int maxLength, bool hideDots = false) + public static Stream ToStream(this IEnumerable bytes, bool canWrite = false) { - if (maxLength < 0) - throw new ArgumentOutOfRangeException(nameof(maxLength), $"Argument {nameof(maxLength)} can't be negative."); - if (maxLength == 0) - return string.Empty; - if (maxLength <= 3) - return string.Concat(str.Select(c => '.')); - if (str.Length < maxLength) - return str; - return string.Concat(str.Take(maxLength - 3)) + (hideDots ? "" : "..."); - } - - public static string ToTitleCase(this string str) - { - var tokens = str.Split(new[] { " " }, StringSplitOptions.RemoveEmptyEntries); - for (var i = 0; i < tokens.Length; i++) - { - var token = tokens[i]; - tokens[i] = token.Substring(0, 1).ToUpper() + token.Substring(1); - } - - return string.Join(" ", tokens); - } - - /// - /// Removes trailing S or ES (if specified) on the given string if the num is 1 - /// - /// - /// - /// - /// String with the correct singular/plural form - public static string SnPl(this string str, int? num, bool es = false) - { - if (str == null) - throw new ArgumentNullException(nameof(str)); - if (num == null) - throw new ArgumentNullException(nameof(num)); - return num == 1 ? str.Remove(str.Length - 1, es ? 2 : 1) : str; - } - - //http://www.dotnetperls.com/levenshtein - public static int LevenshteinDistance(this string s, string t) - { - var n = s.Length; - var m = t.Length; - var d = new int[n + 1, m + 1]; - - // Step 1 - if (n == 0) - { - return m; - } - - if (m == 0) - { - return n; - } - - // Step 2 - for (var i = 0; i <= n; d[i, 0] = i++) - { - } - - for (var j = 0; j <= m; d[0, j] = j++) - { - } - - // Step 3 - for (var i = 1; i <= n; i++) - { - //Step 4 - for (var j = 1; j <= m; j++) - { - // Step 5 - var cost = (t[j - 1] == s[i - 1]) ? 0 : 1; - - // Step 6 - d[i, j] = Math.Min( - Math.Min(d[i - 1, j] + 1, d[i, j - 1] + 1), - d[i - 1, j - 1] + cost); - } - } - // Step 7 - return d[n, m]; - } - - public static async Task ToStream(this string str) - { - var ms = new MemoryStream(); - var sw = new StreamWriter(ms); - await sw.WriteAsync(str); - await sw.FlushAsync(); - ms.Position = 0; + var ms = new MemoryStream(bytes as byte[] ?? bytes.ToArray(), canWrite); + ms.Seek(0, SeekOrigin.Begin); return ms; - } - public static string ToJson(this T any, Formatting formatting = Formatting.Indented) => - JsonConvert.SerializeObject(any, formatting); + public static IEnumerable GetRoles(this IGuildUser user) => + user.RoleIds.Select(r => user.Guild.GetRole(r)).Where(r => r != null); - public static int KiB(this int value) => value * 1024; - public static int KB(this int value) => value * 1000; + public static async Task SendMessageToOwnerAsync(this IGuild guild, string message) + { + var ownerPrivate = await (await guild.GetOwnerAsync().ConfigureAwait(false)).GetOrCreateDMChannelAsync() + .ConfigureAwait(false); - public static int MiB(this int value) => value.KiB() * 1024; - public static int MB(this int value) => value.KB() * 1000; - - public static int GiB(this int value) => value.MiB() * 1024; - public static int GB(this int value) => value.MB() * 1000; - - public static ulong KiB(this ulong value) => value * 1024; - public static ulong KB(this ulong value) => value * 1000; - - public static ulong MiB(this ulong value) => value.KiB() * 1024; - public static ulong MB(this ulong value) => value.KB() * 1000; - - public static ulong GiB(this ulong value) => value.MiB() * 1024; - public static ulong GB(this ulong value) => value.MB() * 1000; - - public static string Unmention(this string str) => str.Replace("@", "ම"); + return await ownerPrivate.SendMessageAsync(message).ConfigureAwait(false); + } public static ImageSharp.Image Merge(this IEnumerable images) { @@ -484,25 +212,5 @@ namespace NadekoBot.Extensions return canvas; } - - public static Stream ToStream(this ImageSharp.Image img) - { - var imageStream = new MemoryStream(); - img.Save(imageStream); - imageStream.Position = 0; - return imageStream; - } - - private static readonly Regex filterRegex = new Regex(@"(?:discord(?:\.gg|.me|app\.com\/invite)\/(?([\w]{16}|(?:[\w]+-?){3})))", RegexOptions.Compiled | RegexOptions.IgnoreCase); - - public static bool IsDiscordInvite(this string str) - => filterRegex.IsMatch(str); - - public static string RealAvatarUrl(this IUser usr) - { - return usr.AvatarId.StartsWith("a_") - ? $"{DiscordConfig.CDNUrl}avatars/{usr.Id}/{usr.AvatarId}.gif" - : usr.GetAvatarUrl(ImageFormat.Auto); - } } } \ No newline at end of file diff --git a/src/NadekoBot/_Extensions/IMessageChannelExtensions.cs b/src/NadekoBot/_Extensions/IMessageChannelExtensions.cs new file mode 100644 index 00000000..806ec904 --- /dev/null +++ b/src/NadekoBot/_Extensions/IMessageChannelExtensions.cs @@ -0,0 +1,119 @@ +using Discord; +using Discord.WebSocket; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace NadekoBot.Extensions +{ + public static class IMessageChannelExtensions + { + public static Task EmbedAsync(this IMessageChannel ch, EmbedBuilder embed, string msg = "") + => ch.SendMessageAsync(msg, embed: embed); + + public static Task SendErrorAsync(this IMessageChannel ch, string title, string error, string url = null, string footer = null) + { + var eb = new EmbedBuilder().WithErrorColor().WithDescription(error) + .WithTitle(title); + if (url != null && Uri.IsWellFormedUriString(url, UriKind.Absolute)) + eb.WithUrl(url); + if (!string.IsNullOrWhiteSpace(footer)) + eb.WithFooter(efb => efb.WithText(footer)); + return ch.SendMessageAsync("", embed: eb); + } + + public static Task SendErrorAsync(this IMessageChannel ch, string error) + => ch.SendMessageAsync("", embed: new EmbedBuilder().WithErrorColor().WithDescription(error)); + + public static Task SendConfirmAsync(this IMessageChannel ch, string title, string text, string url = null, string footer = null) + { + var eb = new EmbedBuilder().WithOkColor().WithDescription(text) + .WithTitle(title); + if (url != null && Uri.IsWellFormedUriString(url, UriKind.Absolute)) + eb.WithUrl(url); + if (!string.IsNullOrWhiteSpace(footer)) + eb.WithFooter(efb => efb.WithText(footer)); + return ch.SendMessageAsync("", embed: eb); + } + + public static Task SendConfirmAsync(this IMessageChannel ch, string text) + => ch.SendMessageAsync("", embed: new EmbedBuilder().WithOkColor().WithDescription(text)); + + public static Task SendTableAsync(this IMessageChannel ch, string seed, IEnumerable items, Func howToPrint, int columns = 3) + { + var i = 0; + return ch.SendMessageAsync($@"{seed}```css +{string.Join("\n", items.GroupBy(item => (i++) / columns) + .Select(ig => string.Concat(ig.Select(el => howToPrint(el)))))} +```"); + } + + public static Task SendTableAsync(this IMessageChannel ch, IEnumerable items, Func howToPrint, int columns = 3) => + ch.SendTableAsync("", items, howToPrint, columns); + + private static readonly IEmote arrow_left = new Emoji("⬅"); + private static readonly IEmote arrow_right = new Emoji("➡"); + + public static Task SendPaginatedConfirmAsync(this IMessageChannel channel, DiscordSocketClient client, int currentPage, Func pageFunc, int? lastPage = null, bool addPaginatedFooter = true) => + channel.SendPaginatedConfirmAsync(client, currentPage, (x) => Task.FromResult(pageFunc(x)), lastPage, addPaginatedFooter); + /// + /// danny kamisama + /// + public static async Task SendPaginatedConfirmAsync(this IMessageChannel channel, DiscordSocketClient client, int currentPage, Func> pageFunc, int? lastPage = null, bool addPaginatedFooter = true) + { + var embed = await pageFunc(currentPage).ConfigureAwait(false); + + if (addPaginatedFooter) + embed.AddPaginatedFooter(currentPage, lastPage); + + var msg = await channel.EmbedAsync(embed) as IUserMessage; + + if (lastPage == 0) + return; + + + await msg.AddReactionAsync(arrow_left).ConfigureAwait(false); + await msg.AddReactionAsync(arrow_right).ConfigureAwait(false); + + await Task.Delay(2000).ConfigureAwait(false); + + Action changePage = async r => + { + try + { + if (r.Emote.Name == arrow_left.Name) + { + if (currentPage == 0) + return; + var toSend = await pageFunc(--currentPage).ConfigureAwait(false); + if (addPaginatedFooter) + toSend.AddPaginatedFooter(currentPage, lastPage); + await msg.ModifyAsync(x => x.Embed = toSend.Build()).ConfigureAwait(false); + } + else if (r.Emote.Name == arrow_right.Name) + { + if (lastPage == null || lastPage > currentPage) + { + var toSend = await pageFunc(++currentPage).ConfigureAwait(false); + if (addPaginatedFooter) + toSend.AddPaginatedFooter(currentPage, lastPage); + await msg.ModifyAsync(x => x.Embed = toSend.Build()).ConfigureAwait(false); + } + } + } + catch (Exception) + { + //ignored + } + }; + + using (msg.OnReaction(client, changePage, changePage)) + { + await Task.Delay(30000).ConfigureAwait(false); + } + + await msg.RemoveAllReactionsAsync().ConfigureAwait(false); + } + } +} diff --git a/src/NadekoBot/_Extensions/IUserExtensions.cs b/src/NadekoBot/_Extensions/IUserExtensions.cs new file mode 100644 index 00000000..d1183e19 --- /dev/null +++ b/src/NadekoBot/_Extensions/IUserExtensions.cs @@ -0,0 +1,43 @@ +using Discord; +using System; +using System.IO; +using System.Threading.Tasks; + +namespace NadekoBot.Extensions +{ + public static class IUserExtensions + { + public static async Task SendConfirmAsync(this IUser user, string text) + => await (await user.GetOrCreateDMChannelAsync()).SendMessageAsync("", embed: new EmbedBuilder().WithOkColor().WithDescription(text)); + + public static async Task SendConfirmAsync(this IUser user, string title, string text, string url = null) + { + var eb = new EmbedBuilder().WithOkColor().WithDescription(text); + if (url != null && Uri.IsWellFormedUriString(url, UriKind.Absolute)) + eb.WithUrl(url); + return await (await user.GetOrCreateDMChannelAsync()).SendMessageAsync("", embed: eb); + } + + public static async Task SendErrorAsync(this IUser user, string title, string error, string url = null) + { + var eb = new EmbedBuilder().WithErrorColor().WithDescription(error); + if (url != null && Uri.IsWellFormedUriString(url, UriKind.Absolute)) + eb.WithUrl(url); + return await (await user.GetOrCreateDMChannelAsync()).SendMessageAsync("", embed: eb); + } + + public static async Task SendErrorAsync(this IUser user, string error) + => await (await user.GetOrCreateDMChannelAsync()).SendMessageAsync("", embed: new EmbedBuilder().WithErrorColor().WithDescription(error)); + + public static async Task SendFileAsync(this IUser user, string filePath, string caption = null, string text = null, bool isTTS = false) => + await (await user.GetOrCreateDMChannelAsync().ConfigureAwait(false)).SendFileAsync(File.Open(filePath, FileMode.Open), caption ?? "x", text, isTTS).ConfigureAwait(false); + + public static async Task SendFileAsync(this IUser user, Stream fileStream, string fileName, string caption = null, bool isTTS = false) => + await (await user.GetOrCreateDMChannelAsync().ConfigureAwait(false)).SendFileAsync(fileStream, fileName, caption, isTTS).ConfigureAwait(false); + + public static string RealAvatarUrl(this IUser usr) => + usr.AvatarId.StartsWith("a_") + ? $"{DiscordConfig.CDNUrl}avatars/{usr.Id}/{usr.AvatarId}.gif" + : usr.GetAvatarUrl(ImageFormat.Auto); + } +} diff --git a/src/NadekoBot/_Extensions/NumberExtensions.cs b/src/NadekoBot/_Extensions/NumberExtensions.cs new file mode 100644 index 00000000..b708841f --- /dev/null +++ b/src/NadekoBot/_Extensions/NumberExtensions.cs @@ -0,0 +1,29 @@ +using System; + +namespace NadekoBot.Extensions +{ + public static class NumberExtensions + { + public static int KiB(this int value) => value * 1024; + public static int KB(this int value) => value * 1000; + + public static int MiB(this int value) => value.KiB() * 1024; + public static int MB(this int value) => value.KB() * 1000; + + public static int GiB(this int value) => value.MiB() * 1024; + public static int GB(this int value) => value.MB() * 1000; + + public static ulong KiB(this ulong value) => value * 1024; + public static ulong KB(this ulong value) => value * 1000; + + public static ulong MiB(this ulong value) => value.KiB() * 1024; + public static ulong MB(this ulong value) => value.KB() * 1000; + + public static ulong GiB(this ulong value) => value.MiB() * 1024; + public static ulong GB(this ulong value) => value.MB() * 1000; + + public static bool IsInteger(this decimal number) => number == Math.Truncate(number); + + public static DateTime ToUnixTimestamp(this double number) => new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc).AddSeconds(number); + } +} diff --git a/src/NadekoBot/_Extensions/StringExtensions.cs b/src/NadekoBot/_Extensions/StringExtensions.cs new file mode 100644 index 00000000..fb083882 --- /dev/null +++ b/src/NadekoBot/_Extensions/StringExtensions.cs @@ -0,0 +1,136 @@ +using System; +using System.IO; +using System.Linq; +using System.Text; +using System.Text.RegularExpressions; +using System.Threading.Tasks; + +namespace NadekoBot.Extensions +{ + public static class StringExtensions + { + /// + /// Easy use of fast, efficient case-insensitive Contains check with StringComparison Member Types + /// CurrentCulture, CurrentCultureIgnoreCase, InvariantCulture, InvariantCultureIgnoreCase, Ordinal, OrdinalIgnoreCase + /// + public static bool ContainsNoCase(this string str, string contains, StringComparison compare) + { + return str.IndexOf(contains, compare) >= 0; + } + + public static string TrimTo(this string str, int maxLength, bool hideDots = false) + { + if (maxLength < 0) + throw new ArgumentOutOfRangeException(nameof(maxLength), $"Argument {nameof(maxLength)} can't be negative."); + if (maxLength == 0) + return string.Empty; + if (maxLength <= 3) + return string.Concat(str.Select(c => '.')); + if (str.Length < maxLength) + return str; + return string.Concat(str.Take(maxLength - 3)) + (hideDots ? "" : "..."); + } + + public static string ToTitleCase(this string str) + { + var tokens = str.Split(new[] { " " }, StringSplitOptions.RemoveEmptyEntries); + for (var i = 0; i < tokens.Length; i++) + { + var token = tokens[i]; + tokens[i] = token.Substring(0, 1).ToUpper() + token.Substring(1); + } + + return string.Join(" ", tokens); + } + + /// + /// Removes trailing S or ES (if specified) on the given string if the num is 1 + /// + /// + /// + /// + /// String with the correct singular/plural form + public static string SnPl(this string str, int? num, bool es = false) + { + if (str == null) + throw new ArgumentNullException(nameof(str)); + if (num == null) + throw new ArgumentNullException(nameof(num)); + return num == 1 ? str.Remove(str.Length - 1, es ? 2 : 1) : str; + } + + //http://www.dotnetperls.com/levenshtein + public static int LevenshteinDistance(this string s, string t) + { + var n = s.Length; + var m = t.Length; + var d = new int[n + 1, m + 1]; + + // Step 1 + if (n == 0) + { + return m; + } + + if (m == 0) + { + return n; + } + + // Step 2 + for (var i = 0; i <= n; d[i, 0] = i++) + { + } + + for (var j = 0; j <= m; d[0, j] = j++) + { + } + + // Step 3 + for (var i = 1; i <= n; i++) + { + //Step 4 + for (var j = 1; j <= m; j++) + { + // Step 5 + var cost = (t[j - 1] == s[i - 1]) ? 0 : 1; + + // Step 6 + d[i, j] = Math.Min( + Math.Min(d[i - 1, j] + 1, d[i, j - 1] + 1), + d[i - 1, j - 1] + cost); + } + } + // Step 7 + return d[n, m]; + } + + public static async Task ToStream(this string str) + { + var ms = new MemoryStream(); + var sw = new StreamWriter(ms); + await sw.WriteAsync(str); + await sw.FlushAsync(); + ms.Position = 0; + return ms; + } + + private static readonly Regex filterRegex = new Regex(@"(?:discord(?:\.gg|.me|app\.com\/invite)\/(?([\w]{16}|(?:[\w]+-?){3})))", RegexOptions.Compiled | RegexOptions.IgnoreCase); + public static bool IsDiscordInvite(this string str) + => filterRegex.IsMatch(str); + + public static string Unmention(this string str) => str.Replace("@", "ම"); + + public static string SanitizeMentions(this string str) => + str.Replace("@everyone", "@everyοne").Replace("@here", "@һere"); + + public static string ToBase64(this string plainText) + { + var plainTextBytes = Encoding.UTF8.GetBytes(plainText); + return Convert.ToBase64String(plainTextBytes); + } + + public static string GetInitials(this string txt, string glue = "") => + string.Join(glue, txt.Split(' ').Select(x => x.FirstOrDefault())); + } +} From 4e11a6c8bc7424d31ad3c6fc7e1feb63a0472577 Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Sat, 15 Jul 2017 18:34:34 +0200 Subject: [PATCH 192/346] More refactoring --- .../Modules/Administration/Administration.cs | 10 +-- .../Commands/AutoAssignRoleCommands.cs | 6 +- .../Commands/GameChannelCommands.cs | 6 +- .../Administration/Commands/LogCommand.cs | 12 ++- .../Administration/Commands/MuteCommands.cs | 6 +- .../Commands/PlayingRotateCommands.cs | 6 +- .../Commands/ProtectionCommands.cs | 6 +- .../Administration/Commands/PruneCommands.cs | 17 +--- .../Commands/RatelimitCommand.cs | 6 +- .../Administration/Commands/SelfCommands.cs | 7 +- .../Commands/ServerGreetCommands.cs | 22 +++-- .../Commands/TimeZoneCommands.cs | 9 +-- .../Commands/UserPunishCommands.cs | 6 +- .../Administration/Commands/VcRoleCommands.cs | 6 +- .../Commands/VoicePlusTextCommands.cs | 6 +- .../CustomReactions/CustomReactions.cs | 46 +++++------ .../Games/Commands/CleverBotCommands.cs | 10 +-- .../Modules/Games/Commands/PollCommands.cs | 12 ++- src/NadekoBot/Modules/Games/Games.cs | 10 +-- src/NadekoBot/Modules/Help/Help.cs | 10 +-- src/NadekoBot/Modules/Music/Music.cs | 80 +++++++++---------- src/NadekoBot/Modules/NSFW/NSFW.cs | 9 +-- src/NadekoBot/Modules/NadekoModule.cs | 14 ++++ .../Modules/Permissions/Permissions.cs | 6 +- src/NadekoBot/Modules/Pokemon/Pokemon.cs | 6 +- .../Searches/Commands/AnimeSearchCommands.cs | 9 +-- .../Modules/Searches/Commands/JokeCommands.cs | 16 ++-- .../Commands/PokemonSearchCommands.cs | 13 +-- .../Commands/StreamNotificationCommands.cs | 6 +- src/NadekoBot/Modules/Searches/Searches.cs | 5 +- .../Utility/Commands/CommandMapCommands.cs | 6 +- .../Utility/Commands/PatreonCommands.cs | 12 ++- .../Modules/Utility/Commands/Remind.cs | 6 +- .../Utility/Commands/RepeatCommands.cs | 6 +- .../Utility/Commands/StreamRoleCommands.cs | 13 +-- .../Utility/Commands/UnitConversion.cs | 9 +-- .../Utility/Commands/VerboseCommandErrors.cs | 11 +-- src/NadekoBot/NadekoBot.csproj | 7 +- src/NadekoBot/Services/ServiceProvider.cs | 5 -- 39 files changed, 174 insertions(+), 284 deletions(-) diff --git a/src/NadekoBot/Modules/Administration/Administration.cs b/src/NadekoBot/Modules/Administration/Administration.cs index fafd9f48..e30d1664 100644 --- a/src/NadekoBot/Modules/Administration/Administration.cs +++ b/src/NadekoBot/Modules/Administration/Administration.cs @@ -12,16 +12,14 @@ using NadekoBot.Services.Administration; namespace NadekoBot.Modules.Administration { - public partial class Administration : NadekoTopLevelModule + public partial class Administration : NadekoTopLevelModule { private IGuild _nadekoSupportServer; private readonly DbService _db; - private readonly AdministrationService _admin; - public Administration(DbService db, AdministrationService admin) + public Administration(DbService db) { _db = db; - _admin = admin; } [NadekoCommand, Usage, Description, Aliases] @@ -40,12 +38,12 @@ namespace NadekoBot.Modules.Administration } if (enabled) { - _admin.DeleteMessagesOnCommand.Add(Context.Guild.Id); + _service.DeleteMessagesOnCommand.Add(Context.Guild.Id); await ReplyConfirmLocalized("delmsg_on").ConfigureAwait(false); } else { - _admin.DeleteMessagesOnCommand.TryRemove(Context.Guild.Id); + _service.DeleteMessagesOnCommand.TryRemove(Context.Guild.Id); await ReplyConfirmLocalized("delmsg_off").ConfigureAwait(false); } } diff --git a/src/NadekoBot/Modules/Administration/Commands/AutoAssignRoleCommands.cs b/src/NadekoBot/Modules/Administration/Commands/AutoAssignRoleCommands.cs index b7930e66..18a1d367 100644 --- a/src/NadekoBot/Modules/Administration/Commands/AutoAssignRoleCommands.cs +++ b/src/NadekoBot/Modules/Administration/Commands/AutoAssignRoleCommands.cs @@ -12,15 +12,13 @@ namespace NadekoBot.Modules.Administration public partial class Administration { [Group] - public class AutoAssignRoleCommands : NadekoSubmodule + public class AutoAssignRoleCommands : NadekoSubmodule { private readonly DbService _db; - private readonly AutoAssignRoleService _service; - public AutoAssignRoleCommands(AutoAssignRoleService service, DbService db) + public AutoAssignRoleCommands(DbService db) { _db = db; - _service = service; } [NadekoCommand, Usage, Description, Aliases] diff --git a/src/NadekoBot/Modules/Administration/Commands/GameChannelCommands.cs b/src/NadekoBot/Modules/Administration/Commands/GameChannelCommands.cs index 46a250e1..93cbd5e7 100644 --- a/src/NadekoBot/Modules/Administration/Commands/GameChannelCommands.cs +++ b/src/NadekoBot/Modules/Administration/Commands/GameChannelCommands.cs @@ -10,15 +10,13 @@ namespace NadekoBot.Modules.Administration public partial class Administration { [Group] - public class GameChannelCommands : NadekoSubmodule + public class GameChannelCommands : NadekoSubmodule { private readonly DbService _db; - private readonly GameVoiceChannelService _service; - public GameChannelCommands(GameVoiceChannelService service, DbService db) + public GameChannelCommands(DbService db) { _db = db; - _service = service; } [NadekoCommand, Usage, Description, Aliases] diff --git a/src/NadekoBot/Modules/Administration/Commands/LogCommand.cs b/src/NadekoBot/Modules/Administration/Commands/LogCommand.cs index dbdb63dc..cb83822c 100644 --- a/src/NadekoBot/Modules/Administration/Commands/LogCommand.cs +++ b/src/NadekoBot/Modules/Administration/Commands/LogCommand.cs @@ -18,14 +18,12 @@ namespace NadekoBot.Modules.Administration { [Group] [NoPublicBot] - public class LogCommands : NadekoSubmodule + public class LogCommands : NadekoSubmodule { - private readonly LogCommandService _lc; private readonly DbService _db; - public LogCommands(LogCommandService lc, DbService db) + public LogCommands(DbService db) { - _lc = lc; _db = db; } @@ -46,7 +44,7 @@ namespace NadekoBot.Modules.Administration using (var uow = _db.UnitOfWork) { logSetting = uow.GuildConfigs.LogSettingsFor(channel.Guild.Id).LogSetting; - _lc.GuildLogSettings.AddOrUpdate(channel.Guild.Id, (id) => logSetting, (id, old) => logSetting); + _service.GuildLogSettings.AddOrUpdate(channel.Guild.Id, (id) => logSetting, (id, old) => logSetting); logSetting.LogOtherId = logSetting.MessageUpdatedId = logSetting.MessageDeletedId = @@ -82,7 +80,7 @@ namespace NadekoBot.Modules.Administration using (var uow = _db.UnitOfWork) { var config = uow.GuildConfigs.LogSettingsFor(channel.Guild.Id); - LogSetting logSetting = _lc.GuildLogSettings.GetOrAdd(channel.Guild.Id, (id) => config.LogSetting); + LogSetting logSetting = _service.GuildLogSettings.GetOrAdd(channel.Guild.Id, (id) => config.LogSetting); removed = logSetting.IgnoredChannels.RemoveWhere(ilc => ilc.ChannelId == channel.Id); config.LogSetting.IgnoredChannels.RemoveWhere(ilc => ilc.ChannelId == channel.Id); if (removed == 0) @@ -122,7 +120,7 @@ namespace NadekoBot.Modules.Administration using (var uow = _db.UnitOfWork) { var logSetting = uow.GuildConfigs.LogSettingsFor(channel.Guild.Id).LogSetting; - _lc.GuildLogSettings.AddOrUpdate(channel.Guild.Id, (id) => logSetting, (id, old) => logSetting); + _service.GuildLogSettings.AddOrUpdate(channel.Guild.Id, (id) => logSetting, (id, old) => logSetting); switch (type) { case LogType.Other: diff --git a/src/NadekoBot/Modules/Administration/Commands/MuteCommands.cs b/src/NadekoBot/Modules/Administration/Commands/MuteCommands.cs index e3ca82b9..e3673b34 100644 --- a/src/NadekoBot/Modules/Administration/Commands/MuteCommands.cs +++ b/src/NadekoBot/Modules/Administration/Commands/MuteCommands.cs @@ -11,14 +11,12 @@ namespace NadekoBot.Modules.Administration public partial class Administration { [Group] - public class MuteCommands : NadekoSubmodule + public class MuteCommands : NadekoSubmodule { - private readonly MuteService _service; private readonly DbService _db; - public MuteCommands(MuteService service, DbService db) + public MuteCommands(DbService db) { - _service = service; _db = db; } diff --git a/src/NadekoBot/Modules/Administration/Commands/PlayingRotateCommands.cs b/src/NadekoBot/Modules/Administration/Commands/PlayingRotateCommands.cs index 03957dfc..549230a3 100644 --- a/src/NadekoBot/Modules/Administration/Commands/PlayingRotateCommands.cs +++ b/src/NadekoBot/Modules/Administration/Commands/PlayingRotateCommands.cs @@ -11,16 +11,14 @@ namespace NadekoBot.Modules.Administration public partial class Administration { [Group] - public class PlayingRotateCommands : NadekoSubmodule + public class PlayingRotateCommands : NadekoSubmodule { private static readonly object _locker = new object(); private readonly DbService _db; - private readonly PlayingRotateService _service; - public PlayingRotateCommands(PlayingRotateService service, DbService db) + public PlayingRotateCommands(DbService db) { _db = db; - _service = service; } [NadekoCommand, Usage, Description, Aliases] diff --git a/src/NadekoBot/Modules/Administration/Commands/ProtectionCommands.cs b/src/NadekoBot/Modules/Administration/Commands/ProtectionCommands.cs index 73bd2de0..cf18fe63 100644 --- a/src/NadekoBot/Modules/Administration/Commands/ProtectionCommands.cs +++ b/src/NadekoBot/Modules/Administration/Commands/ProtectionCommands.cs @@ -15,15 +15,13 @@ namespace NadekoBot.Modules.Administration public partial class Administration { [Group] - public class ProtectionCommands : NadekoSubmodule + public class ProtectionCommands : NadekoSubmodule { - private readonly ProtectionService _service; private readonly MuteService _mute; private readonly DbService _db; - public ProtectionCommands(ProtectionService service, MuteService mute, DbService db) + public ProtectionCommands(MuteService mute, DbService db) { - _service = service; _mute = mute; _db = db; } diff --git a/src/NadekoBot/Modules/Administration/Commands/PruneCommands.cs b/src/NadekoBot/Modules/Administration/Commands/PruneCommands.cs index 44407c19..f3a9d39c 100644 --- a/src/NadekoBot/Modules/Administration/Commands/PruneCommands.cs +++ b/src/NadekoBot/Modules/Administration/Commands/PruneCommands.cs @@ -4,9 +4,6 @@ using NadekoBot.Attributes; using NadekoBot.Extensions; using NadekoBot.Services.Administration; using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; using System.Threading.Tasks; namespace NadekoBot.Modules.Administration @@ -14,15 +11,9 @@ namespace NadekoBot.Modules.Administration public partial class Administration { [Group] - public class PruneCommands : ModuleBase + public class PruneCommands : NadekoSubmodule { private readonly TimeSpan twoWeeks = TimeSpan.FromDays(14); - private readonly PruneService _prune; - - public PruneCommands(PruneService prune) - { - _prune = prune; - } //delets her own messages, no perm required [NadekoCommand, Usage, Description, Aliases] @@ -31,7 +22,7 @@ namespace NadekoBot.Modules.Administration { var user = await Context.Guild.GetCurrentUserAsync().ConfigureAwait(false); - await _prune.PruneWhere((ITextChannel)Context.Channel, 100, (x) => x.Author.Id == user.Id).ConfigureAwait(false); + await _service.PruneWhere((ITextChannel)Context.Channel, 100, (x) => x.Author.Id == user.Id).ConfigureAwait(false); Context.Message.DeleteAfter(3); } // prune x @@ -47,7 +38,7 @@ namespace NadekoBot.Modules.Administration return; if (count > 1000) count = 1000; - await _prune.PruneWhere((ITextChannel)Context.Channel, count, x => true).ConfigureAwait(false); + await _service.PruneWhere((ITextChannel)Context.Channel, count, x => true).ConfigureAwait(false); } //prune @user [x] @@ -66,7 +57,7 @@ namespace NadekoBot.Modules.Administration if (count > 1000) count = 1000; - await _prune.PruneWhere((ITextChannel)Context.Channel, count, m => m.Author.Id == user.Id && DateTime.UtcNow - m.CreatedAt < twoWeeks); + await _service.PruneWhere((ITextChannel)Context.Channel, count, m => m.Author.Id == user.Id && DateTime.UtcNow - m.CreatedAt < twoWeeks); } } } diff --git a/src/NadekoBot/Modules/Administration/Commands/RatelimitCommand.cs b/src/NadekoBot/Modules/Administration/Commands/RatelimitCommand.cs index bf23d99b..bb829d37 100644 --- a/src/NadekoBot/Modules/Administration/Commands/RatelimitCommand.cs +++ b/src/NadekoBot/Modules/Administration/Commands/RatelimitCommand.cs @@ -15,14 +15,12 @@ namespace NadekoBot.Modules.Administration public partial class Administration { [Group] - public class SlowModeCommands : NadekoSubmodule + public class SlowModeCommands : NadekoSubmodule { - private readonly SlowmodeService _service; private readonly DbService _db; - public SlowModeCommands(SlowmodeService service, DbService db) + public SlowModeCommands(DbService db) { - _service = service; _db = db; } diff --git a/src/NadekoBot/Modules/Administration/Commands/SelfCommands.cs b/src/NadekoBot/Modules/Administration/Commands/SelfCommands.cs index 2205a1c8..4e348f62 100644 --- a/src/NadekoBot/Modules/Administration/Commands/SelfCommands.cs +++ b/src/NadekoBot/Modules/Administration/Commands/SelfCommands.cs @@ -14,7 +14,6 @@ using NadekoBot.Services.Database.Models; using Microsoft.EntityFrameworkCore; using NadekoBot.Services.Administration; using System.Diagnostics; -using NadekoBot.DataStructures; using NadekoBot.Services.Music; namespace NadekoBot.Modules.Administration @@ -22,21 +21,19 @@ namespace NadekoBot.Modules.Administration public partial class Administration { [Group] - public class SelfCommands : NadekoSubmodule + public class SelfCommands : NadekoSubmodule { private readonly DbService _db; private static readonly object _locker = new object(); - private readonly SelfService _service; private readonly DiscordSocketClient _client; private readonly IImagesService _images; private readonly MusicService _music; - public SelfCommands(DbService db, SelfService service, DiscordSocketClient client, + public SelfCommands(DbService db, DiscordSocketClient client, MusicService music, IImagesService images) { _db = db; - _service = service; _client = client; _images = images; _music = music; diff --git a/src/NadekoBot/Modules/Administration/Commands/ServerGreetCommands.cs b/src/NadekoBot/Modules/Administration/Commands/ServerGreetCommands.cs index 5a2f9f98..b957a4c9 100644 --- a/src/NadekoBot/Modules/Administration/Commands/ServerGreetCommands.cs +++ b/src/NadekoBot/Modules/Administration/Commands/ServerGreetCommands.cs @@ -11,14 +11,12 @@ namespace NadekoBot.Modules.Administration public partial class Administration { [Group] - public class ServerGreetCommands : NadekoSubmodule + public class ServerGreetCommands : NadekoSubmodule { - private readonly GreetSettingsService _greetService; private readonly DbService _db; - public ServerGreetCommands(GreetSettingsService greetService, DbService db) + public ServerGreetCommands(DbService db) { - _greetService = greetService; _db = db; } @@ -30,7 +28,7 @@ namespace NadekoBot.Modules.Administration if (timer < 0 || timer > 600) return; - await _greetService.SetGreetDel(Context.Guild.Id, timer).ConfigureAwait(false); + await _service.SetGreetDel(Context.Guild.Id, timer).ConfigureAwait(false); if (timer > 0) await ReplyConfirmLocalized("greetdel_on", timer).ConfigureAwait(false); @@ -43,7 +41,7 @@ namespace NadekoBot.Modules.Administration [RequireUserPermission(GuildPermission.ManageGuild)] public async Task Greet() { - var enabled = await _greetService.SetGreet(Context.Guild.Id, Context.Channel.Id).ConfigureAwait(false); + var enabled = await _service.SetGreet(Context.Guild.Id, Context.Channel.Id).ConfigureAwait(false); if (enabled) await ReplyConfirmLocalized("greet_on").ConfigureAwait(false); @@ -67,7 +65,7 @@ namespace NadekoBot.Modules.Administration return; } - var sendGreetEnabled = _greetService.SetGreetMessage(Context.Guild.Id, ref text); + var sendGreetEnabled = _service.SetGreetMessage(Context.Guild.Id, ref text); await ReplyConfirmLocalized("greetmsg_new").ConfigureAwait(false); if (!sendGreetEnabled) @@ -79,7 +77,7 @@ namespace NadekoBot.Modules.Administration [RequireUserPermission(GuildPermission.ManageGuild)] public async Task GreetDm() { - var enabled = await _greetService.SetGreetDm(Context.Guild.Id).ConfigureAwait(false); + var enabled = await _service.SetGreetDm(Context.Guild.Id).ConfigureAwait(false); if (enabled) await ReplyConfirmLocalized("greetdm_on").ConfigureAwait(false); @@ -103,7 +101,7 @@ namespace NadekoBot.Modules.Administration return; } - var sendGreetEnabled = _greetService.SetGreetDmMessage(Context.Guild.Id, ref text); + var sendGreetEnabled = _service.SetGreetDmMessage(Context.Guild.Id, ref text); await ReplyConfirmLocalized("greetdmmsg_new").ConfigureAwait(false); if (!sendGreetEnabled) @@ -115,7 +113,7 @@ namespace NadekoBot.Modules.Administration [RequireUserPermission(GuildPermission.ManageGuild)] public async Task Bye() { - var enabled = await _greetService.SetBye(Context.Guild.Id, Context.Channel.Id).ConfigureAwait(false); + var enabled = await _service.SetBye(Context.Guild.Id, Context.Channel.Id).ConfigureAwait(false); if (enabled) await ReplyConfirmLocalized("bye_on").ConfigureAwait(false); @@ -139,7 +137,7 @@ namespace NadekoBot.Modules.Administration return; } - var sendByeEnabled = _greetService.SetByeMessage(Context.Guild.Id, ref text); + var sendByeEnabled = _service.SetByeMessage(Context.Guild.Id, ref text); await ReplyConfirmLocalized("byemsg_new").ConfigureAwait(false); if (!sendByeEnabled) @@ -151,7 +149,7 @@ namespace NadekoBot.Modules.Administration [RequireUserPermission(GuildPermission.ManageGuild)] public async Task ByeDel(int timer = 30) { - await _greetService.SetByeDel(Context.Guild.Id, timer).ConfigureAwait(false); + await _service.SetByeDel(Context.Guild.Id, timer).ConfigureAwait(false); if (timer > 0) await ReplyConfirmLocalized("byedel_on", timer).ConfigureAwait(false); diff --git a/src/NadekoBot/Modules/Administration/Commands/TimeZoneCommands.cs b/src/NadekoBot/Modules/Administration/Commands/TimeZoneCommands.cs index c97fdbca..8f014c47 100644 --- a/src/NadekoBot/Modules/Administration/Commands/TimeZoneCommands.cs +++ b/src/NadekoBot/Modules/Administration/Commands/TimeZoneCommands.cs @@ -13,15 +13,8 @@ namespace NadekoBot.Modules.Administration public partial class Administration { [Group] - public class TimeZoneCommands : NadekoSubmodule + public class TimeZoneCommands : NadekoSubmodule { - private readonly GuildTimezoneService _service; - - public TimeZoneCommands(GuildTimezoneService service) - { - _service = service; - } - [NadekoCommand, Usage, Description, Aliases] [RequireContext(ContextType.Guild)] public async Task Timezones(int page = 1) diff --git a/src/NadekoBot/Modules/Administration/Commands/UserPunishCommands.cs b/src/NadekoBot/Modules/Administration/Commands/UserPunishCommands.cs index 363a9222..49d1fc38 100644 --- a/src/NadekoBot/Modules/Administration/Commands/UserPunishCommands.cs +++ b/src/NadekoBot/Modules/Administration/Commands/UserPunishCommands.cs @@ -15,15 +15,13 @@ namespace NadekoBot.Modules.Administration public partial class Administration { [Group] - public class UserPunishCommands : NadekoSubmodule + public class UserPunishCommands : NadekoSubmodule { private readonly DbService _db; - private readonly UserPunishService _service; - public UserPunishCommands(UserPunishService service, DbService db, MuteService muteService) + public UserPunishCommands(DbService db, MuteService muteService) { _db = db; - _service = service; } [NadekoCommand, Usage, Description, Aliases] diff --git a/src/NadekoBot/Modules/Administration/Commands/VcRoleCommands.cs b/src/NadekoBot/Modules/Administration/Commands/VcRoleCommands.cs index 5fe31422..5865b9a9 100644 --- a/src/NadekoBot/Modules/Administration/Commands/VcRoleCommands.cs +++ b/src/NadekoBot/Modules/Administration/Commands/VcRoleCommands.cs @@ -16,14 +16,12 @@ namespace NadekoBot.Modules.Administration public partial class Administration { [Group] - public class VcRoleCommands : NadekoSubmodule + public class VcRoleCommands : NadekoSubmodule { - private readonly VcRoleService _service; private readonly DbService _db; - public VcRoleCommands(VcRoleService service, DbService db) + public VcRoleCommands(DbService db) { - _service = service; _db = db; } diff --git a/src/NadekoBot/Modules/Administration/Commands/VoicePlusTextCommands.cs b/src/NadekoBot/Modules/Administration/Commands/VoicePlusTextCommands.cs index 53f85510..9f328bea 100644 --- a/src/NadekoBot/Modules/Administration/Commands/VoicePlusTextCommands.cs +++ b/src/NadekoBot/Modules/Administration/Commands/VoicePlusTextCommands.cs @@ -15,14 +15,12 @@ namespace NadekoBot.Modules.Administration public partial class Administration { [Group] - public class VoicePlusTextCommands : NadekoSubmodule + public class VoicePlusTextCommands : NadekoSubmodule { - private readonly VplusTService _service; private readonly DbService _db; - public VoicePlusTextCommands(VplusTService service, DbService db) + public VoicePlusTextCommands(DbService db) { - _service = service; _db = db; } diff --git a/src/NadekoBot/Modules/CustomReactions/CustomReactions.cs b/src/NadekoBot/Modules/CustomReactions/CustomReactions.cs index b80a5921..da99f55d 100644 --- a/src/NadekoBot/Modules/CustomReactions/CustomReactions.cs +++ b/src/NadekoBot/Modules/CustomReactions/CustomReactions.cs @@ -12,19 +12,17 @@ using NadekoBot.Services.CustomReactions; namespace NadekoBot.Modules.CustomReactions { - public class CustomReactions : NadekoTopLevelModule + public class CustomReactions : NadekoTopLevelModule { private readonly IBotCredentials _creds; private readonly DbService _db; - private readonly CustomReactionsService _crs; private readonly DiscordSocketClient _client; - public CustomReactions(IBotCredentials creds, DbService db, CustomReactionsService crs, + public CustomReactions(IBotCredentials creds, DbService db, DiscordSocketClient client) { _creds = creds; _db = db; - _crs = crs; _client = client; } @@ -60,12 +58,12 @@ namespace NadekoBot.Modules.CustomReactions if (channel == null) { - Array.Resize(ref _crs.GlobalReactions, _crs.GlobalReactions.Length + 1); - _crs.GlobalReactions[_crs.GlobalReactions.Length - 1] = cr; + Array.Resize(ref _service.GlobalReactions, _service.GlobalReactions.Length + 1); + _service.GlobalReactions[_service.GlobalReactions.Length - 1] = cr; } else { - _crs.GuildReactions.AddOrUpdate(Context.Guild.Id, + _service.GuildReactions.AddOrUpdate(Context.Guild.Id, new CustomReaction[] { cr }, (k, old) => { @@ -91,9 +89,9 @@ namespace NadekoBot.Modules.CustomReactions return; CustomReaction[] customReactions; if (Context.Guild == null) - customReactions = _crs.GlobalReactions.Where(cr => cr != null).ToArray(); + customReactions = _service.GlobalReactions.Where(cr => cr != null).ToArray(); else - customReactions = _crs.GuildReactions.GetOrAdd(Context.Guild.Id, Array.Empty()).Where(cr => cr != null).ToArray(); + customReactions = _service.GuildReactions.GetOrAdd(Context.Guild.Id, Array.Empty()).Where(cr => cr != null).ToArray(); if (customReactions == null || !customReactions.Any()) { @@ -135,9 +133,9 @@ namespace NadekoBot.Modules.CustomReactions { CustomReaction[] customReactions; if (Context.Guild == null) - customReactions = _crs.GlobalReactions.Where(cr => cr != null).ToArray(); + customReactions = _service.GlobalReactions.Where(cr => cr != null).ToArray(); else - customReactions = _crs.GuildReactions.GetOrAdd(Context.Guild.Id, new CustomReaction[]{ }).Where(cr => cr != null).ToArray(); + customReactions = _service.GuildReactions.GetOrAdd(Context.Guild.Id, new CustomReaction[]{ }).Where(cr => cr != null).ToArray(); if (customReactions == null || !customReactions.Any()) { @@ -165,9 +163,9 @@ namespace NadekoBot.Modules.CustomReactions return; CustomReaction[] customReactions; if (Context.Guild == null) - customReactions = _crs.GlobalReactions.Where(cr => cr != null).ToArray(); + customReactions = _service.GlobalReactions.Where(cr => cr != null).ToArray(); else - customReactions = _crs.GuildReactions.GetOrAdd(Context.Guild.Id, new CustomReaction[]{ }).Where(cr => cr != null).ToArray(); + customReactions = _service.GuildReactions.GetOrAdd(Context.Guild.Id, new CustomReaction[]{ }).Where(cr => cr != null).ToArray(); if (customReactions == null || !customReactions.Any()) { @@ -197,9 +195,9 @@ namespace NadekoBot.Modules.CustomReactions { CustomReaction[] customReactions; if (Context.Guild == null) - customReactions = _crs.GlobalReactions; + customReactions = _service.GlobalReactions; else - customReactions = _crs.GuildReactions.GetOrAdd(Context.Guild.Id, new CustomReaction[]{ }); + customReactions = _service.GuildReactions.GetOrAdd(Context.Guild.Id, new CustomReaction[]{ }); var found = customReactions.FirstOrDefault(cr => cr?.Id == id); @@ -240,13 +238,13 @@ namespace NadekoBot.Modules.CustomReactions { uow.CustomReactions.Remove(toDelete); //todo 91 i can dramatically improve performance of this, if Ids are ordered. - _crs.GlobalReactions = _crs.GlobalReactions.Where(cr => cr?.Id != toDelete.Id).ToArray(); + _service.GlobalReactions = _service.GlobalReactions.Where(cr => cr?.Id != toDelete.Id).ToArray(); success = true; } else if ((toDelete.GuildId != null && toDelete.GuildId != 0) && Context.Guild.Id == toDelete.GuildId) { uow.CustomReactions.Remove(toDelete); - _crs.GuildReactions.AddOrUpdate(Context.Guild.Id, new CustomReaction[] { }, (key, old) => + _service.GuildReactions.AddOrUpdate(Context.Guild.Id, new CustomReaction[] { }, (key, old) => { return old.Where(cr => cr?.Id != toDelete.Id).ToArray(); }); @@ -284,10 +282,10 @@ namespace NadekoBot.Modules.CustomReactions CustomReaction[] reactions = new CustomReaction[0]; if (Context.Guild == null) - reactions = _crs.GlobalReactions; + reactions = _service.GlobalReactions; else { - _crs.GuildReactions.TryGetValue(Context.Guild.Id, out reactions); + _service.GuildReactions.TryGetValue(Context.Guild.Id, out reactions); } if (reactions.Any()) { @@ -335,10 +333,10 @@ namespace NadekoBot.Modules.CustomReactions CustomReaction[] reactions = new CustomReaction[0]; if (Context.Guild == null) - reactions = _crs.GlobalReactions; + reactions = _service.GlobalReactions; else { - _crs.GuildReactions.TryGetValue(Context.Guild.Id, out reactions); + _service.GuildReactions.TryGetValue(Context.Guild.Id, out reactions); } if (reactions.Any()) { @@ -379,13 +377,13 @@ namespace NadekoBot.Modules.CustomReactions { if (string.IsNullOrWhiteSpace(trigger)) { - _crs.ClearStats(); + _service.ClearStats(); await ReplyConfirmLocalized("all_stats_cleared").ConfigureAwait(false); } else { uint throwaway; - if (_crs.ReactionStats.TryRemove(trigger, out throwaway)) + if (_service.ReactionStats.TryRemove(trigger, out throwaway)) { await ReplyErrorLocalized("stats_cleared", Format.Bold(trigger)).ConfigureAwait(false); } @@ -401,7 +399,7 @@ namespace NadekoBot.Modules.CustomReactions { if (--page < 0) return; - var ordered = _crs.ReactionStats.OrderByDescending(x => x.Value).ToArray(); + var ordered = _service.ReactionStats.OrderByDescending(x => x.Value).ToArray(); if (!ordered.Any()) return; var lastPage = ordered.Length / 9; diff --git a/src/NadekoBot/Modules/Games/Commands/CleverBotCommands.cs b/src/NadekoBot/Modules/Games/Commands/CleverBotCommands.cs index ddf0e78d..ad0e9189 100644 --- a/src/NadekoBot/Modules/Games/Commands/CleverBotCommands.cs +++ b/src/NadekoBot/Modules/Games/Commands/CleverBotCommands.cs @@ -11,15 +11,13 @@ namespace NadekoBot.Modules.Games public partial class Games { [Group] - public class CleverBotCommands : NadekoSubmodule + public class CleverBotCommands : NadekoSubmodule { private readonly DbService _db; - private readonly ChatterBotService _games; - public CleverBotCommands(DbService db, ChatterBotService games) + public CleverBotCommands(DbService db) { _db = db; - _games = games; } [NadekoCommand, Usage, Description, Aliases] @@ -29,7 +27,7 @@ namespace NadekoBot.Modules.Games { var channel = (ITextChannel)Context.Channel; - if (_games.ChatterBotGuilds.TryRemove(channel.Guild.Id, out Lazy throwaway)) + if (_service.ChatterBotGuilds.TryRemove(channel.Guild.Id, out Lazy throwaway)) { using (var uow = _db.UnitOfWork) { @@ -40,7 +38,7 @@ namespace NadekoBot.Modules.Games return; } - _games.ChatterBotGuilds.TryAdd(channel.Guild.Id, new Lazy(() => new ChatterBotSession(Context.Guild.Id), true)); + _service.ChatterBotGuilds.TryAdd(channel.Guild.Id, new Lazy(() => new ChatterBotSession(Context.Guild.Id), true)); using (var uow = _db.UnitOfWork) { diff --git a/src/NadekoBot/Modules/Games/Commands/PollCommands.cs b/src/NadekoBot/Modules/Games/Commands/PollCommands.cs index 0d6b6cb2..4679fa3e 100644 --- a/src/NadekoBot/Modules/Games/Commands/PollCommands.cs +++ b/src/NadekoBot/Modules/Games/Commands/PollCommands.cs @@ -11,15 +11,13 @@ namespace NadekoBot.Modules.Games public partial class Games { [Group] - public class PollCommands : NadekoSubmodule + public class PollCommands : NadekoSubmodule { private readonly DiscordSocketClient _client; - private readonly PollService _polls; - public PollCommands(DiscordSocketClient client, PollService polls) + public PollCommands(DiscordSocketClient client) { _client = client; - _polls = polls; } [NadekoCommand, Usage, Description, Aliases] @@ -33,7 +31,7 @@ namespace NadekoBot.Modules.Games [RequireContext(ContextType.Guild)] public async Task PollStats() { - if (!_polls.ActivePolls.TryGetValue(Context.Guild.Id, out var poll)) + if (!_service.ActivePolls.TryGetValue(Context.Guild.Id, out var poll)) return; await Context.Channel.EmbedAsync(poll.GetStats(GetText("current_poll_results"))); @@ -41,7 +39,7 @@ namespace NadekoBot.Modules.Games private async Task InternalStartPoll(string arg) { - if(await _polls.StartPoll((ITextChannel)Context.Channel, Context.Message, arg) == false) + if(await _service.StartPoll((ITextChannel)Context.Channel, Context.Message, arg) == false) await ReplyErrorLocalized("poll_already_running").ConfigureAwait(false); } @@ -52,7 +50,7 @@ namespace NadekoBot.Modules.Games { var channel = (ITextChannel)Context.Channel; - _polls.ActivePolls.TryRemove(channel.Guild.Id, out var poll); + _service.ActivePolls.TryRemove(channel.Guild.Id, out var poll); await poll.StopPoll().ConfigureAwait(false); } } diff --git a/src/NadekoBot/Modules/Games/Games.cs b/src/NadekoBot/Modules/Games/Games.cs index 5c4e0f61..399f500a 100644 --- a/src/NadekoBot/Modules/Games/Games.cs +++ b/src/NadekoBot/Modules/Games/Games.cs @@ -9,14 +9,12 @@ using NadekoBot.Services.Games; namespace NadekoBot.Modules.Games { - public partial class Games : NadekoTopLevelModule + public partial class Games : NadekoTopLevelModule { - private readonly GamesService _games; private readonly IImagesService _images; - public Games(GamesService games, IImagesService images) + public Games(IImagesService images) { - _games = games; _images = images; } @@ -40,7 +38,7 @@ namespace NadekoBot.Modules.Games await Context.Channel.EmbedAsync(new EmbedBuilder().WithColor(NadekoBot.OkColor) .AddField(efb => efb.WithName("❓ " + GetText("question") ).WithValue(question).WithIsInline(false)) - .AddField(efb => efb.WithName("🎱 " + GetText("8ball")).WithValue(_games.EightBallResponses[new NadekoRandom().Next(0, _games.EightBallResponses.Length)]).WithIsInline(false))); + .AddField(efb => efb.WithName("🎱 " + GetText("8ball")).WithValue(_service.EightBallResponses[new NadekoRandom().Next(0, _service.EightBallResponses.Length)]).WithIsInline(false))); } [NadekoCommand, Usage, Description, Aliases] @@ -100,7 +98,7 @@ namespace NadekoBot.Modules.Games [RequireContext(ContextType.Guild)] public async Task RateGirl(IGuildUser usr) { - var gr = _games.GirlRatings.GetOrAdd(usr.Id, GetGirl); + var gr = _service.GirlRatings.GetOrAdd(usr.Id, GetGirl); var img = await gr.Url; await Context.Channel.EmbedAsync(new EmbedBuilder().WithOkColor() .WithTitle("Girl Rating For " + usr) diff --git a/src/NadekoBot/Modules/Help/Help.cs b/src/NadekoBot/Modules/Help/Help.cs index 7d3162cc..c492810d 100644 --- a/src/NadekoBot/Modules/Help/Help.cs +++ b/src/NadekoBot/Modules/Help/Help.cs @@ -15,7 +15,7 @@ using NadekoBot.Services.Help; namespace NadekoBot.Modules.Help { - public class Help : NadekoTopLevelModule + public class Help : NadekoTopLevelModule { public const string PatreonUrl = "https://patreon.com/nadekobot"; public const string PaypalUrl = "https://paypal.me/Kwoth"; @@ -23,18 +23,16 @@ namespace NadekoBot.Modules.Help private readonly BotConfig _config; private readonly CommandService _cmds; private readonly GlobalPermissionService _perms; - private readonly HelpService _h; public string HelpString => String.Format(_config.HelpString, _creds.ClientId, Prefix); public string DMHelpString => _config.DMHelpString; - public Help(IBotCredentials creds, GlobalPermissionService perms, BotConfig config, CommandService cmds, HelpService h) + public Help(IBotCredentials creds, GlobalPermissionService perms, BotConfig config, CommandService cmds) { _creds = creds; _config = config; _cmds = cmds; _perms = perms; - _h = h; } [NadekoCommand, Usage, Description, Aliases] @@ -107,7 +105,7 @@ namespace NadekoBot.Modules.Help // return; //} - var embed = _h.GetCommandHelp(com, Context.Guild); + var embed = _service.GetCommandHelp(com, Context.Guild); await channel.EmbedAsync(embed).ConfigureAwait(false); } @@ -144,7 +142,7 @@ namespace NadekoBot.Modules.Help lastModule = module.Name; } helpstr.AppendLine($"{string.Join(" ", com.Aliases.Select(a => "`" + Prefix + a + "`"))} |" + - $" {string.Format(com.Summary, Prefix)} {_h.GetCommandRequirements(com, Context.Guild)} |" + + $" {string.Format(com.Summary, Prefix)} {_service.GetCommandRequirements(com, Context.Guild)} |" + $" {string.Format(com.Remarks, Prefix)}"); } File.WriteAllText("../../docs/Commands List.md", helpstr.ToString()); diff --git a/src/NadekoBot/Modules/Music/Music.cs b/src/NadekoBot/Modules/Music/Music.cs index 1d84dcee..b20f1589 100644 --- a/src/NadekoBot/Modules/Music/Music.cs +++ b/src/NadekoBot/Modules/Music/Music.cs @@ -21,22 +21,20 @@ using NadekoBot.Services.Impl; namespace NadekoBot.Modules.Music { [NoPublicBot] - public class Music : NadekoTopLevelModule + public class Music : NadekoTopLevelModule { - private static MusicService _music; private readonly DiscordSocketClient _client; private readonly IBotCredentials _creds; private readonly IGoogleApiService _google; private readonly DbService _db; public Music(DiscordSocketClient client, IBotCredentials creds, IGoogleApiService google, - DbService db, MusicService music) + DbService db) { _client = client; _creds = creds; _google = google; _db = db; - _music = music; //_client.UserVoiceStateUpdated += Client_UserVoiceStateUpdated; _client.LeftGuild += _client_LeftGuild; @@ -44,7 +42,7 @@ namespace NadekoBot.Modules.Music private Task _client_LeftGuild(SocketGuild arg) { - var t = _music.DestroyPlayer(arg.Id); + var t = _service.DestroyPlayer(arg.Id); return Task.CompletedTask; } @@ -151,7 +149,7 @@ namespace NadekoBot.Modules.Music [RequireContext(ContextType.Guild)] public async Task Play([Remainder] string query = null) { - var mp = await _music.GetOrCreatePlayer(Context); + var mp = await _service.GetOrCreatePlayer(Context); if (string.IsNullOrWhiteSpace(query)) { await Next(); @@ -176,9 +174,9 @@ namespace NadekoBot.Modules.Music public async Task Queue([Remainder] string query) { _log.Info("Getting player"); - var mp = await _music.GetOrCreatePlayer(Context); + var mp = await _service.GetOrCreatePlayer(Context); _log.Info("Resolving song"); - var songInfo = await _music.ResolveSong(query, Context.User.ToString()); + var songInfo = await _service.ResolveSong(query, Context.User.ToString()); _log.Info("Queueing song"); try { await InternalQueue(mp, songInfo, false); } catch (QueueFullException) { return; } _log.Info("--------------"); @@ -229,7 +227,7 @@ namespace NadekoBot.Modules.Music [RequireContext(ContextType.Guild)] public async Task ListQueue(int page = 0) { - var mp = await _music.GetOrCreatePlayer(Context); + var mp = await _service.GetOrCreatePlayer(Context); var (current, songs) = mp.QueueArray(); if (!songs.Any()) @@ -316,7 +314,7 @@ namespace NadekoBot.Modules.Music if (skipCount < 1) return; - var mp = await _music.GetOrCreatePlayer(Context); + var mp = await _service.GetOrCreatePlayer(Context); mp.Next(skipCount); } @@ -325,7 +323,7 @@ namespace NadekoBot.Modules.Music [RequireContext(ContextType.Guild)] public async Task Stop() { - var mp = await _music.GetOrCreatePlayer(Context); + var mp = await _service.GetOrCreatePlayer(Context); mp.Stop(); } @@ -333,14 +331,14 @@ namespace NadekoBot.Modules.Music [RequireContext(ContextType.Guild)] public async Task Destroy() { - await _music.DestroyPlayer(Context.Guild.Id); + await _service.DestroyPlayer(Context.Guild.Id); } [NadekoCommand, Usage, Description, Aliases] [RequireContext(ContextType.Guild)] public async Task Pause() { - var mp = await _music.GetOrCreatePlayer(Context); + var mp = await _service.GetOrCreatePlayer(Context); mp.TogglePause(); } @@ -348,7 +346,7 @@ namespace NadekoBot.Modules.Music [RequireContext(ContextType.Guild)] public async Task Volume(int val) { - var mp = await _music.GetOrCreatePlayer(Context); + var mp = await _service.GetOrCreatePlayer(Context); if (val < 0 || val > 100) { await ReplyErrorLocalized("volume_input_invalid").ConfigureAwait(false); @@ -385,7 +383,7 @@ namespace NadekoBot.Modules.Music await ReplyErrorLocalized("removed_song_error").ConfigureAwait(false); return; } - var mp = await _music.GetOrCreatePlayer(Context); + var mp = await _service.GetOrCreatePlayer(Context); try { var song = mp.RemoveAt(index - 1); @@ -409,7 +407,7 @@ namespace NadekoBot.Modules.Music [Priority(0)] public async Task SongRemove(All all) { - var mp = _music.GetPlayerOrDefault(Context.Guild.Id); + var mp = _service.GetPlayerOrDefault(Context.Guild.Id); if (mp == null) return; mp.Stop(true); @@ -476,7 +474,7 @@ namespace NadekoBot.Modules.Music [RequireContext(ContextType.Guild)] public async Task Save([Remainder] string name) { - var mp = await _music.GetOrCreatePlayer(Context); + var mp = await _service.GetOrCreatePlayer(Context); var songs = mp.QueueArray().Songs .Select(s => new PlaylistSong() @@ -517,7 +515,7 @@ namespace NadekoBot.Modules.Music return; try { - var mp = await _music.GetOrCreatePlayer(Context); + var mp = await _service.GetOrCreatePlayer(Context); MusicPlaylist mpl; using (var uow = _db.UnitOfWork) { @@ -537,7 +535,7 @@ namespace NadekoBot.Modules.Music { await Task.Yield(); - await Task.WhenAll(Task.Delay(1000), InternalQueue(mp, await _music.ResolveSong(item.Query, Context.User.ToString(), item.ProviderType), true)).ConfigureAwait(false); + await Task.WhenAll(Task.Delay(1000), InternalQueue(mp, await _service.ResolveSong(item.Query, Context.User.ToString(), item.ProviderType), true)).ConfigureAwait(false); } catch (SongNotFoundException) { } catch { break; } @@ -555,7 +553,7 @@ namespace NadekoBot.Modules.Music [RequireContext(ContextType.Guild)] public async Task Fairplay() { - var mp = await _music.GetOrCreatePlayer(Context); + var mp = await _service.GetOrCreatePlayer(Context); var val = mp.FairPlay = !mp.FairPlay; if (val) @@ -572,8 +570,8 @@ namespace NadekoBot.Modules.Music [RequireContext(ContextType.Guild)] public async Task SoundCloudQueue([Remainder] string query) { - var mp = await _music.GetOrCreatePlayer(Context); - var song = await _music.ResolveSong(query, Context.User.ToString(), MusicType.Soundcloud); + var mp = await _service.GetOrCreatePlayer(Context); + var song = await _service.ResolveSong(query, Context.User.ToString(), MusicType.Soundcloud); await InternalQueue(mp, song, false).ConfigureAwait(false); } @@ -586,7 +584,7 @@ namespace NadekoBot.Modules.Music if (string.IsNullOrWhiteSpace(pl)) return; - var mp = await _music.GetOrCreatePlayer(Context); + var mp = await _service.GetOrCreatePlayer(Context); using (var http = new HttpClient()) { @@ -617,7 +615,7 @@ namespace NadekoBot.Modules.Music [RequireContext(ContextType.Guild)] public async Task NowPlaying() { - var mp = await _music.GetOrCreatePlayer(Context); + var mp = await _service.GetOrCreatePlayer(Context); var (_, currentSong) = mp.Current; if (currentSong == null) return; @@ -636,7 +634,7 @@ namespace NadekoBot.Modules.Music [RequireContext(ContextType.Guild)] public async Task ShufflePlaylist() { - var mp = await _music.GetOrCreatePlayer(Context); + var mp = await _service.GetOrCreatePlayer(Context); var val = mp.ToggleShuffle(); if(val) await ReplyConfirmLocalized("songs_shuffle_enable").ConfigureAwait(false); @@ -651,7 +649,7 @@ namespace NadekoBot.Modules.Music if (string.IsNullOrWhiteSpace(playlist)) return; - var mp = await _music.GetOrCreatePlayer(Context); + var mp = await _service.GetOrCreatePlayer(Context); var plId = (await _google.GetPlaylistIdsByKeywordsAsync(playlist).ConfigureAwait(false)).FirstOrDefault(); if (plId == null) @@ -676,7 +674,7 @@ namespace NadekoBot.Modules.Music if (mp.Exited) return; - await Task.WhenAll(Task.Delay(150), InternalQueue(mp, await _music.ResolveSong(song, Context.User.ToString(), MusicType.YouTube), true)); + await Task.WhenAll(Task.Delay(150), InternalQueue(mp, await _service.ResolveSong(song, Context.User.ToString(), MusicType.YouTube), true)); } catch (SongNotFoundException) { } catch { break; } @@ -690,8 +688,8 @@ namespace NadekoBot.Modules.Music [RequireContext(ContextType.Guild)] public async Task Radio(string radioLink) { - var mp = await _music.GetOrCreatePlayer(Context); - var song = await _music.ResolveSong(radioLink, Context.User.ToString(), MusicType.Radio); + var mp = await _service.GetOrCreatePlayer(Context); + var song = await _service.ResolveSong(radioLink, Context.User.ToString(), MusicType.Radio); await InternalQueue(mp, song, false).ConfigureAwait(false); } @@ -700,8 +698,8 @@ namespace NadekoBot.Modules.Music [OwnerOnly] public async Task Local([Remainder] string path) { - var mp = await _music.GetOrCreatePlayer(Context); - var song = await _music.ResolveSong(path, Context.User.ToString(), MusicType.Local); + var mp = await _service.GetOrCreatePlayer(Context); + var song = await _service.ResolveSong(path, Context.User.ToString(), MusicType.Local); await InternalQueue(mp, song, false).ConfigureAwait(false); } @@ -713,7 +711,7 @@ namespace NadekoBot.Modules.Music if (string.IsNullOrWhiteSpace(dirPath)) return; - var mp = await _music.GetOrCreatePlayer(Context); + var mp = await _service.GetOrCreatePlayer(Context); DirectoryInfo dir; try { dir = new DirectoryInfo(dirPath); } catch { return; } @@ -724,7 +722,7 @@ namespace NadekoBot.Modules.Music try { await Task.Yield(); - var song = await _music.ResolveSong(file.FullName, Context.User.ToString(), MusicType.Local); + var song = await _service.ResolveSong(file.FullName, Context.User.ToString(), MusicType.Local); await InternalQueue(mp, song, true).ConfigureAwait(false); } catch (QueueFullException) @@ -749,7 +747,7 @@ namespace NadekoBot.Modules.Music if (vch == null) return; - var mp = _music.GetPlayerOrDefault(Context.Guild.Id); + var mp = _service.GetPlayerOrDefault(Context.Guild.Id); if (mp == null) return; @@ -764,7 +762,7 @@ namespace NadekoBot.Modules.Music if (string.IsNullOrWhiteSpace(fromto)) return; - MusicPlayer mp = _music.GetPlayerOrDefault(Context.Guild.Id); + MusicPlayer mp = _service.GetPlayerOrDefault(Context.Guild.Id); if (mp == null) return; @@ -796,7 +794,7 @@ namespace NadekoBot.Modules.Music { if (size < 0) return; - var mp = await _music.GetOrCreatePlayer(Context); + var mp = await _service.GetOrCreatePlayer(Context); mp.MaxQueueSize = size; @@ -813,7 +811,7 @@ namespace NadekoBot.Modules.Music if (seconds < 15 && seconds != 0) return; - var mp = await _music.GetOrCreatePlayer(Context); + var mp = await _service.GetOrCreatePlayer(Context); mp.MaxPlaytimeSeconds = seconds; if (seconds == 0) await ReplyConfirmLocalized("max_playtime_none").ConfigureAwait(false); @@ -825,7 +823,7 @@ namespace NadekoBot.Modules.Music [RequireContext(ContextType.Guild)] public async Task ReptCurSong() { - var mp = await _music.GetOrCreatePlayer(Context); + var mp = await _service.GetOrCreatePlayer(Context); var (_, currentSong) = mp.Current; if (currentSong == null) return; @@ -846,7 +844,7 @@ namespace NadekoBot.Modules.Music [RequireContext(ContextType.Guild)] public async Task RepeatPl() { - var mp = await _music.GetOrCreatePlayer(Context); + var mp = await _service.GetOrCreatePlayer(Context); var currentValue = mp.ToggleRepeatPlaylist(); if (currentValue) await ReplyConfirmLocalized("rpl_enabled").ConfigureAwait(false); @@ -858,7 +856,7 @@ namespace NadekoBot.Modules.Music [RequireContext(ContextType.Guild)] public async Task Autoplay() { - var mp = await _music.GetOrCreatePlayer(Context); + var mp = await _service.GetOrCreatePlayer(Context); if (!mp.ToggleAutoplay()) await ReplyConfirmLocalized("autoplay_disabled").ConfigureAwait(false); @@ -871,7 +869,7 @@ namespace NadekoBot.Modules.Music [RequireUserPermission(GuildPermission.ManageMessages)] public async Task SetMusicChannel() { - var mp = await _music.GetOrCreatePlayer(Context); + var mp = await _service.GetOrCreatePlayer(Context); mp.OutputTextChannel = (ITextChannel)Context.Channel; diff --git a/src/NadekoBot/Modules/NSFW/NSFW.cs b/src/NadekoBot/Modules/NSFW/NSFW.cs index a5815bc6..21db0f12 100644 --- a/src/NadekoBot/Modules/NSFW/NSFW.cs +++ b/src/NadekoBot/Modules/NSFW/NSFW.cs @@ -8,7 +8,6 @@ using System.Threading.Tasks; using NadekoBot.Services; using System.Net.Http; using NadekoBot.Extensions; -using System.Xml; using System.Threading; using System.Collections.Concurrent; using NadekoBot.Services.Searches; @@ -16,16 +15,10 @@ using NadekoBot.DataStructures; namespace NadekoBot.Modules.NSFW { - public class NSFW : NadekoTopLevelModule + public class NSFW : NadekoTopLevelModule { private static readonly ConcurrentDictionary _autoHentaiTimers = new ConcurrentDictionary(); private static readonly ConcurrentHashSet _hentaiBombBlacklist = new ConcurrentHashSet(); - private readonly SearchesService _service; - - public NSFW(SearchesService service) - { - _service = service; - } private async Task InternalHentai(IMessageChannel channel, string tag, bool noError) { diff --git a/src/NadekoBot/Modules/NadekoModule.cs b/src/NadekoBot/Modules/NadekoModule.cs index db7086fa..1138762b 100644 --- a/src/NadekoBot/Modules/NadekoModule.cs +++ b/src/NadekoBot/Modules/NadekoModule.cs @@ -130,8 +130,22 @@ namespace NadekoBot.Modules } } } + + public abstract class NadekoTopLevelModule : NadekoTopLevelModule where TService : INService + { + public TService _service { get; set; } + + public NadekoTopLevelModule(bool isTopLevel = true) : base(isTopLevel) + { + } + } public abstract class NadekoSubmodule : NadekoTopLevelModule + { + protected NadekoSubmodule() : base(false) { } + } + + public abstract class NadekoSubmodule : NadekoTopLevelModule where TService : INService { protected NadekoSubmodule() : base(false) { diff --git a/src/NadekoBot/Modules/Permissions/Permissions.cs b/src/NadekoBot/Modules/Permissions/Permissions.cs index 2fd612a3..0ba34b9f 100644 --- a/src/NadekoBot/Modules/Permissions/Permissions.cs +++ b/src/NadekoBot/Modules/Permissions/Permissions.cs @@ -13,15 +13,13 @@ using NadekoBot.Services.Permissions; namespace NadekoBot.Modules.Permissions { - public partial class Permissions : NadekoTopLevelModule + public partial class Permissions : NadekoTopLevelModule { private readonly DbService _db; - private readonly PermissionService _service; - public Permissions(PermissionService service, DbService db) + public Permissions(DbService db) { _db = db; - _service = service; } [NadekoCommand, Usage, Description, Aliases] diff --git a/src/NadekoBot/Modules/Pokemon/Pokemon.cs b/src/NadekoBot/Modules/Pokemon/Pokemon.cs index 3b8d47a4..22680a29 100644 --- a/src/NadekoBot/Modules/Pokemon/Pokemon.cs +++ b/src/NadekoBot/Modules/Pokemon/Pokemon.cs @@ -12,16 +12,14 @@ using NadekoBot.Services.Pokemon; namespace NadekoBot.Modules.Pokemon { - public class Pokemon : NadekoTopLevelModule + public class Pokemon : NadekoTopLevelModule { - private readonly PokemonService _service; private readonly DbService _db; private readonly BotConfig _bc; private readonly CurrencyService _cs; - public Pokemon(PokemonService pokemonService, DbService db, BotConfig bc, CurrencyService cs) + public Pokemon(DbService db, BotConfig bc, CurrencyService cs) { - _service = pokemonService; _db = db; _bc = bc; _cs = cs; diff --git a/src/NadekoBot/Modules/Searches/Commands/AnimeSearchCommands.cs b/src/NadekoBot/Modules/Searches/Commands/AnimeSearchCommands.cs index ff41e0ca..edf392e2 100644 --- a/src/NadekoBot/Modules/Searches/Commands/AnimeSearchCommands.cs +++ b/src/NadekoBot/Modules/Searches/Commands/AnimeSearchCommands.cs @@ -15,15 +15,8 @@ namespace NadekoBot.Modules.Searches public partial class Searches { [Group] - public class AnimeSearchCommands : NadekoSubmodule + public class AnimeSearchCommands : NadekoSubmodule { - private readonly AnimeSearchService _service; - - public AnimeSearchCommands(AnimeSearchService service) - { - _service = service; - } - [NadekoCommand, Usage, Description, Aliases] [Priority(0)] public async Task Mal([Remainder] string name) diff --git a/src/NadekoBot/Modules/Searches/Commands/JokeCommands.cs b/src/NadekoBot/Modules/Searches/Commands/JokeCommands.cs index ec653b87..a99f77d7 100644 --- a/src/NadekoBot/Modules/Searches/Commands/JokeCommands.cs +++ b/src/NadekoBot/Modules/Searches/Commands/JokeCommands.cs @@ -14,14 +14,8 @@ namespace NadekoBot.Modules.Searches public partial class Searches { [Group] - public class JokeCommands : NadekoSubmodule + public class JokeCommands : NadekoSubmodule { - private readonly SearchesService _searches; - - public JokeCommands(SearchesService searches) - { - _searches = searches; - } [NadekoCommand, Usage, Description, Aliases] public async Task Yomama() @@ -65,24 +59,24 @@ namespace NadekoBot.Modules.Searches [NadekoCommand, Usage, Description, Aliases] public async Task WowJoke() { - if (!_searches.WowJokes.Any()) + if (!_service.WowJokes.Any()) { await ReplyErrorLocalized("jokes_not_loaded").ConfigureAwait(false); return; } - var joke = _searches.WowJokes[new NadekoRandom().Next(0, _searches.WowJokes.Count)]; + var joke = _service.WowJokes[new NadekoRandom().Next(0, _service.WowJokes.Count)]; await Context.Channel.SendConfirmAsync(joke.Question, joke.Answer).ConfigureAwait(false); } [NadekoCommand, Usage, Description, Aliases] public async Task MagicItem() { - if (!_searches.WowJokes.Any()) + if (!_service.WowJokes.Any()) { await ReplyErrorLocalized("magicitems_not_loaded").ConfigureAwait(false); return; } - var item = _searches.MagicItems[new NadekoRandom().Next(0, _searches.MagicItems.Count)]; + var item = _service.MagicItems[new NadekoRandom().Next(0, _service.MagicItems.Count)]; await Context.Channel.SendConfirmAsync("✨" + item.Name, item.Description).ConfigureAwait(false); } diff --git a/src/NadekoBot/Modules/Searches/Commands/PokemonSearchCommands.cs b/src/NadekoBot/Modules/Searches/Commands/PokemonSearchCommands.cs index b457b02b..4cdec0c0 100644 --- a/src/NadekoBot/Modules/Searches/Commands/PokemonSearchCommands.cs +++ b/src/NadekoBot/Modules/Searches/Commands/PokemonSearchCommands.cs @@ -12,17 +12,10 @@ namespace NadekoBot.Modules.Searches public partial class Searches { [Group] - public class PokemonSearchCommands : NadekoSubmodule + public class PokemonSearchCommands : NadekoSubmodule { - private readonly SearchesService _searches; - - public Dictionary Pokemons => _searches.Pokemons; - public Dictionary PokemonAbilities => _searches.PokemonAbilities; - - public PokemonSearchCommands(SearchesService searches) - { - _searches = searches; - } + public Dictionary Pokemons => _service.Pokemons; + public Dictionary PokemonAbilities => _service.PokemonAbilities; [NadekoCommand, Usage, Description, Aliases] public async Task Pokemon([Remainder] string pokemon = null) diff --git a/src/NadekoBot/Modules/Searches/Commands/StreamNotificationCommands.cs b/src/NadekoBot/Modules/Searches/Commands/StreamNotificationCommands.cs index 30e18523..4ec1a325 100644 --- a/src/NadekoBot/Modules/Searches/Commands/StreamNotificationCommands.cs +++ b/src/NadekoBot/Modules/Searches/Commands/StreamNotificationCommands.cs @@ -15,15 +15,13 @@ namespace NadekoBot.Modules.Searches public partial class Searches { [Group] - public class StreamNotificationCommands : NadekoSubmodule + public class StreamNotificationCommands : NadekoSubmodule { private readonly DbService _db; - private readonly StreamNotificationService _service; - public StreamNotificationCommands(DbService db, StreamNotificationService service) + public StreamNotificationCommands(DbService db) { _db = db; - _service = service; } [NadekoCommand, Usage, Description, Aliases] diff --git a/src/NadekoBot/Modules/Searches/Searches.cs b/src/NadekoBot/Modules/Searches/Searches.cs index ea2990f5..8c45a953 100644 --- a/src/NadekoBot/Modules/Searches/Searches.cs +++ b/src/NadekoBot/Modules/Searches/Searches.cs @@ -25,17 +25,16 @@ using NadekoBot.DataStructures; namespace NadekoBot.Modules.Searches { - public partial class Searches : NadekoTopLevelModule + public partial class Searches : NadekoTopLevelModule { private readonly IBotCredentials _creds; private readonly IGoogleApiService _google; private readonly SearchesService _searches; - public Searches(IBotCredentials creds, IGoogleApiService google, SearchesService searches) + public Searches(IBotCredentials creds, IGoogleApiService google) { _creds = creds; _google = google; - _searches = searches; } [NadekoCommand, Usage, Description, Aliases] diff --git a/src/NadekoBot/Modules/Utility/Commands/CommandMapCommands.cs b/src/NadekoBot/Modules/Utility/Commands/CommandMapCommands.cs index 03316135..bf79d41a 100644 --- a/src/NadekoBot/Modules/Utility/Commands/CommandMapCommands.cs +++ b/src/NadekoBot/Modules/Utility/Commands/CommandMapCommands.cs @@ -17,15 +17,13 @@ namespace NadekoBot.Modules.Utility public partial class Utility { [Group] - public class CommandMapCommands : NadekoSubmodule + public class CommandMapCommands : NadekoSubmodule { - private readonly CommandMapService _service; private readonly DbService _db; private readonly DiscordSocketClient _client; - public CommandMapCommands(CommandMapService service, DbService db, DiscordSocketClient client) + public CommandMapCommands(DbService db, DiscordSocketClient client) { - _service = service; _db = db; _client = client; } diff --git a/src/NadekoBot/Modules/Utility/Commands/PatreonCommands.cs b/src/NadekoBot/Modules/Utility/Commands/PatreonCommands.cs index 6e287374..940f82b6 100644 --- a/src/NadekoBot/Modules/Utility/Commands/PatreonCommands.cs +++ b/src/NadekoBot/Modules/Utility/Commands/PatreonCommands.cs @@ -13,21 +13,19 @@ namespace NadekoBot.Modules.Utility public partial class Utility { [Group] - public class PatreonCommands : NadekoSubmodule + public class PatreonCommands : NadekoSubmodule { - private readonly PatreonRewardsService _patreon; private readonly IBotCredentials _creds; private readonly BotConfig _config; private readonly DbService _db; private readonly CurrencyService _currency; - public PatreonCommands(PatreonRewardsService p, IBotCredentials creds, BotConfig config, DbService db, CurrencyService currency) + public PatreonCommands(IBotCredentials creds, BotConfig config, DbService db, CurrencyService currency) { _creds = creds; _config = config; _db = db; _currency = currency; - _patreon = p; } [NadekoCommand, Usage, Description, Aliases] @@ -37,7 +35,7 @@ namespace NadekoBot.Modules.Utility { if (string.IsNullOrWhiteSpace(_creds.PatreonAccessToken)) return; - await _patreon.RefreshPledges(true).ConfigureAwait(false); + await _service.RefreshPledges(true).ConfigureAwait(false); await Context.Channel.SendConfirmAsync("👌").ConfigureAwait(false); } @@ -57,7 +55,7 @@ namespace NadekoBot.Modules.Utility int amount = 0; try { - amount = await _patreon.ClaimReward(Context.User.Id).ConfigureAwait(false); + amount = await _service.ClaimReward(Context.User.Id).ConfigureAwait(false); } catch (Exception ex) { @@ -69,7 +67,7 @@ namespace NadekoBot.Modules.Utility await ReplyConfirmLocalized("clpa_success", amount + _config.CurrencySign).ConfigureAwait(false); return; } - var rem = (_patreon.Interval - (DateTime.UtcNow - _patreon.LastUpdate)); + var rem = (_service.Interval - (DateTime.UtcNow - _service.LastUpdate)); var helpcmd = Format.Code(Prefix + "donate"); await Context.Channel.EmbedAsync(new EmbedBuilder().WithOkColor() .WithDescription(GetText("clpa_fail")) diff --git a/src/NadekoBot/Modules/Utility/Commands/Remind.cs b/src/NadekoBot/Modules/Utility/Commands/Remind.cs index e16a0486..e87dcfa9 100644 --- a/src/NadekoBot/Modules/Utility/Commands/Remind.cs +++ b/src/NadekoBot/Modules/Utility/Commands/Remind.cs @@ -16,15 +16,13 @@ namespace NadekoBot.Modules.Utility public partial class Utility { [Group] - public class RemindCommands : NadekoSubmodule + public class RemindCommands : NadekoSubmodule { - private readonly RemindService _service; private readonly DbService _db; private readonly GuildTimezoneService _tz; - public RemindCommands(RemindService service, DbService db, GuildTimezoneService tz) + public RemindCommands(DbService db, GuildTimezoneService tz) { - _service = service; _db = db; _tz = tz; } diff --git a/src/NadekoBot/Modules/Utility/Commands/RepeatCommands.cs b/src/NadekoBot/Modules/Utility/Commands/RepeatCommands.cs index 457e7b67..d7f38e0c 100644 --- a/src/NadekoBot/Modules/Utility/Commands/RepeatCommands.cs +++ b/src/NadekoBot/Modules/Utility/Commands/RepeatCommands.cs @@ -19,15 +19,13 @@ namespace NadekoBot.Modules.Utility public partial class Utility { [Group] - public class RepeatCommands : NadekoSubmodule + public class RepeatCommands : NadekoSubmodule { - private readonly MessageRepeaterService _service; private readonly DiscordSocketClient _client; private readonly DbService _db; - public RepeatCommands(MessageRepeaterService service, DiscordSocketClient client, DbService db) + public RepeatCommands(DiscordSocketClient client, DbService db) { - _service = service; _client = client; _db = db; } diff --git a/src/NadekoBot/Modules/Utility/Commands/StreamRoleCommands.cs b/src/NadekoBot/Modules/Utility/Commands/StreamRoleCommands.cs index 5bfa393b..9b534b12 100644 --- a/src/NadekoBot/Modules/Utility/Commands/StreamRoleCommands.cs +++ b/src/NadekoBot/Modules/Utility/Commands/StreamRoleCommands.cs @@ -8,22 +8,15 @@ namespace NadekoBot.Modules.Utility { public partial class Utility { - public class StreamRoleCommands : NadekoSubmodule + public class StreamRoleCommands : NadekoSubmodule { - private readonly StreamRoleService service; - - public StreamRoleCommands(StreamRoleService service) - { - this.service = service; - } - [NadekoCommand, Usage, Description, Aliases] [RequireBotPermission(GuildPermission.ManageRoles)] [RequireUserPermission(GuildPermission.ManageRoles)] [RequireContext(ContextType.Guild)] public async Task StreamRole(IRole fromRole, IRole addRole) { - this.service.SetStreamRole(fromRole, addRole); + this._service.SetStreamRole(fromRole, addRole); await ReplyConfirmLocalized("stream_role_enabled", Format.Bold(fromRole.ToString()), Format.Bold(addRole.ToString())).ConfigureAwait(false); } @@ -34,7 +27,7 @@ namespace NadekoBot.Modules.Utility [RequireContext(ContextType.Guild)] public async Task StreamRole() { - this.service.StopStreamRole(Context.Guild.Id); + this._service.StopStreamRole(Context.Guild.Id); await ReplyConfirmLocalized("stream_role_disabled").ConfigureAwait(false); } } diff --git a/src/NadekoBot/Modules/Utility/Commands/UnitConversion.cs b/src/NadekoBot/Modules/Utility/Commands/UnitConversion.cs index 1e7f5d22..aafa3b1f 100644 --- a/src/NadekoBot/Modules/Utility/Commands/UnitConversion.cs +++ b/src/NadekoBot/Modules/Utility/Commands/UnitConversion.cs @@ -11,15 +11,8 @@ namespace NadekoBot.Modules.Utility public partial class Utility { [Group] - public class UnitConverterCommands : NadekoSubmodule + public class UnitConverterCommands : NadekoSubmodule { - private readonly ConverterService _service; - - public UnitConverterCommands(ConverterService service) - { - _service = service; - } - [NadekoCommand, Usage, Description, Aliases] public async Task ConvertList() { diff --git a/src/NadekoBot/Modules/Utility/Commands/VerboseCommandErrors.cs b/src/NadekoBot/Modules/Utility/Commands/VerboseCommandErrors.cs index 8ec131d0..db73bdda 100644 --- a/src/NadekoBot/Modules/Utility/Commands/VerboseCommandErrors.cs +++ b/src/NadekoBot/Modules/Utility/Commands/VerboseCommandErrors.cs @@ -8,21 +8,14 @@ namespace NadekoBot.Modules.Utility public partial class Utility { [Group] - public class VerboseCommandErrors : NadekoSubmodule + public class VerboseCommandErrors : NadekoSubmodule { - private readonly VerboseErrorsService _ves; - - public VerboseCommandErrors(VerboseErrorsService ves) - { - _ves = ves; - } - [NadekoCommand, Usage, Description, Aliases] [RequireContext(ContextType.Guild)] [RequireUserPermission(Discord.GuildPermission.ManageMessages)] public async Task VerboseError() { - var state = _ves.ToggleVerboseErrors(Context.Guild.Id); + var state = _service.ToggleVerboseErrors(Context.Guild.Id); if (state) await ReplyConfirmLocalized("verbose_errors_enabled").ConfigureAwait(false); diff --git a/src/NadekoBot/NadekoBot.csproj b/src/NadekoBot/NadekoBot.csproj index b0d9d8b0..07193a3c 100644 --- a/src/NadekoBot/NadekoBot.csproj +++ b/src/NadekoBot/NadekoBot.csproj @@ -31,8 +31,11 @@ + + + Always @@ -88,8 +91,4 @@ - - - - diff --git a/src/NadekoBot/Services/ServiceProvider.cs b/src/NadekoBot/Services/ServiceProvider.cs index 03eebec6..6e0c642b 100644 --- a/src/NadekoBot/Services/ServiceProvider.cs +++ b/src/NadekoBot/Services/ServiceProvider.cs @@ -4,12 +4,7 @@ using System.Collections.Concurrent; using System.Collections.Generic; using System.Collections.Immutable; using System.Reflection; -using Discord.Commands; -using Discord.WebSocket; -using NadekoBot.Services.Database.Models; -using NadekoBot.Services.Impl; using System.Linq; -using NadekoBot.Extensions; using System.Diagnostics; using NLog; From 618968d2e4e2bead8779d716576c6dbebc70f515 Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Mon, 17 Jul 2017 04:37:51 +0200 Subject: [PATCH 193/346] More refactoring --- .../{DataStructures => Common}/AsyncLazy.cs | 0 .../{DataStructures => Common}/CREmbed.cs | 0 .../ConcurrentHashSet.cs | 0 .../DisposableImutableList.cs | 0 .../IndexedCollection.cs | 0 .../ModuleBehaviors/IEarlyBlocker.cs | 0 .../ModuleBehaviors/IEarlyBlockingExecutor.cs | 0 .../ModuleBehaviors/IEarlyExecutor.cs | 0 .../ModuleBehaviors/IINputTransformer.cs | 0 .../ModuleBehaviors/ILateBlocker.cs | 0 .../ModuleBehaviors/ILateBlockingExecutor.cs | 0 .../ModuleBehaviors/ILateExecutor.cs | 0 .../NadekoRandom.cs | 0 .../NoPublicBotPrecondition.cs | 0 .../PermissionAction.cs | 0 .../PlatformHelper.cs | 0 .../PoopyRingBuffer.cs | 0 .../Replacements/ReplacementBuilder.cs | 2 +- .../Replacements/Replacer.cs | 0 .../SearchImageCacher.cs | 0 .../Shard0Precondition.cs | 0 .../ShardCom/IShardComMessage.cs | 0 .../ShardCom/ShardComClient.cs | 0 .../ShardCom/ShardComServer.cs | 0 .../SyncPrecondition.cs | 0 .../TypeReaders/BotCommandTypeReader.cs | 2 +- .../TypeReaders/GuildDateTimeTypeReader.cs | 0 .../TypeReaders/GuildTypeReader.cs | 0 .../TypeReaders/ModuleTypeReader.cs | 0 .../TypeReaders/PermissionActionTypeReader.cs | 0 .../Administration/Commands/SelfCommands.cs | 2 +- .../CustomReactions/CustomReactions.cs | 2 +- .../CustomReactions/Extensions}/Extensions.cs | 4 +- .../Services}/CustomReactionsService.cs | 5 +- .../Gambling/Commands/Lucky7Commands.cs | 185 ------------------ src/NadekoBot/Modules/Help/Help.cs | 2 +- .../Help/Services}/HelpService.cs | 3 +- .../Exceptions/NotInVoiceChannelException.cs | 14 ++ .../Common/Exceptions/QueueFullException.cs | 12 ++ .../Exceptions/SongNotFoundException.cs | 12 ++ .../Music/Common}/MusicPlayer.cs | 11 +- .../Music/Common}/MusicQueue.cs | 4 +- .../Music/Common}/SongBuffer.cs | 2 +- .../Music/Common}/SongHandler.cs | 2 +- .../Music/Common}/SongInfo.cs | 2 +- .../SongResolver/ISongResolverFactory.cs | 11 ++ .../SongResolver/SongResolverFactory.cs | 4 +- .../Strategies/IResolverStrategy.cs | 2 +- .../Strategies/LocalSongResolveStrategy.cs | 2 +- .../Strategies/RadioResolveStrategy.cs | 2 +- .../Strategies/SoundCloudResolveStrategy.cs | 6 +- .../Strategies/YoutubeResolveStrategy.cs | 2 +- .../Music/Extensions/Extensions.cs | 5 +- src/NadekoBot/Modules/Music/Music.cs | 6 +- .../Music/Services}/MusicService.cs | 7 +- src/NadekoBot/Modules/Searches/Searches.cs | 3 +- src/NadekoBot/NadekoBot.csproj | 4 + .../Administration/PlayingRotateService.cs | 2 +- src/NadekoBot/Services/Music/Exceptions.cs | 28 --- .../Services/Music/OldSongResolver.cs | 111 ----------- .../SongResolver/ISongResolverFactory.cs | 15 -- .../Services/Utility/VerboseErrorsService.cs | 2 +- 62 files changed, 103 insertions(+), 375 deletions(-) rename src/NadekoBot/{DataStructures => Common}/AsyncLazy.cs (100%) rename src/NadekoBot/{DataStructures => Common}/CREmbed.cs (100%) rename src/NadekoBot/{DataStructures => Common}/ConcurrentHashSet.cs (100%) rename src/NadekoBot/{DataStructures => Common}/DisposableImutableList.cs (100%) rename src/NadekoBot/{DataStructures => Common}/IndexedCollection.cs (100%) rename src/NadekoBot/{DataStructures => Common}/ModuleBehaviors/IEarlyBlocker.cs (100%) rename src/NadekoBot/{DataStructures => Common}/ModuleBehaviors/IEarlyBlockingExecutor.cs (100%) rename src/NadekoBot/{DataStructures => Common}/ModuleBehaviors/IEarlyExecutor.cs (100%) rename src/NadekoBot/{DataStructures => Common}/ModuleBehaviors/IINputTransformer.cs (100%) rename src/NadekoBot/{DataStructures => Common}/ModuleBehaviors/ILateBlocker.cs (100%) rename src/NadekoBot/{DataStructures => Common}/ModuleBehaviors/ILateBlockingExecutor.cs (100%) rename src/NadekoBot/{DataStructures => Common}/ModuleBehaviors/ILateExecutor.cs (100%) rename src/NadekoBot/{DataStructures => Common}/NadekoRandom.cs (100%) rename src/NadekoBot/{DataStructures => Common}/NoPublicBotPrecondition.cs (100%) rename src/NadekoBot/{DataStructures => Common}/PermissionAction.cs (100%) rename src/NadekoBot/{DataStructures => Common}/PlatformHelper.cs (100%) rename src/NadekoBot/{DataStructures => Common}/PoopyRingBuffer.cs (100%) rename src/NadekoBot/{DataStructures => Common}/Replacements/ReplacementBuilder.cs (99%) rename src/NadekoBot/{DataStructures => Common}/Replacements/Replacer.cs (100%) rename src/NadekoBot/{DataStructures => Common}/SearchImageCacher.cs (100%) rename src/NadekoBot/{DataStructures => Common}/Shard0Precondition.cs (100%) rename src/NadekoBot/{DataStructures => Common}/ShardCom/IShardComMessage.cs (100%) rename src/NadekoBot/{DataStructures => Common}/ShardCom/ShardComClient.cs (100%) rename src/NadekoBot/{DataStructures => Common}/ShardCom/ShardComServer.cs (100%) rename src/NadekoBot/{DataStructures => Common}/SyncPrecondition.cs (100%) rename src/NadekoBot/{DataStructures => Common}/TypeReaders/BotCommandTypeReader.cs (98%) rename src/NadekoBot/{DataStructures => Common}/TypeReaders/GuildDateTimeTypeReader.cs (100%) rename src/NadekoBot/{DataStructures => Common}/TypeReaders/GuildTypeReader.cs (100%) rename src/NadekoBot/{DataStructures => Common}/TypeReaders/ModuleTypeReader.cs (100%) rename src/NadekoBot/{DataStructures => Common}/TypeReaders/PermissionActionTypeReader.cs (100%) rename src/NadekoBot/{Services/CustomReactions => Modules/CustomReactions/Extensions}/Extensions.cs (96%) rename src/NadekoBot/{Services/CustomReactions => Modules/CustomReactions/Services}/CustomReactionsService.cs (97%) delete mode 100644 src/NadekoBot/Modules/Gambling/Commands/Lucky7Commands.cs rename src/NadekoBot/{Services/Help => Modules/Help/Services}/HelpService.cs (97%) create mode 100644 src/NadekoBot/Modules/Music/Common/Exceptions/NotInVoiceChannelException.cs create mode 100644 src/NadekoBot/Modules/Music/Common/Exceptions/QueueFullException.cs create mode 100644 src/NadekoBot/Modules/Music/Common/Exceptions/SongNotFoundException.cs rename src/NadekoBot/{Services/Music => Modules/Music/Common}/MusicPlayer.cs (98%) rename src/NadekoBot/{Services/Music => Modules/Music/Common}/MusicQueue.cs (97%) rename src/NadekoBot/{Services/Music => Modules/Music/Common}/SongBuffer.cs (99%) rename src/NadekoBot/{Services/Music => Modules/Music/Common}/SongHandler.cs (78%) rename src/NadekoBot/{Services/Music => Modules/Music/Common}/SongInfo.cs (98%) create mode 100644 src/NadekoBot/Modules/Music/Common/SongResolver/ISongResolverFactory.cs rename src/NadekoBot/{Services/Music => Modules/Music/Common}/SongResolver/SongResolverFactory.cs (92%) rename src/NadekoBot/{Services/Music => Modules/Music/Common}/SongResolver/Strategies/IResolverStrategy.cs (67%) rename src/NadekoBot/{Services/Music => Modules/Music/Common}/SongResolver/Strategies/LocalSongResolveStrategy.cs (91%) rename src/NadekoBot/{Services/Music => Modules/Music/Common}/SongResolver/Strategies/RadioResolveStrategy.cs (98%) rename src/NadekoBot/{Services/Music => Modules/Music/Common}/SongResolver/Strategies/SoundCloudResolveStrategy.cs (82%) rename src/NadekoBot/{Services/Music => Modules/Music/Common}/SongResolver/Strategies/YoutubeResolveStrategy.cs (97%) rename src/NadekoBot/{Services => Modules}/Music/Extensions/Extensions.cs (82%) rename src/NadekoBot/{Services/Music => Modules/Music/Services}/MusicService.cs (98%) delete mode 100644 src/NadekoBot/Services/Music/Exceptions.cs delete mode 100644 src/NadekoBot/Services/Music/OldSongResolver.cs delete mode 100644 src/NadekoBot/Services/Music/SongResolver/ISongResolverFactory.cs diff --git a/src/NadekoBot/DataStructures/AsyncLazy.cs b/src/NadekoBot/Common/AsyncLazy.cs similarity index 100% rename from src/NadekoBot/DataStructures/AsyncLazy.cs rename to src/NadekoBot/Common/AsyncLazy.cs diff --git a/src/NadekoBot/DataStructures/CREmbed.cs b/src/NadekoBot/Common/CREmbed.cs similarity index 100% rename from src/NadekoBot/DataStructures/CREmbed.cs rename to src/NadekoBot/Common/CREmbed.cs diff --git a/src/NadekoBot/DataStructures/ConcurrentHashSet.cs b/src/NadekoBot/Common/ConcurrentHashSet.cs similarity index 100% rename from src/NadekoBot/DataStructures/ConcurrentHashSet.cs rename to src/NadekoBot/Common/ConcurrentHashSet.cs diff --git a/src/NadekoBot/DataStructures/DisposableImutableList.cs b/src/NadekoBot/Common/DisposableImutableList.cs similarity index 100% rename from src/NadekoBot/DataStructures/DisposableImutableList.cs rename to src/NadekoBot/Common/DisposableImutableList.cs diff --git a/src/NadekoBot/DataStructures/IndexedCollection.cs b/src/NadekoBot/Common/IndexedCollection.cs similarity index 100% rename from src/NadekoBot/DataStructures/IndexedCollection.cs rename to src/NadekoBot/Common/IndexedCollection.cs diff --git a/src/NadekoBot/DataStructures/ModuleBehaviors/IEarlyBlocker.cs b/src/NadekoBot/Common/ModuleBehaviors/IEarlyBlocker.cs similarity index 100% rename from src/NadekoBot/DataStructures/ModuleBehaviors/IEarlyBlocker.cs rename to src/NadekoBot/Common/ModuleBehaviors/IEarlyBlocker.cs diff --git a/src/NadekoBot/DataStructures/ModuleBehaviors/IEarlyBlockingExecutor.cs b/src/NadekoBot/Common/ModuleBehaviors/IEarlyBlockingExecutor.cs similarity index 100% rename from src/NadekoBot/DataStructures/ModuleBehaviors/IEarlyBlockingExecutor.cs rename to src/NadekoBot/Common/ModuleBehaviors/IEarlyBlockingExecutor.cs diff --git a/src/NadekoBot/DataStructures/ModuleBehaviors/IEarlyExecutor.cs b/src/NadekoBot/Common/ModuleBehaviors/IEarlyExecutor.cs similarity index 100% rename from src/NadekoBot/DataStructures/ModuleBehaviors/IEarlyExecutor.cs rename to src/NadekoBot/Common/ModuleBehaviors/IEarlyExecutor.cs diff --git a/src/NadekoBot/DataStructures/ModuleBehaviors/IINputTransformer.cs b/src/NadekoBot/Common/ModuleBehaviors/IINputTransformer.cs similarity index 100% rename from src/NadekoBot/DataStructures/ModuleBehaviors/IINputTransformer.cs rename to src/NadekoBot/Common/ModuleBehaviors/IINputTransformer.cs diff --git a/src/NadekoBot/DataStructures/ModuleBehaviors/ILateBlocker.cs b/src/NadekoBot/Common/ModuleBehaviors/ILateBlocker.cs similarity index 100% rename from src/NadekoBot/DataStructures/ModuleBehaviors/ILateBlocker.cs rename to src/NadekoBot/Common/ModuleBehaviors/ILateBlocker.cs diff --git a/src/NadekoBot/DataStructures/ModuleBehaviors/ILateBlockingExecutor.cs b/src/NadekoBot/Common/ModuleBehaviors/ILateBlockingExecutor.cs similarity index 100% rename from src/NadekoBot/DataStructures/ModuleBehaviors/ILateBlockingExecutor.cs rename to src/NadekoBot/Common/ModuleBehaviors/ILateBlockingExecutor.cs diff --git a/src/NadekoBot/DataStructures/ModuleBehaviors/ILateExecutor.cs b/src/NadekoBot/Common/ModuleBehaviors/ILateExecutor.cs similarity index 100% rename from src/NadekoBot/DataStructures/ModuleBehaviors/ILateExecutor.cs rename to src/NadekoBot/Common/ModuleBehaviors/ILateExecutor.cs diff --git a/src/NadekoBot/DataStructures/NadekoRandom.cs b/src/NadekoBot/Common/NadekoRandom.cs similarity index 100% rename from src/NadekoBot/DataStructures/NadekoRandom.cs rename to src/NadekoBot/Common/NadekoRandom.cs diff --git a/src/NadekoBot/DataStructures/NoPublicBotPrecondition.cs b/src/NadekoBot/Common/NoPublicBotPrecondition.cs similarity index 100% rename from src/NadekoBot/DataStructures/NoPublicBotPrecondition.cs rename to src/NadekoBot/Common/NoPublicBotPrecondition.cs diff --git a/src/NadekoBot/DataStructures/PermissionAction.cs b/src/NadekoBot/Common/PermissionAction.cs similarity index 100% rename from src/NadekoBot/DataStructures/PermissionAction.cs rename to src/NadekoBot/Common/PermissionAction.cs diff --git a/src/NadekoBot/DataStructures/PlatformHelper.cs b/src/NadekoBot/Common/PlatformHelper.cs similarity index 100% rename from src/NadekoBot/DataStructures/PlatformHelper.cs rename to src/NadekoBot/Common/PlatformHelper.cs diff --git a/src/NadekoBot/DataStructures/PoopyRingBuffer.cs b/src/NadekoBot/Common/PoopyRingBuffer.cs similarity index 100% rename from src/NadekoBot/DataStructures/PoopyRingBuffer.cs rename to src/NadekoBot/Common/PoopyRingBuffer.cs diff --git a/src/NadekoBot/DataStructures/Replacements/ReplacementBuilder.cs b/src/NadekoBot/Common/Replacements/ReplacementBuilder.cs similarity index 99% rename from src/NadekoBot/DataStructures/Replacements/ReplacementBuilder.cs rename to src/NadekoBot/Common/Replacements/ReplacementBuilder.cs index 0dc367a6..0be7516d 100644 --- a/src/NadekoBot/DataStructures/Replacements/ReplacementBuilder.cs +++ b/src/NadekoBot/Common/Replacements/ReplacementBuilder.cs @@ -2,9 +2,9 @@ using Discord.Commands; using Discord.WebSocket; using NadekoBot.Extensions; +using NadekoBot.Modules.Music.Services; using NadekoBot.Services; using NadekoBot.Services.Administration; -using NadekoBot.Services.Music; using System; using System.Collections.Concurrent; using System.Linq; diff --git a/src/NadekoBot/DataStructures/Replacements/Replacer.cs b/src/NadekoBot/Common/Replacements/Replacer.cs similarity index 100% rename from src/NadekoBot/DataStructures/Replacements/Replacer.cs rename to src/NadekoBot/Common/Replacements/Replacer.cs diff --git a/src/NadekoBot/DataStructures/SearchImageCacher.cs b/src/NadekoBot/Common/SearchImageCacher.cs similarity index 100% rename from src/NadekoBot/DataStructures/SearchImageCacher.cs rename to src/NadekoBot/Common/SearchImageCacher.cs diff --git a/src/NadekoBot/DataStructures/Shard0Precondition.cs b/src/NadekoBot/Common/Shard0Precondition.cs similarity index 100% rename from src/NadekoBot/DataStructures/Shard0Precondition.cs rename to src/NadekoBot/Common/Shard0Precondition.cs diff --git a/src/NadekoBot/DataStructures/ShardCom/IShardComMessage.cs b/src/NadekoBot/Common/ShardCom/IShardComMessage.cs similarity index 100% rename from src/NadekoBot/DataStructures/ShardCom/IShardComMessage.cs rename to src/NadekoBot/Common/ShardCom/IShardComMessage.cs diff --git a/src/NadekoBot/DataStructures/ShardCom/ShardComClient.cs b/src/NadekoBot/Common/ShardCom/ShardComClient.cs similarity index 100% rename from src/NadekoBot/DataStructures/ShardCom/ShardComClient.cs rename to src/NadekoBot/Common/ShardCom/ShardComClient.cs diff --git a/src/NadekoBot/DataStructures/ShardCom/ShardComServer.cs b/src/NadekoBot/Common/ShardCom/ShardComServer.cs similarity index 100% rename from src/NadekoBot/DataStructures/ShardCom/ShardComServer.cs rename to src/NadekoBot/Common/ShardCom/ShardComServer.cs diff --git a/src/NadekoBot/DataStructures/SyncPrecondition.cs b/src/NadekoBot/Common/SyncPrecondition.cs similarity index 100% rename from src/NadekoBot/DataStructures/SyncPrecondition.cs rename to src/NadekoBot/Common/SyncPrecondition.cs diff --git a/src/NadekoBot/DataStructures/TypeReaders/BotCommandTypeReader.cs b/src/NadekoBot/Common/TypeReaders/BotCommandTypeReader.cs similarity index 98% rename from src/NadekoBot/DataStructures/TypeReaders/BotCommandTypeReader.cs rename to src/NadekoBot/Common/TypeReaders/BotCommandTypeReader.cs index d3e7b2c3..84c45c1f 100644 --- a/src/NadekoBot/DataStructures/TypeReaders/BotCommandTypeReader.cs +++ b/src/NadekoBot/Common/TypeReaders/BotCommandTypeReader.cs @@ -1,6 +1,6 @@ using Discord.Commands; +using NadekoBot.Modules.CustomReactions.Services; using NadekoBot.Services; -using NadekoBot.Services.CustomReactions; using System; using System.Linq; using System.Threading.Tasks; diff --git a/src/NadekoBot/DataStructures/TypeReaders/GuildDateTimeTypeReader.cs b/src/NadekoBot/Common/TypeReaders/GuildDateTimeTypeReader.cs similarity index 100% rename from src/NadekoBot/DataStructures/TypeReaders/GuildDateTimeTypeReader.cs rename to src/NadekoBot/Common/TypeReaders/GuildDateTimeTypeReader.cs diff --git a/src/NadekoBot/DataStructures/TypeReaders/GuildTypeReader.cs b/src/NadekoBot/Common/TypeReaders/GuildTypeReader.cs similarity index 100% rename from src/NadekoBot/DataStructures/TypeReaders/GuildTypeReader.cs rename to src/NadekoBot/Common/TypeReaders/GuildTypeReader.cs diff --git a/src/NadekoBot/DataStructures/TypeReaders/ModuleTypeReader.cs b/src/NadekoBot/Common/TypeReaders/ModuleTypeReader.cs similarity index 100% rename from src/NadekoBot/DataStructures/TypeReaders/ModuleTypeReader.cs rename to src/NadekoBot/Common/TypeReaders/ModuleTypeReader.cs diff --git a/src/NadekoBot/DataStructures/TypeReaders/PermissionActionTypeReader.cs b/src/NadekoBot/Common/TypeReaders/PermissionActionTypeReader.cs similarity index 100% rename from src/NadekoBot/DataStructures/TypeReaders/PermissionActionTypeReader.cs rename to src/NadekoBot/Common/TypeReaders/PermissionActionTypeReader.cs diff --git a/src/NadekoBot/Modules/Administration/Commands/SelfCommands.cs b/src/NadekoBot/Modules/Administration/Commands/SelfCommands.cs index 4e348f62..f669542c 100644 --- a/src/NadekoBot/Modules/Administration/Commands/SelfCommands.cs +++ b/src/NadekoBot/Modules/Administration/Commands/SelfCommands.cs @@ -14,7 +14,7 @@ using NadekoBot.Services.Database.Models; using Microsoft.EntityFrameworkCore; using NadekoBot.Services.Administration; using System.Diagnostics; -using NadekoBot.Services.Music; +using NadekoBot.Modules.Music.Services; namespace NadekoBot.Modules.Administration { diff --git a/src/NadekoBot/Modules/CustomReactions/CustomReactions.cs b/src/NadekoBot/Modules/CustomReactions/CustomReactions.cs index da99f55d..e4aa6e3c 100644 --- a/src/NadekoBot/Modules/CustomReactions/CustomReactions.cs +++ b/src/NadekoBot/Modules/CustomReactions/CustomReactions.cs @@ -8,7 +8,7 @@ using Discord; using NadekoBot.Extensions; using Discord.WebSocket; using System; -using NadekoBot.Services.CustomReactions; +using NadekoBot.Modules.CustomReactions.Services; namespace NadekoBot.Modules.CustomReactions { diff --git a/src/NadekoBot/Services/CustomReactions/Extensions.cs b/src/NadekoBot/Modules/CustomReactions/Extensions/Extensions.cs similarity index 96% rename from src/NadekoBot/Services/CustomReactions/Extensions.cs rename to src/NadekoBot/Modules/CustomReactions/Extensions/Extensions.cs index 351ac6d2..3e87ca37 100644 --- a/src/NadekoBot/Services/CustomReactions/Extensions.cs +++ b/src/NadekoBot/Modules/CustomReactions/Extensions/Extensions.cs @@ -5,6 +5,8 @@ using Discord.WebSocket; using NadekoBot.DataStructures; using NadekoBot.DataStructures.Replacements; using NadekoBot.Extensions; +using NadekoBot.Modules.CustomReactions.Services; +using NadekoBot.Services; using NadekoBot.Services.Database.Models; using System; using System.Collections.Generic; @@ -12,7 +14,7 @@ using System.Linq; using System.Text.RegularExpressions; using System.Threading.Tasks; -namespace NadekoBot.Services.CustomReactions +namespace NadekoBot.Modules.CustomReactions.Extensions { public static class Extensions { diff --git a/src/NadekoBot/Services/CustomReactions/CustomReactionsService.cs b/src/NadekoBot/Modules/CustomReactions/Services/CustomReactionsService.cs similarity index 97% rename from src/NadekoBot/Services/CustomReactions/CustomReactionsService.cs rename to src/NadekoBot/Modules/CustomReactions/Services/CustomReactionsService.cs index 2e068cde..b87d4145 100644 --- a/src/NadekoBot/Services/CustomReactions/CustomReactionsService.cs +++ b/src/NadekoBot/Modules/CustomReactions/Services/CustomReactionsService.cs @@ -4,15 +4,16 @@ using NadekoBot.DataStructures.ModuleBehaviors; using NadekoBot.Services.Database.Models; using NLog; using System.Collections.Concurrent; -using System.Diagnostics; using System.Linq; using System; using System.Threading.Tasks; using NadekoBot.Services.Permissions; using NadekoBot.Extensions; using NadekoBot.Services.Database; +using NadekoBot.Services; +using NadekoBot.Modules.CustomReactions.Extensions; -namespace NadekoBot.Services.CustomReactions +namespace NadekoBot.Modules.CustomReactions.Services { public class CustomReactionsService : IEarlyBlockingExecutor, INService { diff --git a/src/NadekoBot/Modules/Gambling/Commands/Lucky7Commands.cs b/src/NadekoBot/Modules/Gambling/Commands/Lucky7Commands.cs deleted file mode 100644 index 725fe7c0..00000000 --- a/src/NadekoBot/Modules/Gambling/Commands/Lucky7Commands.cs +++ /dev/null @@ -1,185 +0,0 @@ -namespace NadekoBot.Modules.Gambling -{ - //public partial class Gambling - //{ - // [Group] - // public class Lucky7Commands : NadekoSubmodule - // { - // [NadekoCommand, Usage, Description, Aliases] - // [RequireContext(ContextType.Guild)] - // [OwnerOnly] - // public async Task Lucky7Test(uint tests) - // { - // if (tests <= 0) - // return; - - // var dict = new Dictionary(); - // var totalWon = 0; - // for (var i = 0; i < tests; i++) - // { - // var g = new Lucky7Game(10); - // while (!g.Ended) - // { - // if (g.CurrentPosition == 0) - // g.Stay(); - // else - // g.Move(); - // } - // totalWon += (int)(g.CurrentMultiplier * g.Bet); - // if (!dict.ContainsKey(g.CurrentMultiplier)) - // dict.Add(g.CurrentMultiplier, 0); - - // dict[g.CurrentMultiplier] ++; - - // } - - // await Context.Channel.EmbedAsync(new EmbedBuilder().WithOkColor() - // .WithTitle("Move Or Stay test") - // .WithDescription(string.Join("\n", - // dict.Select(x => $"x{x.Key} occured {x.Value} times {x.Value * 1.0f / tests * 100:F2}%"))) - // .WithFooter( - // efb => efb.WithText($"Total Bet: {tests * 10} | Payout: {totalWon} | {totalWon *1.0f / tests * 10}%"))); - // } - - // private static readonly ConcurrentDictionary _games = - // new ConcurrentDictionary(); - - // [NadekoCommand, Usage, Description, Aliases] - // [RequireContext(ContextType.Guild)] - // public async Task Lucky7(int bet) - // { - // if (bet < 4) - // return; - // var game = new Lucky7Game(bet); - // if (!_games.TryAdd(Context.User.Id, game)) - // { - // await ReplyAsync("You're already betting on move or stay.").ConfigureAwait(false); - // return; - // } - - // if (!await CurrencyHandler.RemoveCurrencyAsync(Context.User, "MoveOrStay bet", bet, false)) - // { - // _games.TryRemove(Context.User.Id, out game); - // await ReplyConfirmLocalized("not_enough", CurrencySign).ConfigureAwait(false); - // return; - // } - // await Context.Channel.EmbedAsync(GetGameState(game), - // string.Format("{0} rolled {1}.", Context.User, game.Rolled)).ConfigureAwait(false); - // } - - // public enum MoveOrStay - // { - // Move = 1, - // M = 1, - // Stay = 2, - // S = 2 - // } - - // [NadekoCommand, Usage, Description, Aliases] - // [RequireContext(ContextType.Guild)] - // public async Task Lucky7(MoveOrStay action) - // { - // Lucky7Game game; - // if (!_games.TryGetValue(Context.User.Id, out game)) - // { - // await ReplyAsync("You're not betting on move or stay.").ConfigureAwait(false); - // return; - // } - - // if (action == MoveOrStay.Move) - // { - // game.Move(); - // await Context.Channel.EmbedAsync(GetGameState(game), - // string.Format("{0} rolled {1}.", Context.User, game.Rolled)).ConfigureAwait(false); - // if (game.Ended) - // _games.TryRemove(Context.User.Id, out game); - // } - // else if (action == MoveOrStay.Stay) - // { - // var won = game.Stay(); - // await CurrencyHandler.AddCurrencyAsync(Context.User, "MoveOrStay stay", won, false) - // .ConfigureAwait(false); - // _games.TryRemove(Context.User.Id, out game); - // await ReplyAsync(string.Format("You've finished with {0}", - // won + CurrencySign)) - // .ConfigureAwait(false); - // } - - - // } - - // private EmbedBuilder GetGameState(Lucky7Game game) - // { - // var arr = Lucky7Game.Winnings.ToArray(); - // var sb = new StringBuilder(); - // for (var i = 0; i < arr.Length; i++) - // { - // if (i == game.CurrentPosition) - // { - // sb.Append("[" + arr[i] + "]"); - // } - // else - // { - // sb.Append(arr[i].ToString()); - // } - - // if (i != arr.Length - 1) - // sb.Append(' '); - // } - - // return new EmbedBuilder().WithOkColor() - // .WithTitle("Lucky7") - // .WithDescription(sb.ToString()) - // .AddField(efb => efb.WithName("Bet") - // .WithValue(game.Bet.ToString()) - // .WithIsInline(true)) - // .AddField(efb => efb.WithName("Current Value") - // .WithValue((game.CurrentMultiplier * game.Bet).ToString(_cultureInfo)) - // .WithIsInline(true)); - // } - // } - - // public class Lucky7Game - // { - // public int Bet { get; } - // public bool Ended { get; private set; } - // public int PreviousPosition { get; private set; } - // public int CurrentPosition { get; private set; } = -1; - // public int Rolled { get; private set; } - // public float CurrentMultiplier => Winnings[CurrentPosition]; - // private readonly NadekoRandom _rng = new NadekoRandom(); - - // public static readonly ImmutableArray Winnings = new[] - // { - // 1.2f, 0.8f, 0.75f, 0.90f, 0.7f, 0.5f, 1.8f, 0f, 0f - // }.ToImmutableArray(); - - // public Lucky7Game(int bet) - // { - // Bet = bet; - // Move(); - // } - - // public void Move() - // { - // if (Ended) - // return; - // PreviousPosition = CurrentPosition; - // Rolled = _rng.Next(1, 4); - // CurrentPosition += Rolled; - - // if (CurrentPosition >= 6) - // Ended = true; - // } - - // public int Stay() - // { - // if (Ended) - // return 0; - - // Ended = true; - // return (int) (CurrentMultiplier * Bet); - // } - // } - //} -} diff --git a/src/NadekoBot/Modules/Help/Help.cs b/src/NadekoBot/Modules/Help/Help.cs index c492810d..2cd965cb 100644 --- a/src/NadekoBot/Modules/Help/Help.cs +++ b/src/NadekoBot/Modules/Help/Help.cs @@ -11,7 +11,7 @@ using System.Text; using System.Collections.Generic; using NadekoBot.Services.Database.Models; using NadekoBot.Services.Permissions; -using NadekoBot.Services.Help; +using NadekoBot.Modules.Help.Services; namespace NadekoBot.Modules.Help { diff --git a/src/NadekoBot/Services/Help/HelpService.cs b/src/NadekoBot/Modules/Help/Services/HelpService.cs similarity index 97% rename from src/NadekoBot/Services/Help/HelpService.cs rename to src/NadekoBot/Modules/Help/Services/HelpService.cs index b685a50b..0aba1425 100644 --- a/src/NadekoBot/Services/Help/HelpService.cs +++ b/src/NadekoBot/Modules/Help/Services/HelpService.cs @@ -8,8 +8,9 @@ using Discord.Commands; using NadekoBot.Extensions; using System.Linq; using NadekoBot.Attributes; +using NadekoBot.Services; -namespace NadekoBot.Services.Help +namespace NadekoBot.Modules.Help.Services { public class HelpService : ILateExecutor, INService { diff --git a/src/NadekoBot/Modules/Music/Common/Exceptions/NotInVoiceChannelException.cs b/src/NadekoBot/Modules/Music/Common/Exceptions/NotInVoiceChannelException.cs new file mode 100644 index 00000000..b4fbe8a4 --- /dev/null +++ b/src/NadekoBot/Modules/Music/Common/Exceptions/NotInVoiceChannelException.cs @@ -0,0 +1,14 @@ +using System; + +namespace NadekoBot.Modules.Music.Common.Exceptions +{ + // todo use this + public class NotInVoiceChannelException : Exception + { + public NotInVoiceChannelException(string message) : base(message) + { + } + + public NotInVoiceChannelException() : base("You're not in the voice channel on this server.") { } + } +} diff --git a/src/NadekoBot/Modules/Music/Common/Exceptions/QueueFullException.cs b/src/NadekoBot/Modules/Music/Common/Exceptions/QueueFullException.cs new file mode 100644 index 00000000..be92f09d --- /dev/null +++ b/src/NadekoBot/Modules/Music/Common/Exceptions/QueueFullException.cs @@ -0,0 +1,12 @@ +using System; + +namespace NadekoBot.Modules.Music.Common.Exceptions +{ + public class QueueFullException : Exception + { + public QueueFullException(string message) : base(message) + { + } + public QueueFullException() : base("Queue is full.") { } + } +} diff --git a/src/NadekoBot/Modules/Music/Common/Exceptions/SongNotFoundException.cs b/src/NadekoBot/Modules/Music/Common/Exceptions/SongNotFoundException.cs new file mode 100644 index 00000000..823d2776 --- /dev/null +++ b/src/NadekoBot/Modules/Music/Common/Exceptions/SongNotFoundException.cs @@ -0,0 +1,12 @@ +using System; + +namespace NadekoBot.Modules.Music.Common.Exceptions +{ + public class SongNotFoundException : Exception + { + public SongNotFoundException(string message) : base(message) + { + } + public SongNotFoundException() : base("Song is not found.") { } + } +} diff --git a/src/NadekoBot/Services/Music/MusicPlayer.cs b/src/NadekoBot/Modules/Music/Common/MusicPlayer.cs similarity index 98% rename from src/NadekoBot/Services/Music/MusicPlayer.cs rename to src/NadekoBot/Modules/Music/Common/MusicPlayer.cs index 8e47e984..e8411f43 100644 --- a/src/NadekoBot/Services/Music/MusicPlayer.cs +++ b/src/NadekoBot/Modules/Music/Common/MusicPlayer.cs @@ -8,8 +8,11 @@ using System.Linq; using System.Collections.Concurrent; using NadekoBot.Extensions; using System.Diagnostics; +using NadekoBot.Modules.Music.Services; +using NadekoBot.Services; +using NadekoBot.Services.Database.Models; -namespace NadekoBot.Services.Music +namespace NadekoBot.Modules.Music.Common { public enum StreamState { @@ -167,7 +170,7 @@ namespace NadekoBot.Services.Music SongBuffer b = null; try { - b = new SongBuffer(await data.Song.Uri(), "", data.Song.ProviderType == Database.Models.MusicType.Local); + b = new SongBuffer(await data.Song.Uri(), "", data.Song.ProviderType == MusicType.Local); //_log.Info("Created buffer, buffering..."); //var bufferTask = b.StartBuffering(cancelToken); @@ -270,7 +273,7 @@ namespace NadekoBot.Services.Music { //if last song, and autoplay is enabled, and if it's a youtube song // do autplay magix - if (queueCount - 1 == data.Index && Autoplay && data.Song?.ProviderType == Database.Models.MusicType.YouTube) + if (queueCount - 1 == data.Index && Autoplay && data.Song?.ProviderType == MusicType.YouTube) { try { @@ -614,7 +617,7 @@ namespace NadekoBot.Services.Music var sw = Stopwatch.StartNew(); var (_, songs) = Queue.ToArray(); var toUpdate = songs - .Where(x => x.ProviderType == Database.Models.MusicType.YouTube + .Where(x => x.ProviderType == MusicType.YouTube && x.TotalTime == TimeSpan.Zero); var vIds = toUpdate.Select(x => x.VideoId); diff --git a/src/NadekoBot/Services/Music/MusicQueue.cs b/src/NadekoBot/Modules/Music/Common/MusicQueue.cs similarity index 97% rename from src/NadekoBot/Services/Music/MusicQueue.cs rename to src/NadekoBot/Modules/Music/Common/MusicQueue.cs index 861765b3..7fa58a51 100644 --- a/src/NadekoBot/Services/Music/MusicQueue.cs +++ b/src/NadekoBot/Modules/Music/Common/MusicQueue.cs @@ -1,11 +1,13 @@ using NadekoBot.Extensions; +using NadekoBot.Modules.Music.Common.Exceptions; +using NadekoBot.Services; using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; -namespace NadekoBot.Services.Music +namespace NadekoBot.Modules.Music.Common { public class MusicQueue : IDisposable { diff --git a/src/NadekoBot/Services/Music/SongBuffer.cs b/src/NadekoBot/Modules/Music/Common/SongBuffer.cs similarity index 99% rename from src/NadekoBot/Services/Music/SongBuffer.cs rename to src/NadekoBot/Modules/Music/Common/SongBuffer.cs index a9d30261..8f76be79 100644 --- a/src/NadekoBot/Services/Music/SongBuffer.cs +++ b/src/NadekoBot/Modules/Music/Common/SongBuffer.cs @@ -5,7 +5,7 @@ using System.IO; using System.Threading; using System.Threading.Tasks; -namespace NadekoBot.Services.Music +namespace NadekoBot.Modules.Music.Common { public class SongBuffer : IDisposable { diff --git a/src/NadekoBot/Services/Music/SongHandler.cs b/src/NadekoBot/Modules/Music/Common/SongHandler.cs similarity index 78% rename from src/NadekoBot/Services/Music/SongHandler.cs rename to src/NadekoBot/Modules/Music/Common/SongHandler.cs index f716196b..660ec8f8 100644 --- a/src/NadekoBot/Services/Music/SongHandler.cs +++ b/src/NadekoBot/Modules/Music/Common/SongHandler.cs @@ -1,6 +1,6 @@ using NLog; -namespace NadekoBot.Services.Music +namespace NadekoBot.Modules.Music.Common { public static class SongHandler { diff --git a/src/NadekoBot/Services/Music/SongInfo.cs b/src/NadekoBot/Modules/Music/Common/SongInfo.cs similarity index 98% rename from src/NadekoBot/Services/Music/SongInfo.cs rename to src/NadekoBot/Modules/Music/Common/SongInfo.cs index db4a65e7..c19f8057 100644 --- a/src/NadekoBot/Services/Music/SongInfo.cs +++ b/src/NadekoBot/Modules/Music/Common/SongInfo.cs @@ -6,7 +6,7 @@ using System.Net; using System.Text.RegularExpressions; using System.Threading.Tasks; -namespace NadekoBot.Services.Music +namespace NadekoBot.Modules.Music.Common { public class SongInfo { diff --git a/src/NadekoBot/Modules/Music/Common/SongResolver/ISongResolverFactory.cs b/src/NadekoBot/Modules/Music/Common/SongResolver/ISongResolverFactory.cs new file mode 100644 index 00000000..c7d79e73 --- /dev/null +++ b/src/NadekoBot/Modules/Music/Common/SongResolver/ISongResolverFactory.cs @@ -0,0 +1,11 @@ +using NadekoBot.Modules.Music.Common.SongResolver.Strategies; +using NadekoBot.Services.Database.Models; +using System.Threading.Tasks; + +namespace NadekoBot.Modules.Music.Common.SongResolver +{ + public interface ISongResolverFactory + { + Task GetResolveStrategy(string query, MusicType? musicType); + } +} diff --git a/src/NadekoBot/Services/Music/SongResolver/SongResolverFactory.cs b/src/NadekoBot/Modules/Music/Common/SongResolver/SongResolverFactory.cs similarity index 92% rename from src/NadekoBot/Services/Music/SongResolver/SongResolverFactory.cs rename to src/NadekoBot/Modules/Music/Common/SongResolver/SongResolverFactory.cs index a70102f1..7e65701c 100644 --- a/src/NadekoBot/Services/Music/SongResolver/SongResolverFactory.cs +++ b/src/NadekoBot/Modules/Music/Common/SongResolver/SongResolverFactory.cs @@ -1,9 +1,9 @@ using System.Threading.Tasks; using NadekoBot.Services.Database.Models; -using NadekoBot.Services.Music.SongResolver.Strategies; using NadekoBot.Services.Impl; +using NadekoBot.Modules.Music.Common.SongResolver.Strategies; -namespace NadekoBot.Services.Music.SongResolver +namespace NadekoBot.Modules.Music.Common.SongResolver { public class SongResolverFactory : ISongResolverFactory { diff --git a/src/NadekoBot/Services/Music/SongResolver/Strategies/IResolverStrategy.cs b/src/NadekoBot/Modules/Music/Common/SongResolver/Strategies/IResolverStrategy.cs similarity index 67% rename from src/NadekoBot/Services/Music/SongResolver/Strategies/IResolverStrategy.cs rename to src/NadekoBot/Modules/Music/Common/SongResolver/Strategies/IResolverStrategy.cs index 42a77637..ec709dab 100644 --- a/src/NadekoBot/Services/Music/SongResolver/Strategies/IResolverStrategy.cs +++ b/src/NadekoBot/Modules/Music/Common/SongResolver/Strategies/IResolverStrategy.cs @@ -1,6 +1,6 @@ using System.Threading.Tasks; -namespace NadekoBot.Services.Music.SongResolver.Strategies +namespace NadekoBot.Modules.Music.Common.SongResolver.Strategies { public interface IResolveStrategy { diff --git a/src/NadekoBot/Services/Music/SongResolver/Strategies/LocalSongResolveStrategy.cs b/src/NadekoBot/Modules/Music/Common/SongResolver/Strategies/LocalSongResolveStrategy.cs similarity index 91% rename from src/NadekoBot/Services/Music/SongResolver/Strategies/LocalSongResolveStrategy.cs rename to src/NadekoBot/Modules/Music/Common/SongResolver/Strategies/LocalSongResolveStrategy.cs index bac28fd8..413dfa9d 100644 --- a/src/NadekoBot/Services/Music/SongResolver/Strategies/LocalSongResolveStrategy.cs +++ b/src/NadekoBot/Modules/Music/Common/SongResolver/Strategies/LocalSongResolveStrategy.cs @@ -2,7 +2,7 @@ using System.IO; using System.Threading.Tasks; -namespace NadekoBot.Services.Music.SongResolver.Strategies +namespace NadekoBot.Modules.Music.Common.SongResolver.Strategies { public class LocalSongResolveStrategy : IResolveStrategy { diff --git a/src/NadekoBot/Services/Music/SongResolver/Strategies/RadioResolveStrategy.cs b/src/NadekoBot/Modules/Music/Common/SongResolver/Strategies/RadioResolveStrategy.cs similarity index 98% rename from src/NadekoBot/Services/Music/SongResolver/Strategies/RadioResolveStrategy.cs rename to src/NadekoBot/Modules/Music/Common/SongResolver/Strategies/RadioResolveStrategy.cs index 212db609..5e11ae4e 100644 --- a/src/NadekoBot/Services/Music/SongResolver/Strategies/RadioResolveStrategy.cs +++ b/src/NadekoBot/Modules/Music/Common/SongResolver/Strategies/RadioResolveStrategy.cs @@ -5,7 +5,7 @@ using System.Net.Http; using System.Text.RegularExpressions; using System.Threading.Tasks; -namespace NadekoBot.Services.Music.SongResolver.Strategies +namespace NadekoBot.Modules.Music.Common.SongResolver.Strategies { public class RadioResolveStrategy : IResolveStrategy { diff --git a/src/NadekoBot/Services/Music/SongResolver/Strategies/SoundCloudResolveStrategy.cs b/src/NadekoBot/Modules/Music/Common/SongResolver/Strategies/SoundCloudResolveStrategy.cs similarity index 82% rename from src/NadekoBot/Services/Music/SongResolver/Strategies/SoundCloudResolveStrategy.cs rename to src/NadekoBot/Modules/Music/Common/SongResolver/Strategies/SoundCloudResolveStrategy.cs index 3e2b9269..66e42075 100644 --- a/src/NadekoBot/Services/Music/SongResolver/Strategies/SoundCloudResolveStrategy.cs +++ b/src/NadekoBot/Modules/Music/Common/SongResolver/Strategies/SoundCloudResolveStrategy.cs @@ -1,8 +1,8 @@ -using NadekoBot.Services.Impl; -using NadekoBot.Services.Music.Extensions; +using NadekoBot.Modules.Music.Extensions; +using NadekoBot.Services.Impl; using System.Threading.Tasks; -namespace NadekoBot.Services.Music.SongResolver.Strategies +namespace NadekoBot.Modules.Music.Common.SongResolver.Strategies { public class SoundcloudResolveStrategy : IResolveStrategy { diff --git a/src/NadekoBot/Services/Music/SongResolver/Strategies/YoutubeResolveStrategy.cs b/src/NadekoBot/Modules/Music/Common/SongResolver/Strategies/YoutubeResolveStrategy.cs similarity index 97% rename from src/NadekoBot/Services/Music/SongResolver/Strategies/YoutubeResolveStrategy.cs rename to src/NadekoBot/Modules/Music/Common/SongResolver/Strategies/YoutubeResolveStrategy.cs index b183e9d4..e4a0058e 100644 --- a/src/NadekoBot/Services/Music/SongResolver/Strategies/YoutubeResolveStrategy.cs +++ b/src/NadekoBot/Modules/Music/Common/SongResolver/Strategies/YoutubeResolveStrategy.cs @@ -5,7 +5,7 @@ using System; using System.Globalization; using System.Threading.Tasks; -namespace NadekoBot.Services.Music.SongResolver.Strategies +namespace NadekoBot.Modules.Music.Common.SongResolver.Strategies { public class YoutubeResolveStrategy : IResolveStrategy { diff --git a/src/NadekoBot/Services/Music/Extensions/Extensions.cs b/src/NadekoBot/Modules/Music/Extensions/Extensions.cs similarity index 82% rename from src/NadekoBot/Services/Music/Extensions/Extensions.cs rename to src/NadekoBot/Modules/Music/Extensions/Extensions.cs index 4403c2dc..b3eb1d38 100644 --- a/src/NadekoBot/Services/Music/Extensions/Extensions.cs +++ b/src/NadekoBot/Modules/Music/Extensions/Extensions.cs @@ -1,9 +1,10 @@ -using NadekoBot.Services.Database.Models; +using NadekoBot.Modules.Music.Common; +using NadekoBot.Services.Database.Models; using NadekoBot.Services.Impl; using System; using System.Threading.Tasks; -namespace NadekoBot.Services.Music.Extensions +namespace NadekoBot.Modules.Music.Extensions { public static class Extensions { diff --git a/src/NadekoBot/Modules/Music/Music.cs b/src/NadekoBot/Modules/Music/Music.cs index b20f1589..1473e247 100644 --- a/src/NadekoBot/Modules/Music/Music.cs +++ b/src/NadekoBot/Modules/Music/Music.cs @@ -9,14 +9,16 @@ using System.Linq; using NadekoBot.Extensions; using System.Collections.Generic; using NadekoBot.Services.Database.Models; -using NadekoBot.Services.Music; using NadekoBot.DataStructures; using System.Collections.Concurrent; using System.IO; using System.Net.Http; using Newtonsoft.Json.Linq; -using NadekoBot.Services.Music.Extensions; using NadekoBot.Services.Impl; +using NadekoBot.Modules.Music.Services; +using NadekoBot.Modules.Music.Common.Exceptions; +using NadekoBot.Modules.Music.Common; +using NadekoBot.Modules.Music.Extensions; namespace NadekoBot.Modules.Music { diff --git a/src/NadekoBot/Services/Music/MusicService.cs b/src/NadekoBot/Modules/Music/Services/MusicService.cs similarity index 98% rename from src/NadekoBot/Services/Music/MusicService.cs rename to src/NadekoBot/Modules/Music/Services/MusicService.cs index 4b498b42..17178cb6 100644 --- a/src/NadekoBot/Services/Music/MusicService.cs +++ b/src/NadekoBot/Modules/Music/Services/MusicService.cs @@ -10,10 +10,13 @@ using System.IO; using System.Collections.Generic; using Discord.Commands; using Discord.WebSocket; -using NadekoBot.Services.Music.SongResolver; using NadekoBot.Services.Impl; +using NadekoBot.Services; +using NadekoBot.Modules.Music.Common; +using NadekoBot.Modules.Music.Common.Exceptions; +using NadekoBot.Modules.Music.Common.SongResolver; -namespace NadekoBot.Services.Music +namespace NadekoBot.Modules.Music.Services { public class MusicService : INService { diff --git a/src/NadekoBot/Modules/Searches/Searches.cs b/src/NadekoBot/Modules/Searches/Searches.cs index 8c45a953..f9961c38 100644 --- a/src/NadekoBot/Modules/Searches/Searches.cs +++ b/src/NadekoBot/Modules/Searches/Searches.cs @@ -29,7 +29,6 @@ namespace NadekoBot.Modules.Searches { private readonly IBotCredentials _creds; private readonly IGoogleApiService _google; - private readonly SearchesService _searches; public Searches(IBotCredentials creds, IGoogleApiService google) { @@ -791,7 +790,7 @@ namespace NadekoBot.Modules.Searches tag = tag?.Trim() ?? ""; - var imgObj = await _searches.DapiSearch(tag, type, Context.Guild?.Id).ConfigureAwait(false); + var imgObj = await _service.DapiSearch(tag, type, Context.Guild?.Id).ConfigureAwait(false); if (imgObj == null) await channel.SendErrorAsync(umsg.Author.Mention + " " + GetText("no_results")); diff --git a/src/NadekoBot/NadekoBot.csproj b/src/NadekoBot/NadekoBot.csproj index 07193a3c..1a906d3b 100644 --- a/src/NadekoBot/NadekoBot.csproj +++ b/src/NadekoBot/NadekoBot.csproj @@ -91,4 +91,8 @@ + + + + diff --git a/src/NadekoBot/Services/Administration/PlayingRotateService.cs b/src/NadekoBot/Services/Administration/PlayingRotateService.cs index 45603150..a0122ae9 100644 --- a/src/NadekoBot/Services/Administration/PlayingRotateService.cs +++ b/src/NadekoBot/Services/Administration/PlayingRotateService.cs @@ -1,7 +1,7 @@ using Discord.WebSocket; using NadekoBot.DataStructures.Replacements; +using NadekoBot.Modules.Music.Services; using NadekoBot.Services.Database.Models; -using NadekoBot.Services.Music; using NLog; using System; using System.Linq; diff --git a/src/NadekoBot/Services/Music/Exceptions.cs b/src/NadekoBot/Services/Music/Exceptions.cs deleted file mode 100644 index 8d4dab72..00000000 --- a/src/NadekoBot/Services/Music/Exceptions.cs +++ /dev/null @@ -1,28 +0,0 @@ -using System; - -namespace NadekoBot.Services.Music -{ - public class QueueFullException : Exception - { - public QueueFullException(string message) : base(message) - { - } - public QueueFullException() : base("Queue is full.") { } - } - - public class SongNotFoundException : Exception - { - public SongNotFoundException(string message) : base(message) - { - } - public SongNotFoundException() : base("Song is not found.") { } - } - public class NotInVoiceChannelException : Exception - { - public NotInVoiceChannelException(string message) : base(message) - { - } - - public NotInVoiceChannelException() : base("You're not in the voice channel on this server.") { } - } -} diff --git a/src/NadekoBot/Services/Music/OldSongResolver.cs b/src/NadekoBot/Services/Music/OldSongResolver.cs deleted file mode 100644 index 147bb6e5..00000000 --- a/src/NadekoBot/Services/Music/OldSongResolver.cs +++ /dev/null @@ -1,111 +0,0 @@ -//using System; -//using System.Collections.Generic; -//using System.Linq; -//using System.Text; -//using System.Threading.Tasks; - -//namespace NadekoBot.Services.Music -//{ -// public class OldSongResolver -// { -// // public async Task ResolveSong(string query, MusicType musicType = MusicType.Normal) -// // { -// // if (string.IsNullOrWhiteSpace(query)) -// // throw new ArgumentNullException(nameof(query)); - -// // if (musicType != MusicType.Local && IsRadioLink(query)) -// // { -// // musicType = MusicType.Radio; -// // query = await HandleStreamContainers(query).ConfigureAwait(false) ?? query; -// // } - -// // try -// // { -// // switch (musicType) -// // { -// // case MusicType.Local: -// // return new Song(new SongInfo -// // { -// // Uri = "\"" + Path.GetFullPath(query) + "\"", -// // Title = Path.GetFileNameWithoutExtension(query), -// // Provider = "Local File", -// // ProviderType = musicType, -// // Query = query, -// // }); -// // case MusicType.Radio: -// // return new Song(new SongInfo -// // { -// // Uri = query, -// // Title = $"{query}", -// // Provider = "Radio Stream", -// // ProviderType = musicType, -// // Query = query -// // }) -// // { TotalTime = TimeSpan.MaxValue }; -// // } -// // if (_sc.IsSoundCloudLink(query)) -// // { -// // var svideo = await _sc.ResolveVideoAsync(query).ConfigureAwait(false); -// // return new Song(new SongInfo -// // { -// // Title = svideo.FullName, -// // Provider = "SoundCloud", -// // Uri = await svideo.StreamLink(), -// // ProviderType = musicType, -// // Query = svideo.TrackLink, -// // AlbumArt = svideo.artwork_url, -// // }) -// // { TotalTime = TimeSpan.FromMilliseconds(svideo.Duration) }; -// // } - -// // if (musicType == MusicType.Soundcloud) -// // { -// // var svideo = await _sc.GetVideoByQueryAsync(query).ConfigureAwait(false); -// // return new Song(new SongInfo -// // { -// // Title = svideo.FullName, -// // Provider = "SoundCloud", -// // Uri = await svideo.StreamLink(), -// // ProviderType = MusicType.Soundcloud, -// // Query = svideo.TrackLink, -// // AlbumArt = svideo.artwork_url, -// // }) -// // { TotalTime = TimeSpan.FromMilliseconds(svideo.Duration) }; -// // } - -// // var link = (await _google.GetVideoLinksByKeywordAsync(query).ConfigureAwait(false)).FirstOrDefault(); -// // if (string.IsNullOrWhiteSpace(link)) -// // throw new OperationCanceledException("Not a valid youtube query."); -// // var allVideos = await Task.Run(async () => { try { return await YouTube.Default.GetAllVideosAsync(link).ConfigureAwait(false); } catch { return Enumerable.Empty(); } }).ConfigureAwait(false); -// // var videos = allVideos.Where(v => v.AdaptiveKind == AdaptiveKind.Audio); -// // var video = videos -// // .Where(v => v.AudioBitrate < 256) -// // .OrderByDescending(v => v.AudioBitrate) -// // .FirstOrDefault(); - -// // if (video == null) // do something with this error -// // throw new Exception("Could not load any video elements based on the query."); -// // var m = Regex.Match(query, @"\?t=(?\d*)"); -// // int gotoTime = 0; -// // if (m.Captures.Count > 0) -// // int.TryParse(m.Groups["t"].ToString(), out gotoTime); -// // var song = new Song(new SongInfo -// // { -// // Title = video.Title.Substring(0, video.Title.Length - 10), // removing trailing "- You Tube" -// // Provider = "YouTube", -// // Uri = await video.GetUriAsync().ConfigureAwait(false), -// // Query = link, -// // ProviderType = musicType, -// // }); -// // song.SkipTo = gotoTime; -// // return song; -// // } -// // catch (Exception ex) -// // { -// // _log.Warn($"Failed resolving the link.{ex.Message}"); -// // _log.Warn(ex); -// // return null; -// // } -// // } -// } -//} diff --git a/src/NadekoBot/Services/Music/SongResolver/ISongResolverFactory.cs b/src/NadekoBot/Services/Music/SongResolver/ISongResolverFactory.cs deleted file mode 100644 index b2ae8aa8..00000000 --- a/src/NadekoBot/Services/Music/SongResolver/ISongResolverFactory.cs +++ /dev/null @@ -1,15 +0,0 @@ -using NadekoBot.Services.Database.Models; -using NadekoBot.Services.Music.SongResolver.Strategies; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace NadekoBot.Services.Music.SongResolver -{ - public interface ISongResolverFactory - { - Task GetResolveStrategy(string query, MusicType? musicType); - } -} diff --git a/src/NadekoBot/Services/Utility/VerboseErrorsService.cs b/src/NadekoBot/Services/Utility/VerboseErrorsService.cs index 8ed89d46..cb885a12 100644 --- a/src/NadekoBot/Services/Utility/VerboseErrorsService.cs +++ b/src/NadekoBot/Services/Utility/VerboseErrorsService.cs @@ -4,9 +4,9 @@ using System.Collections.Generic; using System.Threading.Tasks; using Discord; using NadekoBot.Extensions; -using NadekoBot.Services.Help; using Discord.Commands; using System.Linq; +using NadekoBot.Modules.Help.Services; namespace NadekoBot.Services.Utility { From c054543d982702ac5ec99810108d1dcdd05d19c7 Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Mon, 17 Jul 2017 21:42:36 +0200 Subject: [PATCH 194/346] Huge refactor is over --- src/NadekoBot/Common/AsyncLazy.cs | 2 +- .../{ => Common}/Attributes/Aliases.cs | 9 +- .../{ => Common}/Attributes/Description.cs | 7 +- .../{ => Common}/Attributes/NadekoCommand.cs | 7 +- .../Attributes/NadekoModuleAttribute.cs | 6 +- .../Attributes/OwnerOnlyAttribute.cs | 6 +- .../{ => Common}/Attributes/Usage.cs | 7 +- src/NadekoBot/Common/CREmbed.cs | 6 +- .../{ => Collections}/ConcurrentHashSet.cs | 5 +- .../DisposableImutableList.cs | 3 +- .../{ => Collections}/IndexedCollection.cs | 8 +- .../{ => Collections}/PoopyRingBuffer.cs | 36 ++- .../Common/ModuleBehaviors/IEarlyBlocker.cs | 6 +- .../ModuleBehaviors/IEarlyBlockingExecutor.cs | 6 +- .../Common/ModuleBehaviors/IEarlyExecutor.cs | 2 +- .../ModuleBehaviors/IINputTransformer.cs | 6 +- .../Common/ModuleBehaviors/ILateBlocker.cs | 6 +- .../ModuleBehaviors/ILateBlockingExecutor.cs | 2 +- .../Common/ModuleBehaviors/ILateExecutor.cs | 6 +- src/NadekoBot/Common/NadekoRandom.cs | 23 +- .../Common/NoPublicBotPrecondition.cs | 6 +- src/NadekoBot/Common/PlatformHelper.cs | 2 +- .../Common/Replacements/ReplacementBuilder.cs | 17 +- src/NadekoBot/Common/Replacements/Replacer.cs | 2 +- src/NadekoBot/Common/Shard0Precondition.cs | 17 +- .../Common/ShardCom/IShardComMessage.cs | 10 +- .../Common/ShardCom/ShardComClient.cs | 7 +- .../Common/ShardCom/ShardComServer.cs | 6 +- src/NadekoBot/Common/SyncPrecondition.cs | 23 -- .../TypeReaders/BotCommandTypeReader.cs | 10 +- .../TypeReaders/GuildDateTimeTypeReader.cs | 9 +- .../Common/TypeReaders/GuildTypeReader.cs | 8 +- .../Models}/PermissionAction.cs | 2 +- .../Common/TypeReaders/ModuleTypeReader.cs | 8 +- .../TypeReaders/PermissionActionTypeReader.cs | 8 +- .../Modules/Administration/Administration.cs | 4 +- .../{Commands => }/AutoAssignRoleCommands.cs | 4 +- .../{Commands => Common}/Migration/0_9..cs | 2 +- .../Migration/MigrationException.cs | 2 +- .../Administration/Common}/ProtectionStats.cs | 7 +- .../Administration/Common}/Ratelimiter.cs | 7 +- .../Administration/Common}/UserSpamStats.cs | 6 +- .../{Commands => }/GameChannelCommands.cs | 4 +- .../{Commands => }/LocalizationCommands.cs | 2 +- .../LogCommand.cs => LogCommands.cs} | 9 +- .../Migration.cs => MigrationCommands.cs} | 9 +- .../{Commands => }/MuteCommands.cs | 4 +- .../{Commands => }/PlayingRotateCommands.cs | 4 +- .../{Commands => }/PrefixCommands.cs | 2 +- .../{Commands => }/ProtectionCommands.cs | 5 +- .../{Commands => }/PruneCommands.cs | 4 +- ...telimitCommand.cs => RatelimitCommands.cs} | 5 +- ...ommand.cs => SelfAssignedRolesCommands.cs} | 3 +- .../{Commands => }/SelfCommands.cs | 4 +- .../{Commands => }/ServerGreetCommands.cs | 2 +- .../Services}/AdministrationService.cs | 16 +- .../Services}/AutoAssignRoleService.cs | 11 +- .../Services}/GameVoiceChannelService.cs | 14 +- .../Services}/GuildTimezoneService.cs | 12 +- .../Services}/LogCommandService.cs | 18 +- .../Administration/Services}/MuteService.cs | 18 +- .../Services}/PlayingRotateService.cs | 15 +- .../Services}/ProtectionService.cs | 14 +- .../Administration/Services}/PruneService.cs | 11 +- .../Services}/RatelimitService.cs | 18 +- .../Administration/Services}/SelfService.cs | 18 +- .../Services}/UserPunishService.cs | 11 +- .../Administration/Services}/VcRoleService.cs | 13 +- .../Administration/Services}/VplusTService.cs | 17 +- .../{Commands => }/TimeZoneCommands.cs | 4 +- .../{Commands => }/UserPunishCommands.cs | 4 +- .../{Commands => }/VcRoleCommands.cs | 4 +- .../{Commands => }/VoicePlusTextCommands.cs | 4 +- .../CustomReactions/CustomReactions.cs | 2 +- .../CustomReactions/Extensions/Extensions.cs | 4 +- .../Services/CustomReactionsService.cs | 7 +- ...nimalRacing.cs => AnimalRacingCommands.cs} | 8 +- .../{Commands/Models => Common}/Cards.cs | 9 +- ...ncyEvents.cs => CurrencyEventsCommands.cs} | 8 +- ...DiceRollCommand.cs => DiceRollCommands.cs} | 3 +- .../DrawCommand.cs => DrawCommands.cs} | 4 +- ...FlipCoinCommand.cs => FlipCoinCommands.cs} | 3 +- .../FlowerShop.cs => FlowerShopCommands.cs} | 9 +- src/NadekoBot/Modules/Gambling/Gambling.cs | 3 +- .../{Commands/Slots.cs => SlotCommands.cs} | 7 +- .../{Commands => }/WaifuClaimCommands.cs | 3 +- .../Acropobia.cs => AcropobiaCommands.cs} | 9 +- .../Games/{Commands => }/CleverBotCommands.cs | 5 +- .../Games/Common}/ChatterBotResponse.cs | 2 +- .../Games/Common}/ChatterBotSession.cs | 10 +- .../Games/Common}/GirlRating.cs | 13 +- .../Hangman/HangmanGame.cs | 18 +- .../Hangman/IHangmanObject.cs | 2 +- .../Games => Modules/Games/Common}/Poll.cs | 12 +- .../{Commands => Common}/Trivia/TriviaGame.cs | 19 +- .../Trivia/TriviaQuestion.cs | 6 +- .../Trivia/TriviaQuestionPool.cs | 11 +- .../Games/Common}/TypingArticle.cs | 2 +- .../{Commands/Models => Common}/TypingGame.cs | 17 +- src/NadekoBot/Modules/Games/Games.cs | 6 +- .../Games/{Commands => }/HangmanCommands.cs | 4 +- .../Games/{Commands => }/LeetCommands.cs | 2 +- .../{Commands => }/PlantAndPickCommands.cs | 4 +- .../Games/{Commands => }/PollCommands.cs | 4 +- .../Games/Services}/ChatterbotService.cs | 22 +- .../Games/Services}/GamesService.cs | 21 +- .../Games/Services}/PollService.cs | 9 +- .../{Commands => }/SpeedTypingCommands.cs | 6 +- .../TicTacToe.cs => TicTacToeCommands.cs} | 13 +- .../Games/{Commands => }/TriviaCommands.cs | 4 +- src/NadekoBot/Modules/Help/Help.cs | 4 +- .../Modules/Help/Services/HelpService.cs | 7 +- .../Modules/Music/Common/MusicPlayer.cs | 1 + .../Modules/Music/Common/MusicQueue.cs | 1 + src/NadekoBot/Modules/Music/Music.cs | 5 +- .../Modules/Music/Services/MusicService.cs | 1 + src/NadekoBot/Modules/NSFW/NSFW.cs | 8 +- src/NadekoBot/Modules/NadekoModule.cs | 1 + .../{Commands => }/BlacklistCommands.cs | 7 +- .../{Commands => }/CmdCdsCommands.cs | 5 +- .../{Commands => }/CommandCostCommands.cs | 0 .../Permissions/Common}/PermissionCache.cs | 2 +- .../Common}/PermissionExtensions.cs | 8 +- .../Common}/PermissionsCollection.cs | 9 +- .../{Commands => }/FilterCommands.cs | 12 +- .../GlobalPermissionCommands.cs | 11 +- .../Modules/Permissions/Permissions.cs | 11 +- .../ResetPermissionsCommands.cs | 4 +- .../Permissions/Services}/BlacklistService.cs | 12 +- .../Permissions/Services}/CmdCdService.cs | 12 +- .../Permissions/Services}/FilterService.cs | 16 +- .../Services}/GlobalPermissionService.cs | 12 +- .../Services}/PermissionsService.cs | 16 +- .../Services}/ResetPermissionsService.cs | 7 +- .../Pokemon/Common}/PokeStats.cs | 2 +- .../Pokemon/Common}/PokemonType.cs | 2 +- src/NadekoBot/Modules/Pokemon/Pokemon.cs | 5 +- .../Pokemon/Services}/PokemonService.cs | 10 +- .../{Commands => }/AnimeSearchCommands.cs | 9 +- .../Searches/Commands/Models/WeatherModels.cs | 63 ----- .../Searches/Common}/AnimeResult.cs | 2 +- .../Models => Common}/DefineModel.cs | 2 +- .../Exceptions}/StreamNotFoundException.cs | 2 +- .../Models => Common}/GoogleSearchResult.cs | 2 +- .../Searches/Common}/MagicItem.cs | 2 +- .../Searches/Common}/MangaResult.cs | 2 +- .../{Commands/OMDB => Common}/OmdbProvider.cs | 10 +- .../Models => Common}/OverwatchApiModel.cs | 2 +- .../Searches}/Common/SearchImageCacher.cs | 14 +- .../Searches/Common}/SearchPokemon.cs | 2 +- .../Searches/Common}/StreamResponses.cs | 2 +- .../{Commands/Models => Common}/TimeModels.cs | 2 +- .../Modules/Searches/Common/WeatherModels.cs | 63 +++++ .../Models => Common}/WikipediaApiModel.cs | 2 +- .../Searches/Common}/WoWJoke.cs | 2 +- .../Searches/{Commands => }/JokeCommands.cs | 5 +- .../Searches/{Commands => }/LoLCommands.cs | 3 +- .../{Commands => }/MemegenCommands.cs | 2 +- .../Searches/{Commands => }/OsuCommands.cs | 2 +- .../{Commands => }/OverwatchCommands.cs | 220 +++++++++--------- .../Searches/{Commands => }/PlaceCommands.cs | 3 +- .../{Commands => }/PokemonSearchCommands.cs | 5 +- src/NadekoBot/Modules/Searches/Searches.cs | 30 ++- .../Searches/Services}/AnimeSearchService.cs | 6 +- .../Searches/Services}/SearchesService.cs | 8 +- .../Services}/StreamNotificationService.cs | 6 +- .../StreamNotificationCommands.cs | 4 +- .../Translator.cs => TranslatorCommands.cs} | 4 +- .../Searches/{Commands => }/XkcdCommands.cs | 3 +- .../CalcCommand.cs => CalcCommands.cs} | 2 +- .../{Commands => }/CommandMapCommands.cs | 4 +- .../Utility/Common}/Patreon/PatreonData.cs | 2 +- .../Utility/Common}/Patreon/PatreonPledge.cs | 2 +- .../Utility/Common}/Patreon/PatreonUser.cs | 2 +- .../Utility/Common}/RepeatRunner.cs | 10 +- .../Utility/{Commands => }/InfoCommands.cs | 2 +- .../Utility/{Commands => }/PatreonCommands.cs | 4 +- .../Utility/{Commands => }/QuoteCommands.cs | 6 +- .../{Commands/Remind.cs => RemindCommands.cs} | 7 +- .../Utility/{Commands => }/RepeatCommands.cs | 7 +- .../Utility/Services}/CommandMapService.cs | 11 +- .../Utility/Services}/ConverterService.cs | 14 +- .../Services}/MessageRepeaterService.cs | 10 +- .../Services}/PatreonRewardsService.cs | 15 +- .../Utility/Services}/RemindService.cs | 21 +- .../Utility/Services}/StreamRoleService.cs | 9 +- .../Utility/Services}/VerboseErrorsService.cs | 12 +- .../{Commands => }/StreamRoleCommands.cs | 4 +- ...onversion.cs => UnitConversionCommands.cs} | 5 +- src/NadekoBot/Modules/Utility/Utility.cs | 4 +- ...mmandErrors.cs => VerboseErrorCommands.cs} | 6 +- src/NadekoBot/NadekoBot.cs | 7 +- src/NadekoBot/NadekoBot.csproj | 4 - src/NadekoBot/Services/CommandHandler.cs | 5 +- .../Repositories/Impl/QuoteRepository.cs | 1 + .../Services/GreetSettingsService.cs | 4 +- src/NadekoBot/Services/Impl/BotCredentials.cs | 1 + src/NadekoBot/Services/Impl/Localization.cs | 6 +- src/NadekoBot/Services/Impl/NadekoStrings.cs | 13 +- src/NadekoBot/ShardsCoordinator.cs | 20 +- src/NadekoBot/_Extensions/Extensions.cs | 1 + 201 files changed, 929 insertions(+), 863 deletions(-) rename src/NadekoBot/{ => Common}/Attributes/Aliases.cs (73%) rename src/NadekoBot/{ => Common}/Attributes/Description.cs (65%) rename src/NadekoBot/{ => Common}/Attributes/NadekoCommand.cs (67%) rename src/NadekoBot/{ => Common}/Attributes/NadekoModuleAttribute.cs (73%) rename src/NadekoBot/{ => Common}/Attributes/OwnerOnlyAttribute.cs (87%) rename src/NadekoBot/{ => Common}/Attributes/Usage.cs (64%) rename src/NadekoBot/Common/{ => Collections}/ConcurrentHashSet.cs (99%) rename src/NadekoBot/Common/{ => Collections}/DisposableImutableList.cs (98%) rename src/NadekoBot/Common/{ => Collections}/IndexedCollection.cs (94%) rename src/NadekoBot/Common/{ => Collections}/PoopyRingBuffer.cs (72%) delete mode 100644 src/NadekoBot/Common/SyncPrecondition.cs rename src/NadekoBot/Common/{ => TypeReaders/Models}/PermissionAction.cs (93%) rename src/NadekoBot/Modules/Administration/{Commands => }/AutoAssignRoleCommands.cs (96%) rename src/NadekoBot/Modules/Administration/{Commands => Common}/Migration/0_9..cs (99%) rename src/NadekoBot/Modules/Administration/{Commands => Common}/Migration/MigrationException.cs (57%) rename src/NadekoBot/{Services/Administration => Modules/Administration/Common}/ProtectionStats.cs (81%) rename src/NadekoBot/{Services/Administration => Modules/Administration/Common}/Ratelimiter.cs (92%) rename src/NadekoBot/{Services/Administration => Modules/Administration/Common}/UserSpamStats.cs (94%) rename src/NadekoBot/Modules/Administration/{Commands => }/GameChannelCommands.cs (96%) rename src/NadekoBot/Modules/Administration/{Commands => }/LocalizationCommands.cs (99%) rename src/NadekoBot/Modules/Administration/{Commands/LogCommand.cs => LogCommands.cs} (97%) rename src/NadekoBot/Modules/Administration/{Commands/Migration.cs => MigrationCommands.cs} (98%) rename src/NadekoBot/Modules/Administration/{Commands => }/MuteCommands.cs (98%) rename src/NadekoBot/Modules/Administration/{Commands => }/PlayingRotateCommands.cs (97%) rename src/NadekoBot/Modules/Administration/{Commands => }/PrefixCommands.cs (98%) rename src/NadekoBot/Modules/Administration/{Commands => }/ProtectionCommands.cs (98%) rename src/NadekoBot/Modules/Administration/{Commands => }/PruneCommands.cs (96%) rename src/NadekoBot/Modules/Administration/{Commands/RatelimitCommand.cs => RatelimitCommands.cs} (97%) rename src/NadekoBot/Modules/Administration/{Commands/SelfAssignedRolesCommand.cs => SelfAssignedRolesCommands.cs} (99%) rename src/NadekoBot/Modules/Administration/{Commands => }/SelfCommands.cs (99%) rename src/NadekoBot/Modules/Administration/{Commands => }/ServerGreetCommands.cs (99%) rename src/NadekoBot/{Services/Administration => Modules/Administration/Services}/AdministrationService.cs (91%) rename src/NadekoBot/{Services/Administration => Modules/Administration/Services}/AutoAssignRoleService.cs (92%) rename src/NadekoBot/{Services/Administration => Modules/Administration/Services}/GameVoiceChannelService.cs (93%) rename src/NadekoBot/{Services/Administration => Modules/Administration/Services}/GuildTimezoneService.cs (96%) rename src/NadekoBot/{Services/Administration => Modules/Administration/Services}/LogCommandService.cs (99%) rename src/NadekoBot/{Services/Administration => Modules/Administration/Services}/MuteService.cs (98%) rename src/NadekoBot/{Services/Administration => Modules/Administration/Services}/PlayingRotateService.cs (94%) rename src/NadekoBot/{Services/Administration => Modules/Administration/Services}/ProtectionService.cs (97%) rename src/NadekoBot/{Services/Administration => Modules/Administration/Services}/PruneService.cs (94%) rename src/NadekoBot/{Services/Administration => Modules/Administration/Services}/RatelimitService.cs (91%) rename src/NadekoBot/{Services/Administration => Modules/Administration/Services}/SelfService.cs (96%) rename src/NadekoBot/{Services/Administration => Modules/Administration/Services}/UserPunishService.cs (96%) rename src/NadekoBot/{Services/Administration => Modules/Administration/Services}/VcRoleService.cs (97%) rename src/NadekoBot/{Services/Administration => Modules/Administration/Services}/VplusTService.cs (97%) rename src/NadekoBot/Modules/Administration/{Commands => }/TimeZoneCommands.cs (96%) rename src/NadekoBot/Modules/Administration/{Commands => }/UserPunishCommands.cs (99%) rename src/NadekoBot/Modules/Administration/{Commands => }/VcRoleCommands.cs (98%) rename src/NadekoBot/Modules/Administration/{Commands => }/VoicePlusTextCommands.cs (98%) rename src/NadekoBot/Modules/Gambling/{Commands/AnimalRacing.cs => AnimalRacingCommands.cs} (98%) rename src/NadekoBot/Modules/Gambling/{Commands/Models => Common}/Cards.cs (98%) rename src/NadekoBot/Modules/Gambling/{Commands/CurrencyEvents.cs => CurrencyEventsCommands.cs} (97%) rename src/NadekoBot/Modules/Gambling/{Commands/DiceRollCommand.cs => DiceRollCommands.cs} (99%) rename src/NadekoBot/Modules/Gambling/{Commands/DrawCommand.cs => DrawCommands.cs} (98%) rename src/NadekoBot/Modules/Gambling/{Commands/FlipCoinCommand.cs => FlipCoinCommands.cs} (98%) rename src/NadekoBot/Modules/Gambling/{Commands/FlowerShop.cs => FlowerShopCommands.cs} (98%) rename src/NadekoBot/Modules/Gambling/{Commands/Slots.cs => SlotCommands.cs} (97%) rename src/NadekoBot/Modules/Gambling/{Commands => }/WaifuClaimCommands.cs (99%) rename src/NadekoBot/Modules/Games/{Commands/Acropobia.cs => AcropobiaCommands.cs} (98%) rename src/NadekoBot/Modules/Games/{Commands => }/CleverBotCommands.cs (94%) rename src/NadekoBot/{Services/Games => Modules/Games/Common}/ChatterBotResponse.cs (76%) rename src/NadekoBot/{Services/Games => Modules/Games/Common}/ChatterBotSession.cs (89%) rename src/NadekoBot/{Services/Games => Modules/Games/Common}/GirlRating.cs (95%) rename src/NadekoBot/Modules/Games/{Commands => Common}/Hangman/HangmanGame.cs (98%) rename src/NadekoBot/Modules/Games/{Commands => Common}/Hangman/IHangmanObject.cs (71%) rename src/NadekoBot/{Services/Games => Modules/Games/Common}/Poll.cs (97%) rename src/NadekoBot/Modules/Games/{Commands => Common}/Trivia/TriviaGame.cs (99%) rename src/NadekoBot/Modules/Games/{Commands => Common}/Trivia/TriviaQuestion.cs (97%) rename src/NadekoBot/Modules/Games/{Commands => Common}/Trivia/TriviaQuestionPool.cs (94%) rename src/NadekoBot/{Services/Games => Modules/Games/Common}/TypingArticle.cs (74%) rename src/NadekoBot/Modules/Games/{Commands/Models => Common}/TypingGame.cs (97%) rename src/NadekoBot/Modules/Games/{Commands => }/HangmanCommands.cs (97%) rename src/NadekoBot/Modules/Games/{Commands => }/LeetCommands.cs (99%) rename src/NadekoBot/Modules/Games/{Commands => }/PlantAndPickCommands.cs (98%) rename src/NadekoBot/Modules/Games/{Commands => }/PollCommands.cs (96%) rename src/NadekoBot/{Services/Games => Modules/Games/Services}/ChatterbotService.cs (93%) rename src/NadekoBot/{Services/Games => Modules/Games/Services}/GamesService.cs (96%) rename src/NadekoBot/{Services/Games => Modules/Games/Services}/PollService.cs (90%) rename src/NadekoBot/Modules/Games/{Commands => }/SpeedTypingCommands.cs (97%) rename src/NadekoBot/Modules/Games/{Commands/TicTacToe.cs => TicTacToeCommands.cs} (97%) rename src/NadekoBot/Modules/Games/{Commands => }/TriviaCommands.cs (97%) rename src/NadekoBot/Modules/Permissions/{Commands => }/BlacklistCommands.cs (97%) rename src/NadekoBot/Modules/Permissions/{Commands => }/CmdCdsCommands.cs (97%) rename src/NadekoBot/Modules/Permissions/{Commands => }/CommandCostCommands.cs (100%) rename src/NadekoBot/{Services/Permissions => Modules/Permissions/Common}/PermissionCache.cs (90%) rename src/NadekoBot/{Services/Permissions => Modules/Permissions/Common}/PermissionExtensions.cs (97%) rename src/NadekoBot/{Services/Permissions => Modules/Permissions/Common}/PermissionsCollection.cs (92%) rename src/NadekoBot/Modules/Permissions/{Commands => }/FilterCommands.cs (94%) rename src/NadekoBot/Modules/Permissions/{Commands => }/GlobalPermissionCommands.cs (93%) rename src/NadekoBot/Modules/Permissions/{Commands => }/ResetPermissionsCommands.cs (94%) rename src/NadekoBot/{Services/Permissions => Modules/Permissions/Services}/BlacklistService.cs (86%) rename src/NadekoBot/{Services/Permissions => Modules/Permissions/Services}/CmdCdService.cs (93%) rename src/NadekoBot/{Services/Permissions => Modules/Permissions/Services}/FilterService.cs (96%) rename src/NadekoBot/{Services/Permissions => Modules/Permissions/Services}/GlobalPermissionService.cs (87%) rename src/NadekoBot/{Services/Permissions => Modules/Permissions/Services}/PermissionsService.cs (98%) rename src/NadekoBot/{Services/Permissions => Modules/Permissions/Services}/ResetPermissionsService.cs (89%) rename src/NadekoBot/{Services/Pokemon => Modules/Pokemon/Common}/PokeStats.cs (90%) rename src/NadekoBot/{Services/Pokemon => Modules/Pokemon/Common}/PokemonType.cs (95%) rename src/NadekoBot/{Services/Pokemon => Modules/Pokemon/Services}/PokemonService.cs (84%) rename src/NadekoBot/Modules/Searches/{Commands => }/AnimeSearchCommands.cs (97%) delete mode 100644 src/NadekoBot/Modules/Searches/Commands/Models/WeatherModels.cs rename src/NadekoBot/{Services/Searches/Models => Modules/Searches/Common}/AnimeResult.cs (93%) rename src/NadekoBot/Modules/Searches/{Commands/Models => Common}/DefineModel.cs (93%) rename src/NadekoBot/{Services/Searches => Modules/Searches/Common/Exceptions}/StreamNotFoundException.cs (78%) rename src/NadekoBot/Modules/Searches/{Commands/Models => Common}/GoogleSearchResult.cs (86%) rename src/NadekoBot/{Services/Searches/Models => Modules/Searches/Common}/MagicItem.cs (73%) rename src/NadekoBot/{Services/Searches/Models => Modules/Searches/Common}/MangaResult.cs (92%) rename src/NadekoBot/Modules/Searches/{Commands/OMDB => Common}/OmdbProvider.cs (96%) rename src/NadekoBot/Modules/Searches/{Commands/Models => Common}/OverwatchApiModel.cs (96%) rename src/NadekoBot/{ => Modules/Searches}/Common/SearchImageCacher.cs (98%) rename src/NadekoBot/{Services/Searches/Models => Modules/Searches/Common}/SearchPokemon.cs (97%) rename src/NadekoBot/{Services/Searches/Models => Modules/Searches/Common}/StreamResponses.cs (95%) rename src/NadekoBot/Modules/Searches/{Commands/Models => Common}/TimeModels.cs (94%) create mode 100644 src/NadekoBot/Modules/Searches/Common/WeatherModels.cs rename src/NadekoBot/Modules/Searches/{Commands/Models => Common}/WikipediaApiModel.cs (89%) rename src/NadekoBot/{Services/Searches/Models => Modules/Searches/Common}/WoWJoke.cs (81%) rename src/NadekoBot/Modules/Searches/{Commands => }/JokeCommands.cs (96%) rename src/NadekoBot/Modules/Searches/{Commands => }/LoLCommands.cs (99%) rename src/NadekoBot/Modules/Searches/{Commands => }/MemegenCommands.cs (98%) rename src/NadekoBot/Modules/Searches/{Commands => }/OsuCommands.cs (99%) rename src/NadekoBot/Modules/Searches/{Commands => }/OverwatchCommands.cs (97%) rename src/NadekoBot/Modules/Searches/{Commands => }/PlaceCommands.cs (97%) rename src/NadekoBot/Modules/Searches/{Commands => }/PokemonSearchCommands.cs (96%) rename src/NadekoBot/{Services/Searches => Modules/Searches/Services}/AnimeSearchService.cs (92%) rename src/NadekoBot/{Services/Searches => Modules/Searches/Services}/SearchesService.cs (97%) rename src/NadekoBot/{Services/Searches => Modules/Searches/Services}/StreamNotificationService.cs (98%) rename src/NadekoBot/Modules/Searches/{Commands => }/StreamNotificationCommands.cs (98%) rename src/NadekoBot/Modules/Searches/{Commands/Translator.cs => TranslatorCommands.cs} (98%) rename src/NadekoBot/Modules/Searches/{Commands => }/XkcdCommands.cs (98%) rename src/NadekoBot/Modules/Utility/{Commands/CalcCommand.cs => CalcCommands.cs} (98%) rename src/NadekoBot/Modules/Utility/{Commands => }/CommandMapCommands.cs (98%) rename src/NadekoBot/{Services/Utility => Modules/Utility/Common}/Patreon/PatreonData.cs (90%) rename src/NadekoBot/{Services/Utility => Modules/Utility/Common}/Patreon/PatreonPledge.cs (96%) rename src/NadekoBot/{Services/Utility => Modules/Utility/Common}/Patreon/PatreonUser.cs (97%) rename src/NadekoBot/{Services/Utility => Modules/Utility/Common}/RepeatRunner.cs (97%) rename src/NadekoBot/Modules/Utility/{Commands => }/InfoCommands.cs (99%) rename src/NadekoBot/Modules/Utility/{Commands => }/PatreonCommands.cs (97%) rename src/NadekoBot/Modules/Utility/{Commands => }/QuoteCommands.cs (98%) rename src/NadekoBot/Modules/Utility/{Commands/Remind.cs => RemindCommands.cs} (97%) rename src/NadekoBot/Modules/Utility/{Commands => }/RepeatCommands.cs (98%) rename src/NadekoBot/{Services/Utility => Modules/Utility/Services}/CommandMapService.cs (94%) rename src/NadekoBot/{Services/Utility => Modules/Utility/Services}/ConverterService.cs (98%) rename src/NadekoBot/{Services/Utility => Modules/Utility/Services}/MessageRepeaterService.cs (86%) rename src/NadekoBot/{Services/Utility => Modules/Utility/Services}/PatreonRewardsService.cs (97%) rename src/NadekoBot/{Services/Utility => Modules/Utility/Services}/RemindService.cs (95%) rename src/NadekoBot/{Services/Utility => Modules/Utility/Services}/StreamRoleService.cs (98%) rename src/NadekoBot/{Services/Utility => Modules/Utility/Services}/VerboseErrorsService.cs (91%) rename src/NadekoBot/Modules/Utility/{Commands => }/StreamRoleCommands.cs (94%) rename src/NadekoBot/Modules/Utility/{Commands/UnitConversion.cs => UnitConversionCommands.cs} (98%) rename src/NadekoBot/Modules/Utility/{Commands/VerboseCommandErrors.cs => VerboseErrorCommands.cs} (84%) diff --git a/src/NadekoBot/Common/AsyncLazy.cs b/src/NadekoBot/Common/AsyncLazy.cs index f739968f..09d5c989 100644 --- a/src/NadekoBot/Common/AsyncLazy.cs +++ b/src/NadekoBot/Common/AsyncLazy.cs @@ -2,7 +2,7 @@ using System.Runtime.CompilerServices; using System.Threading.Tasks; -namespace NadekoBot.DataStructures +namespace NadekoBot.Common { public class AsyncLazy : Lazy> { diff --git a/src/NadekoBot/Attributes/Aliases.cs b/src/NadekoBot/Common/Attributes/Aliases.cs similarity index 73% rename from src/NadekoBot/Attributes/Aliases.cs rename to src/NadekoBot/Common/Attributes/Aliases.cs index af3e98fb..21e7c2ba 100644 --- a/src/NadekoBot/Attributes/Aliases.cs +++ b/src/NadekoBot/Common/Attributes/Aliases.cs @@ -1,9 +1,10 @@ -using Discord.Commands; -using NadekoBot.Services; -using System.Linq; +using System.Linq; using System.Runtime.CompilerServices; +using Discord.Commands; +using NadekoBot.Services; +using NadekoBot.Services.Impl; -namespace NadekoBot.Attributes +namespace NadekoBot.Common.Attributes { public class Aliases : AliasAttribute { diff --git a/src/NadekoBot/Attributes/Description.cs b/src/NadekoBot/Common/Attributes/Description.cs similarity index 65% rename from src/NadekoBot/Attributes/Description.cs rename to src/NadekoBot/Common/Attributes/Description.cs index a6f32c74..2a071518 100644 --- a/src/NadekoBot/Attributes/Description.cs +++ b/src/NadekoBot/Common/Attributes/Description.cs @@ -1,8 +1,9 @@ -using Discord.Commands; +using System.Runtime.CompilerServices; +using Discord.Commands; using NadekoBot.Services; -using System.Runtime.CompilerServices; +using NadekoBot.Services.Impl; -namespace NadekoBot.Attributes +namespace NadekoBot.Common.Attributes { public class Description : SummaryAttribute { diff --git a/src/NadekoBot/Attributes/NadekoCommand.cs b/src/NadekoBot/Common/Attributes/NadekoCommand.cs similarity index 67% rename from src/NadekoBot/Attributes/NadekoCommand.cs rename to src/NadekoBot/Common/Attributes/NadekoCommand.cs index 3c8010a9..566c2640 100644 --- a/src/NadekoBot/Attributes/NadekoCommand.cs +++ b/src/NadekoBot/Common/Attributes/NadekoCommand.cs @@ -1,8 +1,9 @@ -using Discord.Commands; +using System.Runtime.CompilerServices; +using Discord.Commands; using NadekoBot.Services; -using System.Runtime.CompilerServices; +using NadekoBot.Services.Impl; -namespace NadekoBot.Attributes +namespace NadekoBot.Common.Attributes { public class NadekoCommand : CommandAttribute { diff --git a/src/NadekoBot/Attributes/NadekoModuleAttribute.cs b/src/NadekoBot/Common/Attributes/NadekoModuleAttribute.cs similarity index 73% rename from src/NadekoBot/Attributes/NadekoModuleAttribute.cs rename to src/NadekoBot/Common/Attributes/NadekoModuleAttribute.cs index dc0d21b7..31ba1730 100644 --- a/src/NadekoBot/Attributes/NadekoModuleAttribute.cs +++ b/src/NadekoBot/Common/Attributes/NadekoModuleAttribute.cs @@ -1,7 +1,7 @@ -using Discord.Commands; -using System; +using System; +using Discord.Commands; -namespace NadekoBot.Attributes +namespace NadekoBot.Common.Attributes { [AttributeUsage(AttributeTargets.Class)] sealed class NadekoModuleAttribute : GroupAttribute diff --git a/src/NadekoBot/Attributes/OwnerOnlyAttribute.cs b/src/NadekoBot/Common/Attributes/OwnerOnlyAttribute.cs similarity index 87% rename from src/NadekoBot/Attributes/OwnerOnlyAttribute.cs rename to src/NadekoBot/Common/Attributes/OwnerOnlyAttribute.cs index 9799dbcd..c5227daa 100644 --- a/src/NadekoBot/Attributes/OwnerOnlyAttribute.cs +++ b/src/NadekoBot/Common/Attributes/OwnerOnlyAttribute.cs @@ -1,9 +1,9 @@ -using System.Threading.Tasks; +using System; +using System.Threading.Tasks; using Discord.Commands; -using System; using NadekoBot.Services; -namespace NadekoBot.Attributes +namespace NadekoBot.Common.Attributes { public class OwnerOnlyAttribute : PreconditionAttribute { diff --git a/src/NadekoBot/Attributes/Usage.cs b/src/NadekoBot/Common/Attributes/Usage.cs similarity index 64% rename from src/NadekoBot/Attributes/Usage.cs rename to src/NadekoBot/Common/Attributes/Usage.cs index b3e18519..d7c88ea8 100644 --- a/src/NadekoBot/Attributes/Usage.cs +++ b/src/NadekoBot/Common/Attributes/Usage.cs @@ -1,8 +1,9 @@ -using Discord.Commands; +using System.Runtime.CompilerServices; +using Discord.Commands; using NadekoBot.Services; -using System.Runtime.CompilerServices; +using NadekoBot.Services.Impl; -namespace NadekoBot.Attributes +namespace NadekoBot.Common.Attributes { public class Usage : RemarksAttribute { diff --git a/src/NadekoBot/Common/CREmbed.cs b/src/NadekoBot/Common/CREmbed.cs index 4fdf78e3..8ef9d30d 100644 --- a/src/NadekoBot/Common/CREmbed.cs +++ b/src/NadekoBot/Common/CREmbed.cs @@ -1,10 +1,10 @@ -using Discord; +using System; +using Discord; using NadekoBot.Extensions; using Newtonsoft.Json; using NLog; -using System; -namespace NadekoBot.DataStructures +namespace NadekoBot.Common { public class CREmbed { diff --git a/src/NadekoBot/Common/ConcurrentHashSet.cs b/src/NadekoBot/Common/Collections/ConcurrentHashSet.cs similarity index 99% rename from src/NadekoBot/Common/ConcurrentHashSet.cs rename to src/NadekoBot/Common/Collections/ConcurrentHashSet.cs index 2a2ae1bf..c7b84515 100644 --- a/src/NadekoBot/Common/ConcurrentHashSet.cs +++ b/src/NadekoBot/Common/Collections/ConcurrentHashSet.cs @@ -1,13 +1,14 @@ // License MIT // Source: https://github.com/i3arnon/ConcurrentHashSet -using ConcurrentCollections; +using System; +using System.Collections; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Threading; -namespace System.Collections.Concurrent +namespace NadekoBot.Common.Collections { /// /// Represents a thread-safe hash-based unique collection. diff --git a/src/NadekoBot/Common/DisposableImutableList.cs b/src/NadekoBot/Common/Collections/DisposableImutableList.cs similarity index 98% rename from src/NadekoBot/Common/DisposableImutableList.cs rename to src/NadekoBot/Common/Collections/DisposableImutableList.cs index 391365e5..b97a2cc9 100644 --- a/src/NadekoBot/Common/DisposableImutableList.cs +++ b/src/NadekoBot/Common/Collections/DisposableImutableList.cs @@ -2,9 +2,8 @@ using System.Collections; using System.Collections.Generic; -namespace NadekoBot.DataStructures +namespace NadekoBot.Common.Collections { - public static class DisposableReadOnlyListExtensions { public static IDisposableReadOnlyList AsDisposable(this IReadOnlyList arr) where T : IDisposable diff --git a/src/NadekoBot/Common/IndexedCollection.cs b/src/NadekoBot/Common/Collections/IndexedCollection.cs similarity index 94% rename from src/NadekoBot/Common/IndexedCollection.cs rename to src/NadekoBot/Common/Collections/IndexedCollection.cs index 72b4f343..6ef057c8 100644 --- a/src/NadekoBot/Common/IndexedCollection.cs +++ b/src/NadekoBot/Common/Collections/IndexedCollection.cs @@ -1,11 +1,11 @@ -using NadekoBot.Services.Database.Models; -using System.Collections; +using System.Collections; using System.Collections.Generic; using System.Linq; +using NadekoBot.Services.Database.Models; -namespace NadekoBot.DataStructures +namespace NadekoBot.Common.Collections { - public class IndexedCollection : IList where T : IIndexed + public class IndexedCollection : IList where T : class, IIndexed { public List Source { get; } private readonly object _locker = new object(); diff --git a/src/NadekoBot/Common/PoopyRingBuffer.cs b/src/NadekoBot/Common/Collections/PoopyRingBuffer.cs similarity index 72% rename from src/NadekoBot/Common/PoopyRingBuffer.cs rename to src/NadekoBot/Common/Collections/PoopyRingBuffer.cs index 296a5c47..d1cf34c0 100644 --- a/src/NadekoBot/Common/PoopyRingBuffer.cs +++ b/src/NadekoBot/Common/Collections/PoopyRingBuffer.cs @@ -1,29 +1,19 @@ using System; using System.Threading; -using System.Threading.Tasks; -namespace NadekoBot.DataStructures +namespace NadekoBot.Common.Collections { public class PoopyRingBuffer : IDisposable { // readpos == writepos means empty // writepos == readpos - 1 means full - private byte[] buffer; + private byte[] _buffer; public int Capacity { get; } - private int _readPos = 0; - private int ReadPos - { - get => _readPos; - set => _readPos = value; - } - private int _writePos = 0; - private int WritePos - { - get => _writePos; - set => _writePos = value; - } + private int ReadPos { get; set; } = 0; + private int WritePos { get; set; } = 0; + public int Length => ReadPos <= WritePos ? WritePos - ReadPos : Capacity - (ReadPos - WritePos); @@ -38,7 +28,7 @@ namespace NadekoBot.DataStructures public PoopyRingBuffer(int capacity = 81920 * 100) { this.Capacity = capacity + 1; - this.buffer = new byte[this.Capacity]; + this._buffer = new byte[this.Capacity]; } public int Read(byte[] b, int offset, int toRead) @@ -51,7 +41,7 @@ namespace NadekoBot.DataStructures if (WritePos > ReadPos) { - Array.Copy(buffer, ReadPos, b, offset, toRead); + Array.Copy(_buffer, ReadPos, b, offset, toRead); ReadPos += toRead; } else @@ -60,12 +50,12 @@ namespace NadekoBot.DataStructures var firstRead = toRead > toEnd ? toEnd : toRead; - Array.Copy(buffer, ReadPos, b, offset, firstRead); + Array.Copy(_buffer, ReadPos, b, offset, firstRead); ReadPos += firstRead; var secondRead = toRead - firstRead; if (secondRead > 0) { - Array.Copy(buffer, 0, b, offset + firstRead, secondRead); + Array.Copy(_buffer, 0, b, offset + firstRead, secondRead); ReadPos = secondRead; } } @@ -82,7 +72,7 @@ namespace NadekoBot.DataStructures if (WritePos < ReadPos) { - Array.Copy(b, offset, buffer, WritePos, toWrite); + Array.Copy(b, offset, _buffer, WritePos, toWrite); WritePos += toWrite; } else @@ -91,11 +81,11 @@ namespace NadekoBot.DataStructures var firstWrite = toWrite > toEnd ? toEnd : toWrite; - Array.Copy(b, offset, buffer, WritePos, firstWrite); + Array.Copy(b, offset, _buffer, WritePos, firstWrite); var secondWrite = toWrite - firstWrite; if (secondWrite > 0) { - Array.Copy(b, offset + firstWrite, buffer, 0, secondWrite); + Array.Copy(b, offset + firstWrite, _buffer, 0, secondWrite); WritePos = secondWrite; } else @@ -110,7 +100,7 @@ namespace NadekoBot.DataStructures public void Dispose() { - buffer = null; + _buffer = null; } } } diff --git a/src/NadekoBot/Common/ModuleBehaviors/IEarlyBlocker.cs b/src/NadekoBot/Common/ModuleBehaviors/IEarlyBlocker.cs index 9c10e910..5d69e7b2 100644 --- a/src/NadekoBot/Common/ModuleBehaviors/IEarlyBlocker.cs +++ b/src/NadekoBot/Common/ModuleBehaviors/IEarlyBlocker.cs @@ -1,7 +1,7 @@ -using Discord; -using System.Threading.Tasks; +using System.Threading.Tasks; +using Discord; -namespace NadekoBot.DataStructures.ModuleBehaviors +namespace NadekoBot.Common.ModuleBehaviors { /// /// Implemented by modules which block execution before anything is executed diff --git a/src/NadekoBot/Common/ModuleBehaviors/IEarlyBlockingExecutor.cs b/src/NadekoBot/Common/ModuleBehaviors/IEarlyBlockingExecutor.cs index a3e004b1..536d829e 100644 --- a/src/NadekoBot/Common/ModuleBehaviors/IEarlyBlockingExecutor.cs +++ b/src/NadekoBot/Common/ModuleBehaviors/IEarlyBlockingExecutor.cs @@ -1,8 +1,8 @@ -using Discord; +using System.Threading.Tasks; +using Discord; using Discord.WebSocket; -using System.Threading.Tasks; -namespace NadekoBot.DataStructures.ModuleBehaviors +namespace NadekoBot.Common.ModuleBehaviors { /// /// Implemented by modules which can execute something and prevent further commands from being executed. diff --git a/src/NadekoBot/Common/ModuleBehaviors/IEarlyExecutor.cs b/src/NadekoBot/Common/ModuleBehaviors/IEarlyExecutor.cs index f761ef85..2f86be12 100644 --- a/src/NadekoBot/Common/ModuleBehaviors/IEarlyExecutor.cs +++ b/src/NadekoBot/Common/ModuleBehaviors/IEarlyExecutor.cs @@ -1,4 +1,4 @@ -namespace NadekoBot.DataStructures.ModuleBehaviors +namespace NadekoBot.Common.ModuleBehaviors { public interface IEarlyExecutor { diff --git a/src/NadekoBot/Common/ModuleBehaviors/IINputTransformer.cs b/src/NadekoBot/Common/ModuleBehaviors/IINputTransformer.cs index 3dd96464..8f4be470 100644 --- a/src/NadekoBot/Common/ModuleBehaviors/IINputTransformer.cs +++ b/src/NadekoBot/Common/ModuleBehaviors/IINputTransformer.cs @@ -1,7 +1,7 @@ -using Discord; -using System.Threading.Tasks; +using System.Threading.Tasks; +using Discord; -namespace NadekoBot.DataStructures.ModuleBehaviors +namespace NadekoBot.Common.ModuleBehaviors { public interface IInputTransformer { diff --git a/src/NadekoBot/Common/ModuleBehaviors/ILateBlocker.cs b/src/NadekoBot/Common/ModuleBehaviors/ILateBlocker.cs index 68f33206..58299714 100644 --- a/src/NadekoBot/Common/ModuleBehaviors/ILateBlocker.cs +++ b/src/NadekoBot/Common/ModuleBehaviors/ILateBlocker.cs @@ -1,8 +1,8 @@ -using Discord; +using System.Threading.Tasks; +using Discord; using Discord.WebSocket; -using System.Threading.Tasks; -namespace NadekoBot.DataStructures.ModuleBehaviors +namespace NadekoBot.Common.ModuleBehaviors { public interface ILateBlocker { diff --git a/src/NadekoBot/Common/ModuleBehaviors/ILateBlockingExecutor.cs b/src/NadekoBot/Common/ModuleBehaviors/ILateBlockingExecutor.cs index b4ebe54d..d1062524 100644 --- a/src/NadekoBot/Common/ModuleBehaviors/ILateBlockingExecutor.cs +++ b/src/NadekoBot/Common/ModuleBehaviors/ILateBlockingExecutor.cs @@ -1,4 +1,4 @@ -namespace NadekoBot.DataStructures.ModuleBehaviors +namespace NadekoBot.Common.ModuleBehaviors { public interface ILateBlockingExecutor { diff --git a/src/NadekoBot/Common/ModuleBehaviors/ILateExecutor.cs b/src/NadekoBot/Common/ModuleBehaviors/ILateExecutor.cs index a7b3e52e..53e878e4 100644 --- a/src/NadekoBot/Common/ModuleBehaviors/ILateExecutor.cs +++ b/src/NadekoBot/Common/ModuleBehaviors/ILateExecutor.cs @@ -1,8 +1,8 @@ -using Discord; +using System.Threading.Tasks; +using Discord; using Discord.WebSocket; -using System.Threading.Tasks; -namespace NadekoBot.DataStructures.ModuleBehaviors +namespace NadekoBot.Common.ModuleBehaviors { /// /// Last thing to be executed, won't stop further executions diff --git a/src/NadekoBot/Common/NadekoRandom.cs b/src/NadekoBot/Common/NadekoRandom.cs index f1af50c1..b6f68ac6 100644 --- a/src/NadekoBot/Common/NadekoRandom.cs +++ b/src/NadekoBot/Common/NadekoRandom.cs @@ -1,26 +1,21 @@ using System; using System.Security.Cryptography; -namespace NadekoBot.Services +namespace NadekoBot.Common { public class NadekoRandom : Random { - RandomNumberGenerator rng; + readonly RandomNumberGenerator _rng; public NadekoRandom() : base() { - rng = RandomNumberGenerator.Create(); - } - - private NadekoRandom(int Seed) : base(Seed) - { - rng = RandomNumberGenerator.Create(); + _rng = RandomNumberGenerator.Create(); } public override int Next() { var bytes = new byte[sizeof(int)]; - rng.GetBytes(bytes); + _rng.GetBytes(bytes); return Math.Abs(BitConverter.ToInt32(bytes, 0)); } @@ -29,7 +24,7 @@ namespace NadekoBot.Services if (maxValue <= 0) throw new ArgumentOutOfRangeException(); var bytes = new byte[sizeof(int)]; - rng.GetBytes(bytes); + _rng.GetBytes(bytes); return Math.Abs(BitConverter.ToInt32(bytes, 0)) % maxValue; } @@ -40,27 +35,27 @@ namespace NadekoBot.Services if (minValue == maxValue) return minValue; var bytes = new byte[sizeof(int)]; - rng.GetBytes(bytes); + _rng.GetBytes(bytes); var sign = Math.Sign(BitConverter.ToInt32(bytes, 0)); return (sign * BitConverter.ToInt32(bytes, 0)) % (maxValue - minValue) + minValue; } public override void NextBytes(byte[] buffer) { - rng.GetBytes(buffer); + _rng.GetBytes(buffer); } protected override double Sample() { var bytes = new byte[sizeof(double)]; - rng.GetBytes(bytes); + _rng.GetBytes(bytes); return Math.Abs(BitConverter.ToDouble(bytes, 0) / double.MaxValue + 1); } public override double NextDouble() { var bytes = new byte[sizeof(double)]; - rng.GetBytes(bytes); + _rng.GetBytes(bytes); return BitConverter.ToDouble(bytes, 0); } } diff --git a/src/NadekoBot/Common/NoPublicBotPrecondition.cs b/src/NadekoBot/Common/NoPublicBotPrecondition.cs index c5e06578..95f35566 100644 --- a/src/NadekoBot/Common/NoPublicBotPrecondition.cs +++ b/src/NadekoBot/Common/NoPublicBotPrecondition.cs @@ -1,8 +1,8 @@ -using Discord.Commands; -using System; +using System; using System.Threading.Tasks; +using Discord.Commands; -namespace NadekoBot.DataStructures +namespace NadekoBot.Common { public class NoPublicBot : PreconditionAttribute { diff --git a/src/NadekoBot/Common/PlatformHelper.cs b/src/NadekoBot/Common/PlatformHelper.cs index 8c523ec9..a8a53b0a 100644 --- a/src/NadekoBot/Common/PlatformHelper.cs +++ b/src/NadekoBot/Common/PlatformHelper.cs @@ -1,6 +1,6 @@ using System; -namespace ConcurrentCollections +namespace NadekoBot.Common { public static class PlatformHelper { diff --git a/src/NadekoBot/Common/Replacements/ReplacementBuilder.cs b/src/NadekoBot/Common/Replacements/ReplacementBuilder.cs index 0be7516d..4df63980 100644 --- a/src/NadekoBot/Common/Replacements/ReplacementBuilder.cs +++ b/src/NadekoBot/Common/Replacements/ReplacementBuilder.cs @@ -1,16 +1,15 @@ -using Discord; -using Discord.Commands; -using Discord.WebSocket; -using NadekoBot.Extensions; -using NadekoBot.Modules.Music.Services; -using NadekoBot.Services; -using NadekoBot.Services.Administration; -using System; +using System; using System.Collections.Concurrent; using System.Linq; using System.Text.RegularExpressions; +using Discord; +using Discord.Commands; +using Discord.WebSocket; +using NadekoBot.Extensions; +using NadekoBot.Modules.Administration.Services; +using NadekoBot.Modules.Music.Services; -namespace NadekoBot.DataStructures.Replacements +namespace NadekoBot.Common.Replacements { public class ReplacementBuilder { diff --git a/src/NadekoBot/Common/Replacements/Replacer.cs b/src/NadekoBot/Common/Replacements/Replacer.cs index ec1bbe7e..410b6c9a 100644 --- a/src/NadekoBot/Common/Replacements/Replacer.cs +++ b/src/NadekoBot/Common/Replacements/Replacer.cs @@ -2,7 +2,7 @@ using System.Collections.Generic; using System.Text.RegularExpressions; -namespace NadekoBot.DataStructures.Replacements +namespace NadekoBot.Common.Replacements { public class Replacer { diff --git a/src/NadekoBot/Common/Shard0Precondition.cs b/src/NadekoBot/Common/Shard0Precondition.cs index eaa5c591..965101a3 100644 --- a/src/NadekoBot/Common/Shard0Precondition.cs +++ b/src/NadekoBot/Common/Shard0Precondition.cs @@ -1,22 +1,19 @@ -using Discord.Commands; -using Discord.WebSocket; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; +using System; using System.Threading.Tasks; +using Discord.Commands; +using Discord.WebSocket; -namespace NadekoBot.DataStructures +namespace NadekoBot.Common { public class Shard0Precondition : PreconditionAttribute { public override Task CheckPermissions(ICommandContext context, CommandInfo command, IServiceProvider services) { var c = (DiscordSocketClient)context.Client; - if (c.ShardId == 0) - return Task.FromResult(PreconditionResult.FromSuccess()); - else + if (c.ShardId != 0) return Task.FromResult(PreconditionResult.FromError("Must be ran from shard #0")); + + return Task.FromResult(PreconditionResult.FromSuccess()); } } } diff --git a/src/NadekoBot/Common/ShardCom/IShardComMessage.cs b/src/NadekoBot/Common/ShardCom/IShardComMessage.cs index 6f54df59..1ea37c67 100644 --- a/src/NadekoBot/Common/ShardCom/IShardComMessage.cs +++ b/src/NadekoBot/Common/ShardCom/IShardComMessage.cs @@ -1,11 +1,7 @@ -using Discord; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; +using System; +using Discord; -namespace NadekoBot.DataStructures.ShardCom +namespace NadekoBot.Common.ShardCom { public class ShardComMessage { diff --git a/src/NadekoBot/Common/ShardCom/ShardComClient.cs b/src/NadekoBot/Common/ShardCom/ShardComClient.cs index 67e1c9f6..9c10a11d 100644 --- a/src/NadekoBot/Common/ShardCom/ShardComClient.cs +++ b/src/NadekoBot/Common/ShardCom/ShardComClient.cs @@ -1,11 +1,10 @@ -using Newtonsoft.Json; -using System; -using System.Net; +using System.Net; using System.Net.Sockets; using System.Text; using System.Threading.Tasks; +using Newtonsoft.Json; -namespace NadekoBot.DataStructures.ShardCom +namespace NadekoBot.Common.ShardCom { public class ShardComClient { diff --git a/src/NadekoBot/Common/ShardCom/ShardComServer.cs b/src/NadekoBot/Common/ShardCom/ShardComServer.cs index d0e1cbf6..b6b5a0ba 100644 --- a/src/NadekoBot/Common/ShardCom/ShardComServer.cs +++ b/src/NadekoBot/Common/ShardCom/ShardComServer.cs @@ -1,11 +1,11 @@ -using Newtonsoft.Json; -using System; +using System; using System.Net; using System.Net.Sockets; using System.Text; using System.Threading.Tasks; +using Newtonsoft.Json; -namespace NadekoBot.DataStructures.ShardCom +namespace NadekoBot.Common.ShardCom { public class ShardComServer : IDisposable { diff --git a/src/NadekoBot/Common/SyncPrecondition.cs b/src/NadekoBot/Common/SyncPrecondition.cs deleted file mode 100644 index 6dd675e5..00000000 --- a/src/NadekoBot/Common/SyncPrecondition.cs +++ /dev/null @@ -1,23 +0,0 @@ -using Discord.Commands; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace NadekoBot.DataStructures -{ - //public class SyncPrecondition : PreconditionAttribute - //{ - // public override Task CheckPermissions(ICommandContext context, - // CommandInfo command, - // IServiceProvider services) - // { - - // } - //} - //public enum SyncType - //{ - // Guild - //} -} diff --git a/src/NadekoBot/Common/TypeReaders/BotCommandTypeReader.cs b/src/NadekoBot/Common/TypeReaders/BotCommandTypeReader.cs index 84c45c1f..d80fbd52 100644 --- a/src/NadekoBot/Common/TypeReaders/BotCommandTypeReader.cs +++ b/src/NadekoBot/Common/TypeReaders/BotCommandTypeReader.cs @@ -1,11 +1,11 @@ -using Discord.Commands; -using NadekoBot.Modules.CustomReactions.Services; -using NadekoBot.Services; -using System; +using System; using System.Linq; using System.Threading.Tasks; +using Discord.Commands; +using NadekoBot.Modules.CustomReactions.Services; +using NadekoBot.Services; -namespace NadekoBot.TypeReaders +namespace NadekoBot.Common.TypeReaders { public class CommandTypeReader : TypeReader { diff --git a/src/NadekoBot/Common/TypeReaders/GuildDateTimeTypeReader.cs b/src/NadekoBot/Common/TypeReaders/GuildDateTimeTypeReader.cs index 8b6ea976..b1fa1f00 100644 --- a/src/NadekoBot/Common/TypeReaders/GuildDateTimeTypeReader.cs +++ b/src/NadekoBot/Common/TypeReaders/GuildDateTimeTypeReader.cs @@ -1,10 +1,9 @@ -using Discord.Commands; -using NadekoBot.Services; -using NadekoBot.Services.Administration; -using System; +using System; using System.Threading.Tasks; +using Discord.Commands; +using NadekoBot.Modules.Administration.Services; -namespace NadekoBot.TypeReaders +namespace NadekoBot.Common.TypeReaders { public class GuildDateTimeTypeReader : TypeReader { diff --git a/src/NadekoBot/Common/TypeReaders/GuildTypeReader.cs b/src/NadekoBot/Common/TypeReaders/GuildTypeReader.cs index 068514fb..132e1f0f 100644 --- a/src/NadekoBot/Common/TypeReaders/GuildTypeReader.cs +++ b/src/NadekoBot/Common/TypeReaders/GuildTypeReader.cs @@ -1,10 +1,10 @@ -using Discord.Commands; -using Discord.WebSocket; -using System; +using System; using System.Linq; using System.Threading.Tasks; +using Discord.Commands; +using Discord.WebSocket; -namespace NadekoBot.TypeReaders +namespace NadekoBot.Common.TypeReaders { public class GuildTypeReader : TypeReader { diff --git a/src/NadekoBot/Common/PermissionAction.cs b/src/NadekoBot/Common/TypeReaders/Models/PermissionAction.cs similarity index 93% rename from src/NadekoBot/Common/PermissionAction.cs rename to src/NadekoBot/Common/TypeReaders/Models/PermissionAction.cs index 324bbf44..f9a80058 100644 --- a/src/NadekoBot/Common/PermissionAction.cs +++ b/src/NadekoBot/Common/TypeReaders/Models/PermissionAction.cs @@ -1,4 +1,4 @@ -namespace NadekoBot.Modules.Permissions +namespace NadekoBot.Common.TypeReaders.Models { public class PermissionAction { diff --git a/src/NadekoBot/Common/TypeReaders/ModuleTypeReader.cs b/src/NadekoBot/Common/TypeReaders/ModuleTypeReader.cs index 60adfc26..1978732d 100644 --- a/src/NadekoBot/Common/TypeReaders/ModuleTypeReader.cs +++ b/src/NadekoBot/Common/TypeReaders/ModuleTypeReader.cs @@ -1,10 +1,10 @@ -using Discord.Commands; -using NadekoBot.Extensions; -using System; +using System; using System.Linq; using System.Threading.Tasks; +using Discord.Commands; +using NadekoBot.Extensions; -namespace NadekoBot.TypeReaders +namespace NadekoBot.Common.TypeReaders { public class ModuleTypeReader : TypeReader { diff --git a/src/NadekoBot/Common/TypeReaders/PermissionActionTypeReader.cs b/src/NadekoBot/Common/TypeReaders/PermissionActionTypeReader.cs index 9ca20229..82e16e16 100644 --- a/src/NadekoBot/Common/TypeReaders/PermissionActionTypeReader.cs +++ b/src/NadekoBot/Common/TypeReaders/PermissionActionTypeReader.cs @@ -1,9 +1,9 @@ -using Discord.Commands; +using System; using System.Threading.Tasks; -using NadekoBot.Modules.Permissions; -using System; +using Discord.Commands; +using NadekoBot.Common.TypeReaders.Models; -namespace NadekoBot.TypeReaders +namespace NadekoBot.Common.TypeReaders { /// /// Used instead of bool for more flexible keywords for true/false only in the permission module diff --git a/src/NadekoBot/Modules/Administration/Administration.cs b/src/NadekoBot/Modules/Administration/Administration.cs index e30d1664..84941722 100644 --- a/src/NadekoBot/Modules/Administration/Administration.cs +++ b/src/NadekoBot/Modules/Administration/Administration.cs @@ -5,10 +5,10 @@ using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using NadekoBot.Common.Attributes; using NadekoBot.Services; -using NadekoBot.Attributes; +using NadekoBot.Modules.Administration.Services; using NadekoBot.Services.Database.Models; -using NadekoBot.Services.Administration; namespace NadekoBot.Modules.Administration { diff --git a/src/NadekoBot/Modules/Administration/Commands/AutoAssignRoleCommands.cs b/src/NadekoBot/Modules/Administration/AutoAssignRoleCommands.cs similarity index 96% rename from src/NadekoBot/Modules/Administration/Commands/AutoAssignRoleCommands.cs rename to src/NadekoBot/Modules/Administration/AutoAssignRoleCommands.cs index 18a1d367..a2f53f4e 100644 --- a/src/NadekoBot/Modules/Administration/Commands/AutoAssignRoleCommands.cs +++ b/src/NadekoBot/Modules/Administration/AutoAssignRoleCommands.cs @@ -1,11 +1,11 @@ using Discord; using Discord.Commands; -using NadekoBot.Attributes; using NadekoBot.Extensions; using NadekoBot.Services; -using NadekoBot.Services.Administration; using System.Linq; using System.Threading.Tasks; +using NadekoBot.Common.Attributes; +using NadekoBot.Modules.Administration.Services; namespace NadekoBot.Modules.Administration { diff --git a/src/NadekoBot/Modules/Administration/Commands/Migration/0_9..cs b/src/NadekoBot/Modules/Administration/Common/Migration/0_9..cs similarity index 99% rename from src/NadekoBot/Modules/Administration/Commands/Migration/0_9..cs rename to src/NadekoBot/Modules/Administration/Common/Migration/0_9..cs index b7de1523..b7e2bc0a 100644 --- a/src/NadekoBot/Modules/Administration/Commands/Migration/0_9..cs +++ b/src/NadekoBot/Modules/Administration/Common/Migration/0_9..cs @@ -2,7 +2,7 @@ using System.Collections.Generic; using System.Linq; -namespace NadekoBot.Modules.Administration.Commands.Migration +namespace NadekoBot.Modules.Administration.Common.Migration { public class CommandPrefixes0_9 { diff --git a/src/NadekoBot/Modules/Administration/Commands/Migration/MigrationException.cs b/src/NadekoBot/Modules/Administration/Common/Migration/MigrationException.cs similarity index 57% rename from src/NadekoBot/Modules/Administration/Commands/Migration/MigrationException.cs rename to src/NadekoBot/Modules/Administration/Common/Migration/MigrationException.cs index 307ed63d..639ff5bc 100644 --- a/src/NadekoBot/Modules/Administration/Commands/Migration/MigrationException.cs +++ b/src/NadekoBot/Modules/Administration/Common/Migration/MigrationException.cs @@ -1,6 +1,6 @@ using System; -namespace NadekoBot.Modules.Administration.Commands.Migration +namespace NadekoBot.Modules.Administration.Common.Migration { public class MigrationException : Exception { diff --git a/src/NadekoBot/Services/Administration/ProtectionStats.cs b/src/NadekoBot/Modules/Administration/Common/ProtectionStats.cs similarity index 81% rename from src/NadekoBot/Services/Administration/ProtectionStats.cs rename to src/NadekoBot/Modules/Administration/Common/ProtectionStats.cs index 01d9f5f5..6c1a5dea 100644 --- a/src/NadekoBot/Services/Administration/ProtectionStats.cs +++ b/src/NadekoBot/Modules/Administration/Common/ProtectionStats.cs @@ -1,8 +1,9 @@ -using Discord; +using System.Collections.Concurrent; +using Discord; +using NadekoBot.Common.Collections; using NadekoBot.Services.Database.Models; -using System.Collections.Concurrent; -namespace NadekoBot.Services.Administration +namespace NadekoBot.Modules.Administration.Common { public enum ProtectionType { diff --git a/src/NadekoBot/Services/Administration/Ratelimiter.cs b/src/NadekoBot/Modules/Administration/Common/Ratelimiter.cs similarity index 92% rename from src/NadekoBot/Services/Administration/Ratelimiter.cs rename to src/NadekoBot/Modules/Administration/Common/Ratelimiter.cs index 1fd17a6a..12d115ae 100644 --- a/src/NadekoBot/Services/Administration/Ratelimiter.cs +++ b/src/NadekoBot/Modules/Administration/Common/Ratelimiter.cs @@ -1,12 +1,13 @@ -using Discord.WebSocket; -using System; +using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; +using Discord.WebSocket; +using NadekoBot.Modules.Administration.Services; -namespace NadekoBot.Services.Administration +namespace NadekoBot.Modules.Administration.Common { public class Ratelimiter { diff --git a/src/NadekoBot/Services/Administration/UserSpamStats.cs b/src/NadekoBot/Modules/Administration/Common/UserSpamStats.cs similarity index 94% rename from src/NadekoBot/Services/Administration/UserSpamStats.cs rename to src/NadekoBot/Modules/Administration/Common/UserSpamStats.cs index ad2efcbe..28f338ea 100644 --- a/src/NadekoBot/Services/Administration/UserSpamStats.cs +++ b/src/NadekoBot/Modules/Administration/Common/UserSpamStats.cs @@ -1,10 +1,10 @@ -using Discord; -using System; +using System; using System.Collections.Concurrent; using System.Linq; using System.Threading; +using Discord; -namespace NadekoBot.Services.Administration +namespace NadekoBot.Modules.Administration.Common { public class UserSpamStats : IDisposable { diff --git a/src/NadekoBot/Modules/Administration/Commands/GameChannelCommands.cs b/src/NadekoBot/Modules/Administration/GameChannelCommands.cs similarity index 96% rename from src/NadekoBot/Modules/Administration/Commands/GameChannelCommands.cs rename to src/NadekoBot/Modules/Administration/GameChannelCommands.cs index 93cbd5e7..647874a6 100644 --- a/src/NadekoBot/Modules/Administration/Commands/GameChannelCommands.cs +++ b/src/NadekoBot/Modules/Administration/GameChannelCommands.cs @@ -1,9 +1,9 @@ using Discord; using Discord.Commands; -using NadekoBot.Attributes; using NadekoBot.Services; using System.Threading.Tasks; -using NadekoBot.Services.Administration; +using NadekoBot.Common.Attributes; +using NadekoBot.Modules.Administration.Services; namespace NadekoBot.Modules.Administration { diff --git a/src/NadekoBot/Modules/Administration/Commands/LocalizationCommands.cs b/src/NadekoBot/Modules/Administration/LocalizationCommands.cs similarity index 99% rename from src/NadekoBot/Modules/Administration/Commands/LocalizationCommands.cs rename to src/NadekoBot/Modules/Administration/LocalizationCommands.cs index eef4b14c..bdef0799 100644 --- a/src/NadekoBot/Modules/Administration/Commands/LocalizationCommands.cs +++ b/src/NadekoBot/Modules/Administration/LocalizationCommands.cs @@ -1,6 +1,5 @@ using Discord; using Discord.Commands; -using NadekoBot.Attributes; using NadekoBot.Extensions; using System; using System.Collections.Generic; @@ -8,6 +7,7 @@ using System.Collections.Immutable; using System.Globalization; using System.Linq; using System.Threading.Tasks; +using NadekoBot.Common.Attributes; namespace NadekoBot.Modules.Administration { diff --git a/src/NadekoBot/Modules/Administration/Commands/LogCommand.cs b/src/NadekoBot/Modules/Administration/LogCommands.cs similarity index 97% rename from src/NadekoBot/Modules/Administration/Commands/LogCommand.cs rename to src/NadekoBot/Modules/Administration/LogCommands.cs index cb83822c..3ee5fc5a 100644 --- a/src/NadekoBot/Modules/Administration/Commands/LogCommand.cs +++ b/src/NadekoBot/Modules/Administration/LogCommands.cs @@ -1,16 +1,17 @@ using Discord; using Discord.Commands; -using NadekoBot.Attributes; -using NadekoBot.DataStructures; using NadekoBot.Extensions; using NadekoBot.Modules.Permissions; using NadekoBot.Services; -using NadekoBot.Services.Administration; using NadekoBot.Services.Database.Models; using System; using System.Linq; using System.Threading.Tasks; -using static NadekoBot.Services.Administration.LogCommandService; +using NadekoBot.Common; +using NadekoBot.Common.Attributes; +using NadekoBot.Common.TypeReaders.Models; +using NadekoBot.Modules.Administration.Services; +using static NadekoBot.Modules.Administration.Services.LogCommandService; namespace NadekoBot.Modules.Administration { diff --git a/src/NadekoBot/Modules/Administration/Commands/Migration.cs b/src/NadekoBot/Modules/Administration/MigrationCommands.cs similarity index 98% rename from src/NadekoBot/Modules/Administration/Commands/Migration.cs rename to src/NadekoBot/Modules/Administration/MigrationCommands.cs index e1497999..fe14afe6 100644 --- a/src/NadekoBot/Modules/Administration/Commands/Migration.cs +++ b/src/NadekoBot/Modules/Administration/MigrationCommands.cs @@ -4,27 +4,28 @@ using System.IO; using System.Linq; using System.Threading.Tasks; using Discord.Commands; -using NadekoBot.Attributes; using NadekoBot.Services; using NadekoBot.Services.Database.Models; using Newtonsoft.Json; -using NadekoBot.Modules.Administration.Commands.Migration; using System.Collections.Concurrent; using NadekoBot.Extensions; using NadekoBot.Services.Database; using Microsoft.Data.Sqlite; +using NadekoBot.Common.Attributes; +using NadekoBot.Common.Collections; +using NadekoBot.Modules.Administration.Common.Migration; namespace NadekoBot.Modules.Administration { public partial class Administration { [Group] - public class Migration : NadekoSubmodule + public class MigrationCommands : NadekoSubmodule { private const int CURRENT_VERSION = 1; private readonly DbService _db; - public Migration(DbService db) + public MigrationCommands(DbService db) { _db = db; } diff --git a/src/NadekoBot/Modules/Administration/Commands/MuteCommands.cs b/src/NadekoBot/Modules/Administration/MuteCommands.cs similarity index 98% rename from src/NadekoBot/Modules/Administration/Commands/MuteCommands.cs rename to src/NadekoBot/Modules/Administration/MuteCommands.cs index e3673b34..a8322de4 100644 --- a/src/NadekoBot/Modules/Administration/Commands/MuteCommands.cs +++ b/src/NadekoBot/Modules/Administration/MuteCommands.cs @@ -1,10 +1,10 @@ using Discord; using Discord.Commands; -using NadekoBot.Attributes; using NadekoBot.Services; -using NadekoBot.Services.Administration; using System; using System.Threading.Tasks; +using NadekoBot.Common.Attributes; +using NadekoBot.Modules.Administration.Services; namespace NadekoBot.Modules.Administration { diff --git a/src/NadekoBot/Modules/Administration/Commands/PlayingRotateCommands.cs b/src/NadekoBot/Modules/Administration/PlayingRotateCommands.cs similarity index 97% rename from src/NadekoBot/Modules/Administration/Commands/PlayingRotateCommands.cs rename to src/NadekoBot/Modules/Administration/PlayingRotateCommands.cs index 549230a3..4cccf226 100644 --- a/src/NadekoBot/Modules/Administration/Commands/PlayingRotateCommands.cs +++ b/src/NadekoBot/Modules/Administration/PlayingRotateCommands.cs @@ -1,10 +1,10 @@ using Discord.Commands; -using NadekoBot.Attributes; using NadekoBot.Services; -using NadekoBot.Services.Administration; using NadekoBot.Services.Database.Models; using System.Linq; using System.Threading.Tasks; +using NadekoBot.Common.Attributes; +using NadekoBot.Modules.Administration.Services; namespace NadekoBot.Modules.Administration { diff --git a/src/NadekoBot/Modules/Administration/Commands/PrefixCommands.cs b/src/NadekoBot/Modules/Administration/PrefixCommands.cs similarity index 98% rename from src/NadekoBot/Modules/Administration/Commands/PrefixCommands.cs rename to src/NadekoBot/Modules/Administration/PrefixCommands.cs index 012b26c5..617ebc30 100644 --- a/src/NadekoBot/Modules/Administration/Commands/PrefixCommands.cs +++ b/src/NadekoBot/Modules/Administration/PrefixCommands.cs @@ -1,7 +1,7 @@ using Discord; using Discord.Commands; -using NadekoBot.Attributes; using System.Threading.Tasks; +using NadekoBot.Common.Attributes; namespace NadekoBot.Modules.Administration { diff --git a/src/NadekoBot/Modules/Administration/Commands/ProtectionCommands.cs b/src/NadekoBot/Modules/Administration/ProtectionCommands.cs similarity index 98% rename from src/NadekoBot/Modules/Administration/Commands/ProtectionCommands.cs rename to src/NadekoBot/Modules/Administration/ProtectionCommands.cs index cf18fe63..8468f270 100644 --- a/src/NadekoBot/Modules/Administration/Commands/ProtectionCommands.cs +++ b/src/NadekoBot/Modules/Administration/ProtectionCommands.cs @@ -1,14 +1,15 @@ using Discord; using Discord.Commands; using Microsoft.EntityFrameworkCore; -using NadekoBot.Attributes; using NadekoBot.Extensions; using NadekoBot.Services; using NadekoBot.Services.Database.Models; using System; using System.Linq; using System.Threading.Tasks; -using NadekoBot.Services.Administration; +using NadekoBot.Common.Attributes; +using NadekoBot.Modules.Administration.Common; +using NadekoBot.Modules.Administration.Services; namespace NadekoBot.Modules.Administration { diff --git a/src/NadekoBot/Modules/Administration/Commands/PruneCommands.cs b/src/NadekoBot/Modules/Administration/PruneCommands.cs similarity index 96% rename from src/NadekoBot/Modules/Administration/Commands/PruneCommands.cs rename to src/NadekoBot/Modules/Administration/PruneCommands.cs index f3a9d39c..5fa42f28 100644 --- a/src/NadekoBot/Modules/Administration/Commands/PruneCommands.cs +++ b/src/NadekoBot/Modules/Administration/PruneCommands.cs @@ -1,10 +1,10 @@ using Discord; using Discord.Commands; -using NadekoBot.Attributes; using NadekoBot.Extensions; -using NadekoBot.Services.Administration; using System; using System.Threading.Tasks; +using NadekoBot.Common.Attributes; +using NadekoBot.Modules.Administration.Services; namespace NadekoBot.Modules.Administration { diff --git a/src/NadekoBot/Modules/Administration/Commands/RatelimitCommand.cs b/src/NadekoBot/Modules/Administration/RatelimitCommands.cs similarity index 97% rename from src/NadekoBot/Modules/Administration/Commands/RatelimitCommand.cs rename to src/NadekoBot/Modules/Administration/RatelimitCommands.cs index bb829d37..7be7acd2 100644 --- a/src/NadekoBot/Modules/Administration/Commands/RatelimitCommand.cs +++ b/src/NadekoBot/Modules/Administration/RatelimitCommands.cs @@ -1,14 +1,15 @@ using Discord; using Discord.Commands; using Microsoft.EntityFrameworkCore; -using NadekoBot.Attributes; using NadekoBot.Extensions; using NadekoBot.Services; -using NadekoBot.Services.Administration; using NadekoBot.Services.Database.Models; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using NadekoBot.Common.Attributes; +using NadekoBot.Modules.Administration.Common; +using NadekoBot.Modules.Administration.Services; namespace NadekoBot.Modules.Administration { diff --git a/src/NadekoBot/Modules/Administration/Commands/SelfAssignedRolesCommand.cs b/src/NadekoBot/Modules/Administration/SelfAssignedRolesCommands.cs similarity index 99% rename from src/NadekoBot/Modules/Administration/Commands/SelfAssignedRolesCommand.cs rename to src/NadekoBot/Modules/Administration/SelfAssignedRolesCommands.cs index 3ad63210..2d568519 100644 --- a/src/NadekoBot/Modules/Administration/Commands/SelfAssignedRolesCommand.cs +++ b/src/NadekoBot/Modules/Administration/SelfAssignedRolesCommands.cs @@ -1,7 +1,6 @@ using Discord; using Discord.Commands; using Discord.WebSocket; -using NadekoBot.Attributes; using NadekoBot.Extensions; using NadekoBot.Services; using NadekoBot.Services.Database.Models; @@ -11,6 +10,8 @@ using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; +using NadekoBot.Common.Attributes; +using NadekoBot.Common.Collections; namespace NadekoBot.Modules.Administration { diff --git a/src/NadekoBot/Modules/Administration/Commands/SelfCommands.cs b/src/NadekoBot/Modules/Administration/SelfCommands.cs similarity index 99% rename from src/NadekoBot/Modules/Administration/Commands/SelfCommands.cs rename to src/NadekoBot/Modules/Administration/SelfCommands.cs index f669542c..96f175d0 100644 --- a/src/NadekoBot/Modules/Administration/Commands/SelfCommands.cs +++ b/src/NadekoBot/Modules/Administration/SelfCommands.cs @@ -1,6 +1,5 @@ using Discord; using Discord.Commands; -using NadekoBot.Attributes; using NadekoBot.Extensions; using System; using System.Collections.Generic; @@ -12,8 +11,9 @@ using Discord.WebSocket; using NadekoBot.Services; using NadekoBot.Services.Database.Models; using Microsoft.EntityFrameworkCore; -using NadekoBot.Services.Administration; using System.Diagnostics; +using NadekoBot.Common.Attributes; +using NadekoBot.Modules.Administration.Services; using NadekoBot.Modules.Music.Services; namespace NadekoBot.Modules.Administration diff --git a/src/NadekoBot/Modules/Administration/Commands/ServerGreetCommands.cs b/src/NadekoBot/Modules/Administration/ServerGreetCommands.cs similarity index 99% rename from src/NadekoBot/Modules/Administration/Commands/ServerGreetCommands.cs rename to src/NadekoBot/Modules/Administration/ServerGreetCommands.cs index b957a4c9..0f94e002 100644 --- a/src/NadekoBot/Modules/Administration/Commands/ServerGreetCommands.cs +++ b/src/NadekoBot/Modules/Administration/ServerGreetCommands.cs @@ -1,10 +1,10 @@ using Discord; using Discord.Commands; -using NadekoBot.Attributes; using NadekoBot.Extensions; using NadekoBot.Services; using NadekoBot.Services.Database.Models; using System.Threading.Tasks; +using NadekoBot.Common.Attributes; namespace NadekoBot.Modules.Administration { diff --git a/src/NadekoBot/Services/Administration/AdministrationService.cs b/src/NadekoBot/Modules/Administration/Services/AdministrationService.cs similarity index 91% rename from src/NadekoBot/Services/Administration/AdministrationService.cs rename to src/NadekoBot/Modules/Administration/Services/AdministrationService.cs index 3fe34164..104f9174 100644 --- a/src/NadekoBot/Services/Administration/AdministrationService.cs +++ b/src/NadekoBot/Modules/Administration/Services/AdministrationService.cs @@ -1,15 +1,17 @@ -using Discord; -using Discord.Commands; -using Discord.WebSocket; -using NadekoBot.Services.Database.Models; -using NLog; -using System; +using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using Discord; +using Discord.Commands; +using Discord.WebSocket; +using NadekoBot.Common.Collections; +using NadekoBot.Services; +using NadekoBot.Services.Database.Models; +using NLog; -namespace NadekoBot.Services.Administration +namespace NadekoBot.Modules.Administration.Services { public class AdministrationService : INService { diff --git a/src/NadekoBot/Services/Administration/AutoAssignRoleService.cs b/src/NadekoBot/Modules/Administration/Services/AutoAssignRoleService.cs similarity index 92% rename from src/NadekoBot/Services/Administration/AutoAssignRoleService.cs rename to src/NadekoBot/Modules/Administration/Services/AutoAssignRoleService.cs index bba16d44..040986cb 100644 --- a/src/NadekoBot/Services/Administration/AutoAssignRoleService.cs +++ b/src/NadekoBot/Modules/Administration/Services/AutoAssignRoleService.cs @@ -1,13 +1,14 @@ -using Discord.WebSocket; -using NadekoBot.Services.Database.Models; -using NLog; -using System; +using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using Discord.WebSocket; +using NadekoBot.Services; +using NadekoBot.Services.Database.Models; +using NLog; -namespace NadekoBot.Services.Administration +namespace NadekoBot.Modules.Administration.Services { public class AutoAssignRoleService : INService { diff --git a/src/NadekoBot/Services/Administration/GameVoiceChannelService.cs b/src/NadekoBot/Modules/Administration/Services/GameVoiceChannelService.cs similarity index 93% rename from src/NadekoBot/Services/Administration/GameVoiceChannelService.cs rename to src/NadekoBot/Modules/Administration/Services/GameVoiceChannelService.cs index b7389bed..523b1dac 100644 --- a/src/NadekoBot/Services/Administration/GameVoiceChannelService.cs +++ b/src/NadekoBot/Modules/Administration/Services/GameVoiceChannelService.cs @@ -1,14 +1,16 @@ -using Discord.WebSocket; -using NadekoBot.Extensions; -using NadekoBot.Services.Database.Models; -using NLog; -using System; +using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using Discord.WebSocket; +using NadekoBot.Common.Collections; +using NadekoBot.Extensions; +using NadekoBot.Services; +using NadekoBot.Services.Database.Models; +using NLog; -namespace NadekoBot.Services.Administration +namespace NadekoBot.Modules.Administration.Services { public class GameVoiceChannelService : INService { diff --git a/src/NadekoBot/Services/Administration/GuildTimezoneService.cs b/src/NadekoBot/Modules/Administration/Services/GuildTimezoneService.cs similarity index 96% rename from src/NadekoBot/Services/Administration/GuildTimezoneService.cs rename to src/NadekoBot/Modules/Administration/Services/GuildTimezoneService.cs index caa9340d..fdef20c5 100644 --- a/src/NadekoBot/Services/Administration/GuildTimezoneService.cs +++ b/src/NadekoBot/Modules/Administration/Services/GuildTimezoneService.cs @@ -1,13 +1,13 @@ -using NadekoBot.Extensions; -using NadekoBot.Services.Database.Models; -using System; +using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; -using System.Collections.Concurrent; -using NadekoBot.Services; using Discord.WebSocket; +using NadekoBot.Extensions; +using NadekoBot.Services; +using NadekoBot.Services.Database.Models; -namespace NadekoBot.Services.Administration +namespace NadekoBot.Modules.Administration.Services { public class GuildTimezoneService : INService { diff --git a/src/NadekoBot/Services/Administration/LogCommandService.cs b/src/NadekoBot/Modules/Administration/Services/LogCommandService.cs similarity index 99% rename from src/NadekoBot/Services/Administration/LogCommandService.cs rename to src/NadekoBot/Modules/Administration/Services/LogCommandService.cs index d5221eb2..b185fff7 100644 --- a/src/NadekoBot/Services/Administration/LogCommandService.cs +++ b/src/NadekoBot/Modules/Administration/Services/LogCommandService.cs @@ -1,17 +1,19 @@ -using Discord; -using Discord.WebSocket; -using NadekoBot.Extensions; -using NadekoBot.Services.Database.Models; -using NLog; -using System; +using System; using System.Collections.Concurrent; using System.Collections.Generic; -using System.Diagnostics; using System.Linq; using System.Threading; using System.Threading.Tasks; +using Discord; +using Discord.WebSocket; +using NadekoBot.Extensions; +using NadekoBot.Modules.Administration.Common; +using NadekoBot.Services; +using NadekoBot.Services.Database.Models; +using NadekoBot.Services.Impl; +using NLog; -namespace NadekoBot.Services.Administration +namespace NadekoBot.Modules.Administration.Services { public class LogCommandService : INService { diff --git a/src/NadekoBot/Services/Administration/MuteService.cs b/src/NadekoBot/Modules/Administration/Services/MuteService.cs similarity index 98% rename from src/NadekoBot/Services/Administration/MuteService.cs rename to src/NadekoBot/Modules/Administration/Services/MuteService.cs index 04042e5f..77b4136c 100644 --- a/src/NadekoBot/Services/Administration/MuteService.cs +++ b/src/NadekoBot/Modules/Administration/Services/MuteService.cs @@ -1,17 +1,19 @@ -using Discord; -using Discord.WebSocket; -using Microsoft.EntityFrameworkCore; -using NadekoBot.Extensions; -using NadekoBot.Services.Database.Models; -using NLog; -using System; +using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; +using Discord; +using Discord.WebSocket; +using Microsoft.EntityFrameworkCore; +using NadekoBot.Common.Collections; +using NadekoBot.Extensions; +using NadekoBot.Services; +using NadekoBot.Services.Database.Models; +using NLog; -namespace NadekoBot.Services.Administration +namespace NadekoBot.Modules.Administration.Services { public enum MuteType { diff --git a/src/NadekoBot/Services/Administration/PlayingRotateService.cs b/src/NadekoBot/Modules/Administration/Services/PlayingRotateService.cs similarity index 94% rename from src/NadekoBot/Services/Administration/PlayingRotateService.cs rename to src/NadekoBot/Modules/Administration/Services/PlayingRotateService.cs index a0122ae9..d7b5fe2e 100644 --- a/src/NadekoBot/Services/Administration/PlayingRotateService.cs +++ b/src/NadekoBot/Modules/Administration/Services/PlayingRotateService.cs @@ -1,13 +1,14 @@ -using Discord.WebSocket; -using NadekoBot.DataStructures.Replacements; -using NadekoBot.Modules.Music.Services; -using NadekoBot.Services.Database.Models; -using NLog; -using System; +using System; using System.Linq; using System.Threading; +using Discord.WebSocket; +using NadekoBot.Common.Replacements; +using NadekoBot.Modules.Music.Services; +using NadekoBot.Services; +using NadekoBot.Services.Database.Models; +using NLog; -namespace NadekoBot.Services.Administration +namespace NadekoBot.Modules.Administration.Services { public class PlayingRotateService : INService { diff --git a/src/NadekoBot/Services/Administration/ProtectionService.cs b/src/NadekoBot/Modules/Administration/Services/ProtectionService.cs similarity index 97% rename from src/NadekoBot/Services/Administration/ProtectionService.cs rename to src/NadekoBot/Modules/Administration/Services/ProtectionService.cs index 6996b3a1..21796df3 100644 --- a/src/NadekoBot/Services/Administration/ProtectionService.cs +++ b/src/NadekoBot/Modules/Administration/Services/ProtectionService.cs @@ -1,14 +1,16 @@ -using Discord; -using Discord.WebSocket; -using NadekoBot.Services.Database.Models; -using NLog; -using System; +using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using Discord; +using Discord.WebSocket; +using NadekoBot.Modules.Administration.Common; +using NadekoBot.Services; +using NadekoBot.Services.Database.Models; +using NLog; -namespace NadekoBot.Services.Administration +namespace NadekoBot.Modules.Administration.Services { public class ProtectionService : INService { diff --git a/src/NadekoBot/Services/Administration/PruneService.cs b/src/NadekoBot/Modules/Administration/Services/PruneService.cs similarity index 94% rename from src/NadekoBot/Services/Administration/PruneService.cs rename to src/NadekoBot/Modules/Administration/Services/PruneService.cs index 59da41b5..fd13daa5 100644 --- a/src/NadekoBot/Services/Administration/PruneService.cs +++ b/src/NadekoBot/Modules/Administration/Services/PruneService.cs @@ -1,13 +1,14 @@ -using Discord; -using NadekoBot.Extensions; -using System; +using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; -using System.Text; using System.Threading.Tasks; +using Discord; +using NadekoBot.Common.Collections; +using NadekoBot.Extensions; +using NadekoBot.Services; -namespace NadekoBot.Services.Administration +namespace NadekoBot.Modules.Administration.Services { public class PruneService : INService { diff --git a/src/NadekoBot/Services/Administration/RatelimitService.cs b/src/NadekoBot/Modules/Administration/Services/RatelimitService.cs similarity index 91% rename from src/NadekoBot/Services/Administration/RatelimitService.cs rename to src/NadekoBot/Modules/Administration/Services/RatelimitService.cs index 0c68accd..9667d193 100644 --- a/src/NadekoBot/Services/Administration/RatelimitService.cs +++ b/src/NadekoBot/Modules/Administration/Services/RatelimitService.cs @@ -1,16 +1,18 @@ -using Discord.WebSocket; -using NadekoBot.DataStructures.ModuleBehaviors; -using NadekoBot.Extensions; -using NadekoBot.Services.Database.Models; -using NLog; -using System; +using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; -using Discord; using System.Threading.Tasks; +using Discord; +using Discord.WebSocket; +using NadekoBot.Common.ModuleBehaviors; +using NadekoBot.Extensions; +using NadekoBot.Modules.Administration.Common; +using NadekoBot.Services; +using NadekoBot.Services.Database.Models; +using NLog; -namespace NadekoBot.Services.Administration +namespace NadekoBot.Modules.Administration.Services { public class SlowmodeService : IEarlyBlocker, INService { diff --git a/src/NadekoBot/Services/Administration/SelfService.cs b/src/NadekoBot/Modules/Administration/Services/SelfService.cs similarity index 96% rename from src/NadekoBot/Services/Administration/SelfService.cs rename to src/NadekoBot/Modules/Administration/Services/SelfService.cs index 8b567c42..dd0e6735 100644 --- a/src/NadekoBot/Services/Administration/SelfService.cs +++ b/src/NadekoBot/Modules/Administration/Services/SelfService.cs @@ -1,16 +1,18 @@ -using Discord; -using NadekoBot.DataStructures; -using NadekoBot.DataStructures.ModuleBehaviors; -using NadekoBot.Extensions; -using NadekoBot.Services.Database.Models; -using NLog; +using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; using System.Threading.Tasks; +using Discord; using Discord.WebSocket; -using System.Collections.Generic; +using NadekoBot.Common; +using NadekoBot.Common.ModuleBehaviors; +using NadekoBot.Extensions; +using NadekoBot.Services; +using NadekoBot.Services.Database.Models; +using NadekoBot.Services.Impl; +using NLog; -namespace NadekoBot.Services.Administration +namespace NadekoBot.Modules.Administration.Services { public class SelfService : ILateExecutor, INService { diff --git a/src/NadekoBot/Services/Administration/UserPunishService.cs b/src/NadekoBot/Modules/Administration/Services/UserPunishService.cs similarity index 96% rename from src/NadekoBot/Services/Administration/UserPunishService.cs rename to src/NadekoBot/Modules/Administration/Services/UserPunishService.cs index 84ec319c..55b886f3 100644 --- a/src/NadekoBot/Services/Administration/UserPunishService.cs +++ b/src/NadekoBot/Modules/Administration/Services/UserPunishService.cs @@ -1,12 +1,13 @@ -using Discord; -using Microsoft.EntityFrameworkCore; -using NadekoBot.Services.Database.Models; -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using Discord; +using Microsoft.EntityFrameworkCore; +using NadekoBot.Services; +using NadekoBot.Services.Database.Models; -namespace NadekoBot.Services.Administration +namespace NadekoBot.Modules.Administration.Services { public class UserPunishService : INService { diff --git a/src/NadekoBot/Services/Administration/VcRoleService.cs b/src/NadekoBot/Modules/Administration/Services/VcRoleService.cs similarity index 97% rename from src/NadekoBot/Services/Administration/VcRoleService.cs rename to src/NadekoBot/Modules/Administration/Services/VcRoleService.cs index e1d8e5d4..9f91d114 100644 --- a/src/NadekoBot/Services/Administration/VcRoleService.cs +++ b/src/NadekoBot/Modules/Administration/Services/VcRoleService.cs @@ -1,14 +1,15 @@ -using Discord; -using Discord.WebSocket; -using NadekoBot.Services.Database.Models; -using NLog; -using System; +using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using Discord; +using Discord.WebSocket; +using NadekoBot.Services; +using NadekoBot.Services.Database.Models; +using NLog; -namespace NadekoBot.Services.Administration +namespace NadekoBot.Modules.Administration.Services { public class VcRoleService : INService { diff --git a/src/NadekoBot/Services/Administration/VplusTService.cs b/src/NadekoBot/Modules/Administration/Services/VplusTService.cs similarity index 97% rename from src/NadekoBot/Services/Administration/VplusTService.cs rename to src/NadekoBot/Modules/Administration/Services/VplusTService.cs index d61bb9f8..9ffd8dcb 100644 --- a/src/NadekoBot/Services/Administration/VplusTService.cs +++ b/src/NadekoBot/Modules/Administration/Services/VplusTService.cs @@ -1,17 +1,20 @@ -using Discord; -using Discord.WebSocket; -using NadekoBot.Extensions; -using NadekoBot.Services.Database.Models; -using NLog; -using System; +using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; +using Discord; +using Discord.WebSocket; +using NadekoBot.Common.Collections; +using NadekoBot.Extensions; +using NadekoBot.Services; +using NadekoBot.Services.Database.Models; +using NadekoBot.Services.Impl; +using NLog; -namespace NadekoBot.Services.Administration +namespace NadekoBot.Modules.Administration.Services { public class VplusTService : INService { diff --git a/src/NadekoBot/Modules/Administration/Commands/TimeZoneCommands.cs b/src/NadekoBot/Modules/Administration/TimeZoneCommands.cs similarity index 96% rename from src/NadekoBot/Modules/Administration/Commands/TimeZoneCommands.cs rename to src/NadekoBot/Modules/Administration/TimeZoneCommands.cs index 8f014c47..25d55dd2 100644 --- a/src/NadekoBot/Modules/Administration/Commands/TimeZoneCommands.cs +++ b/src/NadekoBot/Modules/Administration/TimeZoneCommands.cs @@ -1,12 +1,12 @@ using Discord; using Discord.Commands; using Discord.WebSocket; -using NadekoBot.Attributes; using NadekoBot.Extensions; -using NadekoBot.Services.Administration; using System; using System.Linq; using System.Threading.Tasks; +using NadekoBot.Common.Attributes; +using NadekoBot.Modules.Administration.Services; namespace NadekoBot.Modules.Administration { diff --git a/src/NadekoBot/Modules/Administration/Commands/UserPunishCommands.cs b/src/NadekoBot/Modules/Administration/UserPunishCommands.cs similarity index 99% rename from src/NadekoBot/Modules/Administration/Commands/UserPunishCommands.cs rename to src/NadekoBot/Modules/Administration/UserPunishCommands.cs index 49d1fc38..d4b29723 100644 --- a/src/NadekoBot/Modules/Administration/Commands/UserPunishCommands.cs +++ b/src/NadekoBot/Modules/Administration/UserPunishCommands.cs @@ -2,13 +2,13 @@ using Discord.Commands; using Discord.WebSocket; using Microsoft.EntityFrameworkCore; -using NadekoBot.Attributes; using NadekoBot.Extensions; using NadekoBot.Services; -using NadekoBot.Services.Administration; using NadekoBot.Services.Database.Models; using System.Linq; using System.Threading.Tasks; +using NadekoBot.Common.Attributes; +using NadekoBot.Modules.Administration.Services; namespace NadekoBot.Modules.Administration { diff --git a/src/NadekoBot/Modules/Administration/Commands/VcRoleCommands.cs b/src/NadekoBot/Modules/Administration/VcRoleCommands.cs similarity index 98% rename from src/NadekoBot/Modules/Administration/Commands/VcRoleCommands.cs rename to src/NadekoBot/Modules/Administration/VcRoleCommands.cs index 5865b9a9..ad0c7feb 100644 --- a/src/NadekoBot/Modules/Administration/Commands/VcRoleCommands.cs +++ b/src/NadekoBot/Modules/Administration/VcRoleCommands.cs @@ -2,14 +2,14 @@ using System.Linq; using Discord; using Discord.Commands; -using NadekoBot.Attributes; using System.Threading.Tasks; using Discord.WebSocket; using Microsoft.EntityFrameworkCore; +using NadekoBot.Common.Attributes; using NadekoBot.Extensions; +using NadekoBot.Modules.Administration.Services; using NadekoBot.Services; using NadekoBot.Services.Database.Models; -using NadekoBot.Services.Administration; namespace NadekoBot.Modules.Administration { diff --git a/src/NadekoBot/Modules/Administration/Commands/VoicePlusTextCommands.cs b/src/NadekoBot/Modules/Administration/VoicePlusTextCommands.cs similarity index 98% rename from src/NadekoBot/Modules/Administration/Commands/VoicePlusTextCommands.cs rename to src/NadekoBot/Modules/Administration/VoicePlusTextCommands.cs index 9f328bea..ee063f8b 100644 --- a/src/NadekoBot/Modules/Administration/Commands/VoicePlusTextCommands.cs +++ b/src/NadekoBot/Modules/Administration/VoicePlusTextCommands.cs @@ -1,14 +1,14 @@ using Discord; using Discord.Commands; -using NadekoBot.Attributes; using NadekoBot.Extensions; using NadekoBot.Services; -using NadekoBot.Services.Administration; using System; using System.Collections.Generic; using System.Linq; using System.Text.RegularExpressions; using System.Threading.Tasks; +using NadekoBot.Common.Attributes; +using NadekoBot.Modules.Administration.Services; namespace NadekoBot.Modules.Administration { diff --git a/src/NadekoBot/Modules/CustomReactions/CustomReactions.cs b/src/NadekoBot/Modules/CustomReactions/CustomReactions.cs index e4aa6e3c..ff062bac 100644 --- a/src/NadekoBot/Modules/CustomReactions/CustomReactions.cs +++ b/src/NadekoBot/Modules/CustomReactions/CustomReactions.cs @@ -2,12 +2,12 @@ using System.Threading.Tasks; using Discord.Commands; using NadekoBot.Services; -using NadekoBot.Attributes; using NadekoBot.Services.Database.Models; using Discord; using NadekoBot.Extensions; using Discord.WebSocket; using System; +using NadekoBot.Common.Attributes; using NadekoBot.Modules.CustomReactions.Services; namespace NadekoBot.Modules.CustomReactions diff --git a/src/NadekoBot/Modules/CustomReactions/Extensions/Extensions.cs b/src/NadekoBot/Modules/CustomReactions/Extensions/Extensions.cs index 3e87ca37..25bbfce3 100644 --- a/src/NadekoBot/Modules/CustomReactions/Extensions/Extensions.cs +++ b/src/NadekoBot/Modules/CustomReactions/Extensions/Extensions.cs @@ -2,8 +2,6 @@ using AngleSharp.Dom.Html; using Discord; using Discord.WebSocket; -using NadekoBot.DataStructures; -using NadekoBot.DataStructures.Replacements; using NadekoBot.Extensions; using NadekoBot.Modules.CustomReactions.Services; using NadekoBot.Services; @@ -13,6 +11,8 @@ using System.Collections.Generic; using System.Linq; using System.Text.RegularExpressions; using System.Threading.Tasks; +using NadekoBot.Common; +using NadekoBot.Common.Replacements; namespace NadekoBot.Modules.CustomReactions.Extensions { diff --git a/src/NadekoBot/Modules/CustomReactions/Services/CustomReactionsService.cs b/src/NadekoBot/Modules/CustomReactions/Services/CustomReactionsService.cs index b87d4145..1241e664 100644 --- a/src/NadekoBot/Modules/CustomReactions/Services/CustomReactionsService.cs +++ b/src/NadekoBot/Modules/CustomReactions/Services/CustomReactionsService.cs @@ -1,17 +1,20 @@ using Discord; using Discord.WebSocket; -using NadekoBot.DataStructures.ModuleBehaviors; using NadekoBot.Services.Database.Models; using NLog; using System.Collections.Concurrent; using System.Linq; using System; using System.Threading.Tasks; -using NadekoBot.Services.Permissions; +using NadekoBot.Common; +using NadekoBot.Common.ModuleBehaviors; using NadekoBot.Extensions; using NadekoBot.Services.Database; using NadekoBot.Services; using NadekoBot.Modules.CustomReactions.Extensions; +using NadekoBot.Modules.Permissions.Common; +using NadekoBot.Modules.Permissions.Services; +using NadekoBot.Services.Impl; namespace NadekoBot.Modules.CustomReactions.Services { diff --git a/src/NadekoBot/Modules/Gambling/Commands/AnimalRacing.cs b/src/NadekoBot/Modules/Gambling/AnimalRacingCommands.cs similarity index 98% rename from src/NadekoBot/Modules/Gambling/Commands/AnimalRacing.cs rename to src/NadekoBot/Modules/Gambling/AnimalRacingCommands.cs index 46cd25b3..18411490 100644 --- a/src/NadekoBot/Modules/Gambling/Commands/AnimalRacing.cs +++ b/src/NadekoBot/Modules/Gambling/AnimalRacingCommands.cs @@ -1,7 +1,6 @@ using Discord; using Discord.Commands; using Discord.WebSocket; -using NadekoBot.Attributes; using NadekoBot.Extensions; using NadekoBot.Services; using NadekoBot.Services.Database.Models; @@ -12,13 +11,16 @@ using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; +using NadekoBot.Common; +using NadekoBot.Common.Attributes; +using NadekoBot.Services.Impl; namespace NadekoBot.Modules.Gambling { public partial class Gambling { [Group] - public class AnimalRacing : NadekoSubmodule + public class AnimalRacingCommands : NadekoSubmodule { private readonly BotConfig _bc; private readonly CurrencyService _cs; @@ -27,7 +29,7 @@ namespace NadekoBot.Modules.Gambling public static ConcurrentDictionary AnimalRaces { get; } = new ConcurrentDictionary(); - public AnimalRacing(BotConfig bc, CurrencyService cs, DiscordSocketClient client) + public AnimalRacingCommands(BotConfig bc, CurrencyService cs, DiscordSocketClient client) { _bc = bc; _cs = cs; diff --git a/src/NadekoBot/Modules/Gambling/Commands/Models/Cards.cs b/src/NadekoBot/Modules/Gambling/Common/Cards.cs similarity index 98% rename from src/NadekoBot/Modules/Gambling/Commands/Models/Cards.cs rename to src/NadekoBot/Modules/Gambling/Common/Cards.cs index 2c2624c6..b51765d0 100644 --- a/src/NadekoBot/Modules/Gambling/Commands/Models/Cards.cs +++ b/src/NadekoBot/Modules/Gambling/Common/Cards.cs @@ -1,10 +1,11 @@ -using NadekoBot.Extensions; -using NadekoBot.Services; -using System; +using System; using System.Collections.Generic; using System.Linq; +using NadekoBot.Common; +using NadekoBot.Extensions; +using NadekoBot.Services; -namespace NadekoBot.Modules.Gambling.Models +namespace NadekoBot.Modules.Gambling.Common { public class Cards { diff --git a/src/NadekoBot/Modules/Gambling/Commands/CurrencyEvents.cs b/src/NadekoBot/Modules/Gambling/CurrencyEventsCommands.cs similarity index 97% rename from src/NadekoBot/Modules/Gambling/Commands/CurrencyEvents.cs rename to src/NadekoBot/Modules/Gambling/CurrencyEventsCommands.cs index a892e594..7a4ecf42 100644 --- a/src/NadekoBot/Modules/Gambling/Commands/CurrencyEvents.cs +++ b/src/NadekoBot/Modules/Gambling/CurrencyEventsCommands.cs @@ -1,6 +1,5 @@ using Discord; using Discord.Commands; -using NadekoBot.Attributes; using NadekoBot.Extensions; using NadekoBot.Services; using System; @@ -9,6 +8,9 @@ using System.Linq; using System.Threading.Tasks; using Discord.WebSocket; using System.Threading; +using NadekoBot.Common; +using NadekoBot.Common.Attributes; +using NadekoBot.Common.Collections; using NLog; using NadekoBot.Services.Database.Models; @@ -17,7 +19,7 @@ namespace NadekoBot.Modules.Gambling public partial class Gambling { [Group] - public class CurrencyEvents : NadekoSubmodule + public class CurrencyEventsCommands : NadekoSubmodule { public enum CurrencyEvent { @@ -38,7 +40,7 @@ namespace NadekoBot.Modules.Gambling private readonly BotConfig _bc; private readonly CurrencyService _cs; - public CurrencyEvents(DiscordSocketClient client, BotConfig bc, CurrencyService cs) + public CurrencyEventsCommands(DiscordSocketClient client, BotConfig bc, CurrencyService cs) { _client = client; _bc = bc; diff --git a/src/NadekoBot/Modules/Gambling/Commands/DiceRollCommand.cs b/src/NadekoBot/Modules/Gambling/DiceRollCommands.cs similarity index 99% rename from src/NadekoBot/Modules/Gambling/Commands/DiceRollCommand.cs rename to src/NadekoBot/Modules/Gambling/DiceRollCommands.cs index 0a81a943..274f310c 100644 --- a/src/NadekoBot/Modules/Gambling/Commands/DiceRollCommand.cs +++ b/src/NadekoBot/Modules/Gambling/DiceRollCommands.cs @@ -1,6 +1,5 @@ using Discord; using Discord.Commands; -using NadekoBot.Attributes; using NadekoBot.Extensions; using NadekoBot.Services; using System; @@ -9,6 +8,8 @@ using System.IO; using System.Linq; using System.Text.RegularExpressions; using System.Threading.Tasks; +using NadekoBot.Common; +using NadekoBot.Common.Attributes; using Image = ImageSharp.Image; namespace NadekoBot.Modules.Gambling diff --git a/src/NadekoBot/Modules/Gambling/Commands/DrawCommand.cs b/src/NadekoBot/Modules/Gambling/DrawCommands.cs similarity index 98% rename from src/NadekoBot/Modules/Gambling/Commands/DrawCommand.cs rename to src/NadekoBot/Modules/Gambling/DrawCommands.cs index f2fe5f4c..b2e5d636 100644 --- a/src/NadekoBot/Modules/Gambling/Commands/DrawCommand.cs +++ b/src/NadekoBot/Modules/Gambling/DrawCommands.cs @@ -1,13 +1,13 @@ using Discord; using Discord.Commands; -using NadekoBot.Attributes; using NadekoBot.Extensions; -using NadekoBot.Modules.Gambling.Models; using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.IO; using System.Threading.Tasks; +using NadekoBot.Common.Attributes; +using NadekoBot.Modules.Gambling.Common; using Image = ImageSharp.Image; namespace NadekoBot.Modules.Gambling diff --git a/src/NadekoBot/Modules/Gambling/Commands/FlipCoinCommand.cs b/src/NadekoBot/Modules/Gambling/FlipCoinCommands.cs similarity index 98% rename from src/NadekoBot/Modules/Gambling/Commands/FlipCoinCommand.cs rename to src/NadekoBot/Modules/Gambling/FlipCoinCommands.cs index 98c526f9..c89a175c 100644 --- a/src/NadekoBot/Modules/Gambling/Commands/FlipCoinCommand.cs +++ b/src/NadekoBot/Modules/Gambling/FlipCoinCommands.cs @@ -1,12 +1,13 @@ using Discord; using Discord.Commands; -using NadekoBot.Attributes; using NadekoBot.Extensions; using NadekoBot.Services; using NadekoBot.Services.Database.Models; using System; using System.Collections.Generic; using System.Threading.Tasks; +using NadekoBot.Common; +using NadekoBot.Common.Attributes; using Image = ImageSharp.Image; namespace NadekoBot.Modules.Gambling diff --git a/src/NadekoBot/Modules/Gambling/Commands/FlowerShop.cs b/src/NadekoBot/Modules/Gambling/FlowerShopCommands.cs similarity index 98% rename from src/NadekoBot/Modules/Gambling/Commands/FlowerShop.cs rename to src/NadekoBot/Modules/Gambling/FlowerShopCommands.cs index 942ead4c..59a2fd78 100644 --- a/src/NadekoBot/Modules/Gambling/Commands/FlowerShop.cs +++ b/src/NadekoBot/Modules/Gambling/FlowerShopCommands.cs @@ -2,8 +2,6 @@ using Discord.Commands; using Discord.WebSocket; using Microsoft.EntityFrameworkCore; -using NadekoBot.Attributes; -using NadekoBot.DataStructures; using NadekoBot.Extensions; using NadekoBot.Services; using NadekoBot.Services.Database.Models; @@ -11,13 +9,16 @@ using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using NadekoBot.Common; +using NadekoBot.Common.Attributes; +using NadekoBot.Common.Collections; namespace NadekoBot.Modules.Gambling { public partial class Gambling { [Group] - public class FlowerShop : NadekoSubmodule + public class FlowerShopCommands : NadekoSubmodule { private readonly BotConfig _bc; private readonly DbService _db; @@ -34,7 +35,7 @@ namespace NadekoBot.Modules.Gambling List } - public FlowerShop(BotConfig bc, DbService db, CurrencyService cs, DiscordSocketClient client) + public FlowerShopCommands(BotConfig bc, DbService db, CurrencyService cs, DiscordSocketClient client) { _db = db; _bc = bc; diff --git a/src/NadekoBot/Modules/Gambling/Gambling.cs b/src/NadekoBot/Modules/Gambling/Gambling.cs index c8013f0d..46ecc8c5 100644 --- a/src/NadekoBot/Modules/Gambling/Gambling.cs +++ b/src/NadekoBot/Modules/Gambling/Gambling.cs @@ -1,12 +1,13 @@ using Discord; using Discord.Commands; -using NadekoBot.Attributes; using NadekoBot.Extensions; using System.Linq; using System.Threading.Tasks; using NadekoBot.Services; using NadekoBot.Services.Database.Models; using System.Collections.Generic; +using NadekoBot.Common; +using NadekoBot.Common.Attributes; namespace NadekoBot.Modules.Gambling { diff --git a/src/NadekoBot/Modules/Gambling/Commands/Slots.cs b/src/NadekoBot/Modules/Gambling/SlotCommands.cs similarity index 97% rename from src/NadekoBot/Modules/Gambling/Commands/Slots.cs rename to src/NadekoBot/Modules/Gambling/SlotCommands.cs index 99e1229a..f4d2e43b 100644 --- a/src/NadekoBot/Modules/Gambling/Commands/Slots.cs +++ b/src/NadekoBot/Modules/Gambling/SlotCommands.cs @@ -1,7 +1,6 @@ using Discord; using Discord.Commands; using ImageSharp; -using NadekoBot.Attributes; using NadekoBot.Extensions; using NadekoBot.Services; using NadekoBot.Services.Database.Models; @@ -11,13 +10,15 @@ using System.Linq; using System.Text; using System.Threading; using System.Threading.Tasks; +using NadekoBot.Common; +using NadekoBot.Common.Attributes; namespace NadekoBot.Modules.Gambling { public partial class Gambling { [Group] - public class Slots : NadekoSubmodule + public class SlotCommands : NadekoSubmodule { private static int _totalBet; private static int _totalPaidOut; @@ -34,7 +35,7 @@ namespace NadekoBot.Modules.Gambling private readonly IImagesService _images; private readonly CurrencyService _cs; - public Slots(IImagesService images, BotConfig bc, CurrencyService cs) + public SlotCommands(IImagesService images, BotConfig bc, CurrencyService cs) { _images = images; _bc = bc; diff --git a/src/NadekoBot/Modules/Gambling/Commands/WaifuClaimCommands.cs b/src/NadekoBot/Modules/Gambling/WaifuClaimCommands.cs similarity index 99% rename from src/NadekoBot/Modules/Gambling/Commands/WaifuClaimCommands.cs rename to src/NadekoBot/Modules/Gambling/WaifuClaimCommands.cs index 962e24dc..43113928 100644 --- a/src/NadekoBot/Modules/Gambling/Commands/WaifuClaimCommands.cs +++ b/src/NadekoBot/Modules/Gambling/WaifuClaimCommands.cs @@ -1,6 +1,5 @@ using Discord; using Discord.Commands; -using NadekoBot.Attributes; using NadekoBot.Extensions; using NadekoBot.Services; using NadekoBot.Services.Database.Models; @@ -9,6 +8,8 @@ using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using NadekoBot.Common; +using NadekoBot.Common.Attributes; namespace NadekoBot.Modules.Gambling { diff --git a/src/NadekoBot/Modules/Games/Commands/Acropobia.cs b/src/NadekoBot/Modules/Games/AcropobiaCommands.cs similarity index 98% rename from src/NadekoBot/Modules/Games/Commands/Acropobia.cs rename to src/NadekoBot/Modules/Games/AcropobiaCommands.cs index 44b64808..f905a4eb 100644 --- a/src/NadekoBot/Modules/Games/Commands/Acropobia.cs +++ b/src/NadekoBot/Modules/Games/AcropobiaCommands.cs @@ -1,7 +1,6 @@ using Discord; using Discord.Commands; using Discord.WebSocket; -using NadekoBot.Attributes; using NadekoBot.Extensions; using NadekoBot.Services; using NLog; @@ -12,20 +11,24 @@ using System.Collections.Immutable; using System.Linq; using System.Threading; using System.Threading.Tasks; +using NadekoBot.Common; +using NadekoBot.Common.Attributes; +using NadekoBot.Common.Collections; +using NadekoBot.Services.Impl; namespace NadekoBot.Modules.Games { public partial class Games { [Group] - public class Acropobia : NadekoSubmodule + public class AcropobiaCommands : NadekoSubmodule { private readonly DiscordSocketClient _client; //channelId, game public static ConcurrentDictionary AcrophobiaGames { get; } = new ConcurrentDictionary(); - public Acropobia(DiscordSocketClient client) + public AcropobiaCommands(DiscordSocketClient client) { _client = client; } diff --git a/src/NadekoBot/Modules/Games/Commands/CleverBotCommands.cs b/src/NadekoBot/Modules/Games/CleverBotCommands.cs similarity index 94% rename from src/NadekoBot/Modules/Games/Commands/CleverBotCommands.cs rename to src/NadekoBot/Modules/Games/CleverBotCommands.cs index ad0e9189..47ab69b6 100644 --- a/src/NadekoBot/Modules/Games/Commands/CleverBotCommands.cs +++ b/src/NadekoBot/Modules/Games/CleverBotCommands.cs @@ -1,10 +1,11 @@ using Discord; using Discord.Commands; -using NadekoBot.Attributes; using NadekoBot.Services; using System; using System.Threading.Tasks; -using NadekoBot.Services.Games; +using NadekoBot.Common.Attributes; +using NadekoBot.Modules.Games.Common; +using NadekoBot.Modules.Games.Services; namespace NadekoBot.Modules.Games { diff --git a/src/NadekoBot/Services/Games/ChatterBotResponse.cs b/src/NadekoBot/Modules/Games/Common/ChatterBotResponse.cs similarity index 76% rename from src/NadekoBot/Services/Games/ChatterBotResponse.cs rename to src/NadekoBot/Modules/Games/Common/ChatterBotResponse.cs index b064af4c..098673bc 100644 --- a/src/NadekoBot/Services/Games/ChatterBotResponse.cs +++ b/src/NadekoBot/Modules/Games/Common/ChatterBotResponse.cs @@ -1,4 +1,4 @@ -namespace NadekoBot.Services.Games +namespace NadekoBot.Modules.Games.Common { public class ChatterBotResponse { diff --git a/src/NadekoBot/Services/Games/ChatterBotSession.cs b/src/NadekoBot/Modules/Games/Common/ChatterBotSession.cs similarity index 89% rename from src/NadekoBot/Services/Games/ChatterBotSession.cs rename to src/NadekoBot/Modules/Games/Common/ChatterBotSession.cs index 920ea709..4c3ad823 100644 --- a/src/NadekoBot/Services/Games/ChatterBotSession.cs +++ b/src/NadekoBot/Modules/Games/Common/ChatterBotSession.cs @@ -1,9 +1,11 @@ -using NadekoBot.Extensions; -using Newtonsoft.Json; -using System.Net.Http; +using System.Net.Http; using System.Threading.Tasks; +using NadekoBot.Common; +using NadekoBot.Extensions; +using NadekoBot.Services; +using Newtonsoft.Json; -namespace NadekoBot.Services.Games +namespace NadekoBot.Modules.Games.Common { public class ChatterBotSession { diff --git a/src/NadekoBot/Services/Games/GirlRating.cs b/src/NadekoBot/Modules/Games/Common/GirlRating.cs similarity index 95% rename from src/NadekoBot/Services/Games/GirlRating.cs rename to src/NadekoBot/Modules/Games/Common/GirlRating.cs index 9cf72945..9e7a12cb 100644 --- a/src/NadekoBot/Services/Games/GirlRating.cs +++ b/src/NadekoBot/Modules/Games/Common/GirlRating.cs @@ -1,13 +1,14 @@ -using ImageSharp; -using NadekoBot.DataStructures; -using NadekoBot.Extensions; -using NLog; -using System; +using System; using System.IO; using System.Linq; using System.Net.Http; +using ImageSharp; +using NadekoBot.Common; +using NadekoBot.Extensions; +using NadekoBot.Services; +using NLog; -namespace NadekoBot.Services.Games +namespace NadekoBot.Modules.Games.Common { public class GirlRating { diff --git a/src/NadekoBot/Modules/Games/Commands/Hangman/HangmanGame.cs b/src/NadekoBot/Modules/Games/Common/Hangman/HangmanGame.cs similarity index 98% rename from src/NadekoBot/Modules/Games/Commands/Hangman/HangmanGame.cs rename to src/NadekoBot/Modules/Games/Common/Hangman/HangmanGame.cs index 8818a798..540821b7 100644 --- a/src/NadekoBot/Modules/Games/Commands/Hangman/HangmanGame.cs +++ b/src/NadekoBot/Modules/Games/Common/Hangman/HangmanGame.cs @@ -1,17 +1,17 @@ -using Discord; -using Discord.WebSocket; -using NadekoBot.Extensions; -using NadekoBot.Services; -using Newtonsoft.Json; -using NLog; -using System; +using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Threading.Tasks; -using NadekoBot.Modules.Games.Commands.Hangman; +using Discord; +using Discord.WebSocket; +using NadekoBot.Common; +using NadekoBot.Extensions; +using NadekoBot.Services; +using Newtonsoft.Json; +using NLog; -namespace NadekoBot.Modules.Games.Hangman +namespace NadekoBot.Modules.Games.Common.Hangman { public class HangmanTermPool { diff --git a/src/NadekoBot/Modules/Games/Commands/Hangman/IHangmanObject.cs b/src/NadekoBot/Modules/Games/Common/Hangman/IHangmanObject.cs similarity index 71% rename from src/NadekoBot/Modules/Games/Commands/Hangman/IHangmanObject.cs rename to src/NadekoBot/Modules/Games/Common/Hangman/IHangmanObject.cs index c2069477..4179e7bc 100644 --- a/src/NadekoBot/Modules/Games/Commands/Hangman/IHangmanObject.cs +++ b/src/NadekoBot/Modules/Games/Common/Hangman/IHangmanObject.cs @@ -1,4 +1,4 @@ -namespace NadekoBot.Modules.Games.Commands.Hangman +namespace NadekoBot.Modules.Games.Common.Hangman { public class HangmanObject { diff --git a/src/NadekoBot/Services/Games/Poll.cs b/src/NadekoBot/Modules/Games/Common/Poll.cs similarity index 97% rename from src/NadekoBot/Services/Games/Poll.cs rename to src/NadekoBot/Modules/Games/Common/Poll.cs index ea06e3b6..c14a490d 100644 --- a/src/NadekoBot/Services/Games/Poll.cs +++ b/src/NadekoBot/Modules/Games/Common/Poll.cs @@ -1,14 +1,16 @@ -using Discord; -using Discord.WebSocket; -using NadekoBot.Extensions; -using System; +using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; +using Discord; +using Discord.WebSocket; +using NadekoBot.Extensions; +using NadekoBot.Services; +using NadekoBot.Services.Impl; -namespace NadekoBot.Services.Games +namespace NadekoBot.Modules.Games.Common { //todo 75 rewrite public class Poll diff --git a/src/NadekoBot/Modules/Games/Commands/Trivia/TriviaGame.cs b/src/NadekoBot/Modules/Games/Common/Trivia/TriviaGame.cs similarity index 99% rename from src/NadekoBot/Modules/Games/Commands/Trivia/TriviaGame.cs rename to src/NadekoBot/Modules/Games/Common/Trivia/TriviaGame.cs index cc8089a0..90effc81 100644 --- a/src/NadekoBot/Modules/Games/Commands/Trivia/TriviaGame.cs +++ b/src/NadekoBot/Modules/Games/Common/Trivia/TriviaGame.cs @@ -1,19 +1,20 @@ -using Discord; -using Discord.Net; -using Discord.WebSocket; -using NadekoBot.Extensions; -using NadekoBot.Services; -using NadekoBot.Services.Database.Models; -using NLog; -using System; +using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading; using System.Threading.Tasks; +using Discord; +using Discord.Net; +using Discord.WebSocket; +using NadekoBot.Extensions; +using NadekoBot.Services; +using NadekoBot.Services.Database.Models; +using NadekoBot.Services.Impl; +using NLog; -namespace NadekoBot.Modules.Games.Trivia +namespace NadekoBot.Modules.Games.Common.Trivia { public class TriviaGame { diff --git a/src/NadekoBot/Modules/Games/Commands/Trivia/TriviaQuestion.cs b/src/NadekoBot/Modules/Games/Common/Trivia/TriviaQuestion.cs similarity index 97% rename from src/NadekoBot/Modules/Games/Commands/Trivia/TriviaQuestion.cs rename to src/NadekoBot/Modules/Games/Common/Trivia/TriviaQuestion.cs index 01e676fd..06a40dec 100644 --- a/src/NadekoBot/Modules/Games/Commands/Trivia/TriviaQuestion.cs +++ b/src/NadekoBot/Modules/Games/Common/Trivia/TriviaQuestion.cs @@ -1,11 +1,11 @@ -using NadekoBot.Extensions; -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Text.RegularExpressions; +using NadekoBot.Extensions; // THANKS @ShoMinamimoto for suggestions and coding help -namespace NadekoBot.Modules.Games.Trivia +namespace NadekoBot.Modules.Games.Common.Trivia { public class TriviaQuestion { diff --git a/src/NadekoBot/Modules/Games/Commands/Trivia/TriviaQuestionPool.cs b/src/NadekoBot/Modules/Games/Common/Trivia/TriviaQuestionPool.cs similarity index 94% rename from src/NadekoBot/Modules/Games/Commands/Trivia/TriviaQuestionPool.cs rename to src/NadekoBot/Modules/Games/Common/Trivia/TriviaQuestionPool.cs index 694112dc..22d38fe9 100644 --- a/src/NadekoBot/Modules/Games/Commands/Trivia/TriviaQuestionPool.cs +++ b/src/NadekoBot/Modules/Games/Common/Trivia/TriviaQuestionPool.cs @@ -1,13 +1,14 @@ -using NadekoBot.Extensions; -using NadekoBot.Services; -using Newtonsoft.Json; -using System; +using System; using System.Collections.Generic; using System.Collections.Immutable; using System.IO; using System.Linq; +using NadekoBot.Common; +using NadekoBot.Extensions; +using NadekoBot.Services; +using Newtonsoft.Json; -namespace NadekoBot.Modules.Games.Trivia +namespace NadekoBot.Modules.Games.Common.Trivia { public class TriviaQuestionPool { diff --git a/src/NadekoBot/Services/Games/TypingArticle.cs b/src/NadekoBot/Modules/Games/Common/TypingArticle.cs similarity index 74% rename from src/NadekoBot/Services/Games/TypingArticle.cs rename to src/NadekoBot/Modules/Games/Common/TypingArticle.cs index 2024704f..2b69b577 100644 --- a/src/NadekoBot/Services/Games/TypingArticle.cs +++ b/src/NadekoBot/Modules/Games/Common/TypingArticle.cs @@ -1,4 +1,4 @@ -namespace NadekoBot.Services.Games +namespace NadekoBot.Modules.Games.Common { public class TypingArticle { diff --git a/src/NadekoBot/Modules/Games/Commands/Models/TypingGame.cs b/src/NadekoBot/Modules/Games/Common/TypingGame.cs similarity index 97% rename from src/NadekoBot/Modules/Games/Commands/Models/TypingGame.cs rename to src/NadekoBot/Modules/Games/Common/TypingGame.cs index 92b01eb3..c07484bc 100644 --- a/src/NadekoBot/Modules/Games/Commands/Models/TypingGame.cs +++ b/src/NadekoBot/Modules/Games/Common/TypingGame.cs @@ -1,16 +1,17 @@ -using Discord; -using Discord.WebSocket; -using NadekoBot.Extensions; -using NadekoBot.Services; -using NadekoBot.Services.Games; -using NLog; -using System; +using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Threading.Tasks; +using Discord; +using Discord.WebSocket; +using NadekoBot.Common; +using NadekoBot.Extensions; +using NadekoBot.Modules.Games.Services; +using NadekoBot.Services; +using NLog; -namespace NadekoBot.Modules.Games.Models +namespace NadekoBot.Modules.Games.Common { public class TypingGame { diff --git a/src/NadekoBot/Modules/Games/Games.cs b/src/NadekoBot/Modules/Games/Games.cs index 399f500a..27ddd19e 100644 --- a/src/NadekoBot/Modules/Games/Games.cs +++ b/src/NadekoBot/Modules/Games/Games.cs @@ -2,10 +2,12 @@ using Discord; using NadekoBot.Services; using System.Threading.Tasks; -using NadekoBot.Attributes; using System; +using NadekoBot.Common; +using NadekoBot.Common.Attributes; using NadekoBot.Extensions; -using NadekoBot.Services.Games; +using NadekoBot.Modules.Games.Common; +using NadekoBot.Modules.Games.Services; namespace NadekoBot.Modules.Games { diff --git a/src/NadekoBot/Modules/Games/Commands/HangmanCommands.cs b/src/NadekoBot/Modules/Games/HangmanCommands.cs similarity index 97% rename from src/NadekoBot/Modules/Games/Commands/HangmanCommands.cs rename to src/NadekoBot/Modules/Games/HangmanCommands.cs index e74b74f5..f3e6a32a 100644 --- a/src/NadekoBot/Modules/Games/Commands/HangmanCommands.cs +++ b/src/NadekoBot/Modules/Games/HangmanCommands.cs @@ -1,12 +1,12 @@ using Discord.Commands; -using NadekoBot.Attributes; using NadekoBot.Extensions; using System; using System.Collections.Concurrent; using System.Threading.Tasks; -using NadekoBot.Modules.Games.Hangman; using Discord; using Discord.WebSocket; +using NadekoBot.Common.Attributes; +using NadekoBot.Modules.Games.Common.Hangman; namespace NadekoBot.Modules.Games { diff --git a/src/NadekoBot/Modules/Games/Commands/LeetCommands.cs b/src/NadekoBot/Modules/Games/LeetCommands.cs similarity index 99% rename from src/NadekoBot/Modules/Games/Commands/LeetCommands.cs rename to src/NadekoBot/Modules/Games/LeetCommands.cs index e1f79c56..4b0088eb 100644 --- a/src/NadekoBot/Modules/Games/Commands/LeetCommands.cs +++ b/src/NadekoBot/Modules/Games/LeetCommands.cs @@ -1,8 +1,8 @@ using Discord.Commands; -using NadekoBot.Attributes; using NadekoBot.Extensions; using System.Text; using System.Threading.Tasks; +using NadekoBot.Common.Attributes; // taken from // http://www.codeproject.com/Tips/207582/L-t-Tr-nsl-t-r-Leet-Translator (thanks) diff --git a/src/NadekoBot/Modules/Games/Commands/PlantAndPickCommands.cs b/src/NadekoBot/Modules/Games/PlantAndPickCommands.cs similarity index 98% rename from src/NadekoBot/Modules/Games/Commands/PlantAndPickCommands.cs rename to src/NadekoBot/Modules/Games/PlantAndPickCommands.cs index f055e0bb..19b792fc 100644 --- a/src/NadekoBot/Modules/Games/Commands/PlantAndPickCommands.cs +++ b/src/NadekoBot/Modules/Games/PlantAndPickCommands.cs @@ -1,14 +1,14 @@ using Discord; using Discord.Commands; using Microsoft.EntityFrameworkCore; -using NadekoBot.Attributes; using NadekoBot.Extensions; using NadekoBot.Services; using NadekoBot.Services.Database.Models; -using NadekoBot.Services.Games; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using NadekoBot.Common.Attributes; +using NadekoBot.Modules.Games.Services; namespace NadekoBot.Modules.Games { diff --git a/src/NadekoBot/Modules/Games/Commands/PollCommands.cs b/src/NadekoBot/Modules/Games/PollCommands.cs similarity index 96% rename from src/NadekoBot/Modules/Games/Commands/PollCommands.cs rename to src/NadekoBot/Modules/Games/PollCommands.cs index 4679fa3e..bd136e7f 100644 --- a/src/NadekoBot/Modules/Games/Commands/PollCommands.cs +++ b/src/NadekoBot/Modules/Games/PollCommands.cs @@ -1,10 +1,10 @@ using Discord; using Discord.Commands; using Discord.WebSocket; -using NadekoBot.Attributes; using NadekoBot.Extensions; using System.Threading.Tasks; -using NadekoBot.Services.Games; +using NadekoBot.Common.Attributes; +using NadekoBot.Modules.Games.Services; namespace NadekoBot.Modules.Games { diff --git a/src/NadekoBot/Services/Games/ChatterbotService.cs b/src/NadekoBot/Modules/Games/Services/ChatterbotService.cs similarity index 93% rename from src/NadekoBot/Services/Games/ChatterbotService.cs rename to src/NadekoBot/Modules/Games/Services/ChatterbotService.cs index 0f067beb..0327fca2 100644 --- a/src/NadekoBot/Services/Games/ChatterbotService.cs +++ b/src/NadekoBot/Modules/Games/Services/ChatterbotService.cs @@ -1,17 +1,21 @@ -using Discord; -using Discord.WebSocket; -using NadekoBot.DataStructures.ModuleBehaviors; -using NadekoBot.Extensions; -using NadekoBot.Services.Database.Models; -using NadekoBot.Services.Permissions; -using NLog; -using System; +using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using Discord; +using Discord.WebSocket; +using NadekoBot.Common.ModuleBehaviors; +using NadekoBot.Extensions; +using NadekoBot.Modules.Games.Common; +using NadekoBot.Modules.Permissions.Common; +using NadekoBot.Modules.Permissions.Services; +using NadekoBot.Services; +using NadekoBot.Services.Database.Models; +using NadekoBot.Services.Impl; +using NLog; -namespace NadekoBot.Services.Games +namespace NadekoBot.Modules.Games.Services { public class ChatterBotService : IEarlyBlockingExecutor, INService { diff --git a/src/NadekoBot/Services/Games/GamesService.cs b/src/NadekoBot/Modules/Games/Services/GamesService.cs similarity index 96% rename from src/NadekoBot/Services/Games/GamesService.cs rename to src/NadekoBot/Modules/Games/Services/GamesService.cs index d5822833..6381156b 100644 --- a/src/NadekoBot/Services/Games/GamesService.cs +++ b/src/NadekoBot/Modules/Games/Services/GamesService.cs @@ -1,10 +1,4 @@ -using Discord; -using Discord.WebSocket; -using NadekoBot.Extensions; -using NadekoBot.Services.Database.Models; -using Newtonsoft.Json; -using NLog; -using System; +using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Collections.Immutable; @@ -12,8 +6,19 @@ using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; +using Discord; +using Discord.WebSocket; +using NadekoBot.Common; +using NadekoBot.Common.Collections; +using NadekoBot.Extensions; +using NadekoBot.Modules.Games.Common; +using NadekoBot.Services; +using NadekoBot.Services.Database.Models; +using NadekoBot.Services.Impl; +using Newtonsoft.Json; +using NLog; -namespace NadekoBot.Services.Games +namespace NadekoBot.Modules.Games.Services { public class GamesService : INService { diff --git a/src/NadekoBot/Services/Games/PollService.cs b/src/NadekoBot/Modules/Games/Services/PollService.cs similarity index 90% rename from src/NadekoBot/Services/Games/PollService.cs rename to src/NadekoBot/Modules/Games/Services/PollService.cs index c2d77066..5c14afbc 100644 --- a/src/NadekoBot/Services/Games/PollService.cs +++ b/src/NadekoBot/Modules/Games/Services/PollService.cs @@ -1,13 +1,16 @@ -using NadekoBot.DataStructures.ModuleBehaviors; -using System; +using System; using System.Collections.Concurrent; using System.Linq; using System.Threading.Tasks; using Discord; using Discord.WebSocket; +using NadekoBot.Common.ModuleBehaviors; +using NadekoBot.Modules.Games.Common; +using NadekoBot.Services; +using NadekoBot.Services.Impl; using NLog; -namespace NadekoBot.Services.Games +namespace NadekoBot.Modules.Games.Services { public class PollService : IEarlyBlockingExecutor, INService { diff --git a/src/NadekoBot/Modules/Games/Commands/SpeedTypingCommands.cs b/src/NadekoBot/Modules/Games/SpeedTypingCommands.cs similarity index 97% rename from src/NadekoBot/Modules/Games/Commands/SpeedTypingCommands.cs rename to src/NadekoBot/Modules/Games/SpeedTypingCommands.cs index 6cce5191..315080ed 100644 --- a/src/NadekoBot/Modules/Games/Commands/SpeedTypingCommands.cs +++ b/src/NadekoBot/Modules/Games/SpeedTypingCommands.cs @@ -1,15 +1,15 @@ using Discord; using Discord.Commands; using Discord.WebSocket; -using NadekoBot.Attributes; using NadekoBot.Extensions; -using NadekoBot.Modules.Games.Models; -using NadekoBot.Services.Games; using Newtonsoft.Json; using System.Collections.Concurrent; using System.IO; using System.Linq; using System.Threading.Tasks; +using NadekoBot.Common.Attributes; +using NadekoBot.Modules.Games.Common; +using NadekoBot.Modules.Games.Services; namespace NadekoBot.Modules.Games { diff --git a/src/NadekoBot/Modules/Games/Commands/TicTacToe.cs b/src/NadekoBot/Modules/Games/TicTacToeCommands.cs similarity index 97% rename from src/NadekoBot/Modules/Games/Commands/TicTacToe.cs rename to src/NadekoBot/Modules/Games/TicTacToeCommands.cs index 6f626325..3f2237f7 100644 --- a/src/NadekoBot/Modules/Games/Commands/TicTacToe.cs +++ b/src/NadekoBot/Modules/Games/TicTacToeCommands.cs @@ -1,7 +1,6 @@ using Discord; using Discord.Commands; using Discord.WebSocket; -using NadekoBot.Attributes; using NadekoBot.Extensions; using NadekoBot.Services; using System; @@ -9,6 +8,8 @@ using System.Collections.Generic; using System.Text; using System.Threading; using System.Threading.Tasks; +using NadekoBot.Common.Attributes; +using NadekoBot.Services.Impl; namespace NadekoBot.Modules.Games { @@ -37,8 +38,7 @@ namespace NadekoBot.Modules.Games await _sem.WaitAsync(1000); try { - TicTacToe game; - if (_games.TryGetValue(channel.Id, out game)) + if (_games.TryGetValue(channel.Id, out TicTacToe game)) { var _ = Task.Run(async () => { @@ -46,7 +46,7 @@ namespace NadekoBot.Modules.Games }); return; } - game = new TicTacToe(_strings, _client, channel, (IGuildUser)Context.User); + game = new TicTacToe(base._strings, (DiscordSocketClient)this._client, channel, (IGuildUser)Context.User); _games.Add(channel.Id, game); await ReplyConfirmLocalized("ttt_created").ConfigureAwait(false); @@ -247,9 +247,8 @@ namespace NadekoBot.Modules.Games var curUser = _users[_curUserIndex]; if (_phase == Phase.Ended || msg.Author?.Id != curUser.Id) return; - - int index; - if (int.TryParse(msg.Content, out index) && + + if (int.TryParse(msg.Content, out var index) && --index >= 0 && index <= 9 && _state[index / 3, index % 3] == null) diff --git a/src/NadekoBot/Modules/Games/Commands/TriviaCommands.cs b/src/NadekoBot/Modules/Games/TriviaCommands.cs similarity index 97% rename from src/NadekoBot/Modules/Games/Commands/TriviaCommands.cs rename to src/NadekoBot/Modules/Games/TriviaCommands.cs index ac9cdcc3..86560835 100644 --- a/src/NadekoBot/Modules/Games/Commands/TriviaCommands.cs +++ b/src/NadekoBot/Modules/Games/TriviaCommands.cs @@ -1,13 +1,13 @@ using Discord; using Discord.Commands; using Discord.WebSocket; -using NadekoBot.Attributes; using NadekoBot.Extensions; -using NadekoBot.Modules.Games.Trivia; using NadekoBot.Services; using NadekoBot.Services.Database.Models; using System.Collections.Concurrent; using System.Threading.Tasks; +using NadekoBot.Common.Attributes; +using NadekoBot.Modules.Games.Common.Trivia; namespace NadekoBot.Modules.Games diff --git a/src/NadekoBot/Modules/Help/Help.cs b/src/NadekoBot/Modules/Help/Help.cs index 2cd965cb..a1d6cb0f 100644 --- a/src/NadekoBot/Modules/Help/Help.cs +++ b/src/NadekoBot/Modules/Help/Help.cs @@ -4,14 +4,14 @@ using System.Linq; using Discord; using NadekoBot.Services; using System.Threading.Tasks; -using NadekoBot.Attributes; using System; using System.IO; using System.Text; using System.Collections.Generic; +using NadekoBot.Common.Attributes; using NadekoBot.Services.Database.Models; -using NadekoBot.Services.Permissions; using NadekoBot.Modules.Help.Services; +using NadekoBot.Modules.Permissions.Services; namespace NadekoBot.Modules.Help { diff --git a/src/NadekoBot/Modules/Help/Services/HelpService.cs b/src/NadekoBot/Modules/Help/Services/HelpService.cs index 0aba1425..2e5cd937 100644 --- a/src/NadekoBot/Modules/Help/Services/HelpService.cs +++ b/src/NadekoBot/Modules/Help/Services/HelpService.cs @@ -1,5 +1,4 @@ -using NadekoBot.DataStructures.ModuleBehaviors; -using NadekoBot.Services.Database.Models; +using NadekoBot.Services.Database.Models; using System.Threading.Tasks; using Discord; using Discord.WebSocket; @@ -7,8 +6,10 @@ using System; using Discord.Commands; using NadekoBot.Extensions; using System.Linq; -using NadekoBot.Attributes; +using NadekoBot.Common.Attributes; +using NadekoBot.Common.ModuleBehaviors; using NadekoBot.Services; +using NadekoBot.Services.Impl; namespace NadekoBot.Modules.Help.Services { diff --git a/src/NadekoBot/Modules/Music/Common/MusicPlayer.cs b/src/NadekoBot/Modules/Music/Common/MusicPlayer.cs index e8411f43..49327512 100644 --- a/src/NadekoBot/Modules/Music/Common/MusicPlayer.cs +++ b/src/NadekoBot/Modules/Music/Common/MusicPlayer.cs @@ -8,6 +8,7 @@ using System.Linq; using System.Collections.Concurrent; using NadekoBot.Extensions; using System.Diagnostics; +using NadekoBot.Common.Collections; using NadekoBot.Modules.Music.Services; using NadekoBot.Services; using NadekoBot.Services.Database.Models; diff --git a/src/NadekoBot/Modules/Music/Common/MusicQueue.cs b/src/NadekoBot/Modules/Music/Common/MusicQueue.cs index 7fa58a51..bcdc59f7 100644 --- a/src/NadekoBot/Modules/Music/Common/MusicQueue.cs +++ b/src/NadekoBot/Modules/Music/Common/MusicQueue.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; +using NadekoBot.Common; namespace NadekoBot.Modules.Music.Common { diff --git a/src/NadekoBot/Modules/Music/Music.cs b/src/NadekoBot/Modules/Music/Music.cs index 1473e247..6b337f41 100644 --- a/src/NadekoBot/Modules/Music/Music.cs +++ b/src/NadekoBot/Modules/Music/Music.cs @@ -3,16 +3,17 @@ using Discord.WebSocket; using NadekoBot.Services; using Discord; using System.Threading.Tasks; -using NadekoBot.Attributes; using System; using System.Linq; using NadekoBot.Extensions; using System.Collections.Generic; using NadekoBot.Services.Database.Models; -using NadekoBot.DataStructures; using System.Collections.Concurrent; using System.IO; using System.Net.Http; +using NadekoBot.Common; +using NadekoBot.Common.Attributes; +using NadekoBot.Common.Collections; using Newtonsoft.Json.Linq; using NadekoBot.Services.Impl; using NadekoBot.Modules.Music.Services; diff --git a/src/NadekoBot/Modules/Music/Services/MusicService.cs b/src/NadekoBot/Modules/Music/Services/MusicService.cs index 17178cb6..c585abb1 100644 --- a/src/NadekoBot/Modules/Music/Services/MusicService.cs +++ b/src/NadekoBot/Modules/Music/Services/MusicService.cs @@ -10,6 +10,7 @@ using System.IO; using System.Collections.Generic; using Discord.Commands; using Discord.WebSocket; +using NadekoBot.Common; using NadekoBot.Services.Impl; using NadekoBot.Services; using NadekoBot.Modules.Music.Common; diff --git a/src/NadekoBot/Modules/NSFW/NSFW.cs b/src/NadekoBot/Modules/NSFW/NSFW.cs index 21db0f12..238071d9 100644 --- a/src/NadekoBot/Modules/NSFW/NSFW.cs +++ b/src/NadekoBot/Modules/NSFW/NSFW.cs @@ -1,6 +1,5 @@ using Discord; using Discord.Commands; -using NadekoBot.Attributes; using Newtonsoft.Json.Linq; using System; using System.Linq; @@ -10,8 +9,11 @@ using System.Net.Http; using NadekoBot.Extensions; using System.Threading; using System.Collections.Concurrent; -using NadekoBot.Services.Searches; -using NadekoBot.DataStructures; +using NadekoBot.Common; +using NadekoBot.Common.Attributes; +using NadekoBot.Common.Collections; +using NadekoBot.Modules.Searches.Common; +using NadekoBot.Modules.Searches.Services; namespace NadekoBot.Modules.NSFW { diff --git a/src/NadekoBot/Modules/NadekoModule.cs b/src/NadekoBot/Modules/NadekoModule.cs index 1138762b..62ad29f9 100644 --- a/src/NadekoBot/Modules/NadekoModule.cs +++ b/src/NadekoBot/Modules/NadekoModule.cs @@ -7,6 +7,7 @@ using System.Globalization; using System.Threading.Tasks; using System; using Discord.WebSocket; +using NadekoBot.Services.Impl; namespace NadekoBot.Modules { diff --git a/src/NadekoBot/Modules/Permissions/Commands/BlacklistCommands.cs b/src/NadekoBot/Modules/Permissions/BlacklistCommands.cs similarity index 97% rename from src/NadekoBot/Modules/Permissions/Commands/BlacklistCommands.cs rename to src/NadekoBot/Modules/Permissions/BlacklistCommands.cs index 4b63c967..655209af 100644 --- a/src/NadekoBot/Modules/Permissions/Commands/BlacklistCommands.cs +++ b/src/NadekoBot/Modules/Permissions/BlacklistCommands.cs @@ -1,13 +1,14 @@ using Discord; using Discord.Commands; -using NadekoBot.Attributes; -using NadekoBot.Modules.Games.Trivia; using NadekoBot.Services; using NadekoBot.Services.Database.Models; -using NadekoBot.Services.Permissions; using System.Collections.Concurrent; using System.Linq; using System.Threading.Tasks; +using NadekoBot.Common.Attributes; +using NadekoBot.Common.Collections; +using NadekoBot.Modules.Games.Common.Trivia; +using NadekoBot.Modules.Permissions.Services; namespace NadekoBot.Modules.Permissions { diff --git a/src/NadekoBot/Modules/Permissions/Commands/CmdCdsCommands.cs b/src/NadekoBot/Modules/Permissions/CmdCdsCommands.cs similarity index 97% rename from src/NadekoBot/Modules/Permissions/Commands/CmdCdsCommands.cs rename to src/NadekoBot/Modules/Permissions/CmdCdsCommands.cs index 817d7c54..5c3fed31 100644 --- a/src/NadekoBot/Modules/Permissions/Commands/CmdCdsCommands.cs +++ b/src/NadekoBot/Modules/Permissions/CmdCdsCommands.cs @@ -1,14 +1,15 @@ using Discord; using Discord.Commands; using Microsoft.EntityFrameworkCore; -using NadekoBot.Attributes; using NadekoBot.Extensions; using NadekoBot.Services; using NadekoBot.Services.Database.Models; -using NadekoBot.Services.Permissions; using System.Collections.Concurrent; using System.Linq; using System.Threading.Tasks; +using NadekoBot.Common.Attributes; +using NadekoBot.Common.Collections; +using NadekoBot.Modules.Permissions.Services; namespace NadekoBot.Modules.Permissions { diff --git a/src/NadekoBot/Modules/Permissions/Commands/CommandCostCommands.cs b/src/NadekoBot/Modules/Permissions/CommandCostCommands.cs similarity index 100% rename from src/NadekoBot/Modules/Permissions/Commands/CommandCostCommands.cs rename to src/NadekoBot/Modules/Permissions/CommandCostCommands.cs diff --git a/src/NadekoBot/Services/Permissions/PermissionCache.cs b/src/NadekoBot/Modules/Permissions/Common/PermissionCache.cs similarity index 90% rename from src/NadekoBot/Services/Permissions/PermissionCache.cs rename to src/NadekoBot/Modules/Permissions/Common/PermissionCache.cs index 5b4dc4b7..fd02a0b3 100644 --- a/src/NadekoBot/Services/Permissions/PermissionCache.cs +++ b/src/NadekoBot/Modules/Permissions/Common/PermissionCache.cs @@ -1,6 +1,6 @@ using NadekoBot.Services.Database.Models; -namespace NadekoBot.Services.Permissions +namespace NadekoBot.Modules.Permissions.Common { public class OldPermissionCache { diff --git a/src/NadekoBot/Services/Permissions/PermissionExtensions.cs b/src/NadekoBot/Modules/Permissions/Common/PermissionExtensions.cs similarity index 97% rename from src/NadekoBot/Services/Permissions/PermissionExtensions.cs rename to src/NadekoBot/Modules/Permissions/Common/PermissionExtensions.cs index 960e7c32..977a319a 100644 --- a/src/NadekoBot/Services/Permissions/PermissionExtensions.cs +++ b/src/NadekoBot/Modules/Permissions/Common/PermissionExtensions.cs @@ -1,10 +1,10 @@ -using Discord; +using System.Collections.Generic; +using System.Linq; +using Discord; using Discord.WebSocket; using NadekoBot.Services.Database.Models; -using System.Collections.Generic; -using System.Linq; -namespace NadekoBot.Services.Permissions +namespace NadekoBot.Modules.Permissions.Common { public static class PermissionExtensions { diff --git a/src/NadekoBot/Services/Permissions/PermissionsCollection.cs b/src/NadekoBot/Modules/Permissions/Common/PermissionsCollection.cs similarity index 92% rename from src/NadekoBot/Services/Permissions/PermissionsCollection.cs rename to src/NadekoBot/Modules/Permissions/Common/PermissionsCollection.cs index 5d37155d..cf4e41b2 100644 --- a/src/NadekoBot/Services/Permissions/PermissionsCollection.cs +++ b/src/NadekoBot/Modules/Permissions/Common/PermissionsCollection.cs @@ -1,11 +1,12 @@ using System; using System.Collections.Generic; +using NadekoBot.Common; +using NadekoBot.Common.Collections; using NadekoBot.Services.Database.Models; -using NadekoBot.DataStructures; -namespace NadekoBot.Services.Permissions +namespace NadekoBot.Modules.Permissions.Common { - public class PermissionsCollection : IndexedCollection where T : IIndexed + public class PermissionsCollection : IndexedCollection where T : class, IIndexed { private readonly object _localLocker = new object(); public PermissionsCollection(IEnumerable source) : base(source) @@ -59,7 +60,7 @@ namespace NadekoBot.Services.Permissions } public override T this[int index] { - get { return Source[index]; } + get => Source[index]; set { lock (_localLocker) { diff --git a/src/NadekoBot/Modules/Permissions/Commands/FilterCommands.cs b/src/NadekoBot/Modules/Permissions/FilterCommands.cs similarity index 94% rename from src/NadekoBot/Modules/Permissions/Commands/FilterCommands.cs rename to src/NadekoBot/Modules/Permissions/FilterCommands.cs index 1329fc0b..79b068fd 100644 --- a/src/NadekoBot/Modules/Permissions/Commands/FilterCommands.cs +++ b/src/NadekoBot/Modules/Permissions/FilterCommands.cs @@ -2,13 +2,15 @@ using Discord.Commands; using Discord.WebSocket; using Microsoft.EntityFrameworkCore; -using NadekoBot.Attributes; using NadekoBot.Extensions; using NadekoBot.Services; -using NadekoBot.Services.Permissions; using System.Collections.Concurrent; using System.Linq; using System.Threading.Tasks; +using NadekoBot.Common.Attributes; +using NadekoBot.Common.Collections; +using NadekoBot.Modules.Permissions.Services; +using NadekoBot.Services.Database.Models; namespace NadekoBot.Modules.Permissions { @@ -65,7 +67,7 @@ namespace NadekoBot.Modules.Permissions removed = config.FilterInvitesChannelIds.RemoveWhere(fc => fc.ChannelId == channel.Id); if (removed == 0) { - config.FilterInvitesChannelIds.Add(new Services.Database.Models.FilterChannelId() + config.FilterInvitesChannelIds.Add(new FilterChannelId() { ChannelId = channel.Id }); @@ -123,7 +125,7 @@ namespace NadekoBot.Modules.Permissions removed = config.FilterWordsChannelIds.RemoveWhere(fc => fc.ChannelId == channel.Id); if (removed == 0) { - config.FilterWordsChannelIds.Add(new Services.Database.Models.FilterChannelId() + config.FilterWordsChannelIds.Add(new FilterChannelId() { ChannelId = channel.Id }); @@ -162,7 +164,7 @@ namespace NadekoBot.Modules.Permissions removed = config.FilteredWords.RemoveWhere(fw => fw.Word.Trim().ToLowerInvariant() == word); if (removed == 0) - config.FilteredWords.Add(new Services.Database.Models.FilteredWord() { Word = word }); + config.FilteredWords.Add(new FilteredWord() { Word = word }); await uow.CompleteAsync().ConfigureAwait(false); } diff --git a/src/NadekoBot/Modules/Permissions/Commands/GlobalPermissionCommands.cs b/src/NadekoBot/Modules/Permissions/GlobalPermissionCommands.cs similarity index 93% rename from src/NadekoBot/Modules/Permissions/Commands/GlobalPermissionCommands.cs rename to src/NadekoBot/Modules/Permissions/GlobalPermissionCommands.cs index eb60f78f..5c49adf7 100644 --- a/src/NadekoBot/Modules/Permissions/Commands/GlobalPermissionCommands.cs +++ b/src/NadekoBot/Modules/Permissions/GlobalPermissionCommands.cs @@ -1,12 +1,13 @@ using Discord; using Discord.Commands; -using NadekoBot.Attributes; using NadekoBot.Extensions; using NadekoBot.Services; -using NadekoBot.Services.Permissions; -using NadekoBot.TypeReaders; using System.Linq; using System.Threading.Tasks; +using NadekoBot.Common.Attributes; +using NadekoBot.Common.TypeReaders; +using NadekoBot.Modules.Permissions.Services; +using NadekoBot.Services.Database.Models; namespace NadekoBot.Modules.Permissions { @@ -55,7 +56,7 @@ namespace NadekoBot.Modules.Permissions using (var uow = _db.UnitOfWork) { var bc = uow.BotConfig.GetOrCreate(); - bc.BlockedModules.Add(new Services.Database.Models.BlockedCmdOrMdl + bc.BlockedModules.Add(new BlockedCmdOrMdl { Name = moduleName, }); @@ -87,7 +88,7 @@ namespace NadekoBot.Modules.Permissions using (var uow = _db.UnitOfWork) { var bc = uow.BotConfig.GetOrCreate(); - bc.BlockedCommands.Add(new Services.Database.Models.BlockedCmdOrMdl + bc.BlockedCommands.Add(new BlockedCmdOrMdl { Name = commandName, }); diff --git a/src/NadekoBot/Modules/Permissions/Permissions.cs b/src/NadekoBot/Modules/Permissions/Permissions.cs index 0ba34b9f..e451eea4 100644 --- a/src/NadekoBot/Modules/Permissions/Permissions.cs +++ b/src/NadekoBot/Modules/Permissions/Permissions.cs @@ -1,5 +1,4 @@ -using NadekoBot.Attributes; -using System; +using System; using System.Linq; using System.Threading.Tasks; using Discord.Commands; @@ -8,8 +7,12 @@ using Discord; using NadekoBot.Services.Database.Models; using System.Collections.Generic; using Discord.WebSocket; -using NadekoBot.TypeReaders; -using NadekoBot.Services.Permissions; +using NadekoBot.Common; +using NadekoBot.Common.Attributes; +using NadekoBot.Common.TypeReaders; +using NadekoBot.Common.TypeReaders.Models; +using NadekoBot.Modules.Permissions.Common; +using NadekoBot.Modules.Permissions.Services; namespace NadekoBot.Modules.Permissions { diff --git a/src/NadekoBot/Modules/Permissions/Commands/ResetPermissionsCommands.cs b/src/NadekoBot/Modules/Permissions/ResetPermissionsCommands.cs similarity index 94% rename from src/NadekoBot/Modules/Permissions/Commands/ResetPermissionsCommands.cs rename to src/NadekoBot/Modules/Permissions/ResetPermissionsCommands.cs index 2e6ea132..d11f523f 100644 --- a/src/NadekoBot/Modules/Permissions/Commands/ResetPermissionsCommands.cs +++ b/src/NadekoBot/Modules/Permissions/ResetPermissionsCommands.cs @@ -1,10 +1,10 @@ using Discord; using Discord.Commands; -using NadekoBot.Attributes; using NadekoBot.Services; using NadekoBot.Services.Database.Models; -using NadekoBot.Services.Permissions; using System.Threading.Tasks; +using NadekoBot.Common.Attributes; +using NadekoBot.Modules.Permissions.Services; namespace NadekoBot.Modules.Permissions { diff --git a/src/NadekoBot/Services/Permissions/BlacklistService.cs b/src/NadekoBot/Modules/Permissions/Services/BlacklistService.cs similarity index 86% rename from src/NadekoBot/Services/Permissions/BlacklistService.cs rename to src/NadekoBot/Modules/Permissions/Services/BlacklistService.cs index cad2f356..0c06b4b1 100644 --- a/src/NadekoBot/Services/Permissions/BlacklistService.cs +++ b/src/NadekoBot/Modules/Permissions/Services/BlacklistService.cs @@ -1,11 +1,13 @@ -using NadekoBot.DataStructures.ModuleBehaviors; -using NadekoBot.Services.Database.Models; -using System.Collections.Concurrent; +using System.Collections.Concurrent; using System.Linq; -using Discord; using System.Threading.Tasks; +using Discord; +using NadekoBot.Common.Collections; +using NadekoBot.Common.ModuleBehaviors; +using NadekoBot.Services; +using NadekoBot.Services.Database.Models; -namespace NadekoBot.Services.Permissions +namespace NadekoBot.Modules.Permissions.Services { public class BlacklistService : IEarlyBlocker, INService { diff --git a/src/NadekoBot/Services/Permissions/CmdCdService.cs b/src/NadekoBot/Modules/Permissions/Services/CmdCdService.cs similarity index 93% rename from src/NadekoBot/Services/Permissions/CmdCdService.cs rename to src/NadekoBot/Modules/Permissions/Services/CmdCdService.cs index 27a24ea5..bead8238 100644 --- a/src/NadekoBot/Services/Permissions/CmdCdService.cs +++ b/src/NadekoBot/Modules/Permissions/Services/CmdCdService.cs @@ -1,13 +1,15 @@ -using NadekoBot.DataStructures.ModuleBehaviors; -using NadekoBot.Services.Database.Models; -using System.Collections.Concurrent; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; +using System.Threading.Tasks; using Discord; using Discord.WebSocket; -using System.Threading.Tasks; +using NadekoBot.Common.Collections; +using NadekoBot.Common.ModuleBehaviors; +using NadekoBot.Services; +using NadekoBot.Services.Database.Models; -namespace NadekoBot.Services.Permissions +namespace NadekoBot.Modules.Permissions.Services { public class CmdCdService : ILateBlocker, INService { diff --git a/src/NadekoBot/Services/Permissions/FilterService.cs b/src/NadekoBot/Modules/Permissions/Services/FilterService.cs similarity index 96% rename from src/NadekoBot/Services/Permissions/FilterService.cs rename to src/NadekoBot/Modules/Permissions/Services/FilterService.cs index 38885646..886f85e7 100644 --- a/src/NadekoBot/Services/Permissions/FilterService.cs +++ b/src/NadekoBot/Modules/Permissions/Services/FilterService.cs @@ -1,16 +1,18 @@ -using NadekoBot.DataStructures.ModuleBehaviors; -using NadekoBot.Services.Database.Models; -using System.Collections.Concurrent; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; -using Discord; -using Discord.WebSocket; using System.Threading.Tasks; -using NadekoBot.Extensions; +using Discord; using Discord.Net; +using Discord.WebSocket; +using NadekoBot.Common.Collections; +using NadekoBot.Common.ModuleBehaviors; +using NadekoBot.Extensions; +using NadekoBot.Services; +using NadekoBot.Services.Database.Models; using NLog; -namespace NadekoBot.Services.Permissions +namespace NadekoBot.Modules.Permissions.Services { public class FilterService : IEarlyBlocker, INService { diff --git a/src/NadekoBot/Services/Permissions/GlobalPermissionService.cs b/src/NadekoBot/Modules/Permissions/Services/GlobalPermissionService.cs similarity index 87% rename from src/NadekoBot/Services/Permissions/GlobalPermissionService.cs rename to src/NadekoBot/Modules/Permissions/Services/GlobalPermissionService.cs index 734dd9a8..0bd2267e 100644 --- a/src/NadekoBot/Services/Permissions/GlobalPermissionService.cs +++ b/src/NadekoBot/Modules/Permissions/Services/GlobalPermissionService.cs @@ -1,12 +1,14 @@ -using NadekoBot.DataStructures.ModuleBehaviors; -using NadekoBot.Services.Database.Models; -using System.Collections.Concurrent; +using System.Collections.Concurrent; using System.Linq; +using System.Threading.Tasks; using Discord; using Discord.WebSocket; -using System.Threading.Tasks; +using NadekoBot.Common.Collections; +using NadekoBot.Common.ModuleBehaviors; +using NadekoBot.Services; +using NadekoBot.Services.Database.Models; -namespace NadekoBot.Services.Permissions +namespace NadekoBot.Modules.Permissions.Services { public class GlobalPermissionService : ILateBlocker, INService { diff --git a/src/NadekoBot/Services/Permissions/PermissionsService.cs b/src/NadekoBot/Modules/Permissions/Services/PermissionsService.cs similarity index 98% rename from src/NadekoBot/Services/Permissions/PermissionsService.cs rename to src/NadekoBot/Modules/Permissions/Services/PermissionsService.cs index 597ab2ba..4329850c 100644 --- a/src/NadekoBot/Services/Permissions/PermissionsService.cs +++ b/src/NadekoBot/Modules/Permissions/Services/PermissionsService.cs @@ -1,20 +1,20 @@ - -using Microsoft.EntityFrameworkCore; -using NadekoBot.DataStructures.ModuleBehaviors; -using NadekoBot.Services.Database.Models; -using NLog; -using System; +using System; using System.Collections.Concurrent; using System.Diagnostics; using System.Linq; using System.Threading.Tasks; using Discord; using Discord.WebSocket; +using Microsoft.EntityFrameworkCore; +using NadekoBot.Common.ModuleBehaviors; using NadekoBot.Extensions; -using NadekoBot.Services.Database; +using NadekoBot.Modules.Permissions.Common; using NadekoBot.Services; +using NadekoBot.Services.Database.Models; +using NadekoBot.Services.Impl; +using NLog; -namespace NadekoBot.Services.Permissions +namespace NadekoBot.Modules.Permissions.Services { public class PermissionService : ILateBlocker, INService { diff --git a/src/NadekoBot/Services/Permissions/ResetPermissionsService.cs b/src/NadekoBot/Modules/Permissions/Services/ResetPermissionsService.cs similarity index 89% rename from src/NadekoBot/Services/Permissions/ResetPermissionsService.cs rename to src/NadekoBot/Modules/Permissions/Services/ResetPermissionsService.cs index 7816aab3..08834b36 100644 --- a/src/NadekoBot/Services/Permissions/ResetPermissionsService.cs +++ b/src/NadekoBot/Modules/Permissions/Services/ResetPermissionsService.cs @@ -1,7 +1,8 @@ -using NadekoBot.Services.Database.Models; -using System.Threading.Tasks; +using System.Threading.Tasks; +using NadekoBot.Services; +using NadekoBot.Services.Database.Models; -namespace NadekoBot.Services.Permissions +namespace NadekoBot.Modules.Permissions.Services { public class ResetPermissionsService : INService { diff --git a/src/NadekoBot/Services/Pokemon/PokeStats.cs b/src/NadekoBot/Modules/Pokemon/Common/PokeStats.cs similarity index 90% rename from src/NadekoBot/Services/Pokemon/PokeStats.cs rename to src/NadekoBot/Modules/Pokemon/Common/PokeStats.cs index 069cfd72..eb06a1a6 100644 --- a/src/NadekoBot/Services/Pokemon/PokeStats.cs +++ b/src/NadekoBot/Modules/Pokemon/Common/PokeStats.cs @@ -1,6 +1,6 @@ using System.Collections.Generic; -namespace NadekoBot.Services.Pokemon +namespace NadekoBot.Modules.Pokemon.Common { public class PokeStats { diff --git a/src/NadekoBot/Services/Pokemon/PokemonType.cs b/src/NadekoBot/Modules/Pokemon/Common/PokemonType.cs similarity index 95% rename from src/NadekoBot/Services/Pokemon/PokemonType.cs rename to src/NadekoBot/Modules/Pokemon/Common/PokemonType.cs index 1b2bae80..ac83c314 100644 --- a/src/NadekoBot/Services/Pokemon/PokemonType.cs +++ b/src/NadekoBot/Modules/Pokemon/Common/PokemonType.cs @@ -1,6 +1,6 @@ using System.Collections.Generic; -namespace NadekoBot.Services.Pokemon +namespace NadekoBot.Modules.Pokemon.Common { public class PokemonType { diff --git a/src/NadekoBot/Modules/Pokemon/Pokemon.cs b/src/NadekoBot/Modules/Pokemon/Pokemon.cs index 22680a29..ba62465f 100644 --- a/src/NadekoBot/Modules/Pokemon/Pokemon.cs +++ b/src/NadekoBot/Modules/Pokemon/Pokemon.cs @@ -1,5 +1,4 @@ using Discord.Commands; -using NadekoBot.Attributes; using NadekoBot.Extensions; using System.Linq; using NadekoBot.Services; @@ -8,7 +7,9 @@ using System.Collections.Generic; using System.Threading.Tasks; using Discord; using System; -using NadekoBot.Services.Pokemon; +using NadekoBot.Common.Attributes; +using NadekoBot.Modules.Pokemon.Common; +using NadekoBot.Modules.Pokemon.Services; namespace NadekoBot.Modules.Pokemon { diff --git a/src/NadekoBot/Services/Pokemon/PokemonService.cs b/src/NadekoBot/Modules/Pokemon/Services/PokemonService.cs similarity index 84% rename from src/NadekoBot/Services/Pokemon/PokemonService.cs rename to src/NadekoBot/Modules/Pokemon/Services/PokemonService.cs index 9ac73bf1..43edc5cd 100644 --- a/src/NadekoBot/Services/Pokemon/PokemonService.cs +++ b/src/NadekoBot/Modules/Pokemon/Services/PokemonService.cs @@ -1,10 +1,12 @@ -using Newtonsoft.Json; -using NLog; -using System.Collections.Concurrent; +using System.Collections.Concurrent; using System.Collections.Generic; using System.IO; +using NadekoBot.Modules.Pokemon.Common; +using NadekoBot.Services; +using Newtonsoft.Json; +using NLog; -namespace NadekoBot.Services.Pokemon +namespace NadekoBot.Modules.Pokemon.Services { public class PokemonService : INService { diff --git a/src/NadekoBot/Modules/Searches/Commands/AnimeSearchCommands.cs b/src/NadekoBot/Modules/Searches/AnimeSearchCommands.cs similarity index 97% rename from src/NadekoBot/Modules/Searches/Commands/AnimeSearchCommands.cs rename to src/NadekoBot/Modules/Searches/AnimeSearchCommands.cs index edf392e2..f6825b7d 100644 --- a/src/NadekoBot/Modules/Searches/Commands/AnimeSearchCommands.cs +++ b/src/NadekoBot/Modules/Searches/AnimeSearchCommands.cs @@ -2,13 +2,12 @@ using AngleSharp.Dom.Html; using Discord; using Discord.Commands; -using NadekoBot.Attributes; using NadekoBot.Extensions; -using NadekoBot.Services.Searches; +using NadekoBot.Modules.Searches.Services; using System; using System.Linq; -using System.Threading; using System.Threading.Tasks; +using NadekoBot.Common.Attributes; namespace NadekoBot.Modules.Searches { @@ -140,7 +139,7 @@ namespace NadekoBot.Modules.Searches .WithImageUrl(animeData.image_url_lge) .AddField(efb => efb.WithName(GetText("episodes")).WithValue(animeData.total_episodes.ToString()).WithIsInline(true)) .AddField(efb => efb.WithName(GetText("status")).WithValue(animeData.AiringStatus.ToString()).WithIsInline(true)) - .AddField(efb => efb.WithName(GetText("genres")).WithValue(String.Join(",\n", animeData.Genres.Any() ? animeData.Genres : new[] { "none" })).WithIsInline(true)) + .AddField(efb => efb.WithName(GetText("genres")).WithValue(string.Join(",\n", animeData.Genres.Any() ? animeData.Genres : new[] { "none" })).WithIsInline(true)) .WithFooter(efb => efb.WithText(GetText("score") + " " + animeData.average_score + " / 100")); await Context.Channel.EmbedAsync(embed).ConfigureAwait(false); } @@ -167,7 +166,7 @@ namespace NadekoBot.Modules.Searches .WithImageUrl(mangaData.image_url_lge) .AddField(efb => efb.WithName(GetText("chapters")).WithValue(mangaData.total_chapters.ToString()).WithIsInline(true)) .AddField(efb => efb.WithName(GetText("status")).WithValue(mangaData.publishing_status.ToString()).WithIsInline(true)) - .AddField(efb => efb.WithName(GetText("genres")).WithValue(String.Join(",\n", mangaData.Genres.Any() ? mangaData.Genres : new[] { "none" })).WithIsInline(true)) + .AddField(efb => efb.WithName(GetText("genres")).WithValue(string.Join(",\n", mangaData.Genres.Any() ? mangaData.Genres : new[] { "none" })).WithIsInline(true)) .WithFooter(efb => efb.WithText(GetText("score") + " " + mangaData.average_score + " / 100")); await Context.Channel.EmbedAsync(embed).ConfigureAwait(false); diff --git a/src/NadekoBot/Modules/Searches/Commands/Models/WeatherModels.cs b/src/NadekoBot/Modules/Searches/Commands/Models/WeatherModels.cs deleted file mode 100644 index d9495625..00000000 --- a/src/NadekoBot/Modules/Searches/Commands/Models/WeatherModels.cs +++ /dev/null @@ -1,63 +0,0 @@ -using System.Collections.Generic; - -namespace NadekoBot.Modules.Searches.Commands.Models -{ - public class Coord - { - public double lon { get; set; } - public double lat { get; set; } - } - - public class Weather - { - public int id { get; set; } - public string main { get; set; } - public string description { get; set; } - public string icon { get; set; } - } - - public class Main - { - public double temp { get; set; } - public float pressure { get; set; } - public float humidity { get; set; } - public double temp_min { get; set; } - public double temp_max { get; set; } - } - - public class Wind - { - public double speed { get; set; } - public double deg { get; set; } - } - - public class Clouds - { - public int all { get; set; } - } - - public class Sys - { - public int type { get; set; } - public int id { get; set; } - public double message { get; set; } - public string country { get; set; } - public double sunrise { get; set; } - public double sunset { get; set; } - } - - public class WeatherData - { - public Coord coord { get; set; } - public List weather { get; set; } - public Main main { get; set; } - public int visibility { get; set; } - public Wind wind { get; set; } - public Clouds clouds { get; set; } - public int dt { get; set; } - public Sys sys { get; set; } - public int id { get; set; } - public string name { get; set; } - public int cod { get; set; } - } -} diff --git a/src/NadekoBot/Services/Searches/Models/AnimeResult.cs b/src/NadekoBot/Modules/Searches/Common/AnimeResult.cs similarity index 93% rename from src/NadekoBot/Services/Searches/Models/AnimeResult.cs rename to src/NadekoBot/Modules/Searches/Common/AnimeResult.cs index 626421d2..f2d3e8f0 100644 --- a/src/NadekoBot/Services/Searches/Models/AnimeResult.cs +++ b/src/NadekoBot/Modules/Searches/Common/AnimeResult.cs @@ -1,6 +1,6 @@ using NadekoBot.Extensions; -namespace NadekoBot.Services.Searches +namespace NadekoBot.Modules.Searches.Common { public class AnimeResult { diff --git a/src/NadekoBot/Modules/Searches/Commands/Models/DefineModel.cs b/src/NadekoBot/Modules/Searches/Common/DefineModel.cs similarity index 93% rename from src/NadekoBot/Modules/Searches/Commands/Models/DefineModel.cs rename to src/NadekoBot/Modules/Searches/Common/DefineModel.cs index f88fd9d3..5a3b4603 100644 --- a/src/NadekoBot/Modules/Searches/Commands/Models/DefineModel.cs +++ b/src/NadekoBot/Modules/Searches/Common/DefineModel.cs @@ -1,6 +1,6 @@ using System.Collections.Generic; -namespace NadekoBot.Modules.Searches.Commands.Models +namespace NadekoBot.Modules.Searches.Common { public class Audio { diff --git a/src/NadekoBot/Services/Searches/StreamNotFoundException.cs b/src/NadekoBot/Modules/Searches/Common/Exceptions/StreamNotFoundException.cs similarity index 78% rename from src/NadekoBot/Services/Searches/StreamNotFoundException.cs rename to src/NadekoBot/Modules/Searches/Common/Exceptions/StreamNotFoundException.cs index f05d250f..3e43016d 100644 --- a/src/NadekoBot/Services/Searches/StreamNotFoundException.cs +++ b/src/NadekoBot/Modules/Searches/Common/Exceptions/StreamNotFoundException.cs @@ -1,6 +1,6 @@ using System; -namespace NadekoBot.Services.Searches +namespace NadekoBot.Modules.Searches.Common.Exceptions { public class StreamNotFoundException : Exception { diff --git a/src/NadekoBot/Modules/Searches/Commands/Models/GoogleSearchResult.cs b/src/NadekoBot/Modules/Searches/Common/GoogleSearchResult.cs similarity index 86% rename from src/NadekoBot/Modules/Searches/Commands/Models/GoogleSearchResult.cs rename to src/NadekoBot/Modules/Searches/Common/GoogleSearchResult.cs index 72fe4483..843db40e 100644 --- a/src/NadekoBot/Modules/Searches/Commands/Models/GoogleSearchResult.cs +++ b/src/NadekoBot/Modules/Searches/Common/GoogleSearchResult.cs @@ -1,4 +1,4 @@ -namespace NadekoBot.Modules.Searches.Commands.Models +namespace NadekoBot.Modules.Searches.Common { public struct GoogleSearchResult { diff --git a/src/NadekoBot/Services/Searches/Models/MagicItem.cs b/src/NadekoBot/Modules/Searches/Common/MagicItem.cs similarity index 73% rename from src/NadekoBot/Services/Searches/Models/MagicItem.cs rename to src/NadekoBot/Modules/Searches/Common/MagicItem.cs index 1d555e7e..a69629bc 100644 --- a/src/NadekoBot/Services/Searches/Models/MagicItem.cs +++ b/src/NadekoBot/Modules/Searches/Common/MagicItem.cs @@ -1,4 +1,4 @@ -namespace NadekoBot.Services.Searches +namespace NadekoBot.Modules.Searches.Common { public class MagicItem { diff --git a/src/NadekoBot/Services/Searches/Models/MangaResult.cs b/src/NadekoBot/Modules/Searches/Common/MangaResult.cs similarity index 92% rename from src/NadekoBot/Services/Searches/Models/MangaResult.cs rename to src/NadekoBot/Modules/Searches/Common/MangaResult.cs index 3109b940..0ea0b961 100644 --- a/src/NadekoBot/Services/Searches/Models/MangaResult.cs +++ b/src/NadekoBot/Modules/Searches/Common/MangaResult.cs @@ -1,4 +1,4 @@ -namespace NadekoBot.Services.Searches +namespace NadekoBot.Modules.Searches.Common { public class MangaResult { diff --git a/src/NadekoBot/Modules/Searches/Commands/OMDB/OmdbProvider.cs b/src/NadekoBot/Modules/Searches/Common/OmdbProvider.cs similarity index 96% rename from src/NadekoBot/Modules/Searches/Commands/OMDB/OmdbProvider.cs rename to src/NadekoBot/Modules/Searches/Common/OmdbProvider.cs index c5a478ea..16ea322a 100644 --- a/src/NadekoBot/Modules/Searches/Commands/OMDB/OmdbProvider.cs +++ b/src/NadekoBot/Modules/Searches/Common/OmdbProvider.cs @@ -1,12 +1,12 @@ -using Discord; +using System; +using System.Net.Http; +using System.Threading.Tasks; +using Discord; using NadekoBot.Extensions; using NadekoBot.Services; using Newtonsoft.Json; -using System; -using System.Net.Http; -using System.Threading.Tasks; -namespace NadekoBot.Modules.Searches.Commands.OMDB +namespace NadekoBot.Modules.Searches.Common { public static class OmdbProvider { diff --git a/src/NadekoBot/Modules/Searches/Commands/Models/OverwatchApiModel.cs b/src/NadekoBot/Modules/Searches/Common/OverwatchApiModel.cs similarity index 96% rename from src/NadekoBot/Modules/Searches/Commands/Models/OverwatchApiModel.cs rename to src/NadekoBot/Modules/Searches/Common/OverwatchApiModel.cs index a373e062..c7f73c1b 100644 --- a/src/NadekoBot/Modules/Searches/Commands/Models/OverwatchApiModel.cs +++ b/src/NadekoBot/Modules/Searches/Common/OverwatchApiModel.cs @@ -1,6 +1,6 @@ using Newtonsoft.Json; -namespace NadekoBot.Modules.Searches.Models +namespace NadekoBot.Modules.Searches.Common { public class OverwatchApiModel { diff --git a/src/NadekoBot/Common/SearchImageCacher.cs b/src/NadekoBot/Modules/Searches/Common/SearchImageCacher.cs similarity index 98% rename from src/NadekoBot/Common/SearchImageCacher.cs rename to src/NadekoBot/Modules/Searches/Common/SearchImageCacher.cs index 8e71db03..bf2582e7 100644 --- a/src/NadekoBot/Common/SearchImageCacher.cs +++ b/src/NadekoBot/Modules/Searches/Common/SearchImageCacher.cs @@ -1,8 +1,4 @@ -using NadekoBot.Extensions; -using NadekoBot.Services; -using Newtonsoft.Json; -using NLog; -using System; +using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; @@ -10,9 +6,13 @@ using System.Net.Http; using System.Threading; using System.Threading.Tasks; using System.Xml; -using System.Xml.Linq; +using NadekoBot.Common; +using NadekoBot.Extensions; +using NadekoBot.Services; +using Newtonsoft.Json; +using NLog; -namespace NadekoBot.DataStructures +namespace NadekoBot.Modules.Searches.Common { public class SearchImageCacher { diff --git a/src/NadekoBot/Services/Searches/Models/SearchPokemon.cs b/src/NadekoBot/Modules/Searches/Common/SearchPokemon.cs similarity index 97% rename from src/NadekoBot/Services/Searches/Models/SearchPokemon.cs rename to src/NadekoBot/Modules/Searches/Common/SearchPokemon.cs index 4fd0674d..29de4e00 100644 --- a/src/NadekoBot/Services/Searches/Models/SearchPokemon.cs +++ b/src/NadekoBot/Modules/Searches/Common/SearchPokemon.cs @@ -1,6 +1,6 @@ using System.Collections.Generic; -namespace NadekoBot.Services.Searches +namespace NadekoBot.Modules.Searches.Common { public class SearchPokemon { diff --git a/src/NadekoBot/Services/Searches/Models/StreamResponses.cs b/src/NadekoBot/Modules/Searches/Common/StreamResponses.cs similarity index 95% rename from src/NadekoBot/Services/Searches/Models/StreamResponses.cs rename to src/NadekoBot/Modules/Searches/Common/StreamResponses.cs index 484a4962..95168773 100644 --- a/src/NadekoBot/Services/Searches/Models/StreamResponses.cs +++ b/src/NadekoBot/Modules/Searches/Common/StreamResponses.cs @@ -1,6 +1,6 @@ using Newtonsoft.Json; -namespace NadekoBot.Services.Searches +namespace NadekoBot.Modules.Searches.Common { public class HitboxResponse { diff --git a/src/NadekoBot/Modules/Searches/Commands/Models/TimeModels.cs b/src/NadekoBot/Modules/Searches/Common/TimeModels.cs similarity index 94% rename from src/NadekoBot/Modules/Searches/Commands/Models/TimeModels.cs rename to src/NadekoBot/Modules/Searches/Common/TimeModels.cs index f30571fc..83eb4437 100644 --- a/src/NadekoBot/Modules/Searches/Commands/Models/TimeModels.cs +++ b/src/NadekoBot/Modules/Searches/Common/TimeModels.cs @@ -1,6 +1,6 @@ using Newtonsoft.Json; -namespace NadekoBot.Modules.Searches.Commands.Models +namespace NadekoBot.Modules.Searches.Common { public class TimeZoneResult { diff --git a/src/NadekoBot/Modules/Searches/Common/WeatherModels.cs b/src/NadekoBot/Modules/Searches/Common/WeatherModels.cs new file mode 100644 index 00000000..7c7922e8 --- /dev/null +++ b/src/NadekoBot/Modules/Searches/Common/WeatherModels.cs @@ -0,0 +1,63 @@ +using System.Collections.Generic; + +namespace NadekoBot.Modules.Searches.Common +{ + public class Coord + { + public double Lon { get; set; } + public double Lat { get; set; } + } + + public class Weather + { + public int Id { get; set; } + public string Main { get; set; } + public string Description { get; set; } + public string Icon { get; set; } + } + + public class Main + { + public double Temp { get; set; } + public float Pressure { get; set; } + public float Humidity { get; set; } + public double TempMin { get; set; } + public double TempMax { get; set; } + } + + public class Wind + { + public double Speed { get; set; } + public double Deg { get; set; } + } + + public class Clouds + { + public int All { get; set; } + } + + public class Sys + { + public int Type { get; set; } + public int Id { get; set; } + public double Message { get; set; } + public string Country { get; set; } + public double Sunrise { get; set; } + public double Sunset { get; set; } + } + + public class WeatherData + { + public Coord Coord { get; set; } + public List Weather { get; set; } + public Main Main { get; set; } + public int Visibility { get; set; } + public Wind Wind { get; set; } + public Clouds Clouds { get; set; } + public int Dt { get; set; } + public Sys Sys { get; set; } + public int Id { get; set; } + public string Name { get; set; } + public int Cod { get; set; } + } +} diff --git a/src/NadekoBot/Modules/Searches/Commands/Models/WikipediaApiModel.cs b/src/NadekoBot/Modules/Searches/Common/WikipediaApiModel.cs similarity index 89% rename from src/NadekoBot/Modules/Searches/Commands/Models/WikipediaApiModel.cs rename to src/NadekoBot/Modules/Searches/Common/WikipediaApiModel.cs index ed961a2a..fd77e482 100644 --- a/src/NadekoBot/Modules/Searches/Commands/Models/WikipediaApiModel.cs +++ b/src/NadekoBot/Modules/Searches/Common/WikipediaApiModel.cs @@ -1,4 +1,4 @@ -namespace NadekoBot.Modules.Searches.Models +namespace NadekoBot.Modules.Searches.Common { public class WikipediaApiModel { diff --git a/src/NadekoBot/Services/Searches/Models/WoWJoke.cs b/src/NadekoBot/Modules/Searches/Common/WoWJoke.cs similarity index 81% rename from src/NadekoBot/Services/Searches/Models/WoWJoke.cs rename to src/NadekoBot/Modules/Searches/Common/WoWJoke.cs index d363b8b2..7e414505 100644 --- a/src/NadekoBot/Services/Searches/Models/WoWJoke.cs +++ b/src/NadekoBot/Modules/Searches/Common/WoWJoke.cs @@ -1,4 +1,4 @@ -namespace NadekoBot.Services.Searches +namespace NadekoBot.Modules.Searches.Common { public class WoWJoke { diff --git a/src/NadekoBot/Modules/Searches/Commands/JokeCommands.cs b/src/NadekoBot/Modules/Searches/JokeCommands.cs similarity index 96% rename from src/NadekoBot/Modules/Searches/Commands/JokeCommands.cs rename to src/NadekoBot/Modules/Searches/JokeCommands.cs index a99f77d7..83dbbd76 100644 --- a/src/NadekoBot/Modules/Searches/Commands/JokeCommands.cs +++ b/src/NadekoBot/Modules/Searches/JokeCommands.cs @@ -1,13 +1,14 @@ using AngleSharp; using Discord.Commands; -using NadekoBot.Attributes; using NadekoBot.Extensions; +using NadekoBot.Modules.Searches.Services; using NadekoBot.Services; -using NadekoBot.Services.Searches; using Newtonsoft.Json.Linq; using System.Linq; using System.Net.Http; using System.Threading.Tasks; +using NadekoBot.Common; +using NadekoBot.Common.Attributes; namespace NadekoBot.Modules.Searches { diff --git a/src/NadekoBot/Modules/Searches/Commands/LoLCommands.cs b/src/NadekoBot/Modules/Searches/LoLCommands.cs similarity index 99% rename from src/NadekoBot/Modules/Searches/Commands/LoLCommands.cs rename to src/NadekoBot/Modules/Searches/LoLCommands.cs index 9e1e1c43..9d1e58cf 100644 --- a/src/NadekoBot/Modules/Searches/Commands/LoLCommands.cs +++ b/src/NadekoBot/Modules/Searches/LoLCommands.cs @@ -1,5 +1,4 @@ using Discord; -using NadekoBot.Attributes; using NadekoBot.Extensions; using NadekoBot.Services; using Newtonsoft.Json; @@ -10,6 +9,8 @@ using System.IO; using System.Linq; using System.Net.Http; using System.Threading.Tasks; +using NadekoBot.Common; +using NadekoBot.Common.Attributes; //todo 50 drawing namespace NadekoBot.Modules.Searches diff --git a/src/NadekoBot/Modules/Searches/Commands/MemegenCommands.cs b/src/NadekoBot/Modules/Searches/MemegenCommands.cs similarity index 98% rename from src/NadekoBot/Modules/Searches/Commands/MemegenCommands.cs rename to src/NadekoBot/Modules/Searches/MemegenCommands.cs index 1425436e..c74fe419 100644 --- a/src/NadekoBot/Modules/Searches/Commands/MemegenCommands.cs +++ b/src/NadekoBot/Modules/Searches/MemegenCommands.cs @@ -4,10 +4,10 @@ using System.Collections.Immutable; using System.IO; using System.Linq; using System.Threading.Tasks; -using NadekoBot.Attributes; using System.Net.Http; using System.Text; using Discord.Commands; +using NadekoBot.Common.Attributes; using NadekoBot.Extensions; namespace NadekoBot.Modules.Searches diff --git a/src/NadekoBot/Modules/Searches/Commands/OsuCommands.cs b/src/NadekoBot/Modules/Searches/OsuCommands.cs similarity index 99% rename from src/NadekoBot/Modules/Searches/Commands/OsuCommands.cs rename to src/NadekoBot/Modules/Searches/OsuCommands.cs index 1fcc789e..b8f05df0 100644 --- a/src/NadekoBot/Modules/Searches/Commands/OsuCommands.cs +++ b/src/NadekoBot/Modules/Searches/OsuCommands.cs @@ -1,6 +1,5 @@ using Discord; using Discord.Commands; -using NadekoBot.Attributes; using NadekoBot.Extensions; using NadekoBot.Services; using Newtonsoft.Json.Linq; @@ -10,6 +9,7 @@ using System.IO; using System.Net.Http; using System.Text.RegularExpressions; using System.Threading.Tasks; +using NadekoBot.Common.Attributes; namespace NadekoBot.Modules.Searches { diff --git a/src/NadekoBot/Modules/Searches/Commands/OverwatchCommands.cs b/src/NadekoBot/Modules/Searches/OverwatchCommands.cs similarity index 97% rename from src/NadekoBot/Modules/Searches/Commands/OverwatchCommands.cs rename to src/NadekoBot/Modules/Searches/OverwatchCommands.cs index d4ffffbe..210aca73 100644 --- a/src/NadekoBot/Modules/Searches/Commands/OverwatchCommands.cs +++ b/src/NadekoBot/Modules/Searches/OverwatchCommands.cs @@ -1,111 +1,111 @@ -using System; -using Discord; -using Discord.Commands; -using NadekoBot.Attributes; -using NadekoBot.Extensions; -using NadekoBot.Modules.Searches.Models; -using Newtonsoft.Json; -using System.Net.Http; -using System.Threading.Tasks; - -namespace NadekoBot.Modules.Searches -{ - public partial class Searches - { - [Group] - public class OverwatchCommands : NadekoSubmodule - { - public enum Region - { - Eu, - Us, - Kr - } - - [NadekoCommand, Usage, Description, Aliases] - public async Task Overwatch(Region region, [Remainder] string query = null) - { - if (string.IsNullOrWhiteSpace(query)) - return; - var battletag = query.Replace("#", "-"); - - await Context.Channel.TriggerTypingAsync().ConfigureAwait(false); - var model = (await GetProfile(region, battletag))?.Stats; - - if (model != null) - { - if (model.Competitive == null) - { - var qp = model.Quickplay; - var embed = new EmbedBuilder() - .WithAuthor(eau => eau.WithName(query) - .WithUrl($"https://www.overbuff.com/players/pc/{battletag}") - .WithIconUrl("https://cdn.discordapp.com/attachments/155726317222887425/255653487512256512/YZ4w2ey.png")) - .WithThumbnailUrl(qp.OverallStats.avatar) - .AddField(fb => fb.WithName(GetText("level")).WithValue((qp.OverallStats.level + (qp.OverallStats.prestige * 100)).ToString()).WithIsInline(true)) - .AddField(fb => fb.WithName(GetText("quick_wins")).WithValue(qp.OverallStats.wins.ToString()).WithIsInline(true)) - .AddField(fb => fb.WithName(GetText("compet_rank")).WithValue("0").WithIsInline(true)) - .AddField(fb => fb.WithName(GetText("quick_playtime")).WithValue($"{qp.GameStats.timePlayed}hrs").WithIsInline(true)) - .WithOkColor(); - await Context.Channel.EmbedAsync(embed).ConfigureAwait(false); - } - else - { - var qp = model.Quickplay; - var compet = model.Competitive; - var embed = new EmbedBuilder() - .WithAuthor(eau => eau.WithName(query) - .WithUrl($"https://www.overbuff.com/players/pc/{battletag}") - .WithIconUrl(compet.OverallStats.rank_image)) - .WithThumbnailUrl(compet.OverallStats.avatar) - .AddField(fb => fb.WithName(GetText("level")).WithValue((qp.OverallStats.level + (qp.OverallStats.prestige * 100)).ToString()).WithIsInline(true)) - .AddField(fb => fb.WithName(GetText("quick_wins")).WithValue(qp.OverallStats.wins.ToString()).WithIsInline(true)) - .AddField(fb => fb.WithName(GetText("compet_wins")).WithValue(compet.OverallStats.wins.ToString()).WithIsInline(true)) - .AddField(fb => fb.WithName(GetText("compet_loses")).WithValue(compet.OverallStats.losses.ToString()).WithIsInline(true)) - .AddField(fb => fb.WithName(GetText("compet_played")).WithValue(compet.OverallStats.games.ToString() ?? "-").WithIsInline(true)) - .AddField(fb => fb.WithName(GetText("compet_rank")).WithValue(compet.OverallStats.comprank?.ToString() ?? "-").WithIsInline(true)) - .AddField(fb => fb.WithName(GetText("compet_playtime")).WithValue(compet.GameStats.timePlayed + "hrs").WithIsInline(true)) - .AddField(fb => fb.WithName(GetText("quick_playtime")).WithValue(qp.GameStats.timePlayed.ToString("F1") + "hrs").WithIsInline(true)) - .WithColor(NadekoBot.OkColor); - await Context.Channel.EmbedAsync(embed).ConfigureAwait(false); - } - } - else - { - await ReplyErrorLocalized("ow_user_not_found").ConfigureAwait(false); - } - } - public async Task GetProfile(Region region, string battletag) - { - try - { - using (var handler = new HttpClientHandler()) - { - handler.ServerCertificateCustomValidationCallback = (x, y, z, e) => true; - using (var http = new HttpClient(handler)) - { - http.AddFakeHeaders(); - var url = $"https://owapi.nadekobot.me/api/v3/u/{battletag}/stats"; - var res = await http.GetStringAsync($"https://owapi.nadekobot.me/api/v3/u/{battletag}/stats"); - var model = JsonConvert.DeserializeObject(res); - switch (region) - { - case Region.Eu: - return model.Eu; - case Region.Kr: - return model.Kr; - default: - return model.Us; - } - } - } - } - catch (Exception ex) - { - _log.Warn(ex); - return null; - } - } - } - } +using System; +using Discord; +using Discord.Commands; +using NadekoBot.Extensions; +using Newtonsoft.Json; +using System.Net.Http; +using System.Threading.Tasks; +using NadekoBot.Common.Attributes; +using NadekoBot.Modules.Searches.Common; + +namespace NadekoBot.Modules.Searches +{ + public partial class Searches + { + [Group] + public class OverwatchCommands : NadekoSubmodule + { + public enum Region + { + Eu, + Us, + Kr + } + + [NadekoCommand, Usage, Description, Aliases] + public async Task Overwatch(Region region, [Remainder] string query = null) + { + if (string.IsNullOrWhiteSpace(query)) + return; + var battletag = query.Replace("#", "-"); + + await Context.Channel.TriggerTypingAsync().ConfigureAwait(false); + var model = (await GetProfile(region, battletag))?.Stats; + + if (model != null) + { + if (model.Competitive == null) + { + var qp = model.Quickplay; + var embed = new EmbedBuilder() + .WithAuthor(eau => eau.WithName(query) + .WithUrl($"https://www.overbuff.com/players/pc/{battletag}") + .WithIconUrl("https://cdn.discordapp.com/attachments/155726317222887425/255653487512256512/YZ4w2ey.png")) + .WithThumbnailUrl(qp.OverallStats.avatar) + .AddField(fb => fb.WithName(GetText("level")).WithValue((qp.OverallStats.level + (qp.OverallStats.prestige * 100)).ToString()).WithIsInline(true)) + .AddField(fb => fb.WithName(GetText("quick_wins")).WithValue(qp.OverallStats.wins.ToString()).WithIsInline(true)) + .AddField(fb => fb.WithName(GetText("compet_rank")).WithValue("0").WithIsInline(true)) + .AddField(fb => fb.WithName(GetText("quick_playtime")).WithValue($"{qp.GameStats.timePlayed}hrs").WithIsInline(true)) + .WithOkColor(); + await Context.Channel.EmbedAsync(embed).ConfigureAwait(false); + } + else + { + var qp = model.Quickplay; + var compet = model.Competitive; + var embed = new EmbedBuilder() + .WithAuthor(eau => eau.WithName(query) + .WithUrl($"https://www.overbuff.com/players/pc/{battletag}") + .WithIconUrl(compet.OverallStats.rank_image)) + .WithThumbnailUrl(compet.OverallStats.avatar) + .AddField(fb => fb.WithName(GetText("level")).WithValue((qp.OverallStats.level + (qp.OverallStats.prestige * 100)).ToString()).WithIsInline(true)) + .AddField(fb => fb.WithName(GetText("quick_wins")).WithValue(qp.OverallStats.wins.ToString()).WithIsInline(true)) + .AddField(fb => fb.WithName(GetText("compet_wins")).WithValue(compet.OverallStats.wins.ToString()).WithIsInline(true)) + .AddField(fb => fb.WithName(GetText("compet_loses")).WithValue(compet.OverallStats.losses.ToString()).WithIsInline(true)) + .AddField(fb => fb.WithName(GetText("compet_played")).WithValue(compet.OverallStats.games.ToString() ?? "-").WithIsInline(true)) + .AddField(fb => fb.WithName(GetText("compet_rank")).WithValue(compet.OverallStats.comprank?.ToString() ?? "-").WithIsInline(true)) + .AddField(fb => fb.WithName(GetText("compet_playtime")).WithValue(compet.GameStats.timePlayed + "hrs").WithIsInline(true)) + .AddField(fb => fb.WithName(GetText("quick_playtime")).WithValue(qp.GameStats.timePlayed.ToString("F1") + "hrs").WithIsInline(true)) + .WithColor(NadekoBot.OkColor); + await Context.Channel.EmbedAsync(embed).ConfigureAwait(false); + } + } + else + { + await ReplyErrorLocalized("ow_user_not_found").ConfigureAwait(false); + } + } + public async Task GetProfile(Region region, string battletag) + { + try + { + using (var handler = new HttpClientHandler()) + { + handler.ServerCertificateCustomValidationCallback = (x, y, z, e) => true; + using (var http = new HttpClient(handler)) + { + http.AddFakeHeaders(); + var url = $"https://owapi.nadekobot.me/api/v3/u/{battletag}/stats"; + var res = await http.GetStringAsync($"https://owapi.nadekobot.me/api/v3/u/{battletag}/stats"); + var model = JsonConvert.DeserializeObject(res); + switch (region) + { + case Region.Eu: + return model.Eu; + case Region.Kr: + return model.Kr; + default: + return model.Us; + } + } + } + } + catch (Exception ex) + { + _log.Warn(ex); + return null; + } + } + } + } } \ No newline at end of file diff --git a/src/NadekoBot/Modules/Searches/Commands/PlaceCommands.cs b/src/NadekoBot/Modules/Searches/PlaceCommands.cs similarity index 97% rename from src/NadekoBot/Modules/Searches/Commands/PlaceCommands.cs rename to src/NadekoBot/Modules/Searches/PlaceCommands.cs index 61f5aec9..62dd4964 100644 --- a/src/NadekoBot/Modules/Searches/Commands/PlaceCommands.cs +++ b/src/NadekoBot/Modules/Searches/PlaceCommands.cs @@ -1,9 +1,10 @@ using Discord.Commands; -using NadekoBot.Attributes; using NadekoBot.Extensions; using NadekoBot.Services; using System; using System.Threading.Tasks; +using NadekoBot.Common; +using NadekoBot.Common.Attributes; namespace NadekoBot.Modules.Searches { diff --git a/src/NadekoBot/Modules/Searches/Commands/PokemonSearchCommands.cs b/src/NadekoBot/Modules/Searches/PokemonSearchCommands.cs similarity index 96% rename from src/NadekoBot/Modules/Searches/Commands/PokemonSearchCommands.cs rename to src/NadekoBot/Modules/Searches/PokemonSearchCommands.cs index 4cdec0c0..2ea5c498 100644 --- a/src/NadekoBot/Modules/Searches/Commands/PokemonSearchCommands.cs +++ b/src/NadekoBot/Modules/Searches/PokemonSearchCommands.cs @@ -1,11 +1,12 @@ using Discord; using Discord.Commands; -using NadekoBot.Attributes; using NadekoBot.Extensions; -using NadekoBot.Services.Searches; +using NadekoBot.Modules.Searches.Services; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using NadekoBot.Common.Attributes; +using NadekoBot.Modules.Searches.Common; namespace NadekoBot.Modules.Searches { diff --git a/src/NadekoBot/Modules/Searches/Searches.cs b/src/NadekoBot/Modules/Searches/Searches.cs index f9961c38..a0733429 100644 --- a/src/NadekoBot/Modules/Searches/Searches.cs +++ b/src/NadekoBot/Modules/Searches/Searches.cs @@ -7,21 +7,19 @@ using System.Net.Http; using NadekoBot.Services; using System.Threading.Tasks; using System.Net; -using NadekoBot.Modules.Searches.Models; using System.Collections.Generic; using NadekoBot.Extensions; using System.IO; -using NadekoBot.Modules.Searches.Commands.OMDB; -using NadekoBot.Modules.Searches.Commands.Models; using AngleSharp; using AngleSharp.Dom.Html; using AngleSharp.Dom; using Configuration = AngleSharp.Configuration; -using NadekoBot.Attributes; using Discord.Commands; using ImageSharp; -using NadekoBot.Services.Searches; -using NadekoBot.DataStructures; +using NadekoBot.Common; +using NadekoBot.Common.Attributes; +using NadekoBot.Modules.Searches.Common; +using NadekoBot.Modules.Searches.Services; namespace NadekoBot.Modules.Searches { @@ -49,17 +47,17 @@ namespace NadekoBot.Modules.Searches var data = JsonConvert.DeserializeObject(response); var embed = new EmbedBuilder() - .AddField(fb => fb.WithName("🌍 " + Format.Bold(GetText("location"))).WithValue($"[{data.name + ", " + data.sys.country}](https://openweathermap.org/city/{data.id})").WithIsInline(true)) - .AddField(fb => fb.WithName("📏 " + Format.Bold(GetText("latlong"))).WithValue($"{data.coord.lat}, {data.coord.lon}").WithIsInline(true)) - .AddField(fb => fb.WithName("☁ " + Format.Bold(GetText("condition"))).WithValue(string.Join(", ", data.weather.Select(w => w.main))).WithIsInline(true)) - .AddField(fb => fb.WithName("😓 " + Format.Bold(GetText("humidity"))).WithValue($"{data.main.humidity}%").WithIsInline(true)) - .AddField(fb => fb.WithName("💨 " + Format.Bold(GetText("wind_speed"))).WithValue(data.wind.speed + " m/s").WithIsInline(true)) - .AddField(fb => fb.WithName("🌡 " + Format.Bold(GetText("temperature"))).WithValue(data.main.temp + "°C").WithIsInline(true)) - .AddField(fb => fb.WithName("🔆 " + Format.Bold(GetText("min_max"))).WithValue($"{data.main.temp_min}°C - {data.main.temp_max}°C").WithIsInline(true)) - .AddField(fb => fb.WithName("🌄 " + Format.Bold(GetText("sunrise"))).WithValue($"{data.sys.sunrise.ToUnixTimestamp():HH:mm} UTC").WithIsInline(true)) - .AddField(fb => fb.WithName("🌇 " + Format.Bold(GetText("sunset"))).WithValue($"{data.sys.sunset.ToUnixTimestamp():HH:mm} UTC").WithIsInline(true)) + .AddField(fb => fb.WithName("🌍 " + Format.Bold(GetText("location"))).WithValue($"[{data.Name + ", " + data.Sys.Country}](https://openweathermap.org/city/{data.Id})").WithIsInline(true)) + .AddField(fb => fb.WithName("📏 " + Format.Bold(GetText("latlong"))).WithValue($"{data.Coord.Lat}, {data.Coord.Lon}").WithIsInline(true)) + .AddField(fb => fb.WithName("☁ " + Format.Bold(GetText("condition"))).WithValue(string.Join(", ", data.Weather.Select(w => w.Main))).WithIsInline(true)) + .AddField(fb => fb.WithName("😓 " + Format.Bold(GetText("humidity"))).WithValue($"{data.Main.Humidity}%").WithIsInline(true)) + .AddField(fb => fb.WithName("💨 " + Format.Bold(GetText("wind_speed"))).WithValue(data.Wind.Speed + " m/s").WithIsInline(true)) + .AddField(fb => fb.WithName("🌡 " + Format.Bold(GetText("temperature"))).WithValue(data.Main.Temp + "°C").WithIsInline(true)) + .AddField(fb => fb.WithName("🔆 " + Format.Bold(GetText("min_max"))).WithValue($"{data.Main.TempMin}°C - {data.Main.TempMax}°C").WithIsInline(true)) + .AddField(fb => fb.WithName("🌄 " + Format.Bold(GetText("sunrise"))).WithValue($"{data.Sys.Sunrise.ToUnixTimestamp():HH:mm} UTC").WithIsInline(true)) + .AddField(fb => fb.WithName("🌇 " + Format.Bold(GetText("sunset"))).WithValue($"{data.Sys.Sunset.ToUnixTimestamp():HH:mm} UTC").WithIsInline(true)) .WithOkColor() - .WithFooter(efb => efb.WithText("Powered by openweathermap.org").WithIconUrl($"http://openweathermap.org/img/w/{data.weather[0].icon}.png")); + .WithFooter(efb => efb.WithText("Powered by openweathermap.org").WithIconUrl($"http://openweathermap.org/img/w/{data.Weather[0].Icon}.png")); await Context.Channel.EmbedAsync(embed).ConfigureAwait(false); } diff --git a/src/NadekoBot/Services/Searches/AnimeSearchService.cs b/src/NadekoBot/Modules/Searches/Services/AnimeSearchService.cs similarity index 92% rename from src/NadekoBot/Services/Searches/AnimeSearchService.cs rename to src/NadekoBot/Modules/Searches/Services/AnimeSearchService.cs index bcf9a3a3..638db338 100644 --- a/src/NadekoBot/Services/Searches/AnimeSearchService.cs +++ b/src/NadekoBot/Modules/Searches/Services/AnimeSearchService.cs @@ -1,10 +1,12 @@ -using Newtonsoft.Json; +using NadekoBot.Services; +using Newtonsoft.Json; using NLog; using System; using System.Net.Http; using System.Threading.Tasks; +using NadekoBot.Modules.Searches.Common; -namespace NadekoBot.Services.Searches +namespace NadekoBot.Modules.Searches.Services { public class AnimeSearchService : INService { diff --git a/src/NadekoBot/Services/Searches/SearchesService.cs b/src/NadekoBot/Modules/Searches/Services/SearchesService.cs similarity index 97% rename from src/NadekoBot/Services/Searches/SearchesService.cs rename to src/NadekoBot/Modules/Searches/Services/SearchesService.cs index 43cc2a52..e8a72402 100644 --- a/src/NadekoBot/Services/Searches/SearchesService.cs +++ b/src/NadekoBot/Modules/Searches/Services/SearchesService.cs @@ -1,19 +1,17 @@ using Discord; using Discord.WebSocket; -using NadekoBot.DataStructures; using NadekoBot.Extensions; +using NadekoBot.Services; using Newtonsoft.Json; using NLog; using System; using System.Collections.Concurrent; using System.Collections.Generic; -using System.Diagnostics; using System.IO; -using System.Net.Http; using System.Threading.Tasks; -using System.Xml; +using NadekoBot.Modules.Searches.Common; -namespace NadekoBot.Services.Searches +namespace NadekoBot.Modules.Searches.Services { public class SearchesService : INService { diff --git a/src/NadekoBot/Services/Searches/StreamNotificationService.cs b/src/NadekoBot/Modules/Searches/Services/StreamNotificationService.cs similarity index 98% rename from src/NadekoBot/Services/Searches/StreamNotificationService.cs rename to src/NadekoBot/Modules/Searches/Services/StreamNotificationService.cs index 3b4039a1..18665738 100644 --- a/src/NadekoBot/Services/Searches/StreamNotificationService.cs +++ b/src/NadekoBot/Modules/Searches/Services/StreamNotificationService.cs @@ -2,7 +2,6 @@ using Discord.WebSocket; using NadekoBot.Extensions; using NadekoBot.Services; -using NadekoBot.Services.Database; using NadekoBot.Services.Database.Models; using Newtonsoft.Json; using System; @@ -12,8 +11,11 @@ using System.Linq; using System.Net.Http; using System.Threading; using System.Threading.Tasks; +using NadekoBot.Modules.Searches.Common; +using NadekoBot.Modules.Searches.Common.Exceptions; +using NadekoBot.Services.Impl; -namespace NadekoBot.Services.Searches +namespace NadekoBot.Modules.Searches.Services { public class StreamNotificationService : INService { diff --git a/src/NadekoBot/Modules/Searches/Commands/StreamNotificationCommands.cs b/src/NadekoBot/Modules/Searches/StreamNotificationCommands.cs similarity index 98% rename from src/NadekoBot/Modules/Searches/Commands/StreamNotificationCommands.cs rename to src/NadekoBot/Modules/Searches/StreamNotificationCommands.cs index 4ec1a325..890ba1d2 100644 --- a/src/NadekoBot/Modules/Searches/Commands/StreamNotificationCommands.cs +++ b/src/NadekoBot/Modules/Searches/StreamNotificationCommands.cs @@ -5,10 +5,10 @@ using System.Threading.Tasks; using NadekoBot.Services; using System.Collections.Generic; using NadekoBot.Services.Database.Models; -using NadekoBot.Attributes; using Microsoft.EntityFrameworkCore; +using NadekoBot.Common.Attributes; using NadekoBot.Extensions; -using NadekoBot.Services.Searches; +using NadekoBot.Modules.Searches.Services; namespace NadekoBot.Modules.Searches { diff --git a/src/NadekoBot/Modules/Searches/Commands/Translator.cs b/src/NadekoBot/Modules/Searches/TranslatorCommands.cs similarity index 98% rename from src/NadekoBot/Modules/Searches/Commands/Translator.cs rename to src/NadekoBot/Modules/Searches/TranslatorCommands.cs index 1dfca6d7..5084e2f3 100644 --- a/src/NadekoBot/Modules/Searches/Commands/Translator.cs +++ b/src/NadekoBot/Modules/Searches/TranslatorCommands.cs @@ -1,11 +1,11 @@ using Discord; using Discord.Commands; -using NadekoBot.Attributes; using NadekoBot.Extensions; using System.Threading.Tasks; using System.Linq; -using NadekoBot.Services.Searches; +using NadekoBot.Common.Attributes; using NadekoBot.Services; +using NadekoBot.Modules.Searches.Services; namespace NadekoBot.Modules.Searches { diff --git a/src/NadekoBot/Modules/Searches/Commands/XkcdCommands.cs b/src/NadekoBot/Modules/Searches/XkcdCommands.cs similarity index 98% rename from src/NadekoBot/Modules/Searches/Commands/XkcdCommands.cs rename to src/NadekoBot/Modules/Searches/XkcdCommands.cs index a3f37f60..5814ff5e 100644 --- a/src/NadekoBot/Modules/Searches/Commands/XkcdCommands.cs +++ b/src/NadekoBot/Modules/Searches/XkcdCommands.cs @@ -1,11 +1,12 @@ using Discord; using Discord.Commands; -using NadekoBot.Attributes; using NadekoBot.Extensions; using NadekoBot.Services; using Newtonsoft.Json; using System.Net.Http; using System.Threading.Tasks; +using NadekoBot.Common; +using NadekoBot.Common.Attributes; namespace NadekoBot.Modules.Searches { diff --git a/src/NadekoBot/Modules/Utility/Commands/CalcCommand.cs b/src/NadekoBot/Modules/Utility/CalcCommands.cs similarity index 98% rename from src/NadekoBot/Modules/Utility/Commands/CalcCommand.cs rename to src/NadekoBot/Modules/Utility/CalcCommands.cs index 7617cdc9..a1944972 100644 --- a/src/NadekoBot/Modules/Utility/Commands/CalcCommand.cs +++ b/src/NadekoBot/Modules/Utility/CalcCommands.cs @@ -1,11 +1,11 @@ using Discord.Commands; -using NadekoBot.Attributes; 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 { diff --git a/src/NadekoBot/Modules/Utility/Commands/CommandMapCommands.cs b/src/NadekoBot/Modules/Utility/CommandMapCommands.cs similarity index 98% rename from src/NadekoBot/Modules/Utility/Commands/CommandMapCommands.cs rename to src/NadekoBot/Modules/Utility/CommandMapCommands.cs index bf79d41a..872c43b6 100644 --- a/src/NadekoBot/Modules/Utility/Commands/CommandMapCommands.cs +++ b/src/NadekoBot/Modules/Utility/CommandMapCommands.cs @@ -2,15 +2,15 @@ using Discord.Commands; using Discord.WebSocket; using Microsoft.EntityFrameworkCore; -using NadekoBot.Attributes; using NadekoBot.Extensions; using NadekoBot.Services; using NadekoBot.Services.Database.Models; -using NadekoBot.Services.Utility; 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 { diff --git a/src/NadekoBot/Services/Utility/Patreon/PatreonData.cs b/src/NadekoBot/Modules/Utility/Common/Patreon/PatreonData.cs similarity index 90% rename from src/NadekoBot/Services/Utility/Patreon/PatreonData.cs rename to src/NadekoBot/Modules/Utility/Common/Patreon/PatreonData.cs index c57ea817..748c7170 100644 --- a/src/NadekoBot/Services/Utility/Patreon/PatreonData.cs +++ b/src/NadekoBot/Modules/Utility/Common/Patreon/PatreonData.cs @@ -1,6 +1,6 @@ using Newtonsoft.Json.Linq; -namespace NadekoBot.Services.Utility.Patreon +namespace NadekoBot.Modules.Utility.Common.Patreon { public class PatreonData { diff --git a/src/NadekoBot/Services/Utility/Patreon/PatreonPledge.cs b/src/NadekoBot/Modules/Utility/Common/Patreon/PatreonPledge.cs similarity index 96% rename from src/NadekoBot/Services/Utility/Patreon/PatreonPledge.cs rename to src/NadekoBot/Modules/Utility/Common/Patreon/PatreonPledge.cs index 7193718c..9960039f 100644 --- a/src/NadekoBot/Services/Utility/Patreon/PatreonPledge.cs +++ b/src/NadekoBot/Modules/Utility/Common/Patreon/PatreonPledge.cs @@ -1,4 +1,4 @@ -namespace NadekoBot.Services.Utility.Patreon +namespace NadekoBot.Modules.Utility.Common.Patreon { public class Attributes { diff --git a/src/NadekoBot/Services/Utility/Patreon/PatreonUser.cs b/src/NadekoBot/Modules/Utility/Common/Patreon/PatreonUser.cs similarity index 97% rename from src/NadekoBot/Services/Utility/Patreon/PatreonUser.cs rename to src/NadekoBot/Modules/Utility/Common/Patreon/PatreonUser.cs index f054544b..e4d16869 100644 --- a/src/NadekoBot/Services/Utility/Patreon/PatreonUser.cs +++ b/src/NadekoBot/Modules/Utility/Common/Patreon/PatreonUser.cs @@ -1,4 +1,4 @@ -namespace NadekoBot.Services.Utility.Patreon +namespace NadekoBot.Modules.Utility.Common.Patreon { public class DiscordConnection { diff --git a/src/NadekoBot/Services/Utility/RepeatRunner.cs b/src/NadekoBot/Modules/Utility/Common/RepeatRunner.cs similarity index 97% rename from src/NadekoBot/Services/Utility/RepeatRunner.cs rename to src/NadekoBot/Modules/Utility/Common/RepeatRunner.cs index abfa21b1..b2633505 100644 --- a/src/NadekoBot/Services/Utility/RepeatRunner.cs +++ b/src/NadekoBot/Modules/Utility/Common/RepeatRunner.cs @@ -1,14 +1,14 @@ -using Discord; +using System; +using System.Threading; +using System.Threading.Tasks; +using Discord; using Discord.Net; using Discord.WebSocket; using NadekoBot.Extensions; using NadekoBot.Services.Database.Models; using NLog; -using System; -using System.Threading; -using System.Threading.Tasks; -namespace NadekoBot.Services.Utility +namespace NadekoBot.Modules.Utility.Common { public class RepeatRunner { diff --git a/src/NadekoBot/Modules/Utility/Commands/InfoCommands.cs b/src/NadekoBot/Modules/Utility/InfoCommands.cs similarity index 99% rename from src/NadekoBot/Modules/Utility/Commands/InfoCommands.cs rename to src/NadekoBot/Modules/Utility/InfoCommands.cs index c59017ea..0eb448f8 100644 --- a/src/NadekoBot/Modules/Utility/Commands/InfoCommands.cs +++ b/src/NadekoBot/Modules/Utility/InfoCommands.cs @@ -1,13 +1,13 @@ using Discord; using Discord.Commands; using Discord.WebSocket; -using NadekoBot.Attributes; using NadekoBot.Extensions; using NadekoBot.Services; using System; using System.Linq; using System.Text; using System.Threading.Tasks; +using NadekoBot.Common.Attributes; namespace NadekoBot.Modules.Utility { diff --git a/src/NadekoBot/Modules/Utility/Commands/PatreonCommands.cs b/src/NadekoBot/Modules/Utility/PatreonCommands.cs similarity index 97% rename from src/NadekoBot/Modules/Utility/Commands/PatreonCommands.cs rename to src/NadekoBot/Modules/Utility/PatreonCommands.cs index 940f82b6..bbfa2f24 100644 --- a/src/NadekoBot/Modules/Utility/Commands/PatreonCommands.cs +++ b/src/NadekoBot/Modules/Utility/PatreonCommands.cs @@ -1,12 +1,12 @@ using System.Threading.Tasks; using Discord.Commands; -using NadekoBot.Attributes; using System; using NadekoBot.Services; using NadekoBot.Services.Database.Models; using NadekoBot.Extensions; using Discord; -using NadekoBot.Services.Utility; +using NadekoBot.Common.Attributes; +using NadekoBot.Modules.Utility.Services; namespace NadekoBot.Modules.Utility { diff --git a/src/NadekoBot/Modules/Utility/Commands/QuoteCommands.cs b/src/NadekoBot/Modules/Utility/QuoteCommands.cs similarity index 98% rename from src/NadekoBot/Modules/Utility/Commands/QuoteCommands.cs rename to src/NadekoBot/Modules/Utility/QuoteCommands.cs index d8ea1e84..141c4783 100644 --- a/src/NadekoBot/Modules/Utility/Commands/QuoteCommands.cs +++ b/src/NadekoBot/Modules/Utility/QuoteCommands.cs @@ -1,6 +1,5 @@ using Discord; using Discord.Commands; -using NadekoBot.Attributes; using NadekoBot.Extensions; using NadekoBot.Services; using NadekoBot.Services.Database.Models; @@ -8,8 +7,9 @@ using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; -using NadekoBot.DataStructures; -using NadekoBot.DataStructures.Replacements; +using NadekoBot.Common; +using NadekoBot.Common.Attributes; +using NadekoBot.Common.Replacements; namespace NadekoBot.Modules.Utility { diff --git a/src/NadekoBot/Modules/Utility/Commands/Remind.cs b/src/NadekoBot/Modules/Utility/RemindCommands.cs similarity index 97% rename from src/NadekoBot/Modules/Utility/Commands/Remind.cs rename to src/NadekoBot/Modules/Utility/RemindCommands.cs index e87dcfa9..caa13aed 100644 --- a/src/NadekoBot/Modules/Utility/Commands/Remind.cs +++ b/src/NadekoBot/Modules/Utility/RemindCommands.cs @@ -1,15 +1,14 @@ using Discord; using Discord.Commands; -using NadekoBot.Attributes; using NadekoBot.Extensions; using NadekoBot.Services; -using NadekoBot.Services.Administration; using NadekoBot.Services.Database.Models; -using NadekoBot.Services.Utility; using System; using System.Collections.Generic; -using System.Text.RegularExpressions; using System.Threading.Tasks; +using NadekoBot.Common.Attributes; +using NadekoBot.Modules.Administration.Services; +using NadekoBot.Modules.Utility.Services; namespace NadekoBot.Modules.Utility { diff --git a/src/NadekoBot/Modules/Utility/Commands/RepeatCommands.cs b/src/NadekoBot/Modules/Utility/RepeatCommands.cs similarity index 98% rename from src/NadekoBot/Modules/Utility/Commands/RepeatCommands.cs rename to src/NadekoBot/Modules/Utility/RepeatCommands.cs index d7f38e0c..68489a87 100644 --- a/src/NadekoBot/Modules/Utility/Commands/RepeatCommands.cs +++ b/src/NadekoBot/Modules/Utility/RepeatCommands.cs @@ -1,7 +1,6 @@ using Discord; using Discord.Commands; using Microsoft.EntityFrameworkCore; -using NadekoBot.Attributes; using NadekoBot.Extensions; using NadekoBot.Services; using NadekoBot.Services.Database.Models; @@ -11,8 +10,10 @@ using System.Linq; using System.Text; using System.Threading.Tasks; using Discord.WebSocket; -using NadekoBot.Services.Utility; -using NadekoBot.TypeReaders; +using NadekoBot.Common.Attributes; +using NadekoBot.Common.TypeReaders; +using NadekoBot.Modules.Utility.Common; +using NadekoBot.Modules.Utility.Services; namespace NadekoBot.Modules.Utility { diff --git a/src/NadekoBot/Services/Utility/CommandMapService.cs b/src/NadekoBot/Modules/Utility/Services/CommandMapService.cs similarity index 94% rename from src/NadekoBot/Services/Utility/CommandMapService.cs rename to src/NadekoBot/Modules/Utility/Services/CommandMapService.cs index 5c07688e..2aa1c419 100644 --- a/src/NadekoBot/Services/Utility/CommandMapService.cs +++ b/src/NadekoBot/Modules/Utility/Services/CommandMapService.cs @@ -1,14 +1,15 @@ -using NadekoBot.DataStructures.ModuleBehaviors; -using NadekoBot.Services.Database.Models; -using System.Collections.Concurrent; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Discord; -using NLog; +using NadekoBot.Common.ModuleBehaviors; using NadekoBot.Extensions; +using NadekoBot.Services; +using NadekoBot.Services.Database.Models; +using NLog; -namespace NadekoBot.Services.Utility +namespace NadekoBot.Modules.Utility.Services { public class CommandMapService : IInputTransformer, INService { diff --git a/src/NadekoBot/Services/Utility/ConverterService.cs b/src/NadekoBot/Modules/Utility/Services/ConverterService.cs similarity index 98% rename from src/NadekoBot/Services/Utility/ConverterService.cs rename to src/NadekoBot/Modules/Utility/Services/ConverterService.cs index 9d4dfcff..d2573421 100644 --- a/src/NadekoBot/Services/Utility/ConverterService.cs +++ b/src/NadekoBot/Modules/Utility/Services/ConverterService.cs @@ -1,17 +1,17 @@ -using Discord.WebSocket; -using NadekoBot.Services; -using NadekoBot.Services.Database.Models; -using Newtonsoft.Json; -using NLog; -using System; +using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Net.Http; using System.Threading; using System.Threading.Tasks; +using Discord.WebSocket; +using NadekoBot.Services; +using NadekoBot.Services.Database.Models; +using Newtonsoft.Json; +using NLog; -namespace NadekoBot.Services.Utility +namespace NadekoBot.Modules.Utility.Services { public class ConverterService : INService { diff --git a/src/NadekoBot/Services/Utility/MessageRepeaterService.cs b/src/NadekoBot/Modules/Utility/Services/MessageRepeaterService.cs similarity index 86% rename from src/NadekoBot/Services/Utility/MessageRepeaterService.cs rename to src/NadekoBot/Modules/Utility/Services/MessageRepeaterService.cs index eeaecf37..0df59c76 100644 --- a/src/NadekoBot/Services/Utility/MessageRepeaterService.cs +++ b/src/NadekoBot/Modules/Utility/Services/MessageRepeaterService.cs @@ -1,11 +1,13 @@ -using Discord.WebSocket; -using NadekoBot.Services.Database.Models; -using System.Collections.Concurrent; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using Discord.WebSocket; +using NadekoBot.Modules.Utility.Common; +using NadekoBot.Services; +using NadekoBot.Services.Database.Models; -namespace NadekoBot.Services.Utility +namespace NadekoBot.Modules.Utility.Services { //todo 50 rewrite public class MessageRepeaterService : INService diff --git a/src/NadekoBot/Services/Utility/PatreonRewardsService.cs b/src/NadekoBot/Modules/Utility/Services/PatreonRewardsService.cs similarity index 97% rename from src/NadekoBot/Services/Utility/PatreonRewardsService.cs rename to src/NadekoBot/Modules/Utility/Services/PatreonRewardsService.cs index b7fae1f4..c0dbc96e 100644 --- a/src/NadekoBot/Services/Utility/PatreonRewardsService.cs +++ b/src/NadekoBot/Modules/Utility/Services/PatreonRewardsService.cs @@ -1,9 +1,4 @@ -using Discord.WebSocket; -using NadekoBot.Services.Database.Models; -using NadekoBot.Services.Utility.Patreon; -using Newtonsoft.Json; -using NLog; -using System; +using System; using System.Collections.Generic; using System.Collections.Immutable; using System.IO; @@ -11,8 +6,14 @@ using System.Linq; using System.Net.Http; using System.Threading; using System.Threading.Tasks; +using Discord.WebSocket; +using NadekoBot.Modules.Utility.Common.Patreon; +using NadekoBot.Services; +using NadekoBot.Services.Database.Models; +using Newtonsoft.Json; +using NLog; -namespace NadekoBot.Services.Utility +namespace NadekoBot.Modules.Utility.Services { public class PatreonRewardsService : INService { diff --git a/src/NadekoBot/Services/Utility/RemindService.cs b/src/NadekoBot/Modules/Utility/Services/RemindService.cs similarity index 95% rename from src/NadekoBot/Services/Utility/RemindService.cs rename to src/NadekoBot/Modules/Utility/Services/RemindService.cs index 89bc20a6..43e809a6 100644 --- a/src/NadekoBot/Services/Utility/RemindService.cs +++ b/src/NadekoBot/Modules/Utility/Services/RemindService.cs @@ -1,18 +1,19 @@ -using Discord; -using Discord.WebSocket; -using NadekoBot.DataStructures.Replacements; -using NadekoBot.Extensions; -using NadekoBot.Services.Database; -using NadekoBot.Services.Database.Models; -using NadekoBot.Services.Impl; -using NLog; -using System; +using System; using System.Linq; using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; +using Discord; +using Discord.WebSocket; +using NadekoBot.Common.Replacements; +using NadekoBot.Extensions; +using NadekoBot.Services; +using NadekoBot.Services.Database; +using NadekoBot.Services.Database.Models; +using NadekoBot.Services.Impl; +using NLog; -namespace NadekoBot.Services.Utility +namespace NadekoBot.Modules.Utility.Services { public class RemindService : INService { diff --git a/src/NadekoBot/Services/Utility/StreamRoleService.cs b/src/NadekoBot/Modules/Utility/Services/StreamRoleService.cs similarity index 98% rename from src/NadekoBot/Services/Utility/StreamRoleService.cs rename to src/NadekoBot/Modules/Utility/Services/StreamRoleService.cs index 25863135..6e759d93 100644 --- a/src/NadekoBot/Services/Utility/StreamRoleService.cs +++ b/src/NadekoBot/Modules/Utility/Services/StreamRoleService.cs @@ -1,16 +1,17 @@ using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Discord; -using NadekoBot.Services.Database.Models; -using System.Collections.Concurrent; -using NadekoBot.Extensions; using Discord.WebSocket; using Microsoft.EntityFrameworkCore; +using NadekoBot.Extensions; +using NadekoBot.Services; +using NadekoBot.Services.Database.Models; using NLog; -namespace NadekoBot.Services.Utility +namespace NadekoBot.Modules.Utility.Services { public class StreamRoleService : INService { diff --git a/src/NadekoBot/Services/Utility/VerboseErrorsService.cs b/src/NadekoBot/Modules/Utility/Services/VerboseErrorsService.cs similarity index 91% rename from src/NadekoBot/Services/Utility/VerboseErrorsService.cs rename to src/NadekoBot/Modules/Utility/Services/VerboseErrorsService.cs index cb885a12..a3f573bd 100644 --- a/src/NadekoBot/Services/Utility/VerboseErrorsService.cs +++ b/src/NadekoBot/Modules/Utility/Services/VerboseErrorsService.cs @@ -1,14 +1,16 @@ -using NadekoBot.Services.Database.Models; -using System.Collections.Concurrent; +using System.Collections.Concurrent; using System.Collections.Generic; +using System.Linq; using System.Threading.Tasks; using Discord; -using NadekoBot.Extensions; using Discord.Commands; -using System.Linq; +using NadekoBot.Common.Collections; +using NadekoBot.Extensions; using NadekoBot.Modules.Help.Services; +using NadekoBot.Services; +using NadekoBot.Services.Database.Models; -namespace NadekoBot.Services.Utility +namespace NadekoBot.Modules.Utility.Services { public class VerboseErrorsService : INService { diff --git a/src/NadekoBot/Modules/Utility/Commands/StreamRoleCommands.cs b/src/NadekoBot/Modules/Utility/StreamRoleCommands.cs similarity index 94% rename from src/NadekoBot/Modules/Utility/Commands/StreamRoleCommands.cs rename to src/NadekoBot/Modules/Utility/StreamRoleCommands.cs index 9b534b12..7f2c7872 100644 --- a/src/NadekoBot/Modules/Utility/Commands/StreamRoleCommands.cs +++ b/src/NadekoBot/Modules/Utility/StreamRoleCommands.cs @@ -1,8 +1,8 @@ using Discord; using Discord.Commands; -using NadekoBot.Attributes; -using NadekoBot.Services.Utility; using System.Threading.Tasks; +using NadekoBot.Common.Attributes; +using NadekoBot.Modules.Utility.Services; namespace NadekoBot.Modules.Utility { diff --git a/src/NadekoBot/Modules/Utility/Commands/UnitConversion.cs b/src/NadekoBot/Modules/Utility/UnitConversionCommands.cs similarity index 98% rename from src/NadekoBot/Modules/Utility/Commands/UnitConversion.cs rename to src/NadekoBot/Modules/Utility/UnitConversionCommands.cs index aafa3b1f..4f9ca0e6 100644 --- a/src/NadekoBot/Modules/Utility/Commands/UnitConversion.cs +++ b/src/NadekoBot/Modules/Utility/UnitConversionCommands.cs @@ -1,11 +1,12 @@ using Discord; using Discord.Commands; -using NadekoBot.Attributes; using NadekoBot.Extensions; -using NadekoBot.Services.Utility; using System; using System.Linq; using System.Threading.Tasks; +using NadekoBot.Common.Attributes; +using NadekoBot.Modules.Utility.Services; + namespace NadekoBot.Modules.Utility { public partial class Utility diff --git a/src/NadekoBot/Modules/Utility/Utility.cs b/src/NadekoBot/Modules/Utility/Utility.cs index b4c8b003..bd3b66f7 100644 --- a/src/NadekoBot/Modules/Utility/Utility.cs +++ b/src/NadekoBot/Modules/Utility/Utility.cs @@ -1,6 +1,5 @@ using Discord; using Discord.Commands; -using NadekoBot.Attributes; using System; using System.Linq; using System.Threading.Tasks; @@ -16,9 +15,10 @@ using System.Collections.Generic; using Newtonsoft.Json; using Discord.WebSocket; using System.Diagnostics; +using NadekoBot.Common; +using NadekoBot.Common.Attributes; using Color = Discord.Color; using NadekoBot.Services; -using NadekoBot.DataStructures; namespace NadekoBot.Modules.Utility { diff --git a/src/NadekoBot/Modules/Utility/Commands/VerboseCommandErrors.cs b/src/NadekoBot/Modules/Utility/VerboseErrorCommands.cs similarity index 84% rename from src/NadekoBot/Modules/Utility/Commands/VerboseCommandErrors.cs rename to src/NadekoBot/Modules/Utility/VerboseErrorCommands.cs index db73bdda..a092dab7 100644 --- a/src/NadekoBot/Modules/Utility/Commands/VerboseCommandErrors.cs +++ b/src/NadekoBot/Modules/Utility/VerboseErrorCommands.cs @@ -1,14 +1,14 @@ using Discord.Commands; -using NadekoBot.Attributes; -using NadekoBot.Services.Utility; using System.Threading.Tasks; +using NadekoBot.Common.Attributes; +using NadekoBot.Modules.Utility.Services; namespace NadekoBot.Modules.Utility { public partial class Utility { [Group] - public class VerboseCommandErrors : NadekoSubmodule + public class VerboseErrorCommands : NadekoSubmodule { [NadekoCommand, Usage, Description, Aliases] [RequireContext(ContextType.Guild)] diff --git a/src/NadekoBot/NadekoBot.cs b/src/NadekoBot/NadekoBot.cs index 41bdda06..9c10bd96 100644 --- a/src/NadekoBot/NadekoBot.cs +++ b/src/NadekoBot/NadekoBot.cs @@ -9,16 +9,17 @@ using System.Linq; using System.Reflection; using System.Threading.Tasks; using NadekoBot.Modules.Permissions; -using NadekoBot.TypeReaders; using System.Collections.Immutable; using System.Diagnostics; using NadekoBot.Services.Database.Models; using System.Threading; using System.IO; -using NadekoBot.DataStructures.ShardCom; -using NadekoBot.DataStructures; using NadekoBot.Extensions; using System.Collections.Generic; +using NadekoBot.Common; +using NadekoBot.Common.ShardCom; +using NadekoBot.Common.TypeReaders; +using NadekoBot.Common.TypeReaders.Models; using NadekoBot.Services.Database; namespace NadekoBot diff --git a/src/NadekoBot/NadekoBot.csproj b/src/NadekoBot/NadekoBot.csproj index 1a906d3b..07193a3c 100644 --- a/src/NadekoBot/NadekoBot.csproj +++ b/src/NadekoBot/NadekoBot.csproj @@ -91,8 +91,4 @@ - - - - diff --git a/src/NadekoBot/Services/CommandHandler.cs b/src/NadekoBot/Services/CommandHandler.cs index 12740fa0..81a70da2 100644 --- a/src/NadekoBot/Services/CommandHandler.cs +++ b/src/NadekoBot/Services/CommandHandler.cs @@ -9,12 +9,13 @@ using Discord.Commands; using NadekoBot.Extensions; using System.Collections.Concurrent; using System.Threading; -using NadekoBot.DataStructures; using System.Collections.Immutable; -using NadekoBot.DataStructures.ModuleBehaviors; using NadekoBot.Services.Database.Models; using System.IO; using Discord.Net; +using NadekoBot.Common; +using NadekoBot.Common.Collections; +using NadekoBot.Common.ModuleBehaviors; namespace NadekoBot.Services { diff --git a/src/NadekoBot/Services/Database/Repositories/Impl/QuoteRepository.cs b/src/NadekoBot/Services/Database/Repositories/Impl/QuoteRepository.cs index 76002d1c..00db27b0 100644 --- a/src/NadekoBot/Services/Database/Repositories/Impl/QuoteRepository.cs +++ b/src/NadekoBot/Services/Database/Repositories/Impl/QuoteRepository.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Microsoft.EntityFrameworkCore; +using NadekoBot.Common; namespace NadekoBot.Services.Database.Repositories.Impl { diff --git a/src/NadekoBot/Services/GreetSettingsService.cs b/src/NadekoBot/Services/GreetSettingsService.cs index 10b0899c..2d8b5e9b 100644 --- a/src/NadekoBot/Services/GreetSettingsService.cs +++ b/src/NadekoBot/Services/GreetSettingsService.cs @@ -1,7 +1,5 @@ using Discord; using Discord.WebSocket; -using NadekoBot.DataStructures; -using NadekoBot.DataStructures.Replacements; using NadekoBot.Extensions; using NadekoBot.Services.Database.Models; using NLog; @@ -10,6 +8,8 @@ using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using NadekoBot.Common; +using NadekoBot.Common.Replacements; namespace NadekoBot.Services { diff --git a/src/NadekoBot/Services/Impl/BotCredentials.cs b/src/NadekoBot/Services/Impl/BotCredentials.cs index 3c69a3ec..d42b1732 100644 --- a/src/NadekoBot/Services/Impl/BotCredentials.cs +++ b/src/NadekoBot/Services/Impl/BotCredentials.cs @@ -6,6 +6,7 @@ using System.Linq; using NLog; using Microsoft.Extensions.Configuration; using System.Collections.Immutable; +using NadekoBot.Common; namespace NadekoBot.Services.Impl { diff --git a/src/NadekoBot/Services/Impl/Localization.cs b/src/NadekoBot/Services/Impl/Localization.cs index d4befd71..a1ac9043 100644 --- a/src/NadekoBot/Services/Impl/Localization.cs +++ b/src/NadekoBot/Services/Impl/Localization.cs @@ -1,11 +1,11 @@ -using Discord; -using System.Collections.Concurrent; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Globalization; using System.Linq; +using Discord; using NLog; -namespace NadekoBot.Services +namespace NadekoBot.Services.Impl { public class Localization : ILocalization { diff --git a/src/NadekoBot/Services/Impl/NadekoStrings.cs b/src/NadekoBot/Services/Impl/NadekoStrings.cs index 651805b8..b50f553e 100644 --- a/src/NadekoBot/Services/Impl/NadekoStrings.cs +++ b/src/NadekoBot/Services/Impl/NadekoStrings.cs @@ -1,15 +1,14 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Collections.Immutable; +using System.Diagnostics; using System.Globalization; using System.IO; -using NLog; -using System.Diagnostics; -using Newtonsoft.Json; -using System; -using System.Linq; using System.Text.RegularExpressions; +using Newtonsoft.Json; +using NLog; -namespace NadekoBot.Services +namespace NadekoBot.Services.Impl { public class NadekoStrings : INService { diff --git a/src/NadekoBot/ShardsCoordinator.cs b/src/NadekoBot/ShardsCoordinator.cs index 9e4851bd..9ab563af 100644 --- a/src/NadekoBot/ShardsCoordinator.cs +++ b/src/NadekoBot/ShardsCoordinator.cs @@ -1,18 +1,18 @@ -using NadekoBot.DataStructures.ShardCom; -using NadekoBot.Services; +using NadekoBot.Services; using NadekoBot.Services.Impl; using NLog; using System; using System.Diagnostics; using System.Linq; using System.Threading.Tasks; +using NadekoBot.Common.ShardCom; namespace NadekoBot { public class ShardsCoordinator { - private readonly BotCredentials Credentials; - private Process[] ShardProcesses; + private readonly BotCredentials _creds; + private readonly Process[] ShardProcesses; public ShardComMessage[] Statuses { get; } public int GuildCount => Statuses.ToArray() .Where(x => x != null) @@ -26,9 +26,9 @@ namespace NadekoBot public ShardsCoordinator(int port) { LogSetup.SetupLogger(); - Credentials = new BotCredentials(); - ShardProcesses = new Process[Credentials.TotalShards]; - Statuses = new ShardComMessage[Credentials.TotalShards]; + _creds = new BotCredentials(); + ShardProcesses = new Process[_creds.TotalShards]; + Statuses = new ShardComMessage[_creds.TotalShards]; _log = LogManager.GetCurrentClassLogger(); _port = port; @@ -51,12 +51,12 @@ namespace NadekoBot public async Task RunAsync() { - for (int i = 1; i < Credentials.TotalShards; i++) + for (int i = 1; i < _creds.TotalShards; i++) { var p = Process.Start(new ProcessStartInfo() { - FileName = Credentials.ShardRunCommand, - Arguments = string.Format(Credentials.ShardRunArguments, i, _curProcessId, _port) + FileName = _creds.ShardRunCommand, + Arguments = string.Format(_creds.ShardRunArguments, i, _curProcessId, _port) }); await Task.Delay(5000); } diff --git a/src/NadekoBot/_Extensions/Extensions.cs b/src/NadekoBot/_Extensions/Extensions.cs index e2d26adc..14cde12e 100644 --- a/src/NadekoBot/_Extensions/Extensions.cs +++ b/src/NadekoBot/_Extensions/Extensions.cs @@ -14,6 +14,7 @@ using System.Security.Cryptography; using System.Text; using System.Text.RegularExpressions; using System.Threading.Tasks; +using NadekoBot.Common.Collections; namespace NadekoBot.Extensions { From 55b1c3945be4c6335539954fdddabd5502c81436 Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Tue, 18 Jul 2017 04:25:30 +0200 Subject: [PATCH 195/346] Updated commandlist. Goodbye clash of clans :wave: closes #1420 --- docs/Commands List.md | 23 +++---------- src/NadekoBot/Resources/CommandStrings.resx | 4 +-- src/NadekoBot/ShardsCoordinator.cs | 36 ++------------------- 3 files changed, 9 insertions(+), 54 deletions(-) diff --git a/docs/Commands List.md b/docs/Commands List.md index b5da3f22..b6609665 100644 --- a/docs/Commands List.md +++ b/docs/Commands List.md @@ -3,7 +3,6 @@ You can support the project on patreon: or paypa ##Table of contents - [Help](#help) - [Administration](#administration) -- [ClashOfClans](#clashofclans) - [CustomReactions](#customreactions) - [Gambling](#gambling) - [Games](#games) @@ -119,21 +118,6 @@ Commands and aliases | Description | Usage ###### [Back to ToC](#table-of-contents) -### ClashOfClans -Commands and aliases | Description | Usage -----------------|--------------|------- -`.createwar` `.cw` | Creates a new war by specifying a size (>10 and multiple of 5) and enemy clan name. **Requires ManageMessages server permission.** | `.cw 15 The Enemy Clan` -`.startwar` `.sw` | Starts a war with a given number. | `.sw 15` -`.listwar` `.lw` | Shows the active war claims by a number. Shows all wars in a short way if no number is specified. | `.lw [war_number]` or `.lw` -`.basecall` | Claims a certain base from a certain war. You can supply a name in the third optional argument to claim in someone else's place. | `.basecall [war_number] [base_number] [optional_other_name]` -`.callfinish1` `.cf1` | Finish your claim with 1 star if you destroyed a base. First argument is the war number, optional second argument is a base number if you want to finish for someone else. | `.cf1 1` or `.cf1 1 5` -`.callfinish2` `.cf2` | Finish your claim with 2 stars if you destroyed a base. First argument is the war number, optional second argument is a base number if you want to finish for someone else. | `.cf2 1` or `.cf2 1 5` -`.callfinish` `.cf` | Finish your claim with 3 stars if you destroyed a base. First argument is the war number, optional second argument is a base number if you want to finish for someone else. | `.cf 1` or `.cf 1 5` -`.endwar` `.ew` | Ends the war with a given index. | `.ew [war_number]` -`.uncall` | Removes your claim from a certain war. Optional second argument denotes a person in whose place to unclaim | `.uc [war_number] [optional_other_name]` - -###### [Back to ToC](#table-of-contents) - ### CustomReactions Commands and aliases | Description | Usage ----------------|--------------|------- @@ -189,12 +173,12 @@ Commands and aliases | Description | Usage ### Games Commands and aliases | Description | Usage ----------------|--------------|------- -`.leet` | Converts a text to leetspeak with 6 (1-6) severity levels | `.leet 3 Hello` `.choose` | Chooses a thing from a list of things | `.choose Get up;Sleep;Sleep more` `.8ball` | Ask the 8ball a yes/no question. | `.8ball should I do something` `.rps` | Play a game of Rocket-Paperclip-Scissors with Nadeko. | `.rps scissors` `.rategirl` | Use the universal hot-crazy wife zone matrix to determine the girl's worth. It is everything young men need to know about women. At any moment in time, any woman you have previously located on this chart can vanish from that location and appear anywhere else on the chart. | `.rategirl @SomeGurl` `.linux` | Prints a customizable Linux interjection | `.linux Spyware Windows` +`.leet` | Converts a text to leetspeak with 6 (1-6) severity levels | `.leet 3 Hello` `.acrophobia` `.acro` | Starts an Acrophobia game. Second argument is optional round length in seconds. (default is 60) | `.acro` or `.acro 30` `.cleverbot` | Toggles cleverbot session. When enabled, the bot will reply to messages starting with bot mention in the server. Custom reactions starting with %mention% won't work if cleverbot is enabled. **Requires ManageMessages server permission.** | `.cleverbot` `.hangmanlist` | Shows a list of hangman term types. | `.hangmanlist` @@ -238,12 +222,12 @@ Commands and aliases | Description | Usage `.queuesearch` `.qs` `.yqs` | Search for top 5 youtube song result using keywords, and type the index of the song to play that song. Bot will join your voice channel. **You must be in a voice channel**. | `.qs Dream Of Venice` `.listqueue` `.lq` | Lists 10 currently queued songs per page. Default page is 1. | `.lq` or `.lq 2` `.next` `.n` | Goes to the next song in the queue. You have to be in the same voice channel as the bot. You can skip multiple songs, but in that case songs will not be requeued if .rcs or .rpl is enabled. | `.n` or `.n 5` -`.stop` `.s` | Stops the music and clears the playlist. Stays in the channel. | `.s` +`.stop` `.s` | Stops the music and preserves the current song index. Stays in the channel. | `.s` `.destroy` `.d` | Completely stops the music and unbinds the bot from the channel. (may cause weird behaviour) | `.d` `.pause` `.p` | Pauses or Unpauses the song. | `.p` `.volume` `.vol` | Sets the music playback volume (0-100%) | `.vol 50` `.defvol` `.dv` | Sets the default music volume when music playback is started (0-100). Persists through restarts. | `.dv 80` -`.songremove` `.srm` | Remove a song by its # in the queue, or 'all' to remove all songs from the queue. | `.srm 5` +`.songremove` `.srm` | Remove a song by its # in the queue, or 'all' to remove all songs from the queue and reset the song index. | `.srm 5` `.playlists` `.pls` | Lists all playlists. Paginated, 20 per page. Default page is 0. | `.pls 1` `.deleteplaylist` `.delpls` | Deletes a saved playlist. Works only if you made it or if you are the bot owner. | `.delpls animu-5` `.save` | Saves a playlist under a certain name. Playlist name must be no longer than 20 characters and must not contain dashes. | `.save classical1` @@ -440,6 +424,7 @@ Commands and aliases | Description | Usage `.repeatremove` `.reprm` | Removes a repeating message on a specified index. Use `.repeatlist` to see indexes. **Requires ManageMessages server permission.** | `.reprm 2` `.repeat` | Repeat a message every `X` minutes in the current channel. You can instead specify time of day for the message to be repeated at daily (make sure you've set your server's timezone). You can have up to 5 repeating messages on the server in total. **Requires ManageMessages server permission.** | `.repeat 5 Hello there` or `.repeat 17:30 tea time` `.repeatlist` `.replst` | Shows currently repeating messages and their indexes. **Requires ManageMessages server permission.** | `.repeatlist` +`.streamrole` | Sets a role which is monitored for streamers (FromRole), and a role to add if a user from 'FromRole' is streaming (AddRole). When a user from 'FromRole' starts streaming, they will receive an 'AddRole'. Provide no arguments to disable **Requires ManageRoles server permission.** | `.streamrole "Eligible Streamers" "Featured Streams"` `.convertlist` | List of the convertible dimensions and currencies. | `.convertlist` `.convert` | Convert quantities. Use `.convertlist` to see supported dimensions and currencies. | `.convert m km 1000` `.verboseerror` `.ve` | Toggles whether the bot should print command errors when a command is incorrectly used. **Requires ManageMessages server permission.** | `.ve` diff --git a/src/NadekoBot/Resources/CommandStrings.resx b/src/NadekoBot/Resources/CommandStrings.resx index b7625645..53e5b1a9 100644 --- a/src/NadekoBot/Resources/CommandStrings.resx +++ b/src/NadekoBot/Resources/CommandStrings.resx @@ -1480,7 +1480,7 @@ stop s - Stops the music and clears the playlist. Stays in the channel. + Stops the music and preserves the current song index. Stays in the channel. `{0}s` @@ -1642,7 +1642,7 @@ songremove srm - Remove a song by its # in the queue, or 'all' to remove all songs from the queue. + Remove a song by its # in the queue, or 'all' to remove all songs from the queue and reset the song index. `{0}srm 5` diff --git a/src/NadekoBot/ShardsCoordinator.cs b/src/NadekoBot/ShardsCoordinator.cs index 9ab563af..72ed349c 100644 --- a/src/NadekoBot/ShardsCoordinator.cs +++ b/src/NadekoBot/ShardsCoordinator.cs @@ -12,7 +12,7 @@ namespace NadekoBot public class ShardsCoordinator { private readonly BotCredentials _creds; - private readonly Process[] ShardProcesses; + private readonly Process[] _shardProcesses; public ShardComMessage[] Statuses { get; } public int GuildCount => Statuses.ToArray() .Where(x => x != null) @@ -27,7 +27,7 @@ namespace NadekoBot { LogSetup.SetupLogger(); _creds = new BotCredentials(); - ShardProcesses = new Process[_creds.TotalShards]; + _shardProcesses = new Process[_creds.TotalShards]; Statuses = new ShardComMessage[_creds.TotalShards]; _log = LogManager.GetCurrentClassLogger(); _port = port; @@ -72,39 +72,9 @@ namespace NadekoBot { _log.Error(ex); } - //await Task.Run(() => - //{ - // string input; - // while ((input = Console.ReadLine()?.ToLowerInvariant()) != "quit") - // { - // try - // { - // switch (input) - // { - // case "ls": - // var groupStr = string.Join(",", Statuses - // .ToArray() - // .Where(x => x != null) - // .GroupBy(x => x.ConnectionState) - // .Select(x => x.Count() + " " + x.Key)); - // _log.Info(string.Join("\n", Statuses - // .ToArray() - // .Where(x => x != null) - // .Select(x => $"Shard {x.ShardId} is in {x.ConnectionState.ToString()} state with {x.Guilds} servers. {(DateTime.UtcNow - x.Time).ToString(@"hh\:mm\:ss")} ago")) + "\n" + groupStr); - // break; - // default: - // break; - // } - // } - // catch (Exception ex) - // { - // _log.Warn(ex); - // } - // } - //}); await Task.Delay(-1); - foreach (var p in ShardProcesses) + foreach (var p in _shardProcesses) { try { p.Kill(); } catch { } try { p.Dispose(); } catch { } From 661d0269733f6d2b775059fa503bca16f2b30109 Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Tue, 18 Jul 2017 18:26:55 +0200 Subject: [PATCH 196/346] Updated imagesharp --- .../Modules/Gambling/DiceRollCommands.cs | 19 ++++++++++--------- .../Modules/Gambling/DrawCommands.cs | 7 ++++--- .../Modules/Gambling/FlipCoinCommands.cs | 7 ++++--- .../Modules/Gambling/SlotCommands.cs | 9 +++++---- .../Modules/Games/Common/GirlRating.cs | 7 ++++--- src/NadekoBot/Modules/Searches/Searches.cs | 12 ++++++------ src/NadekoBot/Modules/Utility/Utility.cs | 6 +++--- src/NadekoBot/NadekoBot.csproj | 4 ++-- src/NadekoBot/_Extensions/Extensions.cs | 10 ++++++---- 9 files changed, 44 insertions(+), 37 deletions(-) diff --git a/src/NadekoBot/Modules/Gambling/DiceRollCommands.cs b/src/NadekoBot/Modules/Gambling/DiceRollCommands.cs index 274f310c..cb2c3b3c 100644 --- a/src/NadekoBot/Modules/Gambling/DiceRollCommands.cs +++ b/src/NadekoBot/Modules/Gambling/DiceRollCommands.cs @@ -11,6 +11,7 @@ using System.Threading.Tasks; using NadekoBot.Common; using NadekoBot.Common.Attributes; using Image = ImageSharp.Image; +using ImageSharp; namespace NadekoBot.Modules.Gambling { @@ -19,8 +20,8 @@ namespace NadekoBot.Modules.Gambling [Group] public class DriceRollCommands : NadekoSubmodule { - private Regex dndRegex { get; } = new Regex(@"^(?\d+)d(?\d+)(?:\+(?\d+))?(?:\-(?\d+))?$", RegexOptions.Compiled); - private Regex fudgeRegex { get; } = new Regex(@"^(?\d+)d(?:F|f)$", RegexOptions.Compiled); + private readonly Regex dndRegex = new Regex(@"^(?\d+)d(?\d+)(?:\+(?\d+))?(?:\-(?\d+))?$", RegexOptions.Compiled); + private readonly Regex fudgeRegex = new Regex(@"^(?\d+)d(?:F|f)$", RegexOptions.Compiled); private readonly char[] _fateRolls = { '-', ' ', '+' }; private readonly IImagesService _images; @@ -42,7 +43,7 @@ namespace NadekoBot.Modules.Gambling var imageStream = await Task.Run(() => { var ms = new MemoryStream(); - new[] { GetDice(num1), GetDice(num2) }.Merge().Save(ms); + new[] { GetDice(num1), GetDice(num2) }.Merge().SaveAsPng(ms); ms.Position = 0; return ms; }).ConfigureAwait(false); @@ -97,7 +98,7 @@ namespace NadekoBot.Modules.Gambling var rng = new NadekoRandom(); - var dice = new List(num); + var dice = new List>(num); var values = new List(num); for (var i = 0; i < num; i++) { @@ -127,7 +128,7 @@ namespace NadekoBot.Modules.Gambling var bitmap = dice.Merge(); var ms = new MemoryStream(); - bitmap.Save(ms); + bitmap.SaveAsPng(ms); ms.Position = 0; await Context.Channel.SendFileAsync(ms, "dice.png", Context.User.Mention + " " + @@ -213,7 +214,7 @@ namespace NadekoBot.Modules.Gambling await ReplyConfirmLocalized("dice_rolled", Format.Bold(rolled.ToString())).ConfigureAwait(false); } - private Image GetDice(int num) + private Image GetDice(int num) { if (num < 0 || num > 10) throw new ArgumentOutOfRangeException(nameof(num)); @@ -224,15 +225,15 @@ namespace NadekoBot.Modules.Gambling using (var imgOneStream = images[1].ToStream()) using (var imgZeroStream = images[0].ToStream()) { - Image imgOne = new Image(imgOneStream); - Image imgZero = new Image(imgZeroStream); + var imgOne = Image.Load(imgOneStream); + var imgZero = Image.Load(imgZeroStream); return new[] { imgOne, imgZero }.Merge(); } } using (var die = _images.Dice[num].ToStream()) { - return new Image(die); + return Image.Load(die); } } } diff --git a/src/NadekoBot/Modules/Gambling/DrawCommands.cs b/src/NadekoBot/Modules/Gambling/DrawCommands.cs index b2e5d636..72bff0f2 100644 --- a/src/NadekoBot/Modules/Gambling/DrawCommands.cs +++ b/src/NadekoBot/Modules/Gambling/DrawCommands.cs @@ -9,6 +9,7 @@ using System.Threading.Tasks; using NadekoBot.Common.Attributes; using NadekoBot.Modules.Gambling.Common; using Image = ImageSharp.Image; +using ImageSharp; namespace NadekoBot.Modules.Gambling { @@ -27,7 +28,7 @@ namespace NadekoBot.Modules.Gambling throw new ArgumentOutOfRangeException(nameof(num)); Cards cards = guildId == null ? new Cards() : _allDecks.GetOrAdd(Context.Guild, (s) => new Cards()); - var images = new List(); + var images = new List>(); var cardObjects = new List(); for (var i = 0; i < num; i++) { @@ -46,10 +47,10 @@ namespace NadekoBot.Modules.Gambling var currentCard = cards.DrawACard(); cardObjects.Add(currentCard); using (var stream = File.OpenRead(Path.Combine(_cardsPath, currentCard.ToString().ToLowerInvariant() + ".jpg").Replace(' ', '_'))) - images.Add(new Image(stream)); + images.Add(Image.Load(stream)); } MemoryStream bitmapStream = new MemoryStream(); - images.Merge().Save(bitmapStream); + images.Merge().SaveAsPng(bitmapStream); bitmapStream.Position = 0; var toSend = $"{Context.User.Mention}"; diff --git a/src/NadekoBot/Modules/Gambling/FlipCoinCommands.cs b/src/NadekoBot/Modules/Gambling/FlipCoinCommands.cs index c89a175c..21ff1411 100644 --- a/src/NadekoBot/Modules/Gambling/FlipCoinCommands.cs +++ b/src/NadekoBot/Modules/Gambling/FlipCoinCommands.cs @@ -9,6 +9,7 @@ using System.Threading.Tasks; using NadekoBot.Common; using NadekoBot.Common.Attributes; using Image = ImageSharp.Image; +using ImageSharp; namespace NadekoBot.Modules.Gambling { @@ -56,7 +57,7 @@ namespace NadekoBot.Modules.Gambling await ReplyErrorLocalized("flip_invalid", 10).ConfigureAwait(false); return; } - var imgs = new Image[count]; + var imgs = new Image[count]; for (var i = 0; i < count; i++) { using (var heads = _images.Heads.ToStream()) @@ -64,11 +65,11 @@ namespace NadekoBot.Modules.Gambling { if (rng.Next(0, 10) < 5) { - imgs[i] = new Image(heads); + imgs[i] = Image.Load(heads); } else { - imgs[i] = new Image(tails); + imgs[i] = Image.Load(tails); } } } diff --git a/src/NadekoBot/Modules/Gambling/SlotCommands.cs b/src/NadekoBot/Modules/Gambling/SlotCommands.cs index f4d2e43b..c51dc2c8 100644 --- a/src/NadekoBot/Modules/Gambling/SlotCommands.cs +++ b/src/NadekoBot/Modules/Gambling/SlotCommands.cs @@ -12,6 +12,7 @@ using System.Threading; using System.Threading.Tasks; using NadekoBot.Common; using NadekoBot.Common.Attributes; +using SixLabors.Primitives; namespace NadekoBot.Modules.Gambling { @@ -166,7 +167,7 @@ namespace NadekoBot.Modules.Gambling Interlocked.Add(ref _totalBet, amount); using (var bgFileStream = _images.SlotBackground.ToStream()) { - var bgImage = new ImageSharp.Image(bgFileStream); + var bgImage = ImageSharp.Image.Load(bgFileStream); var result = SlotMachine.Pull(); int[] numbers = result.Numbers; @@ -174,7 +175,7 @@ namespace NadekoBot.Modules.Gambling for (int i = 0; i < 3; i++) { using (var file = _images.SlotEmojis[numbers[i]].ToStream()) - using (var randomImage = new ImageSharp.Image(file)) + using (var randomImage = ImageSharp.Image.Load(file)) { bgImage.DrawImage(randomImage, 100, default(Size), new Point(95 + 142 * i, 330)); } @@ -187,7 +188,7 @@ namespace NadekoBot.Modules.Gambling { var digit = printWon % 10; using (var fs = _images.SlotNumbers[digit].ToStream()) - using (var img = new ImageSharp.Image(fs)) + using (var img = ImageSharp.Image.Load(fs)) { bgImage.DrawImage(img, 100, default(Size), new Point(230 - n * 16, 462)); } @@ -200,7 +201,7 @@ namespace NadekoBot.Modules.Gambling { var digit = printAmount % 10; using (var fs = _images.SlotNumbers[digit].ToStream()) - using (var img = new ImageSharp.Image(fs)) + using (var img = ImageSharp.Image.Load(fs)) { bgImage.DrawImage(img, 100, default(Size), new Point(395 - n * 16, 462)); } diff --git a/src/NadekoBot/Modules/Games/Common/GirlRating.cs b/src/NadekoBot/Modules/Games/Common/GirlRating.cs index 9e7a12cb..d605be0d 100644 --- a/src/NadekoBot/Modules/Games/Common/GirlRating.cs +++ b/src/NadekoBot/Modules/Games/Common/GirlRating.cs @@ -7,6 +7,7 @@ using NadekoBot.Common; using NadekoBot.Extensions; using NadekoBot.Services; using NLog; +using SixLabors.Primitives; namespace NadekoBot.Modules.Games.Common { @@ -32,7 +33,7 @@ namespace NadekoBot.Modules.Games.Common try { using (var ms = new MemoryStream(_images.WifeMatrix.ToArray(), false)) - using (var img = new ImageSharp.Image(ms)) + using (var img = Image.Load(ms)) { const int minx = 35; const int miny = 385; @@ -42,7 +43,7 @@ namespace NadekoBot.Modules.Games.Common var pointy = (int)(miny - length * ((Crazy - 4) / 6)); using (var pointMs = new MemoryStream(_images.RategirlDot.ToArray(), false)) - using (var pointImg = new ImageSharp.Image(pointMs)) + using (var pointImg = Image.Load(pointMs)) { img.DrawImage(pointImg, 100, default(Size), new Point(pointx - 10, pointy - 10)); } @@ -51,7 +52,7 @@ namespace NadekoBot.Modules.Games.Common using (var http = new HttpClient()) using (var imgStream = new MemoryStream()) { - img.Save(imgStream); + img.SaveAsPng(imgStream); var byteContent = new ByteArrayContent(imgStream.ToArray()); http.AddFakeHeaders(); diff --git a/src/NadekoBot/Modules/Searches/Searches.cs b/src/NadekoBot/Modules/Searches/Searches.cs index a0733429..eadcd2c7 100644 --- a/src/NadekoBot/Modules/Searches/Searches.cs +++ b/src/NadekoBot/Modules/Searches/Searches.cs @@ -390,7 +390,7 @@ namespace NadekoBot.Modules.Searches try { var items = JArray.Parse(response).Shuffle().ToList(); - var images = new List(); + var images = new List>(); if (items == null) throw new KeyNotFoundException("Cannot find a card by that name"); foreach (var item in items.Where(item => item.HasValues && item["img"] != null).Take(4)) @@ -402,7 +402,7 @@ namespace NadekoBot.Modules.Searches var imgStream = new MemoryStream(); await sr.CopyToAsync(imgStream); imgStream.Position = 0; - images.Add(new ImageSharp.Image(imgStream)); + images.Add(ImageSharp.Image.Load(imgStream)); } }).ConfigureAwait(false); } @@ -412,7 +412,7 @@ namespace NadekoBot.Modules.Searches msg = GetText("hs_over_x", 4); } var ms = new MemoryStream(); - await Task.Run(() => images.AsEnumerable().Merge().Save(ms)); + await Task.Run(() => images.AsEnumerable().Merge().SaveAsPng(ms)); ms.Position = 0; await Context.Channel.SendFileAsync(ms, arg + ".png", msg).ConfigureAwait(false); } @@ -634,10 +634,10 @@ namespace NadekoBot.Modules.Searches color = color?.Trim().Replace("#", ""); if (string.IsNullOrWhiteSpace(color)) return; - ImageSharp.Color clr; + Rgba32 clr; try { - clr = ImageSharp.Color.FromHex(color); + clr = Rgba32.FromHex(color); } catch { @@ -646,7 +646,7 @@ namespace NadekoBot.Modules.Searches } - var img = new ImageSharp.Image(50, 50); + var img = new ImageSharp.Image(50, 50); img.BackgroundColor(clr); diff --git a/src/NadekoBot/Modules/Utility/Utility.cs b/src/NadekoBot/Modules/Utility/Utility.cs index bd3b66f7..1bb1b678 100644 --- a/src/NadekoBot/Modules/Utility/Utility.cs +++ b/src/NadekoBot/Modules/Utility/Utility.cs @@ -59,10 +59,10 @@ namespace NadekoBot.Modules.Utility } return; } - + var hexColors = hexes.Select(hex => { - try { return (ImageSharp.Color?)ImageSharp.Color.FromHex(hex.Replace("#", "")); } catch { return null; } + try { return (Rgba32?)Rgba32.FromHex(hex.Replace("#", "")); } catch { return null; } }) .Where(c => c != null) .Select(c => c.Value) @@ -76,7 +76,7 @@ namespace NadekoBot.Modules.Utility var images = hexColors.Select(color => { - var img = new ImageSharp.Image(50, 50); + var img = new ImageSharp.Image(50, 50); img.BackgroundColor(color); return img; }).Merge().ToStream(); diff --git a/src/NadekoBot/NadekoBot.csproj b/src/NadekoBot/NadekoBot.csproj index 07193a3c..810fa907 100644 --- a/src/NadekoBot/NadekoBot.csproj +++ b/src/NadekoBot/NadekoBot.csproj @@ -64,8 +64,8 @@ - - + + diff --git a/src/NadekoBot/_Extensions/Extensions.cs b/src/NadekoBot/_Extensions/Extensions.cs index 14cde12e..b7d95c69 100644 --- a/src/NadekoBot/_Extensions/Extensions.cs +++ b/src/NadekoBot/_Extensions/Extensions.cs @@ -15,6 +15,8 @@ using System.Text; using System.Text.RegularExpressions; using System.Threading.Tasks; using NadekoBot.Common.Collections; +using SixLabors.Primitives; +using ImageSharp.PixelFormats; namespace NadekoBot.Extensions { @@ -131,10 +133,10 @@ namespace NadekoBot.Extensions public static string ToJson(this T any, Formatting formatting = Formatting.Indented) => JsonConvert.SerializeObject(any, formatting); - public static Stream ToStream(this ImageSharp.Image img) + public static Stream ToStream(this ImageSharp.Image img) { var imageStream = new MemoryStream(); - img.Save(imageStream); + img.SaveAsPng(imageStream); imageStream.Position = 0; return imageStream; } @@ -198,11 +200,11 @@ namespace NadekoBot.Extensions return await ownerPrivate.SendMessageAsync(message).ConfigureAwait(false); } - public static ImageSharp.Image Merge(this IEnumerable images) + public static Image Merge(this IEnumerable> images) { var imgs = images.ToArray(); - var canvas = new ImageSharp.Image(imgs.Sum(img => img.Width), imgs.Max(img => img.Height)); + var canvas = new Image(imgs.Sum(img => img.Width), imgs.Max(img => img.Height)); var xOffset = 0; for (int i = 0; i < imgs.Length; i++) From 86743250cfa68b718a4fd237b1d414b27fd5cfb5 Mon Sep 17 00:00:00 2001 From: Deivedux Date: Tue, 18 Jul 2017 21:29:08 +0300 Subject: [PATCH 197/346] Update Upgrading Guide.md Windows part of the upgrading guide is now more straightforward. --- docs/guides/Upgrading Guide.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/guides/Upgrading Guide.md b/docs/guides/Upgrading Guide.md index 70fc6e80..f853267c 100644 --- a/docs/guides/Upgrading Guide.md +++ b/docs/guides/Upgrading Guide.md @@ -1,9 +1,9 @@ #### If you have NadekoBot 1.x on Windows +- Go to `NadekoBot\src\NadekoBot` and backup your `credentials.json` file; then go to `NadekoBot\src\NadekoBot\bin\Release\netcoreapp1.0` and backup your `data` folder. - Follow the [Windows Guide](http://nadekobot.readthedocs.io/en/latest/guides/Windows%20Guide/) and install the latest version of **NadekoBot**. -- Navigate to your **old** `Nadeko` folder and copy your `credentials.json` file and the `data` folder. -- Paste credentials into the **NadekoBot 1.4x+** `C:\Program Files\NadekoBot\system` folder. -- Paste your **old** `Nadeko` data folder into **NadekoBot 1.4x+** `C:\Program Files\NadekoBot\system` folder. +- Paste your `credentials.json` file into the `C:\Program Files\NadekoBot\system` folder. +- Paste your `data` folder into `C:\Program Files\NadekoBot\system` folder. - If it asks you to overwrite files, it is fine to do so. - Next launch your **new** Nadeko as the guide describes, if it is not already running. @@ -27,4 +27,4 @@ - **For Ubuntu, Debian and CentOS Users Only:** use the option `4. Auto-Install Prerequisites` to install the latest version of .NET Core SDK. - Use option `1. Download NadekoBot` to update your NadekoBot to 1.4.x. - Next, just [run your NadekoBot.](http://nadekobot.readthedocs.io/en/latest/guides/Linux%20Guide/#running-nadekobot) -- *NOTE: 1.4.x uses `NadekoBot.db` file from `NadekoBot/src/NadekoBot/bin/Release/netcoreapp1.1/data` folder.* \ No newline at end of file +- *NOTE: 1.4.x uses `NadekoBot.db` file from `NadekoBot/src/NadekoBot/bin/Release/netcoreapp1.1/data` folder.* From 8dd24443c0b64e9dd5b6c0d408bf4764bfda0e4c Mon Sep 17 00:00:00 2001 From: Deivedux Date: Tue, 18 Jul 2017 21:30:27 +0300 Subject: [PATCH 198/346] Update Windows Guide.md Added missing installation part for music features. --- docs/guides/Windows Guide.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/guides/Windows Guide.md b/docs/guides/Windows Guide.md index e0a751f7..fe344a96 100644 --- a/docs/guides/Windows Guide.md +++ b/docs/guides/Windows Guide.md @@ -7,7 +7,7 @@ #### Guide - Download and run the [NadekoBot Updater.][Updater] -- Press **`Install ffmpeg`** button if you want music features. +- Press **`Install ffmpeg`** and **`Install youtube-dl`** if you want music features. ***NOTE:** RESTART YOUR PC IF YOU DO.* - Press **`Update`** and go through the installation wizard. ***NOTE:** If you're upgrading from 1.3, DO NOT select your old nadekobot folder. Install it in a separate directory and read the [upgrading guide](http://nadekobot.readthedocs.io/en/latest/guides/Upgrading%20Guide/).* From 50236d71d5f97eb3ca7ca089b34c0703f8028fe2 Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Wed, 19 Jul 2017 09:31:00 +0200 Subject: [PATCH 199/346] Fixed multi-shard bots --- src/NadekoBot/Modules/Utility/Utility.cs | 4 ++-- src/NadekoBot/NadekoBot.cs | 1 - src/NadekoBot/Services/Impl/StatsService.cs | 10 +++++----- 3 files changed, 7 insertions(+), 8 deletions(-) diff --git a/src/NadekoBot/Modules/Utility/Utility.cs b/src/NadekoBot/Modules/Utility/Utility.cs index 1bb1b678..5ee6de70 100644 --- a/src/NadekoBot/Modules/Utility/Utility.cs +++ b/src/NadekoBot/Modules/Utility/Utility.cs @@ -30,12 +30,12 @@ namespace NadekoBot.Modules.Utility private readonly IBotCredentials _creds; private readonly ShardsCoordinator _shardCoord; - public Utility(ShardsCoordinator shardCoord, DiscordSocketClient client, IStatsService stats, IBotCredentials creds) + public Utility(NadekoBot nadeko, DiscordSocketClient client, IStatsService stats, IBotCredentials creds) { _client = client; _stats = stats; _creds = creds; - _shardCoord = shardCoord; + _shardCoord = nadeko.ShardCoord; } [NadekoCommand, Usage, Description, Aliases] diff --git a/src/NadekoBot/NadekoBot.cs b/src/NadekoBot/NadekoBot.cs index 9c10bd96..8870820e 100644 --- a/src/NadekoBot/NadekoBot.cs +++ b/src/NadekoBot/NadekoBot.cs @@ -137,7 +137,6 @@ namespace NadekoBot .AddManual>(AllGuildConfigs) //todo wrap this .AddManual(this) .AddManual(uow) - .AddManual(ShardCoord) .LoadFrom(Assembly.GetEntryAssembly()) .Build(); diff --git a/src/NadekoBot/Services/Impl/StatsService.cs b/src/NadekoBot/Services/Impl/StatsService.cs index 1065cf2a..eb82d0ce 100644 --- a/src/NadekoBot/Services/Impl/StatsService.cs +++ b/src/NadekoBot/Services/Impl/StatsService.cs @@ -44,11 +44,11 @@ namespace NadekoBot.Services.Impl public int GuildCount => _sc?.GuildCount ?? _client.Guilds.Count(); - public StatsService(DiscordSocketClient client, CommandHandler cmdHandler, IBotCredentials creds, ShardsCoordinator sc) + public StatsService(DiscordSocketClient client, CommandHandler cmdHandler, IBotCredentials creds, NadekoBot nadeko) { _client = client; _creds = creds; - _sc = sc; + _sc = nadeko.ShardCoord; _started = DateTime.UtcNow; _client.MessageReceived += _ => Task.FromResult(Interlocked.Increment(ref _messageCounter)); @@ -130,7 +130,7 @@ namespace NadekoBot.Services.Impl return Task.CompletedTask; }; - if (sc != null) + if (_sc != null) { _carbonitexTimer = new Timer(async (state) => { @@ -142,7 +142,7 @@ namespace NadekoBot.Services.Impl { using (var content = new FormUrlEncodedContent( new Dictionary { - { "servercount", sc.GuildCount.ToString() }, + { "servercount", _sc.GuildCount.ToString() }, { "key", _creds.CarbonKey }})) { content.Headers.Clear(); @@ -175,7 +175,7 @@ namespace NadekoBot.Services.Impl using (var content = new FormUrlEncodedContent( new Dictionary { { "id", string.Concat(MD5.Create().ComputeHash(Encoding.ASCII.GetBytes(_creds.ClientId.ToString())).Select(x => x.ToString("X2"))) }, - { "guildCount", sc.GuildCount.ToString() }, + { "guildCount", _sc.GuildCount.ToString() }, { "version", BotVersion }, { "platform", platform }})) { From 4130317f40f84dff1986a8873bb648232e3e56a1 Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Wed, 19 Jul 2017 10:38:14 +0200 Subject: [PATCH 200/346] .srkw, .srwl and .srbl commands added. --- src/NadekoBot/Common/TypeReaders/AddRemove.cs | 9 + ...0719023924_streamrole-kw-bl-wl.Designer.cs | 1655 +++++++++++++++++ .../20170719023924_streamrole-kw-bl-wl.cs | 94 + .../NadekoSqliteContextModelSnapshot.cs | 58 + .../Modules/Permissions/BlacklistCommands.cs | 8 +- .../Exceptions/StreamRoleNotFoundException.cs | 15 + .../StreamRolePermissionException.cs | 15 + .../Utility/Common/StreamRoleListType.cs | 8 + .../Extensions/StreamRoleExtensions.cs | 27 + .../Utility/Services/StreamRoleService.cs | 260 ++- .../Modules/Utility/StreamRoleCommands.cs | 60 +- src/NadekoBot/NadekoBot.csproj | 4 + src/NadekoBot/Resources/CommandStrings.resx | 27 + .../Services/Database/Models/GuildConfig.cs | 2 +- .../Database/Models/StreamRoleSettings.cs | 64 + .../_strings/ResponseStrings.en-US.json | 10 + 16 files changed, 2240 insertions(+), 76 deletions(-) create mode 100644 src/NadekoBot/Common/TypeReaders/AddRemove.cs create mode 100644 src/NadekoBot/Migrations/20170719023924_streamrole-kw-bl-wl.Designer.cs create mode 100644 src/NadekoBot/Migrations/20170719023924_streamrole-kw-bl-wl.cs create mode 100644 src/NadekoBot/Modules/Utility/Common/Exceptions/StreamRoleNotFoundException.cs create mode 100644 src/NadekoBot/Modules/Utility/Common/Exceptions/StreamRolePermissionException.cs create mode 100644 src/NadekoBot/Modules/Utility/Common/StreamRoleListType.cs create mode 100644 src/NadekoBot/Modules/Utility/Extensions/StreamRoleExtensions.cs diff --git a/src/NadekoBot/Common/TypeReaders/AddRemove.cs b/src/NadekoBot/Common/TypeReaders/AddRemove.cs new file mode 100644 index 00000000..6bd3ea7c --- /dev/null +++ b/src/NadekoBot/Common/TypeReaders/AddRemove.cs @@ -0,0 +1,9 @@ +namespace NadekoBot.Common.TypeReaders +{ + public enum AddRemove + { + Add = 0, + Rem = 1, + Rm = 1, + } +} diff --git a/src/NadekoBot/Migrations/20170719023924_streamrole-kw-bl-wl.Designer.cs b/src/NadekoBot/Migrations/20170719023924_streamrole-kw-bl-wl.Designer.cs new file mode 100644 index 00000000..222865ec --- /dev/null +++ b/src/NadekoBot/Migrations/20170719023924_streamrole-kw-bl-wl.Designer.cs @@ -0,0 +1,1655 @@ +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using NadekoBot.Services.Database; +using NadekoBot.Services.Database.Models; + +namespace NadekoBot.Migrations +{ + [DbContext(typeof(NadekoContext))] + [Migration("20170719023924_streamrole-kw-bl-wl")] + partial class streamrolekwblwl + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { + modelBuilder + .HasAnnotation("ProductVersion", "1.1.1"); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.AntiRaidSetting", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("Action"); + + b.Property("DateAdded"); + + b.Property("GuildConfigId"); + + b.Property("Seconds"); + + b.Property("UserThreshold"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId") + .IsUnique(); + + b.ToTable("AntiRaidSetting"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.AntiSpamIgnore", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AntiSpamSettingId"); + + b.Property("ChannelId"); + + b.Property("DateAdded"); + + b.HasKey("Id"); + + b.HasIndex("AntiSpamSettingId"); + + b.ToTable("AntiSpamIgnore"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.AntiSpamSetting", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("Action"); + + b.Property("DateAdded"); + + b.Property("GuildConfigId"); + + b.Property("MessageThreshold"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId") + .IsUnique(); + + b.ToTable("AntiSpamSetting"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.BlacklistItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("BotConfigId"); + + b.Property("DateAdded"); + + b.Property("ItemId"); + + b.Property("Type"); + + b.HasKey("Id"); + + b.HasIndex("BotConfigId"); + + b.ToTable("BlacklistItem"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.BlockedCmdOrMdl", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("BotConfigId"); + + b.Property("BotConfigId1"); + + b.Property("DateAdded"); + + b.Property("Name"); + + b.HasKey("Id"); + + b.HasIndex("BotConfigId"); + + b.HasIndex("BotConfigId1"); + + b.ToTable("BlockedCmdOrMdl"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.BotConfig", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("BetflipMultiplier"); + + b.Property("Betroll100Multiplier"); + + b.Property("Betroll67Multiplier"); + + b.Property("Betroll91Multiplier"); + + b.Property("BufferSize"); + + b.Property("CurrencyDropAmount"); + + b.Property("CurrencyDropAmountMax"); + + b.Property("CurrencyGenerationChance"); + + b.Property("CurrencyGenerationCooldown"); + + b.Property("CurrencyName"); + + b.Property("CurrencyPluralName"); + + b.Property("CurrencySign"); + + b.Property("CustomReactionsStartWith"); + + b.Property("DMHelpString"); + + b.Property("DateAdded"); + + b.Property("DefaultPrefix"); + + b.Property("ErrorColor"); + + b.Property("ForwardMessages"); + + b.Property("ForwardToAllOwners"); + + b.Property("HelpString"); + + b.Property("Locale"); + + b.Property("MigrationVersion"); + + b.Property("MinimumBetAmount"); + + b.Property("OkColor"); + + b.Property("PermissionVersion"); + + b.Property("RemindMessageFormat"); + + b.Property("RotatingStatuses"); + + b.Property("TriviaCurrencyReward"); + + b.HasKey("Id"); + + b.ToTable("BotConfig"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.ClashCaller", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("BaseDestroyed"); + + b.Property("CallUser"); + + b.Property("ClashWarId"); + + b.Property("DateAdded"); + + b.Property("SequenceNumber"); + + b.Property("Stars"); + + b.Property("TimeAdded"); + + b.HasKey("Id"); + + b.HasIndex("ClashWarId"); + + b.ToTable("ClashCallers"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.ClashWar", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ChannelId"); + + b.Property("DateAdded"); + + b.Property("EnemyClan"); + + b.Property("GuildId"); + + b.Property("Size"); + + b.Property("StartedAt"); + + b.Property("WarState"); + + b.HasKey("Id"); + + b.ToTable("ClashOfClans"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.CommandAlias", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("GuildConfigId"); + + b.Property("Mapping"); + + b.Property("Trigger"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.ToTable("CommandAlias"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.CommandCooldown", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("CommandName"); + + b.Property("DateAdded"); + + b.Property("GuildConfigId"); + + b.Property("Seconds"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.ToTable("CommandCooldown"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.CommandPrice", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("BotConfigId"); + + b.Property("CommandName"); + + b.Property("DateAdded"); + + b.Property("Price"); + + b.HasKey("Id"); + + b.HasIndex("BotConfigId"); + + b.HasIndex("Price") + .IsUnique(); + + b.ToTable("CommandPrice"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.ConvertUnit", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("InternalTrigger"); + + b.Property("Modifier"); + + b.Property("UnitType"); + + b.HasKey("Id"); + + b.ToTable("ConversionUnits"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.Currency", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("Amount"); + + b.Property("DateAdded"); + + b.Property("UserId"); + + b.HasKey("Id"); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("Currency"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.CurrencyTransaction", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("Amount"); + + b.Property("DateAdded"); + + b.Property("Reason"); + + b.Property("UserId"); + + b.HasKey("Id"); + + b.ToTable("CurrencyTransactions"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.CustomReaction", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AutoDeleteTrigger"); + + b.Property("DateAdded"); + + b.Property("DmResponse"); + + b.Property("GuildId"); + + b.Property("IsRegex"); + + b.Property("OwnerOnly"); + + b.Property("Response"); + + b.Property("Trigger"); + + b.HasKey("Id"); + + b.ToTable("CustomReactions"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.DiscordUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AvatarId"); + + b.Property("DateAdded"); + + b.Property("Discriminator"); + + b.Property("UserId"); + + b.Property("Username"); + + b.HasKey("Id"); + + b.HasAlternateKey("UserId"); + + b.ToTable("DiscordUser"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.Donator", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("Amount"); + + b.Property("DateAdded"); + + b.Property("Name"); + + b.Property("UserId"); + + b.HasKey("Id"); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("Donators"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.EightBallResponse", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("BotConfigId"); + + b.Property("DateAdded"); + + b.Property("Text"); + + b.HasKey("Id"); + + b.HasIndex("BotConfigId"); + + b.ToTable("EightBallResponses"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.FilterChannelId", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ChannelId"); + + b.Property("DateAdded"); + + b.Property("GuildConfigId"); + + b.Property("GuildConfigId1"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.HasIndex("GuildConfigId1"); + + b.ToTable("FilterChannelId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.FilteredWord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("GuildConfigId"); + + b.Property("Word"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.ToTable("FilteredWord"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.FollowedStream", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ChannelId"); + + b.Property("DateAdded"); + + b.Property("GuildConfigId"); + + b.Property("GuildId"); + + b.Property("Type"); + + b.Property("Username"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.ToTable("FollowedStream"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.GCChannelId", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ChannelId"); + + b.Property("DateAdded"); + + b.Property("GuildConfigId"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.ToTable("GCChannelId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.GuildConfig", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AutoAssignRoleId"); + + b.Property("AutoDeleteByeMessages"); + + b.Property("AutoDeleteByeMessagesTimer"); + + b.Property("AutoDeleteGreetMessages"); + + b.Property("AutoDeleteGreetMessagesTimer"); + + b.Property("AutoDeleteSelfAssignedRoleMessages"); + + b.Property("ByeMessageChannelId"); + + b.Property("ChannelByeMessageText"); + + b.Property("ChannelGreetMessageText"); + + b.Property("CleverbotEnabled"); + + b.Property("DateAdded"); + + b.Property("DefaultMusicVolume"); + + b.Property("DeleteMessageOnCommand"); + + b.Property("DmGreetMessageText"); + + b.Property("ExclusiveSelfAssignedRoles"); + + b.Property("FilterInvites"); + + b.Property("FilterWords"); + + b.Property("GameVoiceChannel"); + + b.Property("GreetMessageChannelId"); + + b.Property("GuildId"); + + b.Property("Locale"); + + b.Property("LogSettingId"); + + b.Property("MuteRoleName"); + + b.Property("PermissionRole"); + + b.Property("Prefix"); + + b.Property("RootPermissionId"); + + b.Property("SendChannelByeMessage"); + + b.Property("SendChannelGreetMessage"); + + b.Property("SendDmGreetMessage"); + + b.Property("TimeZoneId"); + + b.Property("VerboseErrors"); + + b.Property("VerbosePermissions"); + + b.Property("VoicePlusTextEnabled"); + + b.Property("WarningsInitialized"); + + b.HasKey("Id"); + + b.HasIndex("GuildId") + .IsUnique(); + + b.HasIndex("LogSettingId"); + + b.HasIndex("RootPermissionId"); + + b.ToTable("GuildConfigs"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.GuildRepeater", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ChannelId"); + + b.Property("DateAdded"); + + b.Property("GuildConfigId"); + + b.Property("GuildId"); + + b.Property("Interval"); + + b.Property("Message"); + + b.Property("StartTimeOfDay"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.ToTable("GuildRepeater"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.IgnoredLogChannel", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ChannelId"); + + b.Property("DateAdded"); + + b.Property("LogSettingId"); + + b.HasKey("Id"); + + b.HasIndex("LogSettingId"); + + b.ToTable("IgnoredLogChannels"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.IgnoredVoicePresenceChannel", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ChannelId"); + + b.Property("DateAdded"); + + b.Property("LogSettingId"); + + b.HasKey("Id"); + + b.HasIndex("LogSettingId"); + + b.ToTable("IgnoredVoicePresenceCHannels"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.LogSetting", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ChannelCreated"); + + b.Property("ChannelCreatedId"); + + b.Property("ChannelDestroyed"); + + b.Property("ChannelDestroyedId"); + + b.Property("ChannelId"); + + b.Property("ChannelUpdated"); + + b.Property("ChannelUpdatedId"); + + b.Property("DateAdded"); + + b.Property("IsLogging"); + + b.Property("LogOtherId"); + + b.Property("LogUserPresence"); + + b.Property("LogUserPresenceId"); + + b.Property("LogVoicePresence"); + + b.Property("LogVoicePresenceId"); + + b.Property("LogVoicePresenceTTSId"); + + b.Property("MessageDeleted"); + + b.Property("MessageDeletedId"); + + b.Property("MessageUpdated"); + + b.Property("MessageUpdatedId"); + + b.Property("UserBanned"); + + b.Property("UserBannedId"); + + b.Property("UserJoined"); + + b.Property("UserJoinedId"); + + b.Property("UserLeft"); + + b.Property("UserLeftId"); + + b.Property("UserMutedId"); + + b.Property("UserPresenceChannelId"); + + b.Property("UserUnbanned"); + + b.Property("UserUnbannedId"); + + b.Property("UserUpdated"); + + b.Property("UserUpdatedId"); + + b.Property("VoicePresenceChannelId"); + + b.HasKey("Id"); + + b.ToTable("LogSettings"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.ModulePrefix", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("BotConfigId"); + + b.Property("DateAdded"); + + b.Property("ModuleName"); + + b.Property("Prefix"); + + b.HasKey("Id"); + + b.HasIndex("BotConfigId"); + + b.ToTable("ModulePrefixes"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.MusicPlaylist", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("Author"); + + b.Property("AuthorId"); + + b.Property("DateAdded"); + + b.Property("Name"); + + b.HasKey("Id"); + + b.ToTable("MusicPlaylists"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.MutedUserId", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("GuildConfigId"); + + b.Property("UserId"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.ToTable("MutedUserId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.Permission", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("NextId"); + + b.Property("PrimaryTarget"); + + b.Property("PrimaryTargetId"); + + b.Property("SecondaryTarget"); + + b.Property("SecondaryTargetName"); + + b.Property("State"); + + b.HasKey("Id"); + + b.HasIndex("NextId") + .IsUnique(); + + b.ToTable("Permission"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.Permissionv2", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("GuildConfigId"); + + b.Property("Index"); + + b.Property("PrimaryTarget"); + + b.Property("PrimaryTargetId"); + + b.Property("SecondaryTarget"); + + b.Property("SecondaryTargetName"); + + b.Property("State"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.ToTable("Permissionv2"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.PlayingStatus", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("BotConfigId"); + + b.Property("DateAdded"); + + b.Property("Status"); + + b.HasKey("Id"); + + b.HasIndex("BotConfigId"); + + b.ToTable("PlayingStatus"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.PlaylistSong", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("MusicPlaylistId"); + + b.Property("Provider"); + + b.Property("ProviderType"); + + b.Property("Query"); + + b.Property("Title"); + + b.Property("Uri"); + + b.HasKey("Id"); + + b.HasIndex("MusicPlaylistId"); + + b.ToTable("PlaylistSong"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.Quote", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AuthorId"); + + b.Property("AuthorName") + .IsRequired(); + + b.Property("DateAdded"); + + b.Property("GuildId"); + + b.Property("Keyword") + .IsRequired(); + + b.Property("Text") + .IsRequired(); + + b.HasKey("Id"); + + b.ToTable("Quotes"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.RaceAnimal", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("BotConfigId"); + + b.Property("DateAdded"); + + b.Property("Icon"); + + b.Property("Name"); + + b.HasKey("Id"); + + b.HasIndex("BotConfigId"); + + b.ToTable("RaceAnimals"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.Reminder", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ChannelId"); + + b.Property("DateAdded"); + + b.Property("IsPrivate"); + + b.Property("Message"); + + b.Property("ServerId"); + + b.Property("UserId"); + + b.Property("When"); + + b.HasKey("Id"); + + b.ToTable("Reminders"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.RewardedUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AmountRewardedThisMonth"); + + b.Property("DateAdded"); + + b.Property("LastReward"); + + b.Property("PatreonUserId"); + + b.Property("UserId"); + + b.HasKey("Id"); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("RewardedUsers"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.SelfAssignedRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("GuildId"); + + b.Property("RoleId"); + + b.HasKey("Id"); + + b.HasIndex("GuildId", "RoleId") + .IsUnique(); + + b.ToTable("SelfAssignableRoles"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.ShopEntry", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AuthorId"); + + b.Property("DateAdded"); + + b.Property("GuildConfigId"); + + b.Property("Index"); + + b.Property("Name"); + + b.Property("Price"); + + b.Property("RoleId"); + + b.Property("RoleName"); + + b.Property("Type"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.ToTable("ShopEntry"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.ShopEntryItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("ShopEntryId"); + + b.Property("Text"); + + b.HasKey("Id"); + + b.HasIndex("ShopEntryId"); + + b.ToTable("ShopEntryItem"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.SlowmodeIgnoredRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("GuildConfigId"); + + b.Property("RoleId"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.ToTable("SlowmodeIgnoredRole"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.SlowmodeIgnoredUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("GuildConfigId"); + + b.Property("UserId"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.ToTable("SlowmodeIgnoredUser"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.StartupCommand", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("BotConfigId"); + + b.Property("ChannelId"); + + b.Property("ChannelName"); + + b.Property("CommandText"); + + b.Property("DateAdded"); + + b.Property("GuildId"); + + b.Property("GuildName"); + + b.Property("Index"); + + b.Property("VoiceChannelId"); + + b.Property("VoiceChannelName"); + + b.HasKey("Id"); + + b.HasIndex("BotConfigId"); + + b.ToTable("StartupCommand"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.StreamRoleBlacklistedUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("StreamRoleSettingsId"); + + b.Property("UserId"); + + b.Property("Username"); + + b.HasKey("Id"); + + b.HasIndex("StreamRoleSettingsId"); + + b.ToTable("StreamRoleBlacklistedUser"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.StreamRoleSettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AddRoleId"); + + b.Property("DateAdded"); + + b.Property("Enabled"); + + b.Property("FromRoleId"); + + b.Property("GuildConfigId"); + + b.Property("Keyword"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId") + .IsUnique(); + + b.ToTable("StreamRoleSettings"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.StreamRoleWhitelistedUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("StreamRoleSettingsId"); + + b.Property("UserId"); + + b.Property("Username"); + + b.HasKey("Id"); + + b.HasIndex("StreamRoleSettingsId"); + + b.ToTable("StreamRoleWhitelistedUser"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.UnmuteTimer", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("GuildConfigId"); + + b.Property("UnmuteAt"); + + b.Property("UserId"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.ToTable("UnmuteTimer"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.UserPokeTypes", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("UserId"); + + b.Property("type"); + + b.HasKey("Id"); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("PokeGame"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.VcRoleInfo", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("GuildConfigId"); + + b.Property("RoleId"); + + b.Property("VoiceChannelId"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.ToTable("VcRoleInfo"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.WaifuInfo", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AffinityId"); + + b.Property("ClaimerId"); + + b.Property("DateAdded"); + + b.Property("Price"); + + b.Property("WaifuId"); + + b.HasKey("Id"); + + b.HasIndex("AffinityId"); + + b.HasIndex("ClaimerId"); + + b.HasIndex("WaifuId") + .IsUnique(); + + b.ToTable("WaifuInfo"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.WaifuUpdate", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("NewId"); + + b.Property("OldId"); + + b.Property("UpdateType"); + + b.Property("UserId"); + + b.HasKey("Id"); + + b.HasIndex("NewId"); + + b.HasIndex("OldId"); + + b.HasIndex("UserId"); + + b.ToTable("WaifuUpdates"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.Warning", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("Forgiven"); + + b.Property("ForgivenBy"); + + b.Property("GuildId"); + + b.Property("Moderator"); + + b.Property("Reason"); + + b.Property("UserId"); + + b.HasKey("Id"); + + b.ToTable("Warnings"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.WarningPunishment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("Count"); + + b.Property("DateAdded"); + + b.Property("GuildConfigId"); + + b.Property("Punishment"); + + b.Property("Time"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.ToTable("WarningPunishment"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.AntiRaidSetting", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig", "GuildConfig") + .WithOne("AntiRaidSetting") + .HasForeignKey("NadekoBot.Services.Database.Models.AntiRaidSetting", "GuildConfigId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.AntiSpamIgnore", b => + { + b.HasOne("NadekoBot.Services.Database.Models.AntiSpamSetting") + .WithMany("IgnoredChannels") + .HasForeignKey("AntiSpamSettingId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.AntiSpamSetting", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig", "GuildConfig") + .WithOne("AntiSpamSetting") + .HasForeignKey("NadekoBot.Services.Database.Models.AntiSpamSetting", "GuildConfigId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.BlacklistItem", b => + { + b.HasOne("NadekoBot.Services.Database.Models.BotConfig") + .WithMany("Blacklist") + .HasForeignKey("BotConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.BlockedCmdOrMdl", b => + { + b.HasOne("NadekoBot.Services.Database.Models.BotConfig") + .WithMany("BlockedCommands") + .HasForeignKey("BotConfigId"); + + b.HasOne("NadekoBot.Services.Database.Models.BotConfig") + .WithMany("BlockedModules") + .HasForeignKey("BotConfigId1"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.ClashCaller", b => + { + b.HasOne("NadekoBot.Services.Database.Models.ClashWar", "ClashWar") + .WithMany("Bases") + .HasForeignKey("ClashWarId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.CommandAlias", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig") + .WithMany("CommandAliases") + .HasForeignKey("GuildConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.CommandCooldown", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig") + .WithMany("CommandCooldowns") + .HasForeignKey("GuildConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.CommandPrice", b => + { + b.HasOne("NadekoBot.Services.Database.Models.BotConfig") + .WithMany("CommandPrices") + .HasForeignKey("BotConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.EightBallResponse", b => + { + b.HasOne("NadekoBot.Services.Database.Models.BotConfig") + .WithMany("EightBallResponses") + .HasForeignKey("BotConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.FilterChannelId", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig") + .WithMany("FilterInvitesChannelIds") + .HasForeignKey("GuildConfigId"); + + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig") + .WithMany("FilterWordsChannelIds") + .HasForeignKey("GuildConfigId1"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.FilteredWord", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig") + .WithMany("FilteredWords") + .HasForeignKey("GuildConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.FollowedStream", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig") + .WithMany("FollowedStreams") + .HasForeignKey("GuildConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.GCChannelId", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig") + .WithMany("GenerateCurrencyChannelIds") + .HasForeignKey("GuildConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.GuildConfig", b => + { + b.HasOne("NadekoBot.Services.Database.Models.LogSetting", "LogSetting") + .WithMany() + .HasForeignKey("LogSettingId"); + + b.HasOne("NadekoBot.Services.Database.Models.Permission", "RootPermission") + .WithMany() + .HasForeignKey("RootPermissionId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.GuildRepeater", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig") + .WithMany("GuildRepeaters") + .HasForeignKey("GuildConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.IgnoredLogChannel", b => + { + b.HasOne("NadekoBot.Services.Database.Models.LogSetting", "LogSetting") + .WithMany("IgnoredChannels") + .HasForeignKey("LogSettingId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.IgnoredVoicePresenceChannel", b => + { + b.HasOne("NadekoBot.Services.Database.Models.LogSetting", "LogSetting") + .WithMany("IgnoredVoicePresenceChannelIds") + .HasForeignKey("LogSettingId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.ModulePrefix", b => + { + b.HasOne("NadekoBot.Services.Database.Models.BotConfig") + .WithMany("ModulePrefixes") + .HasForeignKey("BotConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.MutedUserId", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig") + .WithMany("MutedUsers") + .HasForeignKey("GuildConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.Permission", b => + { + b.HasOne("NadekoBot.Services.Database.Models.Permission", "Next") + .WithOne("Previous") + .HasForeignKey("NadekoBot.Services.Database.Models.Permission", "NextId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.Permissionv2", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig") + .WithMany("Permissions") + .HasForeignKey("GuildConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.PlayingStatus", b => + { + b.HasOne("NadekoBot.Services.Database.Models.BotConfig") + .WithMany("RotatingStatusMessages") + .HasForeignKey("BotConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.PlaylistSong", b => + { + b.HasOne("NadekoBot.Services.Database.Models.MusicPlaylist") + .WithMany("Songs") + .HasForeignKey("MusicPlaylistId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.RaceAnimal", b => + { + b.HasOne("NadekoBot.Services.Database.Models.BotConfig") + .WithMany("RaceAnimals") + .HasForeignKey("BotConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.ShopEntry", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig") + .WithMany("ShopEntries") + .HasForeignKey("GuildConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.ShopEntryItem", b => + { + b.HasOne("NadekoBot.Services.Database.Models.ShopEntry") + .WithMany("Items") + .HasForeignKey("ShopEntryId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.SlowmodeIgnoredRole", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig") + .WithMany("SlowmodeIgnoredRoles") + .HasForeignKey("GuildConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.SlowmodeIgnoredUser", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig") + .WithMany("SlowmodeIgnoredUsers") + .HasForeignKey("GuildConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.StartupCommand", b => + { + b.HasOne("NadekoBot.Services.Database.Models.BotConfig") + .WithMany("StartupCommands") + .HasForeignKey("BotConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.StreamRoleBlacklistedUser", b => + { + b.HasOne("NadekoBot.Services.Database.Models.StreamRoleSettings") + .WithMany("Blacklist") + .HasForeignKey("StreamRoleSettingsId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.StreamRoleSettings", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig", "GuildConfig") + .WithOne("StreamRole") + .HasForeignKey("NadekoBot.Services.Database.Models.StreamRoleSettings", "GuildConfigId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.StreamRoleWhitelistedUser", b => + { + b.HasOne("NadekoBot.Services.Database.Models.StreamRoleSettings") + .WithMany("Whitelist") + .HasForeignKey("StreamRoleSettingsId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.UnmuteTimer", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig") + .WithMany("UnmuteTimers") + .HasForeignKey("GuildConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.VcRoleInfo", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig") + .WithMany("VcRoleInfos") + .HasForeignKey("GuildConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.WaifuInfo", b => + { + b.HasOne("NadekoBot.Services.Database.Models.DiscordUser", "Affinity") + .WithMany() + .HasForeignKey("AffinityId"); + + b.HasOne("NadekoBot.Services.Database.Models.DiscordUser", "Claimer") + .WithMany() + .HasForeignKey("ClaimerId"); + + b.HasOne("NadekoBot.Services.Database.Models.DiscordUser", "Waifu") + .WithOne() + .HasForeignKey("NadekoBot.Services.Database.Models.WaifuInfo", "WaifuId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.WaifuUpdate", b => + { + b.HasOne("NadekoBot.Services.Database.Models.DiscordUser", "New") + .WithMany() + .HasForeignKey("NewId"); + + b.HasOne("NadekoBot.Services.Database.Models.DiscordUser", "Old") + .WithMany() + .HasForeignKey("OldId"); + + b.HasOne("NadekoBot.Services.Database.Models.DiscordUser", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.WarningPunishment", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig") + .WithMany("WarnPunishments") + .HasForeignKey("GuildConfigId"); + }); + } + } +} diff --git a/src/NadekoBot/Migrations/20170719023924_streamrole-kw-bl-wl.cs b/src/NadekoBot/Migrations/20170719023924_streamrole-kw-bl-wl.cs new file mode 100644 index 00000000..d2ed14f1 --- /dev/null +++ b/src/NadekoBot/Migrations/20170719023924_streamrole-kw-bl-wl.cs @@ -0,0 +1,94 @@ +using System; +using System.Collections.Generic; +using Microsoft.EntityFrameworkCore.Migrations; + +namespace NadekoBot.Migrations +{ + public partial class streamrolekwblwl : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "Enabled", + table: "StreamRoleSettings", + nullable: false, + defaultValue: false); + + migrationBuilder.AddColumn( + name: "Keyword", + table: "StreamRoleSettings", + nullable: true); + + migrationBuilder.CreateTable( + name: "StreamRoleBlacklistedUser", + columns: table => new + { + Id = table.Column(nullable: false) + .Annotation("Sqlite:Autoincrement", true), + DateAdded = table.Column(nullable: true), + StreamRoleSettingsId = table.Column(nullable: true), + UserId = table.Column(nullable: false), + Username = table.Column(nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_StreamRoleBlacklistedUser", x => x.Id); + table.ForeignKey( + name: "FK_StreamRoleBlacklistedUser_StreamRoleSettings_StreamRoleSettingsId", + column: x => x.StreamRoleSettingsId, + principalTable: "StreamRoleSettings", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + }); + + migrationBuilder.CreateTable( + name: "StreamRoleWhitelistedUser", + columns: table => new + { + Id = table.Column(nullable: false) + .Annotation("Sqlite:Autoincrement", true), + DateAdded = table.Column(nullable: true), + StreamRoleSettingsId = table.Column(nullable: true), + UserId = table.Column(nullable: false), + Username = table.Column(nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_StreamRoleWhitelistedUser", x => x.Id); + table.ForeignKey( + name: "FK_StreamRoleWhitelistedUser_StreamRoleSettings_StreamRoleSettingsId", + column: x => x.StreamRoleSettingsId, + principalTable: "StreamRoleSettings", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + }); + + migrationBuilder.CreateIndex( + name: "IX_StreamRoleBlacklistedUser_StreamRoleSettingsId", + table: "StreamRoleBlacklistedUser", + column: "StreamRoleSettingsId"); + + migrationBuilder.CreateIndex( + name: "IX_StreamRoleWhitelistedUser_StreamRoleSettingsId", + table: "StreamRoleWhitelistedUser", + column: "StreamRoleSettingsId"); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "StreamRoleBlacklistedUser"); + + migrationBuilder.DropTable( + name: "StreamRoleWhitelistedUser"); + + migrationBuilder.DropColumn( + name: "Enabled", + table: "StreamRoleSettings"); + + migrationBuilder.DropColumn( + name: "Keyword", + table: "StreamRoleSettings"); + } + } +} diff --git a/src/NadekoBot/Migrations/NadekoSqliteContextModelSnapshot.cs b/src/NadekoBot/Migrations/NadekoSqliteContextModelSnapshot.cs index 42fa008a..6b57437a 100644 --- a/src/NadekoBot/Migrations/NadekoSqliteContextModelSnapshot.cs +++ b/src/NadekoBot/Migrations/NadekoSqliteContextModelSnapshot.cs @@ -1126,6 +1126,26 @@ namespace NadekoBot.Migrations b.ToTable("StartupCommand"); }); + modelBuilder.Entity("NadekoBot.Services.Database.Models.StreamRoleBlacklistedUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("StreamRoleSettingsId"); + + b.Property("UserId"); + + b.Property("Username"); + + b.HasKey("Id"); + + b.HasIndex("StreamRoleSettingsId"); + + b.ToTable("StreamRoleBlacklistedUser"); + }); + modelBuilder.Entity("NadekoBot.Services.Database.Models.StreamRoleSettings", b => { b.Property("Id") @@ -1135,10 +1155,14 @@ namespace NadekoBot.Migrations b.Property("DateAdded"); + b.Property("Enabled"); + b.Property("FromRoleId"); b.Property("GuildConfigId"); + b.Property("Keyword"); + b.HasKey("Id"); b.HasIndex("GuildConfigId") @@ -1147,6 +1171,26 @@ namespace NadekoBot.Migrations b.ToTable("StreamRoleSettings"); }); + modelBuilder.Entity("NadekoBot.Services.Database.Models.StreamRoleWhitelistedUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("StreamRoleSettingsId"); + + b.Property("UserId"); + + b.Property("Username"); + + b.HasKey("Id"); + + b.HasIndex("StreamRoleSettingsId"); + + b.ToTable("StreamRoleWhitelistedUser"); + }); + modelBuilder.Entity("NadekoBot.Services.Database.Models.UnmuteTimer", b => { b.Property("Id") @@ -1531,6 +1575,13 @@ namespace NadekoBot.Migrations .HasForeignKey("BotConfigId"); }); + modelBuilder.Entity("NadekoBot.Services.Database.Models.StreamRoleBlacklistedUser", b => + { + b.HasOne("NadekoBot.Services.Database.Models.StreamRoleSettings") + .WithMany("Blacklist") + .HasForeignKey("StreamRoleSettingsId"); + }); + modelBuilder.Entity("NadekoBot.Services.Database.Models.StreamRoleSettings", b => { b.HasOne("NadekoBot.Services.Database.Models.GuildConfig", "GuildConfig") @@ -1539,6 +1590,13 @@ namespace NadekoBot.Migrations .OnDelete(DeleteBehavior.Cascade); }); + modelBuilder.Entity("NadekoBot.Services.Database.Models.StreamRoleWhitelistedUser", b => + { + b.HasOne("NadekoBot.Services.Database.Models.StreamRoleSettings") + .WithMany("Whitelist") + .HasForeignKey("StreamRoleSettingsId"); + }); + modelBuilder.Entity("NadekoBot.Services.Database.Models.UnmuteTimer", b => { b.HasOne("NadekoBot.Services.Database.Models.GuildConfig") diff --git a/src/NadekoBot/Modules/Permissions/BlacklistCommands.cs b/src/NadekoBot/Modules/Permissions/BlacklistCommands.cs index 655209af..377631e0 100644 --- a/src/NadekoBot/Modules/Permissions/BlacklistCommands.cs +++ b/src/NadekoBot/Modules/Permissions/BlacklistCommands.cs @@ -2,24 +2,18 @@ using Discord.Commands; using NadekoBot.Services; using NadekoBot.Services.Database.Models; -using System.Collections.Concurrent; using System.Linq; using System.Threading.Tasks; using NadekoBot.Common.Attributes; using NadekoBot.Common.Collections; using NadekoBot.Modules.Games.Common.Trivia; using NadekoBot.Modules.Permissions.Services; +using NadekoBot.Common.TypeReaders; namespace NadekoBot.Modules.Permissions { public partial class Permissions { - public enum AddRemove - { - Add, - Rem - } - [Group] public class BlacklistCommands : NadekoSubmodule { diff --git a/src/NadekoBot/Modules/Utility/Common/Exceptions/StreamRoleNotFoundException.cs b/src/NadekoBot/Modules/Utility/Common/Exceptions/StreamRoleNotFoundException.cs new file mode 100644 index 00000000..ca73e920 --- /dev/null +++ b/src/NadekoBot/Modules/Utility/Common/Exceptions/StreamRoleNotFoundException.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace NadekoBot.Modules.Utility.Common.Exceptions +{ + public class StreamRoleNotFoundException : Exception + { + public StreamRoleNotFoundException() : base("Stream role wasn't found.") + { + } + } +} diff --git a/src/NadekoBot/Modules/Utility/Common/Exceptions/StreamRolePermissionException.cs b/src/NadekoBot/Modules/Utility/Common/Exceptions/StreamRolePermissionException.cs new file mode 100644 index 00000000..a921f3ce --- /dev/null +++ b/src/NadekoBot/Modules/Utility/Common/Exceptions/StreamRolePermissionException.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace NadekoBot.Modules.Utility.Common.Exceptions +{ + public class StreamRolePermissionException : Exception + { + public StreamRolePermissionException() : base("Stream role was unable to be applied.") + { + } + } +} diff --git a/src/NadekoBot/Modules/Utility/Common/StreamRoleListType.cs b/src/NadekoBot/Modules/Utility/Common/StreamRoleListType.cs new file mode 100644 index 00000000..4281744b --- /dev/null +++ b/src/NadekoBot/Modules/Utility/Common/StreamRoleListType.cs @@ -0,0 +1,8 @@ +namespace NadekoBot.Modules.Utility.Common +{ + public enum StreamRoleListType + { + Whitelist, + Blacklist, + } +} diff --git a/src/NadekoBot/Modules/Utility/Extensions/StreamRoleExtensions.cs b/src/NadekoBot/Modules/Utility/Extensions/StreamRoleExtensions.cs new file mode 100644 index 00000000..eaf16e84 --- /dev/null +++ b/src/NadekoBot/Modules/Utility/Extensions/StreamRoleExtensions.cs @@ -0,0 +1,27 @@ +using Microsoft.EntityFrameworkCore; +using NadekoBot.Services.Database.Models; +using NadekoBot.Services.Database.Repositories; + +namespace NadekoBot.Modules.Utility.Extensions +{ + public static class StreamRoleExtensions + { + /// + /// Gets full stream role settings for the guild with the specified id. + /// + /// + /// Id of the guild to get stream role settings for. + /// + public static StreamRoleSettings GetStreamRoleSettings(this IGuildConfigRepository gc, ulong guildId) + { + var conf = gc.For(guildId, x => x.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; + } + } +} diff --git a/src/NadekoBot/Modules/Utility/Services/StreamRoleService.cs b/src/NadekoBot/Modules/Utility/Services/StreamRoleService.cs index 6e759d93..48fc53f0 100644 --- a/src/NadekoBot/Modules/Utility/Services/StreamRoleService.cs +++ b/src/NadekoBot/Modules/Utility/Services/StreamRoleService.cs @@ -5,11 +5,15 @@ using System.Linq; using System.Threading.Tasks; using Discord; using Discord.WebSocket; -using Microsoft.EntityFrameworkCore; using NadekoBot.Extensions; using NadekoBot.Services; using NadekoBot.Services.Database.Models; using NLog; +using NadekoBot.Modules.Utility.Extensions; +using NadekoBot.Common.TypeReaders; +using NadekoBot.Modules.Utility.Common; +using NadekoBot.Modules.Utility.Common.Exceptions; +using Discord.Net; namespace NadekoBot.Modules.Utility.Services { @@ -27,8 +31,8 @@ namespace NadekoBot.Modules.Utility.Services this._log = LogManager.GetCurrentClassLogger(); guildSettings = gcs.ToDictionary(x => x.GuildId, x => x.StreamRole) - .Where(x => x.Value.FromRoleId != 0 && x.Value.AddRoleId != 0) - .ToConcurrent(); + .Where(x => x.Value != null && x.Value.Enabled) + .ToConcurrent(); client.GuildMemberUpdated += Client_GuildMemberUpdated; } @@ -38,53 +42,12 @@ namespace NadekoBot.Modules.Utility.Services var _ = Task.Run(async () => { //if user wasn't streaming or didn't have a game status at all - // and has a game status now - // and that status is a streaming status - // and we are supposed to give him a role - if ((!before.Game.HasValue || before.Game.Value.StreamType == StreamType.NotStreaming) && - after.Game.HasValue && - after.Game.Value.StreamType != StreamType.NotStreaming + if ((!before.Game.HasValue || before.Game.Value.StreamType == StreamType.NotStreaming) && guildSettings.TryGetValue(after.Guild.Id, out var setting)) { - IRole fromRole; - IRole addRole; - try - { - //get needed roles - fromRole = after.Guild.GetRole(setting.FromRoleId); - if (fromRole == null) - throw new InvalidOperationException(); - addRole = after.Guild.GetRole(setting.AddRoleId); - if (addRole == null) - throw new InvalidOperationException(); - } - catch (Exception ex) - { - StopStreamRole(before.Guild.Id); - _log.Warn("Error getting Stream Role(s). Disabling stream role feature."); - _log.Error(ex); - return; - } - - try - { - //check if user is in the fromrole - if (after.Roles.Contains(fromRole)) - { - //check if he doesn't have addrole already, to avoid errors - if(!after.Roles.Contains(addRole)) - await after.AddRoleAsync(addRole).ConfigureAwait(false); - //schedule him for the role removal when he stops streaming - toRemove.TryAdd((addRole.Guild.Id, after.Id), addRole.Id); - } - } - catch (Exception ex) - { - _log.Warn("Failed adding stream role."); - _log.Error(ex); - } + await TryApplyRole(after, setting).ConfigureAwait(false); } - + // try removing a role that was given to the user // if user had a game status // and he was streaming @@ -110,47 +73,210 @@ namespace NadekoBot.Modules.Utility.Services _log.Warn("Failed removing the stream role from the user who stopped streaming."); _log.Error(ex); } - } + } }); return Task.CompletedTask; } - public void SetStreamRole(IRole fromRole, IRole addRole) + private async Task TryApplyRole(IGuildUser user, StreamRoleSettings setting) { + // if the user has a game status now + // and that status is a streaming status + // and the feature is enabled + // and he's not blacklisted + // and keyword is either not set, or the game contains the keyword required, or he's whitelisted + if (user.Game.HasValue && + user.Game.Value.StreamType != StreamType.NotStreaming + && setting.Enabled + && !setting.Blacklist.Any(x => x.UserId == user.Id) + && (string.IsNullOrWhiteSpace(setting.Keyword) + || user.Game.Value.Name.Contains(setting.Keyword) + || setting.Whitelist.Any(x => x.UserId == user.Id))) + { + IRole fromRole; + IRole addRole; + + //get needed roles + fromRole = user.Guild.GetRole(setting.FromRoleId); + if (fromRole == null) + throw new StreamRoleNotFoundException(); + addRole = user.Guild.GetRole(setting.AddRoleId); + if (addRole == null) + throw new StreamRoleNotFoundException(); + + try + { + //check if user is in the fromrole + if (user.RoleIds.Contains(setting.FromRoleId)) + { + //check if he doesn't have addrole already, to avoid errors + if (!user.RoleIds.Contains(setting.AddRoleId)) + await user.AddRoleAsync(addRole).ConfigureAwait(false); + //schedule him for the role removal when he stops streaming + toRemove.TryAdd((addRole.Guild.Id, user.Id), addRole.Id); + } + } + catch (HttpException ex) when (ex.HttpCode == System.Net.HttpStatusCode.Forbidden) + { + StopStreamRole(user.Guild.Id); + _log.Warn("Error adding stream role(s). Disabling stream role feature."); + _log.Error(ex); + throw new StreamRolePermissionException(); + } + catch (Exception ex) + { + _log.Warn("Failed adding stream role."); + _log.Error(ex); + } + } + } + + /// + /// Adds or removes a user from a blacklist or a whitelist in the specified guild. + /// + /// Id of the guild + /// Add or rem action + /// User's Id + /// User's name#discrim + /// Whether the operation was successful + public async Task ApplyListAction(StreamRoleListType listType, ulong guildId, AddRemove action, ulong userId, string userName) + { + userName.ThrowIfNull(nameof(userName)); + + bool success; + using (var uow = _db.UnitOfWork) + { + var streamRoleSettings = uow.GuildConfigs.GetStreamRoleSettings(guildId); + + if (listType == StreamRoleListType.Whitelist) + { + var userObj = new StreamRoleWhitelistedUser() + { + UserId = userId, + Username = userName, + }; + + if (action == AddRemove.Rem) + success = streamRoleSettings.Whitelist.Remove(userObj); + else + success = streamRoleSettings.Whitelist.Add(userObj); + } + else + { + var userObj = new StreamRoleBlacklistedUser() + { + UserId = userId, + Username = userName, + }; + + if (action == AddRemove.Rem) + success = streamRoleSettings.Blacklist.Remove(userObj); + else + success = streamRoleSettings.Blacklist.Add(userObj); + } + + await uow.CompleteAsync().ConfigureAwait(false); + } + return success; + } + + /// + /// Sets keyword on a guild and updates the cache. + /// + /// Guild Id + /// Keyword to set + /// The keyword set + public string SetKeyword(ulong guildId, string keyword) + { + keyword = keyword?.Trim()?.ToLowerInvariant(); + + using (var uow = _db.UnitOfWork) + { + var streamRoleSettings = uow.GuildConfigs.GetStreamRoleSettings(guildId); + + streamRoleSettings.Keyword = keyword; + UpdateCache(guildId, streamRoleSettings); + uow.Complete(); + + return streamRoleSettings.Keyword; + } + + } + + /// + /// Gets the currently set keyword on a guild. + /// + /// Guild Id + /// The keyword set + public string GetKeyword(ulong guildId) + { + if (guildSettings.TryGetValue(guildId, out var outSetting)) + return outSetting.Keyword; + StreamRoleSettings setting; using (var uow = _db.UnitOfWork) { - var gc = uow.GuildConfigs.For(fromRole.Guild.Id, x => x.Include(y => y.StreamRole)); - - if (gc.StreamRole == null) - gc.StreamRole = new StreamRoleSettings() - { - AddRoleId = addRole.Id, - FromRoleId = fromRole.Id - }; - else - { - gc.StreamRole.AddRoleId = addRole.Id; - gc.StreamRole.FromRoleId = fromRole.Id; - } - setting = gc.StreamRole; - uow.Complete(); + setting = uow.GuildConfigs.GetStreamRoleSettings(guildId); } - guildSettings.AddOrUpdate(fromRole.Guild.Id, (key) => setting, (key, old) => setting); + UpdateCache(guildId, setting); + + return setting.Keyword; } + /// + /// Sets the role to monitor, and a role to which to add to + /// the user who starts streaming in the monitored role. + /// + /// Role to monitor + /// Role to add to the user + public async Task SetStreamRole(IRole fromRole, IRole addRole) + { + fromRole.ThrowIfNull(nameof(fromRole)); + addRole.ThrowIfNull(nameof(addRole)); + + StreamRoleSettings setting; + using (var uow = _db.UnitOfWork) + { + var streamRoleSettings = uow.GuildConfigs.GetStreamRoleSettings(fromRole.Guild.Id); + + streamRoleSettings.Enabled = true; + streamRoleSettings.AddRoleId = addRole.Id; + streamRoleSettings.FromRoleId = fromRole.Id; + + setting = streamRoleSettings; + await uow.CompleteAsync().ConfigureAwait(false); + } + + UpdateCache(fromRole.Guild.Id, setting); + + foreach (var usr in await fromRole.Guild.GetUsersAsync(CacheMode.CacheOnly).ConfigureAwait(false)) + { + await TryApplyRole(usr, setting).ConfigureAwait(false); + await Task.Delay(500).ConfigureAwait(false); + } + } + + /// + /// Stops the stream role feature on the specified guild. + /// + /// Guild's Id public void StopStreamRole(ulong guildId) { using (var uow = _db.UnitOfWork) { - var gc = uow.GuildConfigs.For(guildId, x => x.Include(y => y.StreamRole)); - gc.StreamRole = null; + var streamRoleSettings = uow.GuildConfigs.GetStreamRoleSettings(guildId); + streamRoleSettings.Enabled = false; uow.Complete(); } guildSettings.TryRemove(guildId, out _); } + + private void UpdateCache(ulong guildId, StreamRoleSettings setting) + { + guildSettings.AddOrUpdate(guildId, (key) => setting, (key, old) => setting); + } } } diff --git a/src/NadekoBot/Modules/Utility/StreamRoleCommands.cs b/src/NadekoBot/Modules/Utility/StreamRoleCommands.cs index 7f2c7872..8237dce0 100644 --- a/src/NadekoBot/Modules/Utility/StreamRoleCommands.cs +++ b/src/NadekoBot/Modules/Utility/StreamRoleCommands.cs @@ -3,6 +3,8 @@ 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 { @@ -16,7 +18,7 @@ namespace NadekoBot.Modules.Utility [RequireContext(ContextType.Guild)] public async Task StreamRole(IRole fromRole, IRole addRole) { - this._service.SetStreamRole(fromRole, addRole); + await this._service.SetStreamRole(fromRole, addRole).ConfigureAwait(false); await ReplyConfirmLocalized("stream_role_enabled", Format.Bold(fromRole.ToString()), Format.Bold(addRole.ToString())).ConfigureAwait(false); } @@ -30,6 +32,62 @@ namespace NadekoBot.Modules.Utility this._service.StopStreamRole(Context.Guild.Id); await ReplyConfirmLocalized("stream_role_disabled").ConfigureAwait(false); } + + [NadekoCommand, Usage, Description, Aliases] + [RequireBotPermission(GuildPermission.ManageRoles)] + [RequireUserPermission(GuildPermission.ManageRoles)] + [RequireContext(ContextType.Guild)] + public async Task StreamRoleKeyword([Remainder]string keyword = null) + { + string kw = this._service.SetKeyword(Context.Guild.Id, keyword); + + if(string.IsNullOrWhiteSpace(keyword)) + await ReplyConfirmLocalized("stream_role_kw_reset").ConfigureAwait(false); + else + await ReplyConfirmLocalized("stream_role_kw_set", Format.Bold(kw)).ConfigureAwait(false); + } + + [NadekoCommand, Usage, Description, Aliases] + [RequireBotPermission(GuildPermission.ManageRoles)] + [RequireUserPermission(GuildPermission.ManageRoles)] + [RequireContext(ContextType.Guild)] + public async Task StreamRoleBlacklist(AddRemove action, [Remainder] IGuildUser user) + { + var success = await this._service.ApplyListAction(StreamRoleListType.Blacklist, Context.Guild.Id, action, user.Id, user.ToString()) + .ConfigureAwait(false); + + if(action == AddRemove.Add) + if(success) + await ReplyConfirmLocalized("stream_role_bl_add", Format.Bold(user.ToString())).ConfigureAwait(false); + else + await ReplyConfirmLocalized("stream_role_bl_add_fail", Format.Bold(user.ToString())).ConfigureAwait(false); + else + if (success) + await ReplyConfirmLocalized("stream_role_bl_rem", Format.Bold(user.ToString())).ConfigureAwait(false); + else + await ReplyErrorLocalized("stream_role_bl_rem_fail", Format.Bold(user.ToString())).ConfigureAwait(false); + } + + [NadekoCommand, Usage, Description, Aliases] + [RequireBotPermission(GuildPermission.ManageRoles)] + [RequireUserPermission(GuildPermission.ManageRoles)] + [RequireContext(ContextType.Guild)] + public async Task StreamRoleWhitelist(AddRemove action, [Remainder] IGuildUser user) + { + var success = await this._service.ApplyListAction(StreamRoleListType.Whitelist, Context.Guild.Id, action, user.Id, user.ToString()) + .ConfigureAwait(false); + + if (action == AddRemove.Add) + if(success) + await ReplyConfirmLocalized("stream_role_wl_add", Format.Bold(user.ToString())).ConfigureAwait(false); + else + await ReplyConfirmLocalized("stream_role_wl_add_fail", Format.Bold(user.ToString())).ConfigureAwait(false); + else + if (success) + await ReplyConfirmLocalized("stream_role_wl_rem", Format.Bold(user.ToString())).ConfigureAwait(false); + else + await ReplyErrorLocalized("stream_role_wl_rem_fail", Format.Bold(user.ToString())).ConfigureAwait(false); + } } } } \ No newline at end of file diff --git a/src/NadekoBot/NadekoBot.csproj b/src/NadekoBot/NadekoBot.csproj index 810fa907..6d719b32 100644 --- a/src/NadekoBot/NadekoBot.csproj +++ b/src/NadekoBot/NadekoBot.csproj @@ -91,4 +91,8 @@ + + + + diff --git a/src/NadekoBot/Resources/CommandStrings.resx b/src/NadekoBot/Resources/CommandStrings.resx index 53e5b1a9..e1635531 100644 --- a/src/NadekoBot/Resources/CommandStrings.resx +++ b/src/NadekoBot/Resources/CommandStrings.resx @@ -3546,4 +3546,31 @@ Toggles whether the bot should print command errors when a command is incorrectly used. + + streamrolekw srkw + + + `{0}srkw` or `{0}srkw PUBG` + + + Sets keyword which is required in the stream's title in order for the streamrole to apply. Provide no keyword in order to reset. + + + streamrolebl srbl + + + `{0}srbl add @b1nzy#1234` or `{0}srbl rem @b1nzy#1234` + + + Adds or removes a blacklisted user. Blacklisted users will never receive the stream role. + + + streamrolewl srwl + + + `{0}srwl add @b1nzy#1234` or `{0}srwl rem @b1nzy#1234` + + + Adds or removes a whitelisted user. Whitelisted users will receive the stream role even if they don't have the specified keyword in their stream title. + diff --git a/src/NadekoBot/Services/Database/Models/GuildConfig.cs b/src/NadekoBot/Services/Database/Models/GuildConfig.cs index 6222712b..83498097 100644 --- a/src/NadekoBot/Services/Database/Models/GuildConfig.cs +++ b/src/NadekoBot/Services/Database/Models/GuildConfig.cs @@ -83,7 +83,7 @@ namespace NadekoBot.Services.Database.Models public ulong? GameVoiceChannel { get; set; } = null; public bool VerboseErrors { get; set; } = false; - public StreamRoleSettings StreamRole { get; set; } = new StreamRoleSettings(); + public StreamRoleSettings StreamRole { get; set; } //public List ProtectionIgnoredChannels { get; set; } = new List(); } diff --git a/src/NadekoBot/Services/Database/Models/StreamRoleSettings.cs b/src/NadekoBot/Services/Database/Models/StreamRoleSettings.cs index f73abeac..05449039 100644 --- a/src/NadekoBot/Services/Database/Models/StreamRoleSettings.cs +++ b/src/NadekoBot/Services/Database/Models/StreamRoleSettings.cs @@ -11,13 +11,77 @@ namespace NadekoBot.Services.Database.Models public int GuildConfigId { get; set; } public GuildConfig GuildConfig { get; set; } + /// + /// Whether the feature is enabled in the guild. + /// + public bool Enabled { get; set; } + /// /// Id of the role to give to the users in the role 'FromRole' when they start streaming /// public ulong AddRoleId { get; set; } + /// /// Id of the role whose users are eligible to get the 'AddRole' /// public ulong FromRoleId { get; set; } + + /// + /// If set, feature will only apply to users who have this keyword in their streaming status. + /// + public string Keyword { get; set; } + + /// + /// A collection of whitelisted users' IDs. Whitelisted users don't require 'keyword' in + /// order to get the stream role. + /// + public HashSet Whitelist { get; set; } = new HashSet(); + + /// + /// A collection of blacklisted users' IDs. Blacklisted useres will never get the stream role. + /// + public HashSet Blacklist { get; set; } = new HashSet(); + } + + public class StreamRoleBlacklistedUser : DbEntity + { + public ulong UserId { get; set; } + public string Username { get; set; } + + public override bool Equals(object obj) + { + var x = obj as StreamRoleBlacklistedUser; + + if (x == null) + return false; + + return x.UserId == UserId; + } + + public override int GetHashCode() + { + return UserId.GetHashCode(); + } + } + + public class StreamRoleWhitelistedUser : DbEntity + { + public ulong UserId { get; set; } + public string Username { get; set; } + + public override bool Equals(object obj) + { + var x = obj as StreamRoleWhitelistedUser; + + if (x == null) + return false; + + return x.UserId == UserId; + } + + public override int GetHashCode() + { + return UserId.GetHashCode(); + } } } diff --git a/src/NadekoBot/_strings/ResponseStrings.en-US.json b/src/NadekoBot/_strings/ResponseStrings.en-US.json index bfaa9248..3ca43151 100644 --- a/src/NadekoBot/_strings/ResponseStrings.en-US.json +++ b/src/NadekoBot/_strings/ResponseStrings.en-US.json @@ -610,6 +610,16 @@ "searches_yodify_error": "Failed to yodify your sentence.", "utility_stream_role_enabled": "When a user from {0} role starts streaming, I will give them {1} role.", "utility_stream_role_disabled": "Stream role feature has been disabled.", + "utility_stream_role_kw_set": "Streamers now require {0} keyword in order to receive the role.", + "utility_stream_role_kw_reset": "Stream role keyword reset.", + "utility_stream_role_bl_add": "User {0} will never receive the stream role.", + "utility_stream_role_bl_add_fail": "User {0} is already blacklisted.", + "utility_stream_role_bl_rem": "User {0} is no longer blacklisted.", + "utility_stream_role_bl_rem_fail": "User {0} is not blacklisted.", + "utility_stream_role_wl_add": "User {0} will receive the stream role even if they don't have the keyword in the stream title.", + "utility_stream_role_wl_add_fail": "User {0} is already whitelisted.", + "utility_stream_role_wl_rem": "User {0} is no longer whitelisted.", + "utility_stream_role_wl_rem_fail": "User {0} is not whitelisted.", "utiliity_joined": "Joined", "utility_activity_line": "`{0}.` {1} [{2:F2}/s] - {3} total", "utility_activity_page": "Activity page #{0}", From fe886111837dbe0135de283c685ff0f4208ed78c Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Wed, 19 Jul 2017 11:02:14 +0200 Subject: [PATCH 201/346] Fixed .shop pagination --- src/NadekoBot/Modules/Gambling/FlowerShopCommands.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/NadekoBot/Modules/Gambling/FlowerShopCommands.cs b/src/NadekoBot/Modules/Gambling/FlowerShopCommands.cs index 59a2fd78..27620ce6 100644 --- a/src/NadekoBot/Modules/Gambling/FlowerShopCommands.cs +++ b/src/NadekoBot/Modules/Gambling/FlowerShopCommands.cs @@ -59,7 +59,7 @@ namespace NadekoBot.Modules.Gambling await Context.Channel.SendPaginatedConfirmAsync(_client, page, (curPage) => { - var theseEntries = entries.Skip(curPage * 9).Take(9); + var theseEntries = entries.Skip(curPage * 9).Take(9).ToArray(); if (!theseEntries.Any()) return new EmbedBuilder().WithErrorColor() @@ -67,10 +67,10 @@ namespace NadekoBot.Modules.Gambling var embed = new EmbedBuilder().WithOkColor() .WithTitle(GetText("shop", _bc.CurrencySign)); - for (int i = 0; i < entries.Count; i++) + for (int i = 0; i < theseEntries.Length; i++) { var entry = entries[i]; - embed.AddField(efb => efb.WithName($"#{i + 1} - {entry.Price}{_bc.CurrencySign}").WithValue(EntryToString(entry)).WithIsInline(true)); + embed.AddField(efb => efb.WithName($"#{curPage * 9 + i + 1} - {entry.Price}{_bc.CurrencySign}").WithValue(EntryToString(entry)).WithIsInline(true)); } return embed; }, entries.Count / 9, true); From 9163510eee9e5f6270a6cec3b8dfa16d87e6d366 Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Thu, 20 Jul 2017 05:10:39 +0200 Subject: [PATCH 202/346] .bce command added, you can now edit BotConfig without editing the database and restarting the bot. Cleanup --- src/NadekoBot/Common/Attributes/Aliases.cs | 1 - .../Common/Attributes/Description.cs | 1 - .../Common/Attributes/NadekoCommand.cs | 1 - src/NadekoBot/Common/Attributes/Usage.cs | 1 - src/NadekoBot/Common/BotConfigEditType.cs | 20 ++++ .../20170612094138_verbose-errors.cs | 4 +- .../20170612234751_repeat time of day.cs | 1 - .../20170613231358_maxdropamount.cs | 4 +- .../Migrations/20170616154106_crstartswith.cs | 4 +- .../Migrations/20170714021615_stream-role.cs | 1 - .../20170719023924_streamrole-kw-bl-wl.cs | 1 - .../NadekoSqliteContextModelSnapshot.cs | 2 - .../Modules/Administration/LogCommands.cs | 1 - .../SelfAssignedRolesCommands.cs | 1 - .../Modules/Administration/SelfCommands.cs | 12 +- .../Services/AdministrationService.cs | 1 - .../Services/GameVoiceChannelService.cs | 1 - .../Administration/Services/PruneService.cs | 1 - .../Administration/Services/SelfService.cs | 15 ++- .../CustomReactions/Extensions/Extensions.cs | 1 - .../Services/CustomReactionsService.cs | 8 +- .../Modules/Gambling/AnimalRacingCommands.cs | 17 ++- .../Modules/Gambling/Common/Cards.cs | 1 - .../Gambling/CurrencyEventsCommands.cs | 12 +- .../Modules/Gambling/FlipCoinCommands.cs | 15 ++- .../Modules/Gambling/FlowerShopCommands.cs | 12 +- src/NadekoBot/Modules/Gambling/Gambling.cs | 22 ++-- .../Modules/Gambling/SlotCommands.cs | 21 ++-- .../Modules/Gambling/WaifuClaimCommands.cs | 18 +-- .../Modules/Games/AcropobiaCommands.cs | 1 - .../Modules/Games/Common/ChatterBotSession.cs | 1 - .../Games/Common/Hangman/HangmanGame.cs | 1 - src/NadekoBot/Modules/Games/Common/Poll.cs | 1 - .../Modules/Games/Common/Trivia/TriviaGame.cs | 7 +- .../Games/Common/Trivia/TriviaQuestionPool.cs | 1 - .../Modules/Games/Common/TypingGame.cs | 1 - .../Modules/Games/PlantAndPickCommands.cs | 14 +-- .../Modules/Games/Services/GamesService.cs | 18 +-- .../Modules/Games/TicTacToeCommands.cs | 1 - src/NadekoBot/Modules/Games/TriviaCommands.cs | 6 +- src/NadekoBot/Modules/Help/Help.cs | 9 +- .../Modules/Help/Services/HelpService.cs | 9 +- .../Exceptions/NotInVoiceChannelException.cs | 5 - .../Modules/Music/Common/MusicPlayer.cs | 1 - .../Modules/Music/Common/MusicQueue.cs | 2 - src/NadekoBot/Modules/Music/Music.cs | 1 - .../Modules/Music/Services/MusicService.cs | 5 +- src/NadekoBot/Modules/NSFW/NSFW.cs | 1 - src/NadekoBot/Modules/NadekoModule.cs | 1 - .../Common/PermissionsCollection.cs | 1 - .../Modules/Permissions/FilterCommands.cs | 1 - .../Modules/Permissions/Permissions.cs | 1 - .../Permissions/ResetPermissionsCommands.cs | 2 - .../Permissions/Services/BlacklistService.cs | 7 +- .../Services/GlobalPermissionService.cs | 10 +- src/NadekoBot/Modules/Pokemon/Pokemon.cs | 16 +-- .../Searches/Common/SearchImageCacher.cs | 1 - .../Modules/Searches/JokeCommands.cs | 1 - src/NadekoBot/Modules/Searches/LoLCommands.cs | 2 - .../Modules/Searches/PlaceCommands.cs | 1 - .../Modules/Searches/XkcdCommands.cs | 1 - .../Modules/Utility/BotConfigCommands.cs | 39 ++++++ .../Exceptions/StreamRoleNotFoundException.cs | 4 - .../StreamRolePermissionException.cs | 4 - .../Modules/Utility/PatreonCommands.cs | 7 +- .../Modules/Utility/QuoteCommands.cs | 1 - .../Modules/Utility/Services/RemindService.cs | 6 +- .../Utility/Services/VerboseErrorsService.cs | 3 +- src/NadekoBot/NadekoBot.cs | 14 +-- src/NadekoBot/NadekoBot.csproj | 4 - src/NadekoBot/Resources/CommandStrings.resx | 9 ++ src/NadekoBot/Services/CommandHandler.cs | 4 +- src/NadekoBot/Services/CurrencyService.cs | 8 +- .../Database/Models/StreamRoleSettings.cs | 6 +- .../Repositories/IReminderRepository.cs | 1 - .../Repositories/Impl/ReminderRepository.cs | 1 - .../Repositories/Impl/WarningsRepository.cs | 1 - src/NadekoBot/Services/IBotConfigProvider.cs | 12 ++ src/NadekoBot/Services/IImagesService.cs | 3 +- src/NadekoBot/Services/INService.cs | 8 +- .../Services/Impl/BotConfigProvider.cs | 111 ++++++++++++++++++ src/NadekoBot/Services/Impl/ImagesService.cs | 1 - .../Services/Impl/SyncPreconditionService.cs | 8 +- src/NadekoBot/Services/LogSetup.cs | 5 - src/NadekoBot/Services/ServiceProvider.cs | 11 ++ src/NadekoBot/_Extensions/Extensions.cs | 1 - .../_strings/ResponseStrings.en-US.json | 2 + 87 files changed, 353 insertions(+), 251 deletions(-) create mode 100644 src/NadekoBot/Common/BotConfigEditType.cs create mode 100644 src/NadekoBot/Modules/Utility/BotConfigCommands.cs create mode 100644 src/NadekoBot/Services/IBotConfigProvider.cs create mode 100644 src/NadekoBot/Services/Impl/BotConfigProvider.cs diff --git a/src/NadekoBot/Common/Attributes/Aliases.cs b/src/NadekoBot/Common/Attributes/Aliases.cs index 21e7c2ba..7f365078 100644 --- a/src/NadekoBot/Common/Attributes/Aliases.cs +++ b/src/NadekoBot/Common/Attributes/Aliases.cs @@ -1,7 +1,6 @@ using System.Linq; using System.Runtime.CompilerServices; using Discord.Commands; -using NadekoBot.Services; using NadekoBot.Services.Impl; namespace NadekoBot.Common.Attributes diff --git a/src/NadekoBot/Common/Attributes/Description.cs b/src/NadekoBot/Common/Attributes/Description.cs index 2a071518..1b0e7957 100644 --- a/src/NadekoBot/Common/Attributes/Description.cs +++ b/src/NadekoBot/Common/Attributes/Description.cs @@ -1,6 +1,5 @@ using System.Runtime.CompilerServices; using Discord.Commands; -using NadekoBot.Services; using NadekoBot.Services.Impl; namespace NadekoBot.Common.Attributes diff --git a/src/NadekoBot/Common/Attributes/NadekoCommand.cs b/src/NadekoBot/Common/Attributes/NadekoCommand.cs index 566c2640..a471e007 100644 --- a/src/NadekoBot/Common/Attributes/NadekoCommand.cs +++ b/src/NadekoBot/Common/Attributes/NadekoCommand.cs @@ -1,6 +1,5 @@ using System.Runtime.CompilerServices; using Discord.Commands; -using NadekoBot.Services; using NadekoBot.Services.Impl; namespace NadekoBot.Common.Attributes diff --git a/src/NadekoBot/Common/Attributes/Usage.cs b/src/NadekoBot/Common/Attributes/Usage.cs index d7c88ea8..2991c6aa 100644 --- a/src/NadekoBot/Common/Attributes/Usage.cs +++ b/src/NadekoBot/Common/Attributes/Usage.cs @@ -1,6 +1,5 @@ using System.Runtime.CompilerServices; using Discord.Commands; -using NadekoBot.Services; using NadekoBot.Services.Impl; namespace NadekoBot.Common.Attributes diff --git a/src/NadekoBot/Common/BotConfigEditType.cs b/src/NadekoBot/Common/BotConfigEditType.cs new file mode 100644 index 00000000..32b1415c --- /dev/null +++ b/src/NadekoBot/Common/BotConfigEditType.cs @@ -0,0 +1,20 @@ +namespace NadekoBot.Common +{ + public enum BotConfigEditType + { + CurrencyGenerationChance, + CurrencyGenerationCooldown, + CurrencyName, + CurrencyPluralName, + CurrencySign, + DmHelpString, + HelpString, + CurrencyDropAmount, + CurrencyDropAmountMax, + MinimumBetAmount, + TriviaCurrencyReward, + + //ErrorColor, //after i fix the nadekobot.cs static variables + //OkColor + } +} diff --git a/src/NadekoBot/Migrations/20170612094138_verbose-errors.cs b/src/NadekoBot/Migrations/20170612094138_verbose-errors.cs index 3caf4aca..291e4e40 100644 --- a/src/NadekoBot/Migrations/20170612094138_verbose-errors.cs +++ b/src/NadekoBot/Migrations/20170612094138_verbose-errors.cs @@ -1,6 +1,4 @@ -using System; -using System.Collections.Generic; -using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Migrations; namespace NadekoBot.Migrations { diff --git a/src/NadekoBot/Migrations/20170612234751_repeat time of day.cs b/src/NadekoBot/Migrations/20170612234751_repeat time of day.cs index 076d69d3..a5a7bfac 100644 --- a/src/NadekoBot/Migrations/20170612234751_repeat time of day.cs +++ b/src/NadekoBot/Migrations/20170612234751_repeat time of day.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using Microsoft.EntityFrameworkCore.Migrations; namespace NadekoBot.Migrations diff --git a/src/NadekoBot/Migrations/20170613231358_maxdropamount.cs b/src/NadekoBot/Migrations/20170613231358_maxdropamount.cs index ea9d58c8..31495e9b 100644 --- a/src/NadekoBot/Migrations/20170613231358_maxdropamount.cs +++ b/src/NadekoBot/Migrations/20170613231358_maxdropamount.cs @@ -1,6 +1,4 @@ -using System; -using System.Collections.Generic; -using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Migrations; namespace NadekoBot.Migrations { diff --git a/src/NadekoBot/Migrations/20170616154106_crstartswith.cs b/src/NadekoBot/Migrations/20170616154106_crstartswith.cs index 53aa582f..0aeca5bb 100644 --- a/src/NadekoBot/Migrations/20170616154106_crstartswith.cs +++ b/src/NadekoBot/Migrations/20170616154106_crstartswith.cs @@ -1,6 +1,4 @@ -using System; -using System.Collections.Generic; -using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Migrations; namespace NadekoBot.Migrations { diff --git a/src/NadekoBot/Migrations/20170714021615_stream-role.cs b/src/NadekoBot/Migrations/20170714021615_stream-role.cs index 22f6abec..7cbce423 100644 --- a/src/NadekoBot/Migrations/20170714021615_stream-role.cs +++ b/src/NadekoBot/Migrations/20170714021615_stream-role.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using Microsoft.EntityFrameworkCore.Migrations; namespace NadekoBot.Migrations diff --git a/src/NadekoBot/Migrations/20170719023924_streamrole-kw-bl-wl.cs b/src/NadekoBot/Migrations/20170719023924_streamrole-kw-bl-wl.cs index d2ed14f1..7d18d3b3 100644 --- a/src/NadekoBot/Migrations/20170719023924_streamrole-kw-bl-wl.cs +++ b/src/NadekoBot/Migrations/20170719023924_streamrole-kw-bl-wl.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using Microsoft.EntityFrameworkCore.Migrations; namespace NadekoBot.Migrations diff --git a/src/NadekoBot/Migrations/NadekoSqliteContextModelSnapshot.cs b/src/NadekoBot/Migrations/NadekoSqliteContextModelSnapshot.cs index 6b57437a..df766d57 100644 --- a/src/NadekoBot/Migrations/NadekoSqliteContextModelSnapshot.cs +++ b/src/NadekoBot/Migrations/NadekoSqliteContextModelSnapshot.cs @@ -2,9 +2,7 @@ using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Metadata; -using Microsoft.EntityFrameworkCore.Migrations; using NadekoBot.Services.Database; -using NadekoBot.Services.Database.Models; namespace NadekoBot.Migrations { diff --git a/src/NadekoBot/Modules/Administration/LogCommands.cs b/src/NadekoBot/Modules/Administration/LogCommands.cs index 3ee5fc5a..daffd9f7 100644 --- a/src/NadekoBot/Modules/Administration/LogCommands.cs +++ b/src/NadekoBot/Modules/Administration/LogCommands.cs @@ -1,7 +1,6 @@ using Discord; using Discord.Commands; using NadekoBot.Extensions; -using NadekoBot.Modules.Permissions; using NadekoBot.Services; using NadekoBot.Services.Database.Models; using System; diff --git a/src/NadekoBot/Modules/Administration/SelfAssignedRolesCommands.cs b/src/NadekoBot/Modules/Administration/SelfAssignedRolesCommands.cs index 2d568519..271ef2c0 100644 --- a/src/NadekoBot/Modules/Administration/SelfAssignedRolesCommands.cs +++ b/src/NadekoBot/Modules/Administration/SelfAssignedRolesCommands.cs @@ -5,7 +5,6 @@ using NadekoBot.Extensions; using NadekoBot.Services; using NadekoBot.Services.Database.Models; using System; -using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; using System.Text; diff --git a/src/NadekoBot/Modules/Administration/SelfCommands.cs b/src/NadekoBot/Modules/Administration/SelfCommands.cs index 96f175d0..9edc1684 100644 --- a/src/NadekoBot/Modules/Administration/SelfCommands.cs +++ b/src/NadekoBot/Modules/Administration/SelfCommands.cs @@ -29,14 +29,16 @@ namespace NadekoBot.Modules.Administration private readonly DiscordSocketClient _client; private readonly IImagesService _images; private readonly MusicService _music; + private readonly IBotConfigProvider _bc; public SelfCommands(DbService db, DiscordSocketClient client, - MusicService music, IImagesService images) + MusicService music, IImagesService images, IBotConfigProvider bc) { _db = db; _client = client; _images = images; _music = music; + _bc = bc; } [NadekoCommand, Usage, Description, Aliases] @@ -179,9 +181,11 @@ namespace NadekoBot.Modules.Administration using (var uow = _db.UnitOfWork) { var config = uow.BotConfig.GetOrCreate(); - _service.ForwardDMs = config.ForwardMessages = !config.ForwardMessages; + config.ForwardMessages = !config.ForwardMessages; uow.Complete(); } + _bc.Reload(); + if (_service.ForwardDMs) await ReplyConfirmLocalized("fwdm_start").ConfigureAwait(false); else @@ -196,9 +200,11 @@ namespace NadekoBot.Modules.Administration { var config = uow.BotConfig.GetOrCreate(); lock (_locker) - _service.ForwardDMsToAllOwners = config.ForwardToAllOwners = !config.ForwardToAllOwners; + config.ForwardToAllOwners = !config.ForwardToAllOwners; uow.Complete(); } + _bc.Reload(); + if (_service.ForwardDMsToAllOwners) await ReplyConfirmLocalized("fwall_start").ConfigureAwait(false); else diff --git a/src/NadekoBot/Modules/Administration/Services/AdministrationService.cs b/src/NadekoBot/Modules/Administration/Services/AdministrationService.cs index 104f9174..57ec29a8 100644 --- a/src/NadekoBot/Modules/Administration/Services/AdministrationService.cs +++ b/src/NadekoBot/Modules/Administration/Services/AdministrationService.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; diff --git a/src/NadekoBot/Modules/Administration/Services/GameVoiceChannelService.cs b/src/NadekoBot/Modules/Administration/Services/GameVoiceChannelService.cs index 523b1dac..79b1f3ac 100644 --- a/src/NadekoBot/Modules/Administration/Services/GameVoiceChannelService.cs +++ b/src/NadekoBot/Modules/Administration/Services/GameVoiceChannelService.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; diff --git a/src/NadekoBot/Modules/Administration/Services/PruneService.cs b/src/NadekoBot/Modules/Administration/Services/PruneService.cs index fd13daa5..4f25e3c3 100644 --- a/src/NadekoBot/Modules/Administration/Services/PruneService.cs +++ b/src/NadekoBot/Modules/Administration/Services/PruneService.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; diff --git a/src/NadekoBot/Modules/Administration/Services/SelfService.cs b/src/NadekoBot/Modules/Administration/Services/SelfService.cs index dd0e6735..c5ad3ce9 100644 --- a/src/NadekoBot/Modules/Administration/Services/SelfService.cs +++ b/src/NadekoBot/Modules/Administration/Services/SelfService.cs @@ -8,7 +8,6 @@ using NadekoBot.Common; using NadekoBot.Common.ModuleBehaviors; using NadekoBot.Extensions; using NadekoBot.Services; -using NadekoBot.Services.Database.Models; using NadekoBot.Services.Impl; using NLog; @@ -16,8 +15,9 @@ namespace NadekoBot.Modules.Administration.Services { public class SelfService : ILateExecutor, INService { - public volatile bool ForwardDMs; - public volatile bool ForwardDMsToAllOwners; + //todo bot config + public bool ForwardDMs => _bc.BotConfig.ForwardMessages; + public bool ForwardDMsToAllOwners => _bc.BotConfig.ForwardToAllOwners; private readonly NadekoBot _bot; private readonly CommandHandler _cmdHandler; @@ -28,9 +28,10 @@ namespace NadekoBot.Modules.Administration.Services private readonly DiscordSocketClient _client; private readonly IBotCredentials _creds; private ImmutableArray> ownerChannels = new ImmutableArray>(); + private readonly IBotConfigProvider _bc; public SelfService(DiscordSocketClient client, NadekoBot bot, CommandHandler cmdHandler, DbService db, - BotConfig bc, ILocalization localization, NadekoStrings strings, IBotCredentials creds) + IBotConfigProvider bc, ILocalization localization, NadekoStrings strings, IBotCredentials creds) { _bot = bot; _cmdHandler = cmdHandler; @@ -40,15 +41,13 @@ namespace NadekoBot.Modules.Administration.Services _strings = strings; _client = client; _creds = creds; - - ForwardDMs = bc.ForwardMessages; - ForwardDMsToAllOwners = bc.ForwardToAllOwners; + _bc = bc; var _ = Task.Run(async () => { await bot.Ready.Task.ConfigureAwait(false); - foreach (var cmd in bc.StartupCommands) + foreach (var cmd in bc.BotConfig.StartupCommands) { await cmdHandler.ExecuteExternal(cmd.GuildId, cmd.ChannelId, cmd.CommandText); await Task.Delay(400).ConfigureAwait(false); diff --git a/src/NadekoBot/Modules/CustomReactions/Extensions/Extensions.cs b/src/NadekoBot/Modules/CustomReactions/Extensions/Extensions.cs index 25bbfce3..41d58d26 100644 --- a/src/NadekoBot/Modules/CustomReactions/Extensions/Extensions.cs +++ b/src/NadekoBot/Modules/CustomReactions/Extensions/Extensions.cs @@ -4,7 +4,6 @@ using Discord; using Discord.WebSocket; using NadekoBot.Extensions; using NadekoBot.Modules.CustomReactions.Services; -using NadekoBot.Services; using NadekoBot.Services.Database.Models; using System; using System.Collections.Generic; diff --git a/src/NadekoBot/Modules/CustomReactions/Services/CustomReactionsService.cs b/src/NadekoBot/Modules/CustomReactions/Services/CustomReactionsService.cs index 1241e664..cb595124 100644 --- a/src/NadekoBot/Modules/CustomReactions/Services/CustomReactionsService.cs +++ b/src/NadekoBot/Modules/CustomReactions/Services/CustomReactionsService.cs @@ -30,11 +30,11 @@ namespace NadekoBot.Modules.CustomReactions.Services private readonly DiscordSocketClient _client; private readonly PermissionService _perms; private readonly CommandHandler _cmd; - private readonly BotConfig _bc; + private readonly IBotConfigProvider _bc; private readonly NadekoStrings _strings; public CustomReactionsService(PermissionService perms, DbService db, NadekoStrings strings, - DiscordSocketClient client, CommandHandler cmd, BotConfig bc, IUnitOfWork uow) + DiscordSocketClient client, CommandHandler cmd, IBotConfigProvider bc, IUnitOfWork uow) { _log = LogManager.GetCurrentClassLogger(); _db = db; @@ -69,7 +69,7 @@ namespace NadekoBot.Modules.CustomReactions.Services var hasTarget = cr.Response.ToLowerInvariant().Contains("%target%"); var trigger = cr.TriggerWithContext(umsg, _client).Trim().ToLowerInvariant(); - return ((hasTarget && content.StartsWith(trigger + " ")) || (_bc.CustomReactionsStartWith && content.StartsWith(trigger + " ")) || content == trigger); + return ((hasTarget && content.StartsWith(trigger + " ")) || (_bc.BotConfig.CustomReactionsStartWith && content.StartsWith(trigger + " ")) || content == trigger); }).ToArray(); if (rs.Length != 0) @@ -90,7 +90,7 @@ namespace NadekoBot.Modules.CustomReactions.Services return false; var hasTarget = cr.Response.ToLowerInvariant().Contains("%target%"); var trigger = cr.TriggerWithContext(umsg, _client).Trim().ToLowerInvariant(); - return ((hasTarget && content.StartsWith(trigger + " ")) || (_bc.CustomReactionsStartWith && content.StartsWith(trigger + " ")) || content == trigger); + return ((hasTarget && content.StartsWith(trigger + " ")) || (_bc.BotConfig.CustomReactionsStartWith && content.StartsWith(trigger + " ")) || content == trigger); }).ToArray(); if (grs.Length == 0) return null; diff --git a/src/NadekoBot/Modules/Gambling/AnimalRacingCommands.cs b/src/NadekoBot/Modules/Gambling/AnimalRacingCommands.cs index 18411490..f322a517 100644 --- a/src/NadekoBot/Modules/Gambling/AnimalRacingCommands.cs +++ b/src/NadekoBot/Modules/Gambling/AnimalRacingCommands.cs @@ -3,7 +3,6 @@ using Discord.Commands; using Discord.WebSocket; using NadekoBot.Extensions; using NadekoBot.Services; -using NadekoBot.Services.Database.Models; using NLog; using System; using System.Collections.Concurrent; @@ -22,14 +21,14 @@ namespace NadekoBot.Modules.Gambling [Group] public class AnimalRacingCommands : NadekoSubmodule { - private readonly BotConfig _bc; + private readonly IBotConfigProvider _bc; private readonly CurrencyService _cs; private readonly DiscordSocketClient _client; public static ConcurrentDictionary AnimalRaces { get; } = new ConcurrentDictionary(); - public AnimalRacingCommands(BotConfig bc, CurrencyService cs, DiscordSocketClient client) + public AnimalRacingCommands(IBotConfigProvider bc, CurrencyService cs, DiscordSocketClient client) { _bc = bc; _cs = cs; @@ -82,7 +81,7 @@ namespace NadekoBot.Modules.Gambling private readonly Logger _log; private readonly ITextChannel _raceChannel; - private readonly BotConfig _bc; + private readonly IBotConfigProvider _bc; private readonly CurrencyService _cs; private readonly DiscordSocketClient _client; private readonly ILocalization _localization; @@ -90,7 +89,7 @@ namespace NadekoBot.Modules.Gambling public bool Started { get; private set; } - public AnimalRace(ulong serverId, ITextChannel channel, string prefix, BotConfig bc, + public AnimalRace(ulong serverId, ITextChannel channel, string prefix, IBotConfigProvider bc, CurrencyService cs, DiscordSocketClient client, ILocalization localization, NadekoStrings strings) { @@ -110,7 +109,7 @@ namespace NadekoBot.Modules.Gambling return; } - animals = new ConcurrentQueue(_bc.RaceAnimals.Select(ra => ra.Icon).Shuffle()); + animals = new ConcurrentQueue(_bc.BotConfig.RaceAnimals.Select(ra => ra.Icon).Shuffle()); var cancelSource = new CancellationTokenSource(); @@ -238,7 +237,7 @@ namespace NadekoBot.Modules.Gambling .ConfigureAwait(false); await _raceChannel.SendConfirmAsync(GetText("animal_race"), Format.Bold(GetText("animal_race_won_money", winner.User.Mention, - winner.Animal, wonAmount + _bc.CurrencySign))) + winner.Animal, wonAmount + _bc.BotConfig.CurrencySign))) .ConfigureAwait(false); } else @@ -295,13 +294,13 @@ namespace NadekoBot.Modules.Gambling if (amount > 0) if (!await _cs.RemoveAsync(u, "BetRace", amount, false).ConfigureAwait(false)) { - await _raceChannel.SendErrorAsync(GetText("not_enough", _bc.CurrencySign)).ConfigureAwait(false); + await _raceChannel.SendErrorAsync(GetText("not_enough", _bc.BotConfig.CurrencySign)).ConfigureAwait(false); return; } _participants.Add(p); string confStr; if (amount > 0) - confStr = GetText("animal_race_join_bet", u.Mention, p.Animal, amount + _bc.CurrencySign); + confStr = GetText("animal_race_join_bet", u.Mention, p.Animal, amount + _bc.BotConfig.CurrencySign); else confStr = GetText("animal_race_join", u.Mention, p.Animal); await _raceChannel.SendConfirmAsync(GetText("animal_race"), Format.Bold(confStr)).ConfigureAwait(false); diff --git a/src/NadekoBot/Modules/Gambling/Common/Cards.cs b/src/NadekoBot/Modules/Gambling/Common/Cards.cs index b51765d0..41c39834 100644 --- a/src/NadekoBot/Modules/Gambling/Common/Cards.cs +++ b/src/NadekoBot/Modules/Gambling/Common/Cards.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.Linq; using NadekoBot.Common; using NadekoBot.Extensions; -using NadekoBot.Services; namespace NadekoBot.Modules.Gambling.Common { diff --git a/src/NadekoBot/Modules/Gambling/CurrencyEventsCommands.cs b/src/NadekoBot/Modules/Gambling/CurrencyEventsCommands.cs index 7a4ecf42..a250a80d 100644 --- a/src/NadekoBot/Modules/Gambling/CurrencyEventsCommands.cs +++ b/src/NadekoBot/Modules/Gambling/CurrencyEventsCommands.cs @@ -3,7 +3,6 @@ using Discord.Commands; using NadekoBot.Extensions; using NadekoBot.Services; using System; -using System.Collections.Concurrent; using System.Linq; using System.Threading.Tasks; using Discord.WebSocket; @@ -12,7 +11,6 @@ using NadekoBot.Common; using NadekoBot.Common.Attributes; using NadekoBot.Common.Collections; using NLog; -using NadekoBot.Services.Database.Models; namespace NadekoBot.Modules.Gambling { @@ -37,10 +35,10 @@ namespace NadekoBot.Modules.Gambling private string _secretCode = string.Empty; private readonly DiscordSocketClient _client; - private readonly BotConfig _bc; + private readonly IBotConfigProvider _bc; private readonly CurrencyService _cs; - public CurrencyEventsCommands(DiscordSocketClient client, BotConfig bc, CurrencyService cs) + public CurrencyEventsCommands(DiscordSocketClient client, IBotConfigProvider bc, CurrencyService cs) { _client = client; _bc = bc; @@ -80,12 +78,12 @@ namespace NadekoBot.Modules.Gambling _secretCode += _sneakyGameStatusChars[rng.Next(0, _sneakyGameStatusChars.Length)]; } - await _client.SetGameAsync($"type {_secretCode} for " + _bc.CurrencyPluralName) + await _client.SetGameAsync($"type {_secretCode} for " + _bc.BotConfig.CurrencyPluralName) .ConfigureAwait(false); try { var title = GetText("sneakygamestatus_title"); - var desc = GetText("sneakygamestatus_desc", Format.Bold(100.ToString()) + _bc.CurrencySign, Format.Bold(num.ToString())); + var desc = GetText("sneakygamestatus_desc", Format.Bold(100.ToString()) + _bc.BotConfig.CurrencySign, Format.Bold(num.ToString())); await context.Channel.SendConfirmAsync(title, desc).ConfigureAwait(false); } catch @@ -133,7 +131,7 @@ namespace NadekoBot.Modules.Gambling amount = 100; var title = GetText("flowerreaction_title"); - var desc = GetText("flowerreaction_desc", "🌸", Format.Bold(amount.ToString()) + _bc.CurrencySign); + var desc = GetText("flowerreaction_desc", "🌸", Format.Bold(amount.ToString()) + _bc.BotConfig.CurrencySign); var footer = GetText("flowerreaction_footer", 24); var msg = await context.Channel.SendConfirmAsync(title, desc, footer: footer) diff --git a/src/NadekoBot/Modules/Gambling/FlipCoinCommands.cs b/src/NadekoBot/Modules/Gambling/FlipCoinCommands.cs index 21ff1411..96e69acf 100644 --- a/src/NadekoBot/Modules/Gambling/FlipCoinCommands.cs +++ b/src/NadekoBot/Modules/Gambling/FlipCoinCommands.cs @@ -2,7 +2,6 @@ using Discord; using Discord.Commands; using NadekoBot.Extensions; using NadekoBot.Services; -using NadekoBot.Services.Database.Models; using System; using System.Collections.Generic; using System.Threading.Tasks; @@ -19,12 +18,12 @@ namespace NadekoBot.Modules.Gambling public class FlipCoinCommands : NadekoSubmodule { private readonly IImagesService _images; - private readonly BotConfig _bc; + private readonly IBotConfigProvider _bc; private readonly CurrencyService _cs; private readonly NadekoRandom rng = new NadekoRandom(); - public FlipCoinCommands(IImagesService images, CurrencyService cs, BotConfig bc) + public FlipCoinCommands(IImagesService images, CurrencyService cs, IBotConfigProvider bc) { _images = images; _bc = bc; @@ -89,15 +88,15 @@ namespace NadekoBot.Modules.Gambling [NadekoCommand, Usage, Description, Aliases] public async Task Betflip(int amount, BetFlipGuess guess) { - if (amount < _bc.MinimumBetAmount) + if (amount < _bc.BotConfig.MinimumBetAmount) { - await ReplyErrorLocalized("min_bet_limit", _bc.MinimumBetAmount + _bc.CurrencySign).ConfigureAwait(false); + await ReplyErrorLocalized("min_bet_limit", _bc.BotConfig.MinimumBetAmount + _bc.BotConfig.CurrencySign).ConfigureAwait(false); return; } var removed = await _cs.RemoveAsync(Context.User, "Betflip Gamble", amount, false).ConfigureAwait(false); if (!removed) { - await ReplyErrorLocalized("not_enough", _bc.CurrencyPluralName).ConfigureAwait(false); + await ReplyErrorLocalized("not_enough", _bc.BotConfig.CurrencyPluralName).ConfigureAwait(false); return; } BetFlipGuess result; @@ -116,8 +115,8 @@ namespace NadekoBot.Modules.Gambling string str; if (guess == result) { - var toWin = (int)Math.Round(amount * _bc.BetflipMultiplier); - str = Context.User.Mention + " " + GetText("flip_guess", toWin + _bc.CurrencySign); + var toWin = (int)Math.Round(amount * _bc.BotConfig.BetflipMultiplier); + str = Context.User.Mention + " " + GetText("flip_guess", toWin + _bc.BotConfig.CurrencySign); await _cs.AddAsync(Context.User, "Betflip Gamble", toWin, false).ConfigureAwait(false); } else diff --git a/src/NadekoBot/Modules/Gambling/FlowerShopCommands.cs b/src/NadekoBot/Modules/Gambling/FlowerShopCommands.cs index 27620ce6..9c285906 100644 --- a/src/NadekoBot/Modules/Gambling/FlowerShopCommands.cs +++ b/src/NadekoBot/Modules/Gambling/FlowerShopCommands.cs @@ -20,7 +20,7 @@ namespace NadekoBot.Modules.Gambling [Group] public class FlowerShopCommands : NadekoSubmodule { - private readonly BotConfig _bc; + private readonly IBotConfigProvider _bc; private readonly DbService _db; private readonly CurrencyService _cs; private readonly DiscordSocketClient _client; @@ -35,7 +35,7 @@ namespace NadekoBot.Modules.Gambling List } - public FlowerShopCommands(BotConfig bc, DbService db, CurrencyService cs, DiscordSocketClient client) + public FlowerShopCommands(IBotConfigProvider bc, DbService db, CurrencyService cs, DiscordSocketClient client) { _db = db; _bc = bc; @@ -65,12 +65,12 @@ namespace NadekoBot.Modules.Gambling return new EmbedBuilder().WithErrorColor() .WithDescription(GetText("shop_none")); var embed = new EmbedBuilder().WithOkColor() - .WithTitle(GetText("shop", _bc.CurrencySign)); + .WithTitle(GetText("shop", _bc.BotConfig.CurrencySign)); for (int i = 0; i < theseEntries.Length; i++) { var entry = entries[i]; - embed.AddField(efb => efb.WithName($"#{curPage * 9 + i + 1} - {entry.Price}{_bc.CurrencySign}").WithValue(EntryToString(entry)).WithIsInline(true)); + embed.AddField(efb => efb.WithName($"#{curPage * 9 + i + 1} - {entry.Price}{_bc.BotConfig.CurrencySign}").WithValue(EntryToString(entry)).WithIsInline(true)); } return embed; }, entries.Count / 9, true); @@ -130,7 +130,7 @@ namespace NadekoBot.Modules.Gambling } else { - await ReplyErrorLocalized("not_enough", _bc.CurrencySign).ConfigureAwait(false); + await ReplyErrorLocalized("not_enough", _bc.BotConfig.CurrencySign).ConfigureAwait(false); return; } } @@ -186,7 +186,7 @@ namespace NadekoBot.Modules.Gambling } else { - await ReplyErrorLocalized("not_enough", _bc.CurrencySign).ConfigureAwait(false); + await ReplyErrorLocalized("not_enough", _bc.BotConfig.CurrencySign).ConfigureAwait(false); return; } } diff --git a/src/NadekoBot/Modules/Gambling/Gambling.cs b/src/NadekoBot/Modules/Gambling/Gambling.cs index 46ecc8c5..07de5bc8 100644 --- a/src/NadekoBot/Modules/Gambling/Gambling.cs +++ b/src/NadekoBot/Modules/Gambling/Gambling.cs @@ -13,15 +13,15 @@ namespace NadekoBot.Modules.Gambling { public partial class Gambling : NadekoTopLevelModule { - private readonly BotConfig _bc; + private readonly IBotConfigProvider _bc; private readonly DbService _db; private readonly CurrencyService _currency; - private string CurrencyName => _bc.CurrencyName; - private string CurrencyPluralName => _bc.CurrencyPluralName; - private string CurrencySign => _bc.CurrencySign; + private string CurrencyName => _bc.BotConfig.CurrencyName; + private string CurrencyPluralName => _bc.BotConfig.CurrencyPluralName; + private string CurrencySign => _bc.BotConfig.CurrencySign; - public Gambling(BotConfig bc, DbService db, CurrencyService currency) + public Gambling(IBotConfigProvider bc, DbService db, CurrencyService currency) { _bc = bc; _db = db; @@ -230,21 +230,21 @@ namespace NadekoBot.Modules.Gambling { if (rnd < 91) { - str += GetText("br_win", (amount * _bc.Betroll67Multiplier) + CurrencySign, 66); + str += GetText("br_win", (amount * _bc.BotConfig.Betroll67Multiplier) + CurrencySign, 66); await _currency.AddAsync(Context.User, "Betroll Gamble", - (int) (amount * _bc.Betroll67Multiplier), false).ConfigureAwait(false); + (int) (amount * _bc.BotConfig.Betroll67Multiplier), false).ConfigureAwait(false); } else if (rnd < 100) { - str += GetText("br_win", (amount * _bc.Betroll91Multiplier) + CurrencySign, 90); + str += GetText("br_win", (amount * _bc.BotConfig.Betroll91Multiplier) + CurrencySign, 90); await _currency.AddAsync(Context.User, "Betroll Gamble", - (int) (amount * _bc.Betroll91Multiplier), false).ConfigureAwait(false); + (int) (amount * _bc.BotConfig.Betroll91Multiplier), false).ConfigureAwait(false); } else { - str += GetText("br_win", (amount * _bc.Betroll100Multiplier) + CurrencySign, 100) + " 👑"; + str += GetText("br_win", (amount * _bc.BotConfig.Betroll100Multiplier) + CurrencySign, 100) + " 👑"; await _currency.AddAsync(Context.User, "Betroll Gamble", - (int) (amount * _bc.Betroll100Multiplier), false).ConfigureAwait(false); + (int) (amount * _bc.BotConfig.Betroll100Multiplier), false).ConfigureAwait(false); } } await Context.Channel.SendConfirmAsync(str).ConfigureAwait(false); diff --git a/src/NadekoBot/Modules/Gambling/SlotCommands.cs b/src/NadekoBot/Modules/Gambling/SlotCommands.cs index c51dc2c8..43cfb8d4 100644 --- a/src/NadekoBot/Modules/Gambling/SlotCommands.cs +++ b/src/NadekoBot/Modules/Gambling/SlotCommands.cs @@ -3,7 +3,6 @@ using Discord.Commands; using ImageSharp; using NadekoBot.Extensions; using NadekoBot.Services; -using NadekoBot.Services.Database.Models; using System; using System.Collections.Generic; using System.Linq; @@ -25,7 +24,7 @@ namespace NadekoBot.Modules.Gambling private static int _totalPaidOut; private static readonly HashSet _runningUsers = new HashSet(); - private readonly BotConfig _bc; + private readonly IBotConfigProvider _bc; private const int _alphaCutOut = byte.MaxValue / 3; @@ -36,7 +35,7 @@ namespace NadekoBot.Modules.Gambling private readonly IImagesService _images; private readonly CurrencyService _cs; - public SlotCommands(IImagesService images, BotConfig bc, CurrencyService cs) + public SlotCommands(IImagesService images, IBotConfigProvider bc, CurrencyService cs) { _images = images; _bc = bc; @@ -148,20 +147,20 @@ namespace NadekoBot.Modules.Gambling { if (amount < 1) { - await ReplyErrorLocalized("min_bet_limit", 1 + _bc.CurrencySign).ConfigureAwait(false); + await ReplyErrorLocalized("min_bet_limit", 1 + _bc.BotConfig.CurrencySign).ConfigureAwait(false); return; } const int maxAmount = 9999; if (amount > maxAmount) { - GetText("slot_maxbet", maxAmount + _bc.CurrencySign); - await ReplyErrorLocalized("max_bet_limit", maxAmount + _bc.CurrencySign).ConfigureAwait(false); + GetText("slot_maxbet", maxAmount + _bc.BotConfig.CurrencySign); + await ReplyErrorLocalized("max_bet_limit", maxAmount + _bc.BotConfig.CurrencySign).ConfigureAwait(false); return; } if (!await _cs.RemoveAsync(Context.User, "Slot Machine", amount, false)) { - await ReplyErrorLocalized("not_enough", _bc.CurrencySign).ConfigureAwait(false); + await ReplyErrorLocalized("not_enough", _bc.BotConfig.CurrencySign).ConfigureAwait(false); return; } Interlocked.Add(ref _totalBet, amount); @@ -202,7 +201,7 @@ namespace NadekoBot.Modules.Gambling var digit = printAmount % 10; using (var fs = _images.SlotNumbers[digit].ToStream()) using (var img = ImageSharp.Image.Load(fs)) - { + { bgImage.DrawImage(img, 100, default(Size), new Point(395 - n * 16, 462)); } n++; @@ -214,16 +213,16 @@ namespace NadekoBot.Modules.Gambling await _cs.AddAsync(Context.User, $"Slot Machine x{result.Multiplier}", amount * result.Multiplier, false); Interlocked.Add(ref _totalPaidOut, amount * result.Multiplier); if (result.Multiplier == 1) - msg = GetText("slot_single", _bc.CurrencySign, 1); + msg = GetText("slot_single", _bc.BotConfig.CurrencySign, 1); else if (result.Multiplier == 4) - msg = GetText("slot_two", _bc.CurrencySign, 4); + msg = GetText("slot_two", _bc.BotConfig.CurrencySign, 4); else if (result.Multiplier == 10) msg = GetText("slot_three", 10); else if (result.Multiplier == 30) msg = GetText("slot_jackpot", 30); } - await Context.Channel.SendFileAsync(bgImage.ToStream(), "result.png", Context.User.Mention + " " + msg + $"\n`{GetText("slot_bet")}:`{amount} `{GetText("slot_won")}:` {amount * result.Multiplier}{_bc.CurrencySign}").ConfigureAwait(false); + await Context.Channel.SendFileAsync(bgImage.ToStream(), "result.png", Context.User.Mention + " " + msg + $"\n`{GetText("slot_bet")}:`{amount} `{GetText("slot_won")}:` {amount * result.Multiplier}{_bc.BotConfig.CurrencySign}").ConfigureAwait(false); } } finally diff --git a/src/NadekoBot/Modules/Gambling/WaifuClaimCommands.cs b/src/NadekoBot/Modules/Gambling/WaifuClaimCommands.cs index 43113928..baa1781b 100644 --- a/src/NadekoBot/Modules/Gambling/WaifuClaimCommands.cs +++ b/src/NadekoBot/Modules/Gambling/WaifuClaimCommands.cs @@ -58,7 +58,7 @@ namespace NadekoBot.Modules.Gambling InsufficientAmount } - public WaifuClaimCommands(BotConfig bc, CurrencyService cs, DbService db) + public WaifuClaimCommands(IBotConfigProvider bc, CurrencyService cs, DbService db) { _bc = bc; _cs = cs; @@ -71,7 +71,7 @@ namespace NadekoBot.Modules.Gambling { if (amount < 50) { - await ReplyErrorLocalized("waifu_isnt_cheap", 50 + _bc.CurrencySign).ConfigureAwait(false); + await ReplyErrorLocalized("waifu_isnt_cheap", 50 + _bc.BotConfig.CurrencySign).ConfigureAwait(false); return; } @@ -173,14 +173,14 @@ namespace NadekoBot.Modules.Gambling } if (result == WaifuClaimResult.NotEnoughFunds) { - await ReplyErrorLocalized("not_enough", _bc.CurrencySign).ConfigureAwait(false); + await ReplyErrorLocalized("not_enough", _bc.BotConfig.CurrencySign).ConfigureAwait(false); return; } var msg = GetText("waifu_claimed", Format.Bold(target.ToString()), - amount + _bc.CurrencySign); + amount + _bc.BotConfig.CurrencySign); if (w.Affinity?.UserId == Context.User.Id) - msg += "\n" + GetText("waifu_fulfilled", target, w.Price + _bc.CurrencySign); + msg += "\n" + GetText("waifu_fulfilled", target, w.Price + _bc.BotConfig.CurrencySign); else msg = " " + msg; await Context.Channel.SendConfirmAsync(Context.User.Mention + msg).ConfigureAwait(false); @@ -258,11 +258,11 @@ namespace NadekoBot.Modules.Gambling if (result == DivorceResult.SucessWithPenalty) { - await ReplyConfirmLocalized("waifu_divorced_like", Format.Bold(w.Waifu.ToString()), amount + _bc.CurrencySign).ConfigureAwait(false); + await ReplyConfirmLocalized("waifu_divorced_like", Format.Bold(w.Waifu.ToString()), amount + _bc.BotConfig.CurrencySign).ConfigureAwait(false); } else if (result == DivorceResult.Success) { - await ReplyConfirmLocalized("waifu_divorced_notlike", amount + _bc.CurrencySign).ConfigureAwait(false); + await ReplyConfirmLocalized("waifu_divorced_notlike", amount + _bc.BotConfig.CurrencySign).ConfigureAwait(false); } else if (result == DivorceResult.NotYourWife) { @@ -278,7 +278,7 @@ namespace NadekoBot.Modules.Gambling } private static readonly TimeSpan _affinityLimit = TimeSpan.FromMinutes(30); - private readonly BotConfig _bc; + private readonly IBotConfigProvider _bc; private readonly CurrencyService _cs; private readonly DbService _db; @@ -406,7 +406,7 @@ namespace NadekoBot.Modules.Gambling var w = waifus[i]; var j = i; - embed.AddField(efb => efb.WithName("#" + ((page * 9) + j + 1) + " - " + w.Price + _bc.CurrencySign).WithValue(w.ToString()).WithIsInline(false)); + embed.AddField(efb => efb.WithName("#" + ((page * 9) + j + 1) + " - " + w.Price + _bc.BotConfig.CurrencySign).WithValue(w.ToString()).WithIsInline(false)); } await Context.Channel.EmbedAsync(embed).ConfigureAwait(false); diff --git a/src/NadekoBot/Modules/Games/AcropobiaCommands.cs b/src/NadekoBot/Modules/Games/AcropobiaCommands.cs index f905a4eb..e66578fa 100644 --- a/src/NadekoBot/Modules/Games/AcropobiaCommands.cs +++ b/src/NadekoBot/Modules/Games/AcropobiaCommands.cs @@ -2,7 +2,6 @@ using Discord.Commands; using Discord.WebSocket; using NadekoBot.Extensions; -using NadekoBot.Services; using NLog; using System; using System.Collections.Concurrent; diff --git a/src/NadekoBot/Modules/Games/Common/ChatterBotSession.cs b/src/NadekoBot/Modules/Games/Common/ChatterBotSession.cs index 4c3ad823..256d3d05 100644 --- a/src/NadekoBot/Modules/Games/Common/ChatterBotSession.cs +++ b/src/NadekoBot/Modules/Games/Common/ChatterBotSession.cs @@ -2,7 +2,6 @@ using System.Threading.Tasks; using NadekoBot.Common; using NadekoBot.Extensions; -using NadekoBot.Services; using Newtonsoft.Json; namespace NadekoBot.Modules.Games.Common diff --git a/src/NadekoBot/Modules/Games/Common/Hangman/HangmanGame.cs b/src/NadekoBot/Modules/Games/Common/Hangman/HangmanGame.cs index 540821b7..f0f5c820 100644 --- a/src/NadekoBot/Modules/Games/Common/Hangman/HangmanGame.cs +++ b/src/NadekoBot/Modules/Games/Common/Hangman/HangmanGame.cs @@ -7,7 +7,6 @@ using Discord; using Discord.WebSocket; using NadekoBot.Common; using NadekoBot.Extensions; -using NadekoBot.Services; using Newtonsoft.Json; using NLog; diff --git a/src/NadekoBot/Modules/Games/Common/Poll.cs b/src/NadekoBot/Modules/Games/Common/Poll.cs index c14a490d..816e2101 100644 --- a/src/NadekoBot/Modules/Games/Common/Poll.cs +++ b/src/NadekoBot/Modules/Games/Common/Poll.cs @@ -7,7 +7,6 @@ using System.Threading.Tasks; using Discord; using Discord.WebSocket; using NadekoBot.Extensions; -using NadekoBot.Services; using NadekoBot.Services.Impl; namespace NadekoBot.Modules.Games.Common diff --git a/src/NadekoBot/Modules/Games/Common/Trivia/TriviaGame.cs b/src/NadekoBot/Modules/Games/Common/Trivia/TriviaGame.cs index 90effc81..77714d58 100644 --- a/src/NadekoBot/Modules/Games/Common/Trivia/TriviaGame.cs +++ b/src/NadekoBot/Modules/Games/Common/Trivia/TriviaGame.cs @@ -10,7 +10,6 @@ using Discord.Net; using Discord.WebSocket; using NadekoBot.Extensions; using NadekoBot.Services; -using NadekoBot.Services.Database.Models; using NadekoBot.Services.Impl; using NLog; @@ -22,7 +21,7 @@ namespace NadekoBot.Modules.Games.Common.Trivia private readonly Logger _log; private readonly NadekoStrings _strings; private readonly DiscordSocketClient _client; - private readonly BotConfig _bc; + private readonly IBotConfigProvider _bc; private readonly CurrencyService _cs; public IGuild Guild { get; } @@ -44,7 +43,7 @@ namespace NadekoBot.Modules.Games.Common.Trivia public int WinRequirement { get; } - public TriviaGame(NadekoStrings strings, DiscordSocketClient client, BotConfig bc, + public TriviaGame(NadekoStrings strings, DiscordSocketClient client, IBotConfigProvider bc, CurrencyService cs, IGuild guild, ITextChannel channel, bool showHints, int winReq, bool isPokemon) { @@ -232,7 +231,7 @@ namespace NadekoBot.Modules.Games.Common.Trivia { // ignored } - var reward = _bc.TriviaCurrencyReward; + var reward = _bc.BotConfig.TriviaCurrencyReward; if (reward > 0) await _cs.AddAsync(guildUser, "Won trivia", reward, true).ConfigureAwait(false); return; diff --git a/src/NadekoBot/Modules/Games/Common/Trivia/TriviaQuestionPool.cs b/src/NadekoBot/Modules/Games/Common/Trivia/TriviaQuestionPool.cs index 22d38fe9..2307c209 100644 --- a/src/NadekoBot/Modules/Games/Common/Trivia/TriviaQuestionPool.cs +++ b/src/NadekoBot/Modules/Games/Common/Trivia/TriviaQuestionPool.cs @@ -5,7 +5,6 @@ using System.IO; using System.Linq; using NadekoBot.Common; using NadekoBot.Extensions; -using NadekoBot.Services; using Newtonsoft.Json; namespace NadekoBot.Modules.Games.Common.Trivia diff --git a/src/NadekoBot/Modules/Games/Common/TypingGame.cs b/src/NadekoBot/Modules/Games/Common/TypingGame.cs index c07484bc..f2d2980a 100644 --- a/src/NadekoBot/Modules/Games/Common/TypingGame.cs +++ b/src/NadekoBot/Modules/Games/Common/TypingGame.cs @@ -8,7 +8,6 @@ using Discord.WebSocket; using NadekoBot.Common; using NadekoBot.Extensions; using NadekoBot.Modules.Games.Services; -using NadekoBot.Services; using NLog; namespace NadekoBot.Modules.Games.Common diff --git a/src/NadekoBot/Modules/Games/PlantAndPickCommands.cs b/src/NadekoBot/Modules/Games/PlantAndPickCommands.cs index 19b792fc..1ebb7357 100644 --- a/src/NadekoBot/Modules/Games/PlantAndPickCommands.cs +++ b/src/NadekoBot/Modules/Games/PlantAndPickCommands.cs @@ -25,11 +25,11 @@ namespace NadekoBot.Modules.Games public class PlantPickCommands : NadekoSubmodule { private readonly CurrencyService _cs; - private readonly BotConfig _bc; + private readonly IBotConfigProvider _bc; private readonly GamesService _games; private readonly DbService _db; - public PlantPickCommands(BotConfig bc, CurrencyService cs, GamesService games, + public PlantPickCommands(IBotConfigProvider bc, CurrencyService cs, GamesService games, DbService db) { _bc = bc; @@ -54,8 +54,8 @@ namespace NadekoBot.Modules.Games await Task.WhenAll(msgs.Where(m => m != null).Select(toDelete => toDelete.DeleteAsync())).ConfigureAwait(false); - await _cs.AddAsync((IGuildUser)Context.User, $"Picked {_bc.CurrencyPluralName}", msgs.Count, false).ConfigureAwait(false); - var msg = await ReplyConfirmLocalized("picked", msgs.Count + _bc.CurrencySign) + await _cs.AddAsync((IGuildUser)Context.User, $"Picked {_bc.BotConfig.CurrencyPluralName}", msgs.Count, false).ConfigureAwait(false); + var msg = await ReplyConfirmLocalized("picked", msgs.Count + _bc.BotConfig.CurrencySign) .ConfigureAwait(false); msg.DeleteAfter(10); } @@ -67,10 +67,10 @@ namespace NadekoBot.Modules.Games if (amount < 1) return; - var removed = await _cs.RemoveAsync((IGuildUser)Context.User, $"Planted a {_bc.CurrencyName}", amount, false).ConfigureAwait(false); + var removed = await _cs.RemoveAsync((IGuildUser)Context.User, $"Planted a {_bc.BotConfig.CurrencyName}", amount, false).ConfigureAwait(false); if (!removed) { - await ReplyErrorLocalized("not_enough", _bc.CurrencySign).ConfigureAwait(false); + await ReplyErrorLocalized("not_enough", _bc.BotConfig.CurrencySign).ConfigureAwait(false); return; } @@ -79,7 +79,7 @@ namespace NadekoBot.Modules.Games //todo 81 upload all currency images to transfer.sh and use that one as cdn var msgToSend = GetText("planted", Format.Bold(Context.User.ToString()), - amount + _bc.CurrencySign, + amount + _bc.BotConfig.CurrencySign, Prefix); if (amount > 1) diff --git a/src/NadekoBot/Modules/Games/Services/GamesService.cs b/src/NadekoBot/Modules/Games/Services/GamesService.cs index 6381156b..f09c606b 100644 --- a/src/NadekoBot/Modules/Games/Services/GamesService.cs +++ b/src/NadekoBot/Modules/Games/Services/GamesService.cs @@ -22,7 +22,7 @@ namespace NadekoBot.Modules.Games.Services { public class GamesService : INService { - private readonly BotConfig _bc; + private readonly IBotConfigProvider _bc; public readonly ConcurrentDictionary GirlRatings = new ConcurrentDictionary(); public readonly ImmutableArray EightBallResponses; @@ -38,7 +38,7 @@ namespace NadekoBot.Modules.Games.Services public List TypingArticles { get; } = new List(); - public GamesService(DiscordSocketClient client, BotConfig bc, IEnumerable gcs, + public GamesService(DiscordSocketClient client, IBotConfigProvider bc, IEnumerable gcs, NadekoStrings strings, IImagesService images, CommandHandler cmdHandler) { _bc = bc; @@ -49,7 +49,7 @@ namespace NadekoBot.Modules.Games.Services _log = LogManager.GetCurrentClassLogger(); //8ball - EightBallResponses = _bc.EightBallResponses.Select(ebr => ebr.Text).ToImmutableArray(); + EightBallResponses = _bc.BotConfig.EightBallResponses.Select(ebr => ebr.Text).ToImmutableArray(); //girl ratings _t = new Timer((_) => @@ -122,14 +122,14 @@ namespace NadekoBot.Modules.Games.Services var lastGeneration = LastGenerations.GetOrAdd(channel.Id, DateTime.MinValue); var rng = new NadekoRandom(); - if (DateTime.UtcNow - TimeSpan.FromSeconds(_bc.CurrencyGenerationCooldown) < lastGeneration) //recently generated in this channel, don't generate again + if (DateTime.UtcNow - TimeSpan.FromSeconds(_bc.BotConfig.CurrencyGenerationCooldown) < lastGeneration) //recently generated in this channel, don't generate again return; - var num = rng.Next(1, 101) + _bc.CurrencyGenerationChance * 100; + var num = rng.Next(1, 101) + _bc.BotConfig.CurrencyGenerationChance * 100; if (num > 100 && LastGenerations.TryUpdate(channel.Id, DateTime.UtcNow, lastGeneration)) { - var dropAmount = _bc.CurrencyDropAmount; - var dropAmountMax = _bc.CurrencyDropAmountMax; + var dropAmount = _bc.BotConfig.CurrencyDropAmount; + var dropAmountMax = _bc.BotConfig.CurrencyDropAmountMax; if (dropAmountMax != null && dropAmountMax > dropAmount) dropAmount = new NadekoRandom().Next(dropAmount, dropAmountMax.Value + 1); @@ -139,9 +139,9 @@ namespace NadekoBot.Modules.Games.Services var msgs = new IUserMessage[dropAmount]; var prefix = _cmdHandler.GetPrefix(channel.Guild.Id); var toSend = dropAmount == 1 - ? GetText(channel, "curgen_sn", _bc.CurrencySign) + ? GetText(channel, "curgen_sn", _bc.BotConfig.CurrencySign) + " " + GetText(channel, "pick_sn", prefix) - : GetText(channel, "curgen_pl", dropAmount, _bc.CurrencySign) + : GetText(channel, "curgen_pl", dropAmount, _bc.BotConfig.CurrencySign) + " " + GetText(channel, "pick_pl", prefix); var file = GetRandomCurrencyImage(); using (var fileStream = file.Data.ToStream()) diff --git a/src/NadekoBot/Modules/Games/TicTacToeCommands.cs b/src/NadekoBot/Modules/Games/TicTacToeCommands.cs index 3f2237f7..539d9cdd 100644 --- a/src/NadekoBot/Modules/Games/TicTacToeCommands.cs +++ b/src/NadekoBot/Modules/Games/TicTacToeCommands.cs @@ -2,7 +2,6 @@ using Discord.Commands; using Discord.WebSocket; using NadekoBot.Extensions; -using NadekoBot.Services; using System; using System.Collections.Generic; using System.Text; diff --git a/src/NadekoBot/Modules/Games/TriviaCommands.cs b/src/NadekoBot/Modules/Games/TriviaCommands.cs index 86560835..3360c87f 100644 --- a/src/NadekoBot/Modules/Games/TriviaCommands.cs +++ b/src/NadekoBot/Modules/Games/TriviaCommands.cs @@ -3,13 +3,11 @@ using Discord.Commands; using Discord.WebSocket; using NadekoBot.Extensions; using NadekoBot.Services; -using NadekoBot.Services.Database.Models; using System.Collections.Concurrent; using System.Threading.Tasks; using NadekoBot.Common.Attributes; using NadekoBot.Modules.Games.Common.Trivia; - namespace NadekoBot.Modules.Games { public partial class Games @@ -19,11 +17,11 @@ namespace NadekoBot.Modules.Games { private readonly CurrencyService _cs; private readonly DiscordSocketClient _client; - private readonly BotConfig _bc; + private readonly IBotConfigProvider _bc; public static ConcurrentDictionary RunningTrivias { get; } = new ConcurrentDictionary(); - public TriviaCommands(DiscordSocketClient client, BotConfig bc, CurrencyService cs) + public TriviaCommands(DiscordSocketClient client, IBotConfigProvider bc, CurrencyService cs) { _cs = cs; _client = client; diff --git a/src/NadekoBot/Modules/Help/Help.cs b/src/NadekoBot/Modules/Help/Help.cs index a1d6cb0f..0f1376e0 100644 --- a/src/NadekoBot/Modules/Help/Help.cs +++ b/src/NadekoBot/Modules/Help/Help.cs @@ -9,7 +9,6 @@ using System.IO; using System.Text; using System.Collections.Generic; using NadekoBot.Common.Attributes; -using NadekoBot.Services.Database.Models; using NadekoBot.Modules.Help.Services; using NadekoBot.Modules.Permissions.Services; @@ -20,14 +19,14 @@ namespace NadekoBot.Modules.Help public const string PatreonUrl = "https://patreon.com/nadekobot"; public const string PaypalUrl = "https://paypal.me/Kwoth"; private readonly IBotCredentials _creds; - private readonly BotConfig _config; + private readonly IBotConfigProvider _config; private readonly CommandService _cmds; private readonly GlobalPermissionService _perms; - public string HelpString => String.Format(_config.HelpString, _creds.ClientId, Prefix); - public string DMHelpString => _config.DMHelpString; + public string HelpString => String.Format(_config.BotConfig.HelpString, _creds.ClientId, Prefix); + public string DMHelpString => _config.BotConfig.DMHelpString; - public Help(IBotCredentials creds, GlobalPermissionService perms, BotConfig config, CommandService cmds) + public Help(IBotCredentials creds, GlobalPermissionService perms, IBotConfigProvider config, CommandService cmds) { _creds = creds; _config = config; diff --git a/src/NadekoBot/Modules/Help/Services/HelpService.cs b/src/NadekoBot/Modules/Help/Services/HelpService.cs index 2e5cd937..88220154 100644 --- a/src/NadekoBot/Modules/Help/Services/HelpService.cs +++ b/src/NadekoBot/Modules/Help/Services/HelpService.cs @@ -1,5 +1,4 @@ -using NadekoBot.Services.Database.Models; -using System.Threading.Tasks; +using System.Threading.Tasks; using Discord; using Discord.WebSocket; using System; @@ -15,11 +14,11 @@ namespace NadekoBot.Modules.Help.Services { public class HelpService : ILateExecutor, INService { - private readonly BotConfig _bc; + private readonly IBotConfigProvider _bc; private readonly CommandHandler _ch; private readonly NadekoStrings _strings; - public HelpService(BotConfig bc, CommandHandler ch, NadekoStrings strings) + public HelpService(IBotConfigProvider bc, CommandHandler ch, NadekoStrings strings) { _bc = bc; _ch = ch; @@ -31,7 +30,7 @@ namespace NadekoBot.Modules.Help.Services try { if(guild == null) - await msg.Channel.SendMessageAsync(_bc.DMHelpString).ConfigureAwait(false); + await msg.Channel.SendMessageAsync(_bc.BotConfig.DMHelpString).ConfigureAwait(false); } catch (Exception) { diff --git a/src/NadekoBot/Modules/Music/Common/Exceptions/NotInVoiceChannelException.cs b/src/NadekoBot/Modules/Music/Common/Exceptions/NotInVoiceChannelException.cs index b4fbe8a4..7e2a7706 100644 --- a/src/NadekoBot/Modules/Music/Common/Exceptions/NotInVoiceChannelException.cs +++ b/src/NadekoBot/Modules/Music/Common/Exceptions/NotInVoiceChannelException.cs @@ -2,13 +2,8 @@ namespace NadekoBot.Modules.Music.Common.Exceptions { - // todo use this public class NotInVoiceChannelException : Exception { - public NotInVoiceChannelException(string message) : base(message) - { - } - public NotInVoiceChannelException() : base("You're not in the voice channel on this server.") { } } } diff --git a/src/NadekoBot/Modules/Music/Common/MusicPlayer.cs b/src/NadekoBot/Modules/Music/Common/MusicPlayer.cs index 49327512..00e38c98 100644 --- a/src/NadekoBot/Modules/Music/Common/MusicPlayer.cs +++ b/src/NadekoBot/Modules/Music/Common/MusicPlayer.cs @@ -5,7 +5,6 @@ using System.Threading; using System.Threading.Tasks; using NLog; using System.Linq; -using System.Collections.Concurrent; using NadekoBot.Extensions; using System.Diagnostics; using NadekoBot.Common.Collections; diff --git a/src/NadekoBot/Modules/Music/Common/MusicQueue.cs b/src/NadekoBot/Modules/Music/Common/MusicQueue.cs index bcdc59f7..4927b21e 100644 --- a/src/NadekoBot/Modules/Music/Common/MusicQueue.cs +++ b/src/NadekoBot/Modules/Music/Common/MusicQueue.cs @@ -1,10 +1,8 @@ using NadekoBot.Extensions; using NadekoBot.Modules.Music.Common.Exceptions; -using NadekoBot.Services; using System; using System.Collections.Generic; using System.Linq; -using System.Text; using System.Threading.Tasks; using NadekoBot.Common; diff --git a/src/NadekoBot/Modules/Music/Music.cs b/src/NadekoBot/Modules/Music/Music.cs index 6b337f41..6b5499f3 100644 --- a/src/NadekoBot/Modules/Music/Music.cs +++ b/src/NadekoBot/Modules/Music/Music.cs @@ -8,7 +8,6 @@ using System.Linq; using NadekoBot.Extensions; using System.Collections.Generic; using NadekoBot.Services.Database.Models; -using System.Collections.Concurrent; using System.IO; using System.Net.Http; using NadekoBot.Common; diff --git a/src/NadekoBot/Modules/Music/Services/MusicService.cs b/src/NadekoBot/Modules/Music/Services/MusicService.cs index c585abb1..6b7fffc5 100644 --- a/src/NadekoBot/Modules/Music/Services/MusicService.cs +++ b/src/NadekoBot/Modules/Music/Services/MusicService.cs @@ -1,5 +1,4 @@ -using System; -using System.Collections.Concurrent; +using System.Collections.Concurrent; using System.Linq; using System.Threading.Tasks; using Discord; @@ -88,7 +87,7 @@ namespace NadekoBot.Modules.Music.Services { await textCh.SendErrorAsync(GetText("must_be_in_voice")).ConfigureAwait(false); } - throw new ArgumentException(nameof(voiceCh)); + throw new NotInVoiceChannelException(); } _log.Info("Get or add"); return MusicPlayers.GetOrAdd(guildId, _ => diff --git a/src/NadekoBot/Modules/NSFW/NSFW.cs b/src/NadekoBot/Modules/NSFW/NSFW.cs index 238071d9..686c8c33 100644 --- a/src/NadekoBot/Modules/NSFW/NSFW.cs +++ b/src/NadekoBot/Modules/NSFW/NSFW.cs @@ -4,7 +4,6 @@ using Newtonsoft.Json.Linq; using System; using System.Linq; using System.Threading.Tasks; -using NadekoBot.Services; using System.Net.Http; using NadekoBot.Extensions; using System.Threading; diff --git a/src/NadekoBot/Modules/NadekoModule.cs b/src/NadekoBot/Modules/NadekoModule.cs index 62ad29f9..f6f06afd 100644 --- a/src/NadekoBot/Modules/NadekoModule.cs +++ b/src/NadekoBot/Modules/NadekoModule.cs @@ -5,7 +5,6 @@ using NadekoBot.Services; using NLog; using System.Globalization; using System.Threading.Tasks; -using System; using Discord.WebSocket; using NadekoBot.Services.Impl; diff --git a/src/NadekoBot/Modules/Permissions/Common/PermissionsCollection.cs b/src/NadekoBot/Modules/Permissions/Common/PermissionsCollection.cs index cf4e41b2..0f6ab6b9 100644 --- a/src/NadekoBot/Modules/Permissions/Common/PermissionsCollection.cs +++ b/src/NadekoBot/Modules/Permissions/Common/PermissionsCollection.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using NadekoBot.Common; using NadekoBot.Common.Collections; using NadekoBot.Services.Database.Models; diff --git a/src/NadekoBot/Modules/Permissions/FilterCommands.cs b/src/NadekoBot/Modules/Permissions/FilterCommands.cs index 79b068fd..22e22c82 100644 --- a/src/NadekoBot/Modules/Permissions/FilterCommands.cs +++ b/src/NadekoBot/Modules/Permissions/FilterCommands.cs @@ -4,7 +4,6 @@ using Discord.WebSocket; using Microsoft.EntityFrameworkCore; using NadekoBot.Extensions; using NadekoBot.Services; -using System.Collections.Concurrent; using System.Linq; using System.Threading.Tasks; using NadekoBot.Common.Attributes; diff --git a/src/NadekoBot/Modules/Permissions/Permissions.cs b/src/NadekoBot/Modules/Permissions/Permissions.cs index e451eea4..ce7f6fa9 100644 --- a/src/NadekoBot/Modules/Permissions/Permissions.cs +++ b/src/NadekoBot/Modules/Permissions/Permissions.cs @@ -7,7 +7,6 @@ using Discord; using NadekoBot.Services.Database.Models; using System.Collections.Generic; using Discord.WebSocket; -using NadekoBot.Common; using NadekoBot.Common.Attributes; using NadekoBot.Common.TypeReaders; using NadekoBot.Common.TypeReaders.Models; diff --git a/src/NadekoBot/Modules/Permissions/ResetPermissionsCommands.cs b/src/NadekoBot/Modules/Permissions/ResetPermissionsCommands.cs index d11f523f..91e4a9c2 100644 --- a/src/NadekoBot/Modules/Permissions/ResetPermissionsCommands.cs +++ b/src/NadekoBot/Modules/Permissions/ResetPermissionsCommands.cs @@ -1,7 +1,5 @@ using Discord; using Discord.Commands; -using NadekoBot.Services; -using NadekoBot.Services.Database.Models; using System.Threading.Tasks; using NadekoBot.Common.Attributes; using NadekoBot.Modules.Permissions.Services; diff --git a/src/NadekoBot/Modules/Permissions/Services/BlacklistService.cs b/src/NadekoBot/Modules/Permissions/Services/BlacklistService.cs index 0c06b4b1..3c2128dc 100644 --- a/src/NadekoBot/Modules/Permissions/Services/BlacklistService.cs +++ b/src/NadekoBot/Modules/Permissions/Services/BlacklistService.cs @@ -1,5 +1,4 @@ -using System.Collections.Concurrent; -using System.Linq; +using System.Linq; using System.Threading.Tasks; using Discord; using NadekoBot.Common.Collections; @@ -15,9 +14,9 @@ namespace NadekoBot.Modules.Permissions.Services public ConcurrentHashSet BlacklistedGuilds { get; } public ConcurrentHashSet BlacklistedChannels { get; } - public BlacklistService(BotConfig bc) + public BlacklistService(IBotConfigProvider bc) { - var blacklist = bc.Blacklist; + var blacklist = bc.BotConfig.Blacklist; BlacklistedUsers = new ConcurrentHashSet(blacklist.Where(bi => bi.Type == BlacklistType.User).Select(c => c.ItemId)); BlacklistedGuilds = new ConcurrentHashSet(blacklist.Where(bi => bi.Type == BlacklistType.Server).Select(c => c.ItemId)); BlacklistedChannels = new ConcurrentHashSet(blacklist.Where(bi => bi.Type == BlacklistType.Channel).Select(c => c.ItemId)); diff --git a/src/NadekoBot/Modules/Permissions/Services/GlobalPermissionService.cs b/src/NadekoBot/Modules/Permissions/Services/GlobalPermissionService.cs index 0bd2267e..18d5337f 100644 --- a/src/NadekoBot/Modules/Permissions/Services/GlobalPermissionService.cs +++ b/src/NadekoBot/Modules/Permissions/Services/GlobalPermissionService.cs @@ -1,12 +1,10 @@ -using System.Collections.Concurrent; -using System.Linq; +using System.Linq; using System.Threading.Tasks; using Discord; using Discord.WebSocket; using NadekoBot.Common.Collections; using NadekoBot.Common.ModuleBehaviors; using NadekoBot.Services; -using NadekoBot.Services.Database.Models; namespace NadekoBot.Modules.Permissions.Services { @@ -15,10 +13,10 @@ namespace NadekoBot.Modules.Permissions.Services public readonly ConcurrentHashSet BlockedModules; public readonly ConcurrentHashSet BlockedCommands; - public GlobalPermissionService(BotConfig bc) + public GlobalPermissionService(IBotConfigProvider bc) { - BlockedModules = new ConcurrentHashSet(bc.BlockedModules.Select(x => x.Name)); - BlockedCommands = new ConcurrentHashSet(bc.BlockedCommands.Select(x => x.Name)); + BlockedModules = new ConcurrentHashSet(bc.BotConfig.BlockedModules.Select(x => x.Name)); + BlockedCommands = new ConcurrentHashSet(bc.BotConfig.BlockedCommands.Select(x => x.Name)); } public async Task TryBlockLate(DiscordSocketClient client, IUserMessage msg, IGuild guild, IMessageChannel channel, IUser user, string moduleName, string commandName) diff --git a/src/NadekoBot/Modules/Pokemon/Pokemon.cs b/src/NadekoBot/Modules/Pokemon/Pokemon.cs index ba62465f..0389e1b0 100644 --- a/src/NadekoBot/Modules/Pokemon/Pokemon.cs +++ b/src/NadekoBot/Modules/Pokemon/Pokemon.cs @@ -16,10 +16,10 @@ namespace NadekoBot.Modules.Pokemon public class Pokemon : NadekoTopLevelModule { private readonly DbService _db; - private readonly BotConfig _bc; + private readonly IBotConfigProvider _bc; private readonly CurrencyService _cs; - public Pokemon(DbService db, BotConfig bc, CurrencyService cs) + public Pokemon(DbService db, IBotConfigProvider bc, CurrencyService cs) { _db = db; _bc = bc; @@ -230,7 +230,7 @@ namespace NadekoBot.Modules.Pokemon { if (!await _cs.RemoveAsync(user, $"Poke-Heal {target}", amount, true).ConfigureAwait(false)) { - await ReplyErrorLocalized("no_currency", _bc.CurrencySign).ConfigureAwait(false); + await ReplyErrorLocalized("no_currency", _bc.BotConfig.CurrencySign).ConfigureAwait(false); return; } } @@ -243,13 +243,13 @@ namespace NadekoBot.Modules.Pokemon _service.Stats[targetUser.Id].Hp = (targetStats.MaxHp / 2); if (target == "yourself") { - await ReplyConfirmLocalized("revive_yourself", _bc.CurrencySign).ConfigureAwait(false); + await ReplyConfirmLocalized("revive_yourself", _bc.BotConfig.CurrencySign).ConfigureAwait(false); return; } - await ReplyConfirmLocalized("revive_other", Format.Bold(targetUser.ToString()), _bc.CurrencySign).ConfigureAwait(false); + await ReplyConfirmLocalized("revive_other", Format.Bold(targetUser.ToString()), _bc.BotConfig.CurrencySign).ConfigureAwait(false); } - await ReplyConfirmLocalized("healed", Format.Bold(targetUser.ToString()), _bc.CurrencySign).ConfigureAwait(false); + await ReplyConfirmLocalized("healed", Format.Bold(targetUser.ToString()), _bc.BotConfig.CurrencySign).ConfigureAwait(false); } else { @@ -296,7 +296,7 @@ namespace NadekoBot.Modules.Pokemon { if (!await _cs.RemoveAsync(user, $"{user} change type to {typeTargeted}", amount, true).ConfigureAwait(false)) { - await ReplyErrorLocalized("no_currency", _bc.CurrencySign).ConfigureAwait(false); + await ReplyErrorLocalized("no_currency", _bc.BotConfig.CurrencySign).ConfigureAwait(false); return; } } @@ -330,7 +330,7 @@ namespace NadekoBot.Modules.Pokemon //Now for the response await ReplyConfirmLocalized("settype_success", targetType, - _bc.CurrencySign).ConfigureAwait(false); + _bc.BotConfig.CurrencySign).ConfigureAwait(false); } } } diff --git a/src/NadekoBot/Modules/Searches/Common/SearchImageCacher.cs b/src/NadekoBot/Modules/Searches/Common/SearchImageCacher.cs index bf2582e7..cc446af0 100644 --- a/src/NadekoBot/Modules/Searches/Common/SearchImageCacher.cs +++ b/src/NadekoBot/Modules/Searches/Common/SearchImageCacher.cs @@ -8,7 +8,6 @@ using System.Threading.Tasks; using System.Xml; using NadekoBot.Common; using NadekoBot.Extensions; -using NadekoBot.Services; using Newtonsoft.Json; using NLog; diff --git a/src/NadekoBot/Modules/Searches/JokeCommands.cs b/src/NadekoBot/Modules/Searches/JokeCommands.cs index 83dbbd76..3565b482 100644 --- a/src/NadekoBot/Modules/Searches/JokeCommands.cs +++ b/src/NadekoBot/Modules/Searches/JokeCommands.cs @@ -2,7 +2,6 @@ using Discord.Commands; using NadekoBot.Extensions; using NadekoBot.Modules.Searches.Services; -using NadekoBot.Services; using Newtonsoft.Json.Linq; using System.Linq; using System.Net.Http; diff --git a/src/NadekoBot/Modules/Searches/LoLCommands.cs b/src/NadekoBot/Modules/Searches/LoLCommands.cs index 9d1e58cf..78b6928c 100644 --- a/src/NadekoBot/Modules/Searches/LoLCommands.cs +++ b/src/NadekoBot/Modules/Searches/LoLCommands.cs @@ -1,7 +1,5 @@ using Discord; using NadekoBot.Extensions; -using NadekoBot.Services; -using Newtonsoft.Json; using Newtonsoft.Json.Linq; using System; using System.Collections.Generic; diff --git a/src/NadekoBot/Modules/Searches/PlaceCommands.cs b/src/NadekoBot/Modules/Searches/PlaceCommands.cs index 62dd4964..92260965 100644 --- a/src/NadekoBot/Modules/Searches/PlaceCommands.cs +++ b/src/NadekoBot/Modules/Searches/PlaceCommands.cs @@ -1,6 +1,5 @@ using Discord.Commands; using NadekoBot.Extensions; -using NadekoBot.Services; using System; using System.Threading.Tasks; using NadekoBot.Common; diff --git a/src/NadekoBot/Modules/Searches/XkcdCommands.cs b/src/NadekoBot/Modules/Searches/XkcdCommands.cs index 5814ff5e..d5027656 100644 --- a/src/NadekoBot/Modules/Searches/XkcdCommands.cs +++ b/src/NadekoBot/Modules/Searches/XkcdCommands.cs @@ -1,7 +1,6 @@ using Discord; using Discord.Commands; using NadekoBot.Extensions; -using NadekoBot.Services; using Newtonsoft.Json; using System.Net.Http; using System.Threading.Tasks; diff --git a/src/NadekoBot/Modules/Utility/BotConfigCommands.cs b/src/NadekoBot/Modules/Utility/BotConfigCommands.cs new file mode 100644 index 00000000..6262bf98 --- /dev/null +++ b/src/NadekoBot/Modules/Utility/BotConfigCommands.cs @@ -0,0 +1,39 @@ +using Discord; +using Discord.Commands; +using NadekoBot.Common; +using NadekoBot.Common.Attributes; +using NadekoBot.Services; +using System; +using System.Threading.Tasks; + +namespace NadekoBot.Modules.Utility +{ + public partial class Utility + { + public class BotConfigCommands : NadekoSubmodule + { + [NadekoCommand, Usage, Description, Aliases] + [OwnerOnly] + public async Task BotConfigEdit() + { + var names = Enum.GetNames(typeof(BotConfigEditType)); + await ReplyAsync(string.Join(", ", names)).ConfigureAwait(false); + } + + [NadekoCommand, Usage, Description, Aliases] + [OwnerOnly] + public async Task BotConfigEdit(BotConfigEditType type, [Remainder]string newValue = null) + { + if (string.IsNullOrWhiteSpace(newValue)) + newValue = null; + + var success = _service.Edit(type, newValue); + + if (!success) + await ReplyErrorLocalized("bot_config_edit_fail", Format.Bold(type.ToString()), Format.Bold(newValue ?? "NULL")).ConfigureAwait(false); + else + await ReplyConfirmLocalized("bot_config_edit_success", Format.Bold(type.ToString()), Format.Bold(newValue ?? "NULL")).ConfigureAwait(false); + } + } + } +} diff --git a/src/NadekoBot/Modules/Utility/Common/Exceptions/StreamRoleNotFoundException.cs b/src/NadekoBot/Modules/Utility/Common/Exceptions/StreamRoleNotFoundException.cs index ca73e920..d6ac31cf 100644 --- a/src/NadekoBot/Modules/Utility/Common/Exceptions/StreamRoleNotFoundException.cs +++ b/src/NadekoBot/Modules/Utility/Common/Exceptions/StreamRoleNotFoundException.cs @@ -1,8 +1,4 @@ using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; namespace NadekoBot.Modules.Utility.Common.Exceptions { diff --git a/src/NadekoBot/Modules/Utility/Common/Exceptions/StreamRolePermissionException.cs b/src/NadekoBot/Modules/Utility/Common/Exceptions/StreamRolePermissionException.cs index a921f3ce..b0fd0dbe 100644 --- a/src/NadekoBot/Modules/Utility/Common/Exceptions/StreamRolePermissionException.cs +++ b/src/NadekoBot/Modules/Utility/Common/Exceptions/StreamRolePermissionException.cs @@ -1,8 +1,4 @@ using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; namespace NadekoBot.Modules.Utility.Common.Exceptions { diff --git a/src/NadekoBot/Modules/Utility/PatreonCommands.cs b/src/NadekoBot/Modules/Utility/PatreonCommands.cs index bbfa2f24..96ac04c4 100644 --- a/src/NadekoBot/Modules/Utility/PatreonCommands.cs +++ b/src/NadekoBot/Modules/Utility/PatreonCommands.cs @@ -2,7 +2,6 @@ using Discord.Commands; using System; using NadekoBot.Services; -using NadekoBot.Services.Database.Models; using NadekoBot.Extensions; using Discord; using NadekoBot.Common.Attributes; @@ -16,11 +15,11 @@ namespace NadekoBot.Modules.Utility public class PatreonCommands : NadekoSubmodule { private readonly IBotCredentials _creds; - private readonly BotConfig _config; + private readonly IBotConfigProvider _config; private readonly DbService _db; private readonly CurrencyService _currency; - public PatreonCommands(IBotCredentials creds, BotConfig config, DbService db, CurrencyService currency) + public PatreonCommands(IBotCredentials creds, IBotConfigProvider config, DbService db, CurrencyService currency) { _creds = creds; _config = config; @@ -64,7 +63,7 @@ namespace NadekoBot.Modules.Utility if (amount > 0) { - await ReplyConfirmLocalized("clpa_success", amount + _config.CurrencySign).ConfigureAwait(false); + await ReplyConfirmLocalized("clpa_success", amount + _config.BotConfig.CurrencySign).ConfigureAwait(false); return; } var rem = (_service.Interval - (DateTime.UtcNow - _service.LastUpdate)); diff --git a/src/NadekoBot/Modules/Utility/QuoteCommands.cs b/src/NadekoBot/Modules/Utility/QuoteCommands.cs index 141c4783..8b4a6752 100644 --- a/src/NadekoBot/Modules/Utility/QuoteCommands.cs +++ b/src/NadekoBot/Modules/Utility/QuoteCommands.cs @@ -3,7 +3,6 @@ using Discord.Commands; using NadekoBot.Extensions; using NadekoBot.Services; using NadekoBot.Services.Database.Models; -using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; diff --git a/src/NadekoBot/Modules/Utility/Services/RemindService.cs b/src/NadekoBot/Modules/Utility/Services/RemindService.cs index 43e809a6..0e3b2206 100644 --- a/src/NadekoBot/Modules/Utility/Services/RemindService.cs +++ b/src/NadekoBot/Modules/Utility/Services/RemindService.cs @@ -25,11 +25,11 @@ namespace NadekoBot.Modules.Utility.Services private readonly Logger _log; private readonly CancellationTokenSource cancelSource; private readonly CancellationToken cancelAllToken; - private readonly BotConfig _config; + private readonly IBotConfigProvider _config; private readonly DiscordSocketClient _client; private readonly DbService _db; - public RemindService(DiscordSocketClient client, BotConfig config, DbService db, + public RemindService(DiscordSocketClient client, IBotConfigProvider config, DbService db, StartingGuildsService guilds, IUnitOfWork uow) { _config = config; @@ -41,7 +41,7 @@ namespace NadekoBot.Modules.Utility.Services cancelAllToken = cancelSource.Token; var reminders = uow.Reminders.GetIncludedReminders(guilds).ToList(); - RemindMessageFormat = _config.RemindMessageFormat; + RemindMessageFormat = _config.BotConfig.RemindMessageFormat; foreach (var r in reminders) { diff --git a/src/NadekoBot/Modules/Utility/Services/VerboseErrorsService.cs b/src/NadekoBot/Modules/Utility/Services/VerboseErrorsService.cs index a3f573bd..9f438128 100644 --- a/src/NadekoBot/Modules/Utility/Services/VerboseErrorsService.cs +++ b/src/NadekoBot/Modules/Utility/Services/VerboseErrorsService.cs @@ -1,5 +1,4 @@ -using System.Collections.Concurrent; -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Discord; diff --git a/src/NadekoBot/NadekoBot.cs b/src/NadekoBot/NadekoBot.cs index 8870820e..b49b0562 100644 --- a/src/NadekoBot/NadekoBot.cs +++ b/src/NadekoBot/NadekoBot.cs @@ -8,7 +8,6 @@ using System; using System.Linq; using System.Reflection; using System.Threading.Tasks; -using NadekoBot.Modules.Permissions; using System.Collections.Immutable; using System.Diagnostics; using NadekoBot.Services.Database.Models; @@ -34,7 +33,6 @@ namespace NadekoBot public CommandService CommandService { get; } public DbService Db { get; } - public BotConfig BotConfig { get; } public ImmutableArray AllGuildConfigs { get; private set; } /* I don't know how to make this not be static @@ -54,6 +52,8 @@ namespace NadekoBot private readonly ShardComClient _comClient; + private readonly BotConfig _botConfig; + public NadekoBot(int shardId, int parentProcessId, int? port = null) { if (shardId < 0) @@ -85,9 +85,9 @@ namespace NadekoBot using (var uow = Db.UnitOfWork) { - BotConfig = uow.BotConfig.GetOrCreate(); - OkColor = new Color(Convert.ToUInt32(BotConfig.OkColor, 16)); - ErrorColor = new Color(Convert.ToUInt32(BotConfig.ErrorColor, 16)); + _botConfig = uow.BotConfig.GetOrCreate(); + OkColor = new Color(Convert.ToUInt32(_botConfig.OkColor, 16)); + ErrorColor = new Color(Convert.ToUInt32(_botConfig.ErrorColor, 16)); } SetupShard(parentProcessId, port.Value); @@ -124,13 +124,13 @@ namespace NadekoBot { AllGuildConfigs = uow.GuildConfigs.GetAllGuildConfigs(startingGuildIdList).ToImmutableArray(); - var localization = new Localization(BotConfig.Locale, AllGuildConfigs.ToDictionary(x => x.GuildId, x => x.Locale), Db); + var localization = new Localization(_botConfig.Locale, AllGuildConfigs.ToDictionary(x => x.GuildId, x => x.Locale), Db); //initialize Services Services = new NServiceProvider.ServiceProviderBuilder() .AddManual(Credentials) .AddManual(Db) - .AddManual(BotConfig) + .AddManual(_botConfig) .AddManual(Client) .AddManual(CommandService) .AddManual(localization) diff --git a/src/NadekoBot/NadekoBot.csproj b/src/NadekoBot/NadekoBot.csproj index 6d719b32..810fa907 100644 --- a/src/NadekoBot/NadekoBot.csproj +++ b/src/NadekoBot/NadekoBot.csproj @@ -91,8 +91,4 @@ - - - - diff --git a/src/NadekoBot/Resources/CommandStrings.resx b/src/NadekoBot/Resources/CommandStrings.resx index e1635531..03aaed3c 100644 --- a/src/NadekoBot/Resources/CommandStrings.resx +++ b/src/NadekoBot/Resources/CommandStrings.resx @@ -3573,4 +3573,13 @@ Adds or removes a whitelisted user. Whitelisted users will receive the stream role even if they don't have the specified keyword in their stream title. + + botconfigedit bce + + + `{0}bce CurrencyName b1nzy` or `{0}bce` + + + Sets one of available bot config settings to a specified value. Use the command without any parameters to get a list of available settings. + diff --git a/src/NadekoBot/Services/CommandHandler.cs b/src/NadekoBot/Services/CommandHandler.cs index 81a70da2..b1a25463 100644 --- a/src/NadekoBot/Services/CommandHandler.cs +++ b/src/NadekoBot/Services/CommandHandler.cs @@ -50,7 +50,7 @@ namespace NadekoBot.Services public ConcurrentHashSet UsersOnShortCooldown { get; } = new ConcurrentHashSet(); private readonly Timer _clearUsersOnShortCooldown; - public CommandHandler(DiscordSocketClient client, DbService db, BotConfig bc, IEnumerable gcs, CommandService commandService, IBotCredentials credentials, NadekoBot bot) + public CommandHandler(DiscordSocketClient client, DbService db, IBotConfigProvider bc, IEnumerable gcs, CommandService commandService, IBotCredentials credentials, NadekoBot bot) { _client = client; _commandService = commandService; @@ -65,7 +65,7 @@ namespace NadekoBot.Services UsersOnShortCooldown.Clear(); }, null, GlobalCommandsCooldown, GlobalCommandsCooldown); - DefaultPrefix = bc.DefaultPrefix; + DefaultPrefix = bc.BotConfig.DefaultPrefix; _prefixes = gcs .Where(x => x.Prefix != null) .ToDictionary(x => x.GuildId, x => x.Prefix) diff --git a/src/NadekoBot/Services/CurrencyService.cs b/src/NadekoBot/Services/CurrencyService.cs index fb664d62..d7306a81 100644 --- a/src/NadekoBot/Services/CurrencyService.cs +++ b/src/NadekoBot/Services/CurrencyService.cs @@ -9,10 +9,10 @@ namespace NadekoBot.Services { public class CurrencyService : INService { - private readonly BotConfig _config; + private readonly IBotConfigProvider _config; private readonly DbService _db; - public CurrencyService(BotConfig config, DbService db) + public CurrencyService(IBotConfigProvider config, DbService db) { _config = config; _db = db; @@ -23,7 +23,7 @@ namespace NadekoBot.Services var success = await RemoveAsync(author.Id, reason, amount); if (success && sendMessage) - try { await author.SendErrorAsync($"`You lost:` {amount} {_config.CurrencySign}\n`Reason:` {reason}").ConfigureAwait(false); } catch { } + try { await author.SendErrorAsync($"`You lost:` {amount} {_config.BotConfig.CurrencySign}\n`Reason:` {reason}").ConfigureAwait(false); } catch { } return success; } @@ -65,7 +65,7 @@ namespace NadekoBot.Services await AddAsync(author.Id, reason, amount); if (sendMessage) - try { await author.SendConfirmAsync($"`You received:` {amount} {_config.CurrencySign}\n`Reason:` {reason}").ConfigureAwait(false); } catch { } + try { await author.SendConfirmAsync($"`You received:` {amount} {_config.BotConfig.CurrencySign}\n`Reason:` {reason}").ConfigureAwait(false); } catch { } } public async Task AddAsync(ulong receiverId, string reason, long amount, IUnitOfWork uow = null) diff --git a/src/NadekoBot/Services/Database/Models/StreamRoleSettings.cs b/src/NadekoBot/Services/Database/Models/StreamRoleSettings.cs index 05449039..8ef5b690 100644 --- a/src/NadekoBot/Services/Database/Models/StreamRoleSettings.cs +++ b/src/NadekoBot/Services/Database/Models/StreamRoleSettings.cs @@ -1,8 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; +using System.Collections.Generic; namespace NadekoBot.Services.Database.Models { diff --git a/src/NadekoBot/Services/Database/Repositories/IReminderRepository.cs b/src/NadekoBot/Services/Database/Repositories/IReminderRepository.cs index a4812775..dc757dbc 100644 --- a/src/NadekoBot/Services/Database/Repositories/IReminderRepository.cs +++ b/src/NadekoBot/Services/Database/Repositories/IReminderRepository.cs @@ -1,5 +1,4 @@ using NadekoBot.Services.Database.Models; -using System.Collections; using System.Collections.Generic; namespace NadekoBot.Services.Database.Repositories diff --git a/src/NadekoBot/Services/Database/Repositories/Impl/ReminderRepository.cs b/src/NadekoBot/Services/Database/Repositories/Impl/ReminderRepository.cs index b2b13a2b..e29cc3f2 100644 --- a/src/NadekoBot/Services/Database/Repositories/Impl/ReminderRepository.cs +++ b/src/NadekoBot/Services/Database/Repositories/Impl/ReminderRepository.cs @@ -1,6 +1,5 @@ using NadekoBot.Services.Database.Models; using Microsoft.EntityFrameworkCore; -using System; using System.Collections.Generic; using System.Linq; diff --git a/src/NadekoBot/Services/Database/Repositories/Impl/WarningsRepository.cs b/src/NadekoBot/Services/Database/Repositories/Impl/WarningsRepository.cs index ef27ebd9..cb2cc089 100644 --- a/src/NadekoBot/Services/Database/Repositories/Impl/WarningsRepository.cs +++ b/src/NadekoBot/Services/Database/Repositories/Impl/WarningsRepository.cs @@ -2,7 +2,6 @@ using Microsoft.EntityFrameworkCore; using System.Linq; using System.Threading.Tasks; -using System; namespace NadekoBot.Services.Database.Repositories.Impl { diff --git a/src/NadekoBot/Services/IBotConfigProvider.cs b/src/NadekoBot/Services/IBotConfigProvider.cs new file mode 100644 index 00000000..1e65f202 --- /dev/null +++ b/src/NadekoBot/Services/IBotConfigProvider.cs @@ -0,0 +1,12 @@ +using NadekoBot.Common; +using NadekoBot.Services.Database.Models; + +namespace NadekoBot.Services +{ + public interface IBotConfigProvider : INService + { + BotConfig BotConfig { get; } + void Reload(); + bool Edit(BotConfigEditType type, string newValue); + } +} diff --git a/src/NadekoBot/Services/IImagesService.cs b/src/NadekoBot/Services/IImagesService.cs index 5678191c..08d8f504 100644 --- a/src/NadekoBot/Services/IImagesService.cs +++ b/src/NadekoBot/Services/IImagesService.cs @@ -1,5 +1,4 @@ -using System; -using System.Collections.Immutable; +using System.Collections.Immutable; namespace NadekoBot.Services { diff --git a/src/NadekoBot/Services/INService.cs b/src/NadekoBot/Services/INService.cs index 76c30abb..13bbbeea 100644 --- a/src/NadekoBot/Services/INService.cs +++ b/src/NadekoBot/Services/INService.cs @@ -1,10 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace NadekoBot.Services +namespace NadekoBot.Services { /// /// All services must implement this interface in order to be auto-discovered by the DI system diff --git a/src/NadekoBot/Services/Impl/BotConfigProvider.cs b/src/NadekoBot/Services/Impl/BotConfigProvider.cs new file mode 100644 index 00000000..c18a0e1a --- /dev/null +++ b/src/NadekoBot/Services/Impl/BotConfigProvider.cs @@ -0,0 +1,111 @@ +using System; +using NadekoBot.Common; +using NadekoBot.Services.Database.Models; +using NadekoBot.Services; + +namespace NadekoBot.Services.Impl +{ + public class BotConfigProvider : IBotConfigProvider + { + private readonly DbService _db; + public BotConfig BotConfig { get; private set; } + + public BotConfigProvider(DbService db, BotConfig bc) + { + _db = db; + BotConfig = bc; + } + + public void Reload() + { + using (var uow = _db.UnitOfWork) + { + BotConfig = uow.BotConfig.GetOrCreate(); + } + } + + public bool Edit(BotConfigEditType type, string newValue) + { + using (var uow = _db.UnitOfWork) + { + var bc = uow.BotConfig.GetOrCreate(); + switch (type) + { + case BotConfigEditType.CurrencyGenerationChance: + if (float.TryParse(newValue, out var chance) + && chance >= 0 + && chance <= 1) + { + bc.CurrencyGenerationChance = chance; + } + else + { + return false; + } + break; + case BotConfigEditType.CurrencyGenerationCooldown: + if (int.TryParse(newValue, out var cd) && cd >= 1) + { + bc.CurrencyGenerationCooldown = cd; + } + else + { + return false; + } + break; + case BotConfigEditType.CurrencyName: + bc.CurrencyName = newValue ?? "-"; + break; + case BotConfigEditType.CurrencyPluralName: + bc.CurrencyPluralName = newValue ?? bc.CurrencyName + "s"; + break; + case BotConfigEditType.CurrencySign: + bc.CurrencySign = newValue ?? "-"; + break; + case BotConfigEditType.DmHelpString: + bc.DMHelpString = string.IsNullOrWhiteSpace(newValue) + ? "-" + : newValue; + break; + case BotConfigEditType.HelpString: + bc.HelpString = string.IsNullOrWhiteSpace(newValue) + ? "-" + : newValue; + break; + case BotConfigEditType.CurrencyDropAmount: + if (int.TryParse(newValue, out var amount) && amount > 0) + bc.CurrencyDropAmount = amount; + else + return false; + break; + case BotConfigEditType.CurrencyDropAmountMax: + if (newValue == null) + bc.CurrencyDropAmountMax = null; + else if (int.TryParse(newValue, out var maxAmount) && maxAmount > 0) + bc.CurrencyDropAmountMax = maxAmount; + else + return false; + break; + case BotConfigEditType.MinimumBetAmount: + if (int.TryParse(newValue, out var minBetAmount) && minBetAmount > 0) + bc.MinimumBetAmount = minBetAmount; + else + return false; + break; + case BotConfigEditType.TriviaCurrencyReward: + if (int.TryParse(newValue, out var triviaReward) && triviaReward > 0) + bc.TriviaCurrencyReward = triviaReward; + else + return false; + break; + default: + return false; + } + + BotConfig = bc; + uow.Complete(); + } + return true; + } + } +} diff --git a/src/NadekoBot/Services/Impl/ImagesService.cs b/src/NadekoBot/Services/Impl/ImagesService.cs index 95930464..7153328f 100644 --- a/src/NadekoBot/Services/Impl/ImagesService.cs +++ b/src/NadekoBot/Services/Impl/ImagesService.cs @@ -1,7 +1,6 @@ using NLog; using System; using System.Collections.Immutable; -using System.Diagnostics; using System.IO; using System.Linq; diff --git a/src/NadekoBot/Services/Impl/SyncPreconditionService.cs b/src/NadekoBot/Services/Impl/SyncPreconditionService.cs index 30d5b48a..b6a47133 100644 --- a/src/NadekoBot/Services/Impl/SyncPreconditionService.cs +++ b/src/NadekoBot/Services/Impl/SyncPreconditionService.cs @@ -1,10 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace NadekoBot.Services.Impl +namespace NadekoBot.Services.Impl { public class SyncPreconditionService { diff --git a/src/NadekoBot/Services/LogSetup.cs b/src/NadekoBot/Services/LogSetup.cs index 159b447e..0d3234ee 100644 --- a/src/NadekoBot/Services/LogSetup.cs +++ b/src/NadekoBot/Services/LogSetup.cs @@ -1,11 +1,6 @@ using NLog; using NLog.Config; using NLog.Targets; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; namespace NadekoBot.Services { diff --git a/src/NadekoBot/Services/ServiceProvider.cs b/src/NadekoBot/Services/ServiceProvider.cs index 6e0c642b..32a8419d 100644 --- a/src/NadekoBot/Services/ServiceProvider.cs +++ b/src/NadekoBot/Services/ServiceProvider.cs @@ -48,6 +48,8 @@ namespace NadekoBot.Services var interfaces = new HashSet(allTypes .Where(x => x.GetInterfaces().Contains(typeof(INService)) && x.GetTypeInfo().IsInterface)); + var alreadyFailed = new Dictionary(); + var sw = Stopwatch.StartNew(); var swInstance = new Stopwatch(); while (services.Count > 0) @@ -71,11 +73,20 @@ namespace NadekoBot.Services else //if i failed getting it, add it to the end, and break { services.Enqueue(type); + if (alreadyFailed.ContainsKey(type)) + { + alreadyFailed[type]++; + if (alreadyFailed[type] > 3) + _log.Warn(type.Name + " wasn't instantiated in the first 3 attempts. Missing " + arg.Name + " type"); + } + else + alreadyFailed.Add(type, 1); break; } } if (args.Count != argTypes.Length) continue; + // _log.Info("Loading " + type.Name); swInstance.Restart(); var instance = ctor.Invoke(args.ToArray()); swInstance.Stop(); diff --git a/src/NadekoBot/_Extensions/Extensions.cs b/src/NadekoBot/_Extensions/Extensions.cs index b7d95c69..77522709 100644 --- a/src/NadekoBot/_Extensions/Extensions.cs +++ b/src/NadekoBot/_Extensions/Extensions.cs @@ -16,7 +16,6 @@ using System.Text.RegularExpressions; using System.Threading.Tasks; using NadekoBot.Common.Collections; using SixLabors.Primitives; -using ImageSharp.PixelFormats; namespace NadekoBot.Extensions { diff --git a/src/NadekoBot/_strings/ResponseStrings.en-US.json b/src/NadekoBot/_strings/ResponseStrings.en-US.json index 3ca43151..f9e47119 100644 --- a/src/NadekoBot/_strings/ResponseStrings.en-US.json +++ b/src/NadekoBot/_strings/ResponseStrings.en-US.json @@ -626,6 +626,8 @@ "utility_activity_users_total": "{0} users total.", "utility_author": "Author", "utility_botid": "Bot ID", + "utility_bot_config_edit_fail": "Failed setting {0} to the value {1}", + "utility_bot_config_edit_success": "The value of {0} is set to {1}", "utility_calcops": "List of functions in {0}calc command", "utility_channelid": "{0} of this channel is {1}", "utility_channel_topic": "Channel topic", From cedaf73785c17cc55b8edbb209ccb0f54d28e902 Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Thu, 20 Jul 2017 20:03:11 +0200 Subject: [PATCH 203/346] Sped up .streamrole initialization 5x, but it might error out if there are too many users streaming when the command is ran. --- src/NadekoBot/Modules/Utility/Services/StreamRoleService.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/NadekoBot/Modules/Utility/Services/StreamRoleService.cs b/src/NadekoBot/Modules/Utility/Services/StreamRoleService.cs index 48fc53f0..65560db9 100644 --- a/src/NadekoBot/Modules/Utility/Services/StreamRoleService.cs +++ b/src/NadekoBot/Modules/Utility/Services/StreamRoleService.cs @@ -253,8 +253,7 @@ namespace NadekoBot.Modules.Utility.Services foreach (var usr in await fromRole.Guild.GetUsersAsync(CacheMode.CacheOnly).ConfigureAwait(false)) { - await TryApplyRole(usr, setting).ConfigureAwait(false); - await Task.Delay(500).ConfigureAwait(false); + await Task.WhenAll(TryApplyRole(usr, setting), Task.Delay(100)).ConfigureAwait(false); } } From 613655eb95c58f2463309cb971b3b0a01f885793 Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Thu, 20 Jul 2017 22:58:19 +0200 Subject: [PATCH 204/346] You can now add CleverBotApiKey from cleverbot.com/api in order to use official cleverbot, instead of stupid program-o --- docs/Commands List.md | 4 +++ docs/JSON Explanations.md | 12 ++++--- .../Modules/Games/CleverBotCommands.cs | 10 +++--- .../{ => ChatterBot}/ChatterBotResponse.cs | 2 +- .../{ => ChatterBot}/ChatterBotSession.cs | 17 +++++----- .../Common/ChatterBot/CleverbotResponse.cs | 14 ++++++++ .../Common/ChatterBot/IChatterBotSession.cs | 13 ++++++++ .../ChatterBot/OfficialCleverbotSession.cs | 33 +++++++++++++++++++ .../Games/Services/ChatterbotService.cs | 28 +++++++++++----- src/NadekoBot/Services/IBotCredentials.cs | 1 + src/NadekoBot/Services/Impl/BotCredentials.cs | 2 ++ src/NadekoBot/credentials_example.json | 1 + 12 files changed, 109 insertions(+), 28 deletions(-) rename src/NadekoBot/Modules/Games/Common/{ => ChatterBot}/ChatterBotResponse.cs (71%) rename src/NadekoBot/Modules/Games/Common/{ => ChatterBot}/ChatterBotSession.cs (63%) create mode 100644 src/NadekoBot/Modules/Games/Common/ChatterBot/CleverbotResponse.cs create mode 100644 src/NadekoBot/Modules/Games/Common/ChatterBot/IChatterBotSession.cs create mode 100644 src/NadekoBot/Modules/Games/Common/ChatterBot/OfficialCleverbotSession.cs diff --git a/docs/Commands List.md b/docs/Commands List.md index b6609665..925c5e01 100644 --- a/docs/Commands List.md +++ b/docs/Commands List.md @@ -401,6 +401,7 @@ Commands and aliases | Description | Usage `.listservers` | Lists servers the bot is on with some basic info. 15 per page. **Bot owner only** | `.listservers 3` `.savechat` | Saves a number of messages to a text file and sends it to you. **Bot owner only** | `.savechat 150` `.ping` | Ping the bot to see if there are latency issues. | `.ping` +`.botconfigedit` `.bce` | Sets one of available bot config settings to a specified value. Use the command without any parameters to get a list of available settings. **Bot owner only** | `.bce CurrencyName b1nzy` or `.bce` `.calculate` `.calc` | Evaluate a mathematical expression. | `.calc 1+1` `.calcops` | Shows all available operations in the `.calc` command | `.calcops` `.alias` `.cmdmap` | Create a custom alias for a certain Nadeko command. Provide no alias to remove the existing one. **Requires Administrator server permission.** | `.alias allin $bf 100 h` or `.alias "linux thingy" >loonix Spyware Windows` @@ -425,6 +426,9 @@ Commands and aliases | Description | Usage `.repeat` | Repeat a message every `X` minutes in the current channel. You can instead specify time of day for the message to be repeated at daily (make sure you've set your server's timezone). You can have up to 5 repeating messages on the server in total. **Requires ManageMessages server permission.** | `.repeat 5 Hello there` or `.repeat 17:30 tea time` `.repeatlist` `.replst` | Shows currently repeating messages and their indexes. **Requires ManageMessages server permission.** | `.repeatlist` `.streamrole` | Sets a role which is monitored for streamers (FromRole), and a role to add if a user from 'FromRole' is streaming (AddRole). When a user from 'FromRole' starts streaming, they will receive an 'AddRole'. Provide no arguments to disable **Requires ManageRoles server permission.** | `.streamrole "Eligible Streamers" "Featured Streams"` +`.streamrolekw` `.srkw` | Sets keyword which is required in the stream's title in order for the streamrole to apply. Provide no keyword in order to reset. **Requires ManageRoles server permission.** | `.srkw` or `.srkw PUBG` +`.streamrolebl` `.srbl` | Adds or removes a blacklisted user. Blacklisted users will never receive the stream role. **Requires ManageRoles server permission.** | `.srbl add @b1nzy#1234` or `.srbl rem @b1nzy#1234` +`.streamrolewl` `.srwl` | Adds or removes a whitelisted user. Whitelisted users will receive the stream role even if they don't have the specified keyword in their stream title. **Requires ManageRoles server permission.** | `.srwl add @b1nzy#1234` or `.srwl rem @b1nzy#1234` `.convertlist` | List of the convertible dimensions and currencies. | `.convertlist` `.convert` | Convert quantities. Use `.convertlist` to see supported dimensions and currencies. | `.convert m km 1000` `.verboseerror` `.ve` | Toggles whether the bot should print command errors when a command is incorrectly used. **Requires ManageMessages server permission.** | `.ve` diff --git a/docs/JSON Explanations.md b/docs/JSON Explanations.md index eb6fabe4..db9aa239 100644 --- a/docs/JSON Explanations.md +++ b/docs/JSON Explanations.md @@ -16,6 +16,7 @@ If you do not see `credentials.json` you will need to rename `credentials_exampl "GoogleApiKey": "AIzaSyDSci1sdlWQOWNVj1vlXxxxxxbk0oWMEzM", "MashapeKey": "4UrKpcWXc2mshS8RKi00000y8Kf5p1Q8kI6jsn32bmd8oVWiY7", "OsuApiKey": "4c8c8fdff8e1234581725db27fd140a7d93320d6", + "CleverbotApiKey": "", "PatreonAccessToken": "", "PatreonCampaignId": "334038", "Db": null, @@ -148,12 +149,15 @@ It should look like: - Required for Urban Disctionary, Hashtag search, and Hearthstone cards. - You need to create an account on their [api marketplace](https://market.mashape.com/), after that go to `market.mashape.com/YOURNAMEHERE/applications/default-application` and press **Get the keys** in the top right corner. - Copy the key and paste it into `credentials.json` -- **LOLAPIKey** +- **LoLApiKey** - Required for all League of Legends commands. - - You can get this key [here](http://api.champion.gg/) -- **OsuAPIKey** + - You can get this key [here.](http://api.champion.gg/) +- **OsuApiKey** - Required for Osu commands - - You can get this key [here.](https://osu.ppy.sh/p/api) + - You can get this key [here.](https://osu.ppy.sh/p/api) +- **CleverbotApiKey** + - Required if you want to use official cleverobot, instead of program-o + - you can get this key [here.](http://www.cleverbot.com/api/) - **PatreonAccessToken** - For Patreon creators only. - **PatreonCampaignId** diff --git a/src/NadekoBot/Modules/Games/CleverBotCommands.cs b/src/NadekoBot/Modules/Games/CleverBotCommands.cs index 47ab69b6..a1534800 100644 --- a/src/NadekoBot/Modules/Games/CleverBotCommands.cs +++ b/src/NadekoBot/Modules/Games/CleverBotCommands.cs @@ -4,19 +4,19 @@ using NadekoBot.Services; using System; using System.Threading.Tasks; using NadekoBot.Common.Attributes; -using NadekoBot.Modules.Games.Common; using NadekoBot.Modules.Games.Services; +using NadekoBot.Modules.Games.Common.ChatterBot; namespace NadekoBot.Modules.Games { public partial class Games { [Group] - public class CleverBotCommands : NadekoSubmodule + public class ChatterBotCommands : NadekoSubmodule { private readonly DbService _db; - public CleverBotCommands(DbService db) + public ChatterBotCommands(DbService db) { _db = db; } @@ -28,7 +28,7 @@ namespace NadekoBot.Modules.Games { var channel = (ITextChannel)Context.Channel; - if (_service.ChatterBotGuilds.TryRemove(channel.Guild.Id, out Lazy throwaway)) + if (_service.ChatterBotGuilds.TryRemove(channel.Guild.Id, out Lazy throwaway)) { using (var uow = _db.UnitOfWork) { @@ -39,7 +39,7 @@ namespace NadekoBot.Modules.Games return; } - _service.ChatterBotGuilds.TryAdd(channel.Guild.Id, new Lazy(() => new ChatterBotSession(Context.Guild.Id), true)); + _service.ChatterBotGuilds.TryAdd(channel.Guild.Id, new Lazy(() => _service.CreateSession(), true)); using (var uow = _db.UnitOfWork) { diff --git a/src/NadekoBot/Modules/Games/Common/ChatterBotResponse.cs b/src/NadekoBot/Modules/Games/Common/ChatterBot/ChatterBotResponse.cs similarity index 71% rename from src/NadekoBot/Modules/Games/Common/ChatterBotResponse.cs rename to src/NadekoBot/Modules/Games/Common/ChatterBot/ChatterBotResponse.cs index 098673bc..acf5c3de 100644 --- a/src/NadekoBot/Modules/Games/Common/ChatterBotResponse.cs +++ b/src/NadekoBot/Modules/Games/Common/ChatterBot/ChatterBotResponse.cs @@ -1,4 +1,4 @@ -namespace NadekoBot.Modules.Games.Common +namespace NadekoBot.Modules.Games.Common.ChatterBot { public class ChatterBotResponse { diff --git a/src/NadekoBot/Modules/Games/Common/ChatterBotSession.cs b/src/NadekoBot/Modules/Games/Common/ChatterBot/ChatterBotSession.cs similarity index 63% rename from src/NadekoBot/Modules/Games/Common/ChatterBotSession.cs rename to src/NadekoBot/Modules/Games/Common/ChatterBot/ChatterBotSession.cs index 256d3d05..347a705a 100644 --- a/src/NadekoBot/Modules/Games/Common/ChatterBotSession.cs +++ b/src/NadekoBot/Modules/Games/Common/ChatterBot/ChatterBotSession.cs @@ -4,25 +4,24 @@ using NadekoBot.Common; using NadekoBot.Extensions; using Newtonsoft.Json; -namespace NadekoBot.Modules.Games.Common +namespace NadekoBot.Modules.Games.Common.ChatterBot { - public class ChatterBotSession + public class ChatterBotSession : IChatterBotSession { - private static NadekoRandom rng { get; } = new NadekoRandom(); - public string ChatterbotId { get; } - public string ChannelId { get; } + private static NadekoRandom Rng { get; } = new NadekoRandom(); + + private readonly string _chatterBotId; private int _botId = 6; - public ChatterBotSession(ulong channelId) + public ChatterBotSession() { - ChannelId = channelId.ToString().ToBase64(); - ChatterbotId = rng.Next(0, 1000000).ToString().ToBase64(); + _chatterBotId = Rng.Next(0, 1000000).ToString().ToBase64(); } private string apiEndpoint => "http://api.program-o.com/v2/chatbot/" + $"?bot_id={_botId}&" + "say={0}&" + - $"convo_id=nadekobot_{ChatterbotId}_{ChannelId}&" + + $"convo_id=nadekobot_{_chatterBotId}&" + "format=json"; public async Task Think(string message) diff --git a/src/NadekoBot/Modules/Games/Common/ChatterBot/CleverbotResponse.cs b/src/NadekoBot/Modules/Games/Common/ChatterBot/CleverbotResponse.cs new file mode 100644 index 00000000..23e2b9e8 --- /dev/null +++ b/src/NadekoBot/Modules/Games/Common/ChatterBot/CleverbotResponse.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace NadekoBot.Modules.Games.Common.ChatterBot +{ + public class CleverbotResponse + { + public string Cs { get; set; } + public string Output { get; set; } + } +} diff --git a/src/NadekoBot/Modules/Games/Common/ChatterBot/IChatterBotSession.cs b/src/NadekoBot/Modules/Games/Common/ChatterBot/IChatterBotSession.cs new file mode 100644 index 00000000..14b749dc --- /dev/null +++ b/src/NadekoBot/Modules/Games/Common/ChatterBot/IChatterBotSession.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace NadekoBot.Modules.Games.Common.ChatterBot +{ + public interface IChatterBotSession + { + Task Think(string input); + } +} diff --git a/src/NadekoBot/Modules/Games/Common/ChatterBot/OfficialCleverbotSession.cs b/src/NadekoBot/Modules/Games/Common/ChatterBot/OfficialCleverbotSession.cs new file mode 100644 index 00000000..a970f135 --- /dev/null +++ b/src/NadekoBot/Modules/Games/Common/ChatterBot/OfficialCleverbotSession.cs @@ -0,0 +1,33 @@ +using Newtonsoft.Json; +using System.Net.Http; +using System.Threading.Tasks; + +namespace NadekoBot.Modules.Games.Common.ChatterBot +{ + public class OfficialCleverbotSession : IChatterBotSession + { + private readonly string _apiKey; + private string _cs = null; + + private string queryString => $"https://www.cleverbot.com/getreply?key={_apiKey}" + + "&wrapper=nadekobot" + + "&input={0}" + + "&cs={1}"; + + public OfficialCleverbotSession(string apiKey) + { + this._apiKey = apiKey; + } + + public async Task Think(string input) + { + using (var http = new HttpClient()) + { + var dataString = await http.GetStringAsync(string.Format(queryString, input, _cs ?? "")).ConfigureAwait(false); + var data = JsonConvert.DeserializeObject(dataString); + _cs = data?.Cs; + return data?.Output; + } + } + } +} diff --git a/src/NadekoBot/Modules/Games/Services/ChatterbotService.cs b/src/NadekoBot/Modules/Games/Services/ChatterbotService.cs index 0327fca2..a137b953 100644 --- a/src/NadekoBot/Modules/Games/Services/ChatterbotService.cs +++ b/src/NadekoBot/Modules/Games/Services/ChatterbotService.cs @@ -7,13 +7,13 @@ using Discord; using Discord.WebSocket; using NadekoBot.Common.ModuleBehaviors; using NadekoBot.Extensions; -using NadekoBot.Modules.Games.Common; using NadekoBot.Modules.Permissions.Common; using NadekoBot.Modules.Permissions.Services; using NadekoBot.Services; using NadekoBot.Services.Database.Models; using NadekoBot.Services.Impl; using NLog; +using NadekoBot.Modules.Games.Common.ChatterBot; namespace NadekoBot.Modules.Games.Services { @@ -24,24 +24,34 @@ namespace NadekoBot.Modules.Games.Services private readonly PermissionService _perms; private readonly CommandHandler _cmd; private readonly NadekoStrings _strings; + private readonly IBotCredentials _creds; - public ConcurrentDictionary> ChatterBotGuilds { get; } + public ConcurrentDictionary> ChatterBotGuilds { get; } public ChatterBotService(DiscordSocketClient client, PermissionService perms, IEnumerable gcs, - CommandHandler cmd, NadekoStrings strings) + CommandHandler cmd, NadekoStrings strings, IBotCredentials creds) { _client = client; _log = LogManager.GetCurrentClassLogger(); _perms = perms; _cmd = cmd; _strings = strings; + _creds = creds; - ChatterBotGuilds = new ConcurrentDictionary>( + ChatterBotGuilds = new ConcurrentDictionary>( gcs.Where(gc => gc.CleverbotEnabled) - .ToDictionary(gc => gc.GuildId, gc => new Lazy(() => new ChatterBotSession(gc.GuildId), true))); + .ToDictionary(gc => gc.GuildId, gc => new Lazy(() => CreateSession(), true))); } - public string PrepareMessage(IUserMessage msg, out ChatterBotSession cleverbot) + public IChatterBotSession CreateSession() + { + if (string.IsNullOrWhiteSpace(_creds.CleverbotApiKey)) + return new ChatterBotSession(); + else + return new OfficialCleverbotSession(_creds.CleverbotApiKey); + } + + public string PrepareMessage(IUserMessage msg, out IChatterBotSession cleverbot) { var channel = msg.Channel as ITextChannel; cleverbot = null; @@ -49,7 +59,7 @@ namespace NadekoBot.Modules.Games.Services if (channel == null) return null; - if (!ChatterBotGuilds.TryGetValue(channel.Guild.Id, out Lazy lazyCleverbot)) + if (!ChatterBotGuilds.TryGetValue(channel.Guild.Id, out Lazy lazyCleverbot)) return null; cleverbot = lazyCleverbot.Value; @@ -74,7 +84,7 @@ namespace NadekoBot.Modules.Games.Services return message; } - public async Task TryAsk(ChatterBotSession cleverbot, ITextChannel channel, string message) + public async Task TryAsk(IChatterBotSession cleverbot, ITextChannel channel, string message) { await channel.TriggerTypingAsync().ConfigureAwait(false); @@ -96,7 +106,7 @@ namespace NadekoBot.Modules.Games.Services return false; try { - var message = PrepareMessage(usrMsg, out ChatterBotSession cbs); + var message = PrepareMessage(usrMsg, out IChatterBotSession cbs); if (message == null || cbs == null) return false; diff --git a/src/NadekoBot/Services/IBotCredentials.cs b/src/NadekoBot/Services/IBotCredentials.cs index 9ed5ca1b..f5ed37cc 100644 --- a/src/NadekoBot/Services/IBotCredentials.cs +++ b/src/NadekoBot/Services/IBotCredentials.cs @@ -23,6 +23,7 @@ namespace NadekoBot.Services string ShardRunCommand { get; } string ShardRunArguments { get; } string PatreonCampaignId { get; } + string CleverbotApiKey { get; } } public class DBConfig diff --git a/src/NadekoBot/Services/Impl/BotCredentials.cs b/src/NadekoBot/Services/Impl/BotCredentials.cs index d42b1732..14998758 100644 --- a/src/NadekoBot/Services/Impl/BotCredentials.cs +++ b/src/NadekoBot/Services/Impl/BotCredentials.cs @@ -26,6 +26,7 @@ namespace NadekoBot.Services.Impl public string LoLApiKey { get; } public string OsuApiKey { get; } + public string CleverbotApiKey { get; } public DBConfig Db { get; } public int TotalShards { get; } @@ -121,6 +122,7 @@ namespace NadekoBot.Services.Impl public string MashapeKey { get; set; } = ""; public string OsuApiKey { get; set; } = ""; public string SoundCloudClientId { get; set; } = ""; + public string CleverbotApiKey { get; } = ""; public string CarbonKey { get; set; } = ""; public DBConfig Db { get; set; } = new DBConfig("sqlite", "Filename=./data/NadekoBot.db"); public int TotalShards { get; set; } = 1; diff --git a/src/NadekoBot/credentials_example.json b/src/NadekoBot/credentials_example.json index 9745558d..5260d8c0 100644 --- a/src/NadekoBot/credentials_example.json +++ b/src/NadekoBot/credentials_example.json @@ -9,6 +9,7 @@ "MashapeKey": "", "OsuApiKey": "", "SoundCloudClientId": "", + "CleverbotApiKey": "", "CarbonKey": "", "Db": { "Type": "sqlite", From 0131b7713e1aa53ffe7d7d6689ca82143ed9f0a5 Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Thu, 20 Jul 2017 23:48:25 +0200 Subject: [PATCH 205/346] Make sure to assign new bot creds variable --- src/NadekoBot/Services/Impl/BotCredentials.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/NadekoBot/Services/Impl/BotCredentials.cs b/src/NadekoBot/Services/Impl/BotCredentials.cs index 14998758..e8a96de5 100644 --- a/src/NadekoBot/Services/Impl/BotCredentials.cs +++ b/src/NadekoBot/Services/Impl/BotCredentials.cs @@ -71,6 +71,7 @@ namespace NadekoBot.Services.Impl PatreonCampaignId = data[nameof(PatreonCampaignId)] ?? "334038"; ShardRunCommand = data[nameof(ShardRunCommand)]; ShardRunArguments = data[nameof(ShardRunArguments)]; + CleverbotApiKey = data[nameof(CleverbotApiKey)]; if (string.IsNullOrWhiteSpace(ShardRunCommand)) ShardRunCommand = "dotnet"; if (string.IsNullOrWhiteSpace(ShardRunArguments)) From 0d216ad78a3e8e2148d6a7743da90796f93ec0db Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Fri, 21 Jul 2017 03:04:44 +0200 Subject: [PATCH 206/346] .nsfwtbl added. You can now blacklist tags which are used in nsfw commands. --- .../20170721004230_nsfw-blacklist.Designer.cs | 1680 +++++++++++++++++ .../20170721004230_nsfw-blacklist.cs | 44 + .../NadekoSqliteContextModelSnapshot.cs | 27 + .../Exceptions/TagBlacklistedException.cs | 12 + src/NadekoBot/Modules/NSFW/NSFW.cs | 47 +- .../Searches/Services/SearchesService.cs | 52 +- src/NadekoBot/Resources/CommandStrings.resx | 9 + .../Services/Database/Models/GuildConfig.cs | 18 + .../Impl/GuildConfigRepository.cs | 1 + .../_strings/ResponseStrings.en-US.json | 4 + 10 files changed, 1891 insertions(+), 3 deletions(-) create mode 100644 src/NadekoBot/Migrations/20170721004230_nsfw-blacklist.Designer.cs create mode 100644 src/NadekoBot/Migrations/20170721004230_nsfw-blacklist.cs create mode 100644 src/NadekoBot/Modules/NSFW/Exceptions/TagBlacklistedException.cs diff --git a/src/NadekoBot/Migrations/20170721004230_nsfw-blacklist.Designer.cs b/src/NadekoBot/Migrations/20170721004230_nsfw-blacklist.Designer.cs new file mode 100644 index 00000000..918bf926 --- /dev/null +++ b/src/NadekoBot/Migrations/20170721004230_nsfw-blacklist.Designer.cs @@ -0,0 +1,1680 @@ +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using NadekoBot.Services.Database; +using NadekoBot.Services.Database.Models; + +namespace NadekoBot.Migrations +{ + [DbContext(typeof(NadekoContext))] + [Migration("20170721004230_nsfw-blacklist")] + partial class nsfwblacklist + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { + modelBuilder + .HasAnnotation("ProductVersion", "1.1.1"); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.AntiRaidSetting", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("Action"); + + b.Property("DateAdded"); + + b.Property("GuildConfigId"); + + b.Property("Seconds"); + + b.Property("UserThreshold"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId") + .IsUnique(); + + b.ToTable("AntiRaidSetting"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.AntiSpamIgnore", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AntiSpamSettingId"); + + b.Property("ChannelId"); + + b.Property("DateAdded"); + + b.HasKey("Id"); + + b.HasIndex("AntiSpamSettingId"); + + b.ToTable("AntiSpamIgnore"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.AntiSpamSetting", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("Action"); + + b.Property("DateAdded"); + + b.Property("GuildConfigId"); + + b.Property("MessageThreshold"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId") + .IsUnique(); + + b.ToTable("AntiSpamSetting"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.BlacklistItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("BotConfigId"); + + b.Property("DateAdded"); + + b.Property("ItemId"); + + b.Property("Type"); + + b.HasKey("Id"); + + b.HasIndex("BotConfigId"); + + b.ToTable("BlacklistItem"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.BlockedCmdOrMdl", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("BotConfigId"); + + b.Property("BotConfigId1"); + + b.Property("DateAdded"); + + b.Property("Name"); + + b.HasKey("Id"); + + b.HasIndex("BotConfigId"); + + b.HasIndex("BotConfigId1"); + + b.ToTable("BlockedCmdOrMdl"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.BotConfig", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("BetflipMultiplier"); + + b.Property("Betroll100Multiplier"); + + b.Property("Betroll67Multiplier"); + + b.Property("Betroll91Multiplier"); + + b.Property("BufferSize"); + + b.Property("CurrencyDropAmount"); + + b.Property("CurrencyDropAmountMax"); + + b.Property("CurrencyGenerationChance"); + + b.Property("CurrencyGenerationCooldown"); + + b.Property("CurrencyName"); + + b.Property("CurrencyPluralName"); + + b.Property("CurrencySign"); + + b.Property("CustomReactionsStartWith"); + + b.Property("DMHelpString"); + + b.Property("DateAdded"); + + b.Property("DefaultPrefix"); + + b.Property("ErrorColor"); + + b.Property("ForwardMessages"); + + b.Property("ForwardToAllOwners"); + + b.Property("HelpString"); + + b.Property("Locale"); + + b.Property("MigrationVersion"); + + b.Property("MinimumBetAmount"); + + b.Property("OkColor"); + + b.Property("PermissionVersion"); + + b.Property("RemindMessageFormat"); + + b.Property("RotatingStatuses"); + + b.Property("TriviaCurrencyReward"); + + b.HasKey("Id"); + + b.ToTable("BotConfig"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.ClashCaller", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("BaseDestroyed"); + + b.Property("CallUser"); + + b.Property("ClashWarId"); + + b.Property("DateAdded"); + + b.Property("SequenceNumber"); + + b.Property("Stars"); + + b.Property("TimeAdded"); + + b.HasKey("Id"); + + b.HasIndex("ClashWarId"); + + b.ToTable("ClashCallers"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.ClashWar", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ChannelId"); + + b.Property("DateAdded"); + + b.Property("EnemyClan"); + + b.Property("GuildId"); + + b.Property("Size"); + + b.Property("StartedAt"); + + b.Property("WarState"); + + b.HasKey("Id"); + + b.ToTable("ClashOfClans"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.CommandAlias", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("GuildConfigId"); + + b.Property("Mapping"); + + b.Property("Trigger"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.ToTable("CommandAlias"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.CommandCooldown", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("CommandName"); + + b.Property("DateAdded"); + + b.Property("GuildConfigId"); + + b.Property("Seconds"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.ToTable("CommandCooldown"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.CommandPrice", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("BotConfigId"); + + b.Property("CommandName"); + + b.Property("DateAdded"); + + b.Property("Price"); + + b.HasKey("Id"); + + b.HasIndex("BotConfigId"); + + b.HasIndex("Price") + .IsUnique(); + + b.ToTable("CommandPrice"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.ConvertUnit", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("InternalTrigger"); + + b.Property("Modifier"); + + b.Property("UnitType"); + + b.HasKey("Id"); + + b.ToTable("ConversionUnits"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.Currency", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("Amount"); + + b.Property("DateAdded"); + + b.Property("UserId"); + + b.HasKey("Id"); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("Currency"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.CurrencyTransaction", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("Amount"); + + b.Property("DateAdded"); + + b.Property("Reason"); + + b.Property("UserId"); + + b.HasKey("Id"); + + b.ToTable("CurrencyTransactions"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.CustomReaction", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AutoDeleteTrigger"); + + b.Property("DateAdded"); + + b.Property("DmResponse"); + + b.Property("GuildId"); + + b.Property("IsRegex"); + + b.Property("OwnerOnly"); + + b.Property("Response"); + + b.Property("Trigger"); + + b.HasKey("Id"); + + b.ToTable("CustomReactions"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.DiscordUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AvatarId"); + + b.Property("DateAdded"); + + b.Property("Discriminator"); + + b.Property("UserId"); + + b.Property("Username"); + + b.HasKey("Id"); + + b.HasAlternateKey("UserId"); + + b.ToTable("DiscordUser"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.Donator", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("Amount"); + + b.Property("DateAdded"); + + b.Property("Name"); + + b.Property("UserId"); + + b.HasKey("Id"); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("Donators"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.EightBallResponse", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("BotConfigId"); + + b.Property("DateAdded"); + + b.Property("Text"); + + b.HasKey("Id"); + + b.HasIndex("BotConfigId"); + + b.ToTable("EightBallResponses"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.FilterChannelId", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ChannelId"); + + b.Property("DateAdded"); + + b.Property("GuildConfigId"); + + b.Property("GuildConfigId1"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.HasIndex("GuildConfigId1"); + + b.ToTable("FilterChannelId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.FilteredWord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("GuildConfigId"); + + b.Property("Word"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.ToTable("FilteredWord"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.FollowedStream", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ChannelId"); + + b.Property("DateAdded"); + + b.Property("GuildConfigId"); + + b.Property("GuildId"); + + b.Property("Type"); + + b.Property("Username"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.ToTable("FollowedStream"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.GCChannelId", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ChannelId"); + + b.Property("DateAdded"); + + b.Property("GuildConfigId"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.ToTable("GCChannelId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.GuildConfig", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AutoAssignRoleId"); + + b.Property("AutoDeleteByeMessages"); + + b.Property("AutoDeleteByeMessagesTimer"); + + b.Property("AutoDeleteGreetMessages"); + + b.Property("AutoDeleteGreetMessagesTimer"); + + b.Property("AutoDeleteSelfAssignedRoleMessages"); + + b.Property("ByeMessageChannelId"); + + b.Property("ChannelByeMessageText"); + + b.Property("ChannelGreetMessageText"); + + b.Property("CleverbotEnabled"); + + b.Property("DateAdded"); + + b.Property("DefaultMusicVolume"); + + b.Property("DeleteMessageOnCommand"); + + b.Property("DmGreetMessageText"); + + b.Property("ExclusiveSelfAssignedRoles"); + + b.Property("FilterInvites"); + + b.Property("FilterWords"); + + b.Property("GameVoiceChannel"); + + b.Property("GreetMessageChannelId"); + + b.Property("GuildId"); + + b.Property("Locale"); + + b.Property("LogSettingId"); + + b.Property("MuteRoleName"); + + b.Property("PermissionRole"); + + b.Property("Prefix"); + + b.Property("RootPermissionId"); + + b.Property("SendChannelByeMessage"); + + b.Property("SendChannelGreetMessage"); + + b.Property("SendDmGreetMessage"); + + b.Property("TimeZoneId"); + + b.Property("VerboseErrors"); + + b.Property("VerbosePermissions"); + + b.Property("VoicePlusTextEnabled"); + + b.Property("WarningsInitialized"); + + b.HasKey("Id"); + + b.HasIndex("GuildId") + .IsUnique(); + + b.HasIndex("LogSettingId"); + + b.HasIndex("RootPermissionId"); + + b.ToTable("GuildConfigs"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.GuildRepeater", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ChannelId"); + + b.Property("DateAdded"); + + b.Property("GuildConfigId"); + + b.Property("GuildId"); + + b.Property("Interval"); + + b.Property("Message"); + + b.Property("StartTimeOfDay"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.ToTable("GuildRepeater"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.IgnoredLogChannel", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ChannelId"); + + b.Property("DateAdded"); + + b.Property("LogSettingId"); + + b.HasKey("Id"); + + b.HasIndex("LogSettingId"); + + b.ToTable("IgnoredLogChannels"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.IgnoredVoicePresenceChannel", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ChannelId"); + + b.Property("DateAdded"); + + b.Property("LogSettingId"); + + b.HasKey("Id"); + + b.HasIndex("LogSettingId"); + + b.ToTable("IgnoredVoicePresenceCHannels"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.LogSetting", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ChannelCreated"); + + b.Property("ChannelCreatedId"); + + b.Property("ChannelDestroyed"); + + b.Property("ChannelDestroyedId"); + + b.Property("ChannelId"); + + b.Property("ChannelUpdated"); + + b.Property("ChannelUpdatedId"); + + b.Property("DateAdded"); + + b.Property("IsLogging"); + + b.Property("LogOtherId"); + + b.Property("LogUserPresence"); + + b.Property("LogUserPresenceId"); + + b.Property("LogVoicePresence"); + + b.Property("LogVoicePresenceId"); + + b.Property("LogVoicePresenceTTSId"); + + b.Property("MessageDeleted"); + + b.Property("MessageDeletedId"); + + b.Property("MessageUpdated"); + + b.Property("MessageUpdatedId"); + + b.Property("UserBanned"); + + b.Property("UserBannedId"); + + b.Property("UserJoined"); + + b.Property("UserJoinedId"); + + b.Property("UserLeft"); + + b.Property("UserLeftId"); + + b.Property("UserMutedId"); + + b.Property("UserPresenceChannelId"); + + b.Property("UserUnbanned"); + + b.Property("UserUnbannedId"); + + b.Property("UserUpdated"); + + b.Property("UserUpdatedId"); + + b.Property("VoicePresenceChannelId"); + + b.HasKey("Id"); + + b.ToTable("LogSettings"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.ModulePrefix", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("BotConfigId"); + + b.Property("DateAdded"); + + b.Property("ModuleName"); + + b.Property("Prefix"); + + b.HasKey("Id"); + + b.HasIndex("BotConfigId"); + + b.ToTable("ModulePrefixes"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.MusicPlaylist", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("Author"); + + b.Property("AuthorId"); + + b.Property("DateAdded"); + + b.Property("Name"); + + b.HasKey("Id"); + + b.ToTable("MusicPlaylists"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.MutedUserId", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("GuildConfigId"); + + b.Property("UserId"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.ToTable("MutedUserId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.NsfwBlacklitedTag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("GuildConfigId"); + + b.Property("Tag"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.ToTable("NsfwBlacklitedTag"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.Permission", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("NextId"); + + b.Property("PrimaryTarget"); + + b.Property("PrimaryTargetId"); + + b.Property("SecondaryTarget"); + + b.Property("SecondaryTargetName"); + + b.Property("State"); + + b.HasKey("Id"); + + b.HasIndex("NextId") + .IsUnique(); + + b.ToTable("Permission"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.Permissionv2", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("GuildConfigId"); + + b.Property("Index"); + + b.Property("PrimaryTarget"); + + b.Property("PrimaryTargetId"); + + b.Property("SecondaryTarget"); + + b.Property("SecondaryTargetName"); + + b.Property("State"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.ToTable("Permissionv2"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.PlayingStatus", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("BotConfigId"); + + b.Property("DateAdded"); + + b.Property("Status"); + + b.HasKey("Id"); + + b.HasIndex("BotConfigId"); + + b.ToTable("PlayingStatus"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.PlaylistSong", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("MusicPlaylistId"); + + b.Property("Provider"); + + b.Property("ProviderType"); + + b.Property("Query"); + + b.Property("Title"); + + b.Property("Uri"); + + b.HasKey("Id"); + + b.HasIndex("MusicPlaylistId"); + + b.ToTable("PlaylistSong"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.Quote", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AuthorId"); + + b.Property("AuthorName") + .IsRequired(); + + b.Property("DateAdded"); + + b.Property("GuildId"); + + b.Property("Keyword") + .IsRequired(); + + b.Property("Text") + .IsRequired(); + + b.HasKey("Id"); + + b.ToTable("Quotes"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.RaceAnimal", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("BotConfigId"); + + b.Property("DateAdded"); + + b.Property("Icon"); + + b.Property("Name"); + + b.HasKey("Id"); + + b.HasIndex("BotConfigId"); + + b.ToTable("RaceAnimals"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.Reminder", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ChannelId"); + + b.Property("DateAdded"); + + b.Property("IsPrivate"); + + b.Property("Message"); + + b.Property("ServerId"); + + b.Property("UserId"); + + b.Property("When"); + + b.HasKey("Id"); + + b.ToTable("Reminders"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.RewardedUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AmountRewardedThisMonth"); + + b.Property("DateAdded"); + + b.Property("LastReward"); + + b.Property("PatreonUserId"); + + b.Property("UserId"); + + b.HasKey("Id"); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("RewardedUsers"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.SelfAssignedRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("GuildId"); + + b.Property("RoleId"); + + b.HasKey("Id"); + + b.HasIndex("GuildId", "RoleId") + .IsUnique(); + + b.ToTable("SelfAssignableRoles"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.ShopEntry", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AuthorId"); + + b.Property("DateAdded"); + + b.Property("GuildConfigId"); + + b.Property("Index"); + + b.Property("Name"); + + b.Property("Price"); + + b.Property("RoleId"); + + b.Property("RoleName"); + + b.Property("Type"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.ToTable("ShopEntry"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.ShopEntryItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("ShopEntryId"); + + b.Property("Text"); + + b.HasKey("Id"); + + b.HasIndex("ShopEntryId"); + + b.ToTable("ShopEntryItem"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.SlowmodeIgnoredRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("GuildConfigId"); + + b.Property("RoleId"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.ToTable("SlowmodeIgnoredRole"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.SlowmodeIgnoredUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("GuildConfigId"); + + b.Property("UserId"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.ToTable("SlowmodeIgnoredUser"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.StartupCommand", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("BotConfigId"); + + b.Property("ChannelId"); + + b.Property("ChannelName"); + + b.Property("CommandText"); + + b.Property("DateAdded"); + + b.Property("GuildId"); + + b.Property("GuildName"); + + b.Property("Index"); + + b.Property("VoiceChannelId"); + + b.Property("VoiceChannelName"); + + b.HasKey("Id"); + + b.HasIndex("BotConfigId"); + + b.ToTable("StartupCommand"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.StreamRoleBlacklistedUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("StreamRoleSettingsId"); + + b.Property("UserId"); + + b.Property("Username"); + + b.HasKey("Id"); + + b.HasIndex("StreamRoleSettingsId"); + + b.ToTable("StreamRoleBlacklistedUser"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.StreamRoleSettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AddRoleId"); + + b.Property("DateAdded"); + + b.Property("Enabled"); + + b.Property("FromRoleId"); + + b.Property("GuildConfigId"); + + b.Property("Keyword"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId") + .IsUnique(); + + b.ToTable("StreamRoleSettings"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.StreamRoleWhitelistedUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("StreamRoleSettingsId"); + + b.Property("UserId"); + + b.Property("Username"); + + b.HasKey("Id"); + + b.HasIndex("StreamRoleSettingsId"); + + b.ToTable("StreamRoleWhitelistedUser"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.UnmuteTimer", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("GuildConfigId"); + + b.Property("UnmuteAt"); + + b.Property("UserId"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.ToTable("UnmuteTimer"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.UserPokeTypes", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("UserId"); + + b.Property("type"); + + b.HasKey("Id"); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("PokeGame"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.VcRoleInfo", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("GuildConfigId"); + + b.Property("RoleId"); + + b.Property("VoiceChannelId"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.ToTable("VcRoleInfo"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.WaifuInfo", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AffinityId"); + + b.Property("ClaimerId"); + + b.Property("DateAdded"); + + b.Property("Price"); + + b.Property("WaifuId"); + + b.HasKey("Id"); + + b.HasIndex("AffinityId"); + + b.HasIndex("ClaimerId"); + + b.HasIndex("WaifuId") + .IsUnique(); + + b.ToTable("WaifuInfo"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.WaifuUpdate", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("NewId"); + + b.Property("OldId"); + + b.Property("UpdateType"); + + b.Property("UserId"); + + b.HasKey("Id"); + + b.HasIndex("NewId"); + + b.HasIndex("OldId"); + + b.HasIndex("UserId"); + + b.ToTable("WaifuUpdates"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.Warning", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("Forgiven"); + + b.Property("ForgivenBy"); + + b.Property("GuildId"); + + b.Property("Moderator"); + + b.Property("Reason"); + + b.Property("UserId"); + + b.HasKey("Id"); + + b.ToTable("Warnings"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.WarningPunishment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("Count"); + + b.Property("DateAdded"); + + b.Property("GuildConfigId"); + + b.Property("Punishment"); + + b.Property("Time"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.ToTable("WarningPunishment"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.AntiRaidSetting", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig", "GuildConfig") + .WithOne("AntiRaidSetting") + .HasForeignKey("NadekoBot.Services.Database.Models.AntiRaidSetting", "GuildConfigId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.AntiSpamIgnore", b => + { + b.HasOne("NadekoBot.Services.Database.Models.AntiSpamSetting") + .WithMany("IgnoredChannels") + .HasForeignKey("AntiSpamSettingId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.AntiSpamSetting", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig", "GuildConfig") + .WithOne("AntiSpamSetting") + .HasForeignKey("NadekoBot.Services.Database.Models.AntiSpamSetting", "GuildConfigId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.BlacklistItem", b => + { + b.HasOne("NadekoBot.Services.Database.Models.BotConfig") + .WithMany("Blacklist") + .HasForeignKey("BotConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.BlockedCmdOrMdl", b => + { + b.HasOne("NadekoBot.Services.Database.Models.BotConfig") + .WithMany("BlockedCommands") + .HasForeignKey("BotConfigId"); + + b.HasOne("NadekoBot.Services.Database.Models.BotConfig") + .WithMany("BlockedModules") + .HasForeignKey("BotConfigId1"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.ClashCaller", b => + { + b.HasOne("NadekoBot.Services.Database.Models.ClashWar", "ClashWar") + .WithMany("Bases") + .HasForeignKey("ClashWarId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.CommandAlias", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig") + .WithMany("CommandAliases") + .HasForeignKey("GuildConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.CommandCooldown", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig") + .WithMany("CommandCooldowns") + .HasForeignKey("GuildConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.CommandPrice", b => + { + b.HasOne("NadekoBot.Services.Database.Models.BotConfig") + .WithMany("CommandPrices") + .HasForeignKey("BotConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.EightBallResponse", b => + { + b.HasOne("NadekoBot.Services.Database.Models.BotConfig") + .WithMany("EightBallResponses") + .HasForeignKey("BotConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.FilterChannelId", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig") + .WithMany("FilterInvitesChannelIds") + .HasForeignKey("GuildConfigId"); + + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig") + .WithMany("FilterWordsChannelIds") + .HasForeignKey("GuildConfigId1"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.FilteredWord", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig") + .WithMany("FilteredWords") + .HasForeignKey("GuildConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.FollowedStream", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig") + .WithMany("FollowedStreams") + .HasForeignKey("GuildConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.GCChannelId", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig") + .WithMany("GenerateCurrencyChannelIds") + .HasForeignKey("GuildConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.GuildConfig", b => + { + b.HasOne("NadekoBot.Services.Database.Models.LogSetting", "LogSetting") + .WithMany() + .HasForeignKey("LogSettingId"); + + b.HasOne("NadekoBot.Services.Database.Models.Permission", "RootPermission") + .WithMany() + .HasForeignKey("RootPermissionId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.GuildRepeater", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig") + .WithMany("GuildRepeaters") + .HasForeignKey("GuildConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.IgnoredLogChannel", b => + { + b.HasOne("NadekoBot.Services.Database.Models.LogSetting", "LogSetting") + .WithMany("IgnoredChannels") + .HasForeignKey("LogSettingId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.IgnoredVoicePresenceChannel", b => + { + b.HasOne("NadekoBot.Services.Database.Models.LogSetting", "LogSetting") + .WithMany("IgnoredVoicePresenceChannelIds") + .HasForeignKey("LogSettingId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.ModulePrefix", b => + { + b.HasOne("NadekoBot.Services.Database.Models.BotConfig") + .WithMany("ModulePrefixes") + .HasForeignKey("BotConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.MutedUserId", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig") + .WithMany("MutedUsers") + .HasForeignKey("GuildConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.NsfwBlacklitedTag", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig") + .WithMany("NsfwBlacklistedTags") + .HasForeignKey("GuildConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.Permission", b => + { + b.HasOne("NadekoBot.Services.Database.Models.Permission", "Next") + .WithOne("Previous") + .HasForeignKey("NadekoBot.Services.Database.Models.Permission", "NextId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.Permissionv2", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig") + .WithMany("Permissions") + .HasForeignKey("GuildConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.PlayingStatus", b => + { + b.HasOne("NadekoBot.Services.Database.Models.BotConfig") + .WithMany("RotatingStatusMessages") + .HasForeignKey("BotConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.PlaylistSong", b => + { + b.HasOne("NadekoBot.Services.Database.Models.MusicPlaylist") + .WithMany("Songs") + .HasForeignKey("MusicPlaylistId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.RaceAnimal", b => + { + b.HasOne("NadekoBot.Services.Database.Models.BotConfig") + .WithMany("RaceAnimals") + .HasForeignKey("BotConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.ShopEntry", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig") + .WithMany("ShopEntries") + .HasForeignKey("GuildConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.ShopEntryItem", b => + { + b.HasOne("NadekoBot.Services.Database.Models.ShopEntry") + .WithMany("Items") + .HasForeignKey("ShopEntryId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.SlowmodeIgnoredRole", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig") + .WithMany("SlowmodeIgnoredRoles") + .HasForeignKey("GuildConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.SlowmodeIgnoredUser", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig") + .WithMany("SlowmodeIgnoredUsers") + .HasForeignKey("GuildConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.StartupCommand", b => + { + b.HasOne("NadekoBot.Services.Database.Models.BotConfig") + .WithMany("StartupCommands") + .HasForeignKey("BotConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.StreamRoleBlacklistedUser", b => + { + b.HasOne("NadekoBot.Services.Database.Models.StreamRoleSettings") + .WithMany("Blacklist") + .HasForeignKey("StreamRoleSettingsId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.StreamRoleSettings", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig", "GuildConfig") + .WithOne("StreamRole") + .HasForeignKey("NadekoBot.Services.Database.Models.StreamRoleSettings", "GuildConfigId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.StreamRoleWhitelistedUser", b => + { + b.HasOne("NadekoBot.Services.Database.Models.StreamRoleSettings") + .WithMany("Whitelist") + .HasForeignKey("StreamRoleSettingsId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.UnmuteTimer", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig") + .WithMany("UnmuteTimers") + .HasForeignKey("GuildConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.VcRoleInfo", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig") + .WithMany("VcRoleInfos") + .HasForeignKey("GuildConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.WaifuInfo", b => + { + b.HasOne("NadekoBot.Services.Database.Models.DiscordUser", "Affinity") + .WithMany() + .HasForeignKey("AffinityId"); + + b.HasOne("NadekoBot.Services.Database.Models.DiscordUser", "Claimer") + .WithMany() + .HasForeignKey("ClaimerId"); + + b.HasOne("NadekoBot.Services.Database.Models.DiscordUser", "Waifu") + .WithOne() + .HasForeignKey("NadekoBot.Services.Database.Models.WaifuInfo", "WaifuId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.WaifuUpdate", b => + { + b.HasOne("NadekoBot.Services.Database.Models.DiscordUser", "New") + .WithMany() + .HasForeignKey("NewId"); + + b.HasOne("NadekoBot.Services.Database.Models.DiscordUser", "Old") + .WithMany() + .HasForeignKey("OldId"); + + b.HasOne("NadekoBot.Services.Database.Models.DiscordUser", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.WarningPunishment", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig") + .WithMany("WarnPunishments") + .HasForeignKey("GuildConfigId"); + }); + } + } +} diff --git a/src/NadekoBot/Migrations/20170721004230_nsfw-blacklist.cs b/src/NadekoBot/Migrations/20170721004230_nsfw-blacklist.cs new file mode 100644 index 00000000..f255fabc --- /dev/null +++ b/src/NadekoBot/Migrations/20170721004230_nsfw-blacklist.cs @@ -0,0 +1,44 @@ +using System; +using System.Collections.Generic; +using Microsoft.EntityFrameworkCore.Migrations; + +namespace NadekoBot.Migrations +{ + public partial class nsfwblacklist : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "NsfwBlacklitedTag", + columns: table => new + { + Id = table.Column(nullable: false) + .Annotation("Sqlite:Autoincrement", true), + DateAdded = table.Column(nullable: true), + GuildConfigId = table.Column(nullable: true), + Tag = table.Column(nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_NsfwBlacklitedTag", x => x.Id); + table.ForeignKey( + name: "FK_NsfwBlacklitedTag_GuildConfigs_GuildConfigId", + column: x => x.GuildConfigId, + principalTable: "GuildConfigs", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + }); + + migrationBuilder.CreateIndex( + name: "IX_NsfwBlacklitedTag_GuildConfigId", + table: "NsfwBlacklitedTag", + column: "GuildConfigId"); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "NsfwBlacklitedTag"); + } + } +} diff --git a/src/NadekoBot/Migrations/NadekoSqliteContextModelSnapshot.cs b/src/NadekoBot/Migrations/NadekoSqliteContextModelSnapshot.cs index df766d57..46b5b476 100644 --- a/src/NadekoBot/Migrations/NadekoSqliteContextModelSnapshot.cs +++ b/src/NadekoBot/Migrations/NadekoSqliteContextModelSnapshot.cs @@ -2,7 +2,9 @@ using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; using NadekoBot.Services.Database; +using NadekoBot.Services.Database.Models; namespace NadekoBot.Migrations { @@ -798,6 +800,24 @@ namespace NadekoBot.Migrations b.ToTable("MutedUserId"); }); + modelBuilder.Entity("NadekoBot.Services.Database.Models.NsfwBlacklitedTag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("GuildConfigId"); + + b.Property("Tag"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.ToTable("NsfwBlacklitedTag"); + }); + modelBuilder.Entity("NadekoBot.Services.Database.Models.Permission", b => { b.Property("Id") @@ -1502,6 +1522,13 @@ namespace NadekoBot.Migrations .HasForeignKey("GuildConfigId"); }); + modelBuilder.Entity("NadekoBot.Services.Database.Models.NsfwBlacklitedTag", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig") + .WithMany("NsfwBlacklistedTags") + .HasForeignKey("GuildConfigId"); + }); + modelBuilder.Entity("NadekoBot.Services.Database.Models.Permission", b => { b.HasOne("NadekoBot.Services.Database.Models.Permission", "Next") diff --git a/src/NadekoBot/Modules/NSFW/Exceptions/TagBlacklistedException.cs b/src/NadekoBot/Modules/NSFW/Exceptions/TagBlacklistedException.cs new file mode 100644 index 00000000..c0f881f3 --- /dev/null +++ b/src/NadekoBot/Modules/NSFW/Exceptions/TagBlacklistedException.cs @@ -0,0 +1,12 @@ +using System; + +namespace NadekoBot.Modules.NSFW.Exceptions +{ + public class TagBlacklistedException : Exception + { + public TagBlacklistedException() : base("Tag you used is blacklisted.") + { + + } + } +} diff --git a/src/NadekoBot/Modules/NSFW/NSFW.cs b/src/NadekoBot/Modules/NSFW/NSFW.cs index 686c8c33..81daf9c6 100644 --- a/src/NadekoBot/Modules/NSFW/NSFW.cs +++ b/src/NadekoBot/Modules/NSFW/NSFW.cs @@ -13,6 +13,7 @@ using NadekoBot.Common.Attributes; using NadekoBot.Common.Collections; using NadekoBot.Modules.Searches.Common; using NadekoBot.Modules.Searches.Services; +using NadekoBot.Modules.NSFW.Exceptions; namespace NadekoBot.Modules.NSFW { @@ -26,7 +27,16 @@ namespace NadekoBot.Modules.NSFW var rng = new NadekoRandom(); var arr = Enum.GetValues(typeof(DapiSearchType)); var type = (DapiSearchType)arr.GetValue(new NadekoRandom().Next(2, arr.Length)); - var img = await _service.DapiSearch(tag, type, Context.Guild?.Id, true).ConfigureAwait(false); + ImageCacherObject img; + try + { + img = await _service.DapiSearch(tag, type, Context.Guild?.Id, true).ConfigureAwait(false); + } + catch (TagBlacklistedException) + { + await ReplyErrorLocalized("blacklisted_tag").ConfigureAwait(false); + return; + } if (img == null) { @@ -179,9 +189,42 @@ namespace NadekoBot.Modules.NSFW } } + [NadekoCommand, Usage, Description, Aliases] + [RequireContext(ContextType.Guild)] + public async Task NsfwTagBlacklist([Remainder] string tag = null) + { + if (string.IsNullOrWhiteSpace(tag)) + { + var blTags = _service.GetBlacklistedTags(Context.Guild.Id); + await Context.Channel.SendConfirmAsync(GetText("blacklisted_tag_list"), + blTags.Any() + ? string.Join(", ", blTags) + : "-").ConfigureAwait(false); + } + else + { + tag = tag.Trim().ToLowerInvariant(); + var added = _service.ToggleBlacklistedTag(Context.Guild.Id, tag); + + if(added) + await ReplyConfirmLocalized("blacklisted_tag_add", tag).ConfigureAwait(false); + else + await ReplyConfirmLocalized("blacklisted_tag_remove", tag).ConfigureAwait(false); + } + } + public async Task InternalDapiCommand(string tag, DapiSearchType type, bool forceExplicit) { - var imgObj = await _service.DapiSearch(tag, type, Context.Guild?.Id, forceExplicit).ConfigureAwait(false); + ImageCacherObject imgObj; + try + { + imgObj = await _service.DapiSearch(tag, type, Context.Guild?.Id, forceExplicit).ConfigureAwait(false); + } + catch (TagBlacklistedException) + { + await ReplyErrorLocalized("blacklisted_tag").ConfigureAwait(false); + return; + } if (imgObj == null) await ReplyErrorLocalized("not_found").ConfigureAwait(false); diff --git a/src/NadekoBot/Modules/Searches/Services/SearchesService.cs b/src/NadekoBot/Modules/Searches/Services/SearchesService.cs index e8a72402..c8819b11 100644 --- a/src/NadekoBot/Modules/Searches/Services/SearchesService.cs +++ b/src/NadekoBot/Modules/Searches/Services/SearchesService.cs @@ -10,6 +10,11 @@ using System.Collections.Generic; using System.IO; using System.Threading.Tasks; using NadekoBot.Modules.Searches.Common; +using NadekoBot.Common.Collections; +using NadekoBot.Services.Database.Models; +using System.Linq; +using Microsoft.EntityFrameworkCore; +using NadekoBot.Modules.NSFW.Exceptions; namespace NadekoBot.Modules.Searches.Services { @@ -33,13 +38,20 @@ namespace NadekoBot.Modules.Searches.Services private readonly ConcurrentDictionary _imageCacher = new ConcurrentDictionary(); - public SearchesService(DiscordSocketClient client, IGoogleApiService google, DbService db) + private readonly ConcurrentDictionary> _blacklistedTags = new ConcurrentDictionary>(); + + public SearchesService(DiscordSocketClient client, IGoogleApiService google, DbService db, IEnumerable gcs) { _client = client; _google = google; _db = db; _log = LogManager.GetCurrentClassLogger(); + _blacklistedTags = new ConcurrentDictionary>( + gcs.ToDictionary( + x => x.GuildId, + x => new HashSet(x.NsfwBlacklistedTags.Select(y => y.Tag)))); + //translate commands _client.MessageReceived += (msg) => { @@ -117,10 +129,48 @@ namespace NadekoBot.Modules.Searches.Services public Task DapiSearch(string tag, DapiSearchType type, ulong? guild, bool isExplicit = false) { + if (guild.HasValue && GetBlacklistedTags(guild.Value) + .Any(x => tag.ToLowerInvariant().Contains(x))) + { + throw new TagBlacklistedException(); + } var cacher = _imageCacher.GetOrAdd(guild, (key) => new SearchImageCacher()); return cacher.GetImage(tag, isExplicit, type); } + + public HashSet GetBlacklistedTags(ulong guildId) + { + if (_blacklistedTags.TryGetValue(guildId, out var tags)) + return tags; + return new HashSet(); + } + + public bool ToggleBlacklistedTag(ulong guildId, string tag) + { + var tagObj = new NsfwBlacklitedTag + { + Tag = tag + }; + + bool added; + using (var uow = _db.UnitOfWork) + { + var gc = uow.GuildConfigs.For(guildId, set => set.Include(y => y.NsfwBlacklistedTags)); + if (gc.NsfwBlacklistedTags.Add(tagObj)) + added = true; + else + { + gc.NsfwBlacklistedTags.Remove(tagObj); + added = false; + } + var newTags = new HashSet(gc.NsfwBlacklistedTags.Select(x => x.Tag)); + _blacklistedTags.AddOrUpdate(guildId, newTags, delegate { return newTags; }); + + uow.Complete(); + } + return added; + } } public struct UserChannelPair diff --git a/src/NadekoBot/Resources/CommandStrings.resx b/src/NadekoBot/Resources/CommandStrings.resx index 03aaed3c..4b98b292 100644 --- a/src/NadekoBot/Resources/CommandStrings.resx +++ b/src/NadekoBot/Resources/CommandStrings.resx @@ -3582,4 +3582,13 @@ Sets one of available bot config settings to a specified value. Use the command without any parameters to get a list of available settings. + + nsfwtagbl nsfwtbl + + + `{0}nsfwtbl poop` + + + Toggles whether the tag is blacklisted or not in nsfw searches. Provide no parameters to see the list of blacklisted tags. + diff --git a/src/NadekoBot/Services/Database/Models/GuildConfig.cs b/src/NadekoBot/Services/Database/Models/GuildConfig.cs index 83498097..7ae60508 100644 --- a/src/NadekoBot/Services/Database/Models/GuildConfig.cs +++ b/src/NadekoBot/Services/Database/Models/GuildConfig.cs @@ -78,6 +78,7 @@ namespace NadekoBot.Services.Database.Models public bool WarningsInitialized { get; set; } public HashSet SlowmodeIgnoredUsers { get; set; } public HashSet SlowmodeIgnoredRoles { get; set; } + public HashSet NsfwBlacklistedTags { get; set; } = new HashSet(); public List ShopEntries { get; set; } public ulong? GameVoiceChannel { get; set; } = null; @@ -88,6 +89,23 @@ namespace NadekoBot.Services.Database.Models //public List ProtectionIgnoredChannels { get; set; } = new List(); } + public class NsfwBlacklitedTag : DbEntity + { + public string Tag { get; set; } + + public override int GetHashCode() + { + return Tag.GetHashCode(); + } + + public override bool Equals(object obj) + { + return obj is NsfwBlacklitedTag x + ? x.Tag == Tag + : false; + } + } + public class SlowmodeIgnoredUser : DbEntity { public ulong UserId { get; set; } diff --git a/src/NadekoBot/Services/Database/Repositories/Impl/GuildConfigRepository.cs b/src/NadekoBot/Services/Database/Repositories/Impl/GuildConfigRepository.cs index 9c0931d5..e6087bec 100644 --- a/src/NadekoBot/Services/Database/Repositories/Impl/GuildConfigRepository.cs +++ b/src/NadekoBot/Services/Database/Repositories/Impl/GuildConfigRepository.cs @@ -46,6 +46,7 @@ namespace NadekoBot.Services.Database.Repositories.Impl .ThenInclude(x => x.IgnoredChannels) .Include(gc => gc.FollowedStreams) .Include(gc => gc.StreamRole) + .Include(gc => gc.NsfwBlacklistedTags) .ToList(); /// diff --git a/src/NadekoBot/_strings/ResponseStrings.en-US.json b/src/NadekoBot/_strings/ResponseStrings.en-US.json index f9e47119..74303303 100644 --- a/src/NadekoBot/_strings/ResponseStrings.en-US.json +++ b/src/NadekoBot/_strings/ResponseStrings.en-US.json @@ -38,6 +38,10 @@ "customreactions_redacted_too_long": "Redecated because it's too long.", "nsfw_autohentai_stopped": "Autohentai stopped.", "nsfw_not_found": "No results found.", + "nsfw_blacklisted_tag_list": "List of blacklisted tags:", + "nsfw_blacklisted_tag": "One or more tags you've used are blacklisted", + "nsfw_blacklisted_tag_add": "Nsfw tag {0} is now blacklisted.", + "nsfw_blacklisted_tag_remove": "Nsfw tag {0} is no longer blacklisted.", "pokemon_already_fainted": "{0} has already fainted.", "pokemon_already_full": "{0} already has full HP.", "pokemon_already_that_type": "Your type is already {0}", From d074444c26bb48af21ab6210d53a8b65f43b1eeb Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Fri, 21 Jul 2017 03:05:13 +0200 Subject: [PATCH 207/346] commandlist updated --- docs/Commands List.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/Commands List.md b/docs/Commands List.md index 925c5e01..4c9001a8 100644 --- a/docs/Commands List.md +++ b/docs/Commands List.md @@ -266,6 +266,7 @@ Commands and aliases | Description | Usage `.gelbooru` | Shows a random hentai image from gelbooru with a given tag. Tag is optional but preferred. (multiple tags are appended with +) | `.gelbooru yuri+kissing` `.boobs` | Real adult content. | `.boobs` `.butts` `.ass` `.butt` | Real adult content. | `.butts` or `.ass` +`.nsfwtagbl` `.nsfwtbl` | Toggles whether the tag is blacklisted or not in nsfw searches. Provide no parameters to see the list of blacklisted tags. | `.nsfwtbl poop` ###### [Back to ToC](#table-of-contents) From b9bb72f06d64b471bf3a78b9def8c4c6f2eba19c Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Fri, 21 Jul 2017 06:56:21 +0200 Subject: [PATCH 208/346] Streamrole is smarter, but possibly more expensive. It will rescan users when settings are changed. And when the bot is started. --- src/NadekoBot/Modules/Gambling/Gambling.cs | 2 +- .../Utility/Services/StreamRoleService.cs | 213 +++++++++--------- .../Modules/Utility/StreamRoleCommands.cs | 10 +- src/NadekoBot/_Extensions/Extensions.cs | 4 +- 4 files changed, 121 insertions(+), 108 deletions(-) diff --git a/src/NadekoBot/Modules/Gambling/Gambling.cs b/src/NadekoBot/Modules/Gambling/Gambling.cs index 07de5bc8..2ecda7d7 100644 --- a/src/NadekoBot/Modules/Gambling/Gambling.cs +++ b/src/NadekoBot/Modules/Gambling/Gambling.cs @@ -42,7 +42,7 @@ namespace NadekoBot.Modules.Gambling { role = role ?? Context.Guild.EveryoneRole; - var members = role.Members().Where(u => u.Status != UserStatus.Offline); + var members = (await role.GetMembersAsync()).Where(u => u.Status != UserStatus.Offline); var membersArray = members as IUser[] ?? members.ToArray(); if (membersArray.Length == 0) { diff --git a/src/NadekoBot/Modules/Utility/Services/StreamRoleService.cs b/src/NadekoBot/Modules/Utility/Services/StreamRoleService.cs index 65560db9..38ec4668 100644 --- a/src/NadekoBot/Modules/Utility/Services/StreamRoleService.cs +++ b/src/NadekoBot/Modules/Utility/Services/StreamRoleService.cs @@ -21,8 +21,6 @@ namespace NadekoBot.Modules.Utility.Services { private readonly DbService _db; private readonly ConcurrentDictionary guildSettings; - //(guildId, userId), roleId - private readonly ConcurrentDictionary<(ulong GuildId, ulong UserId), ulong> toRemove = new ConcurrentDictionary<(ulong GuildId, ulong UserId), ulong>(); private readonly Logger _log; public StreamRoleService(DiscordSocketClient client, DbService db, IEnumerable gcs) @@ -35,6 +33,18 @@ namespace NadekoBot.Modules.Utility.Services .ToConcurrent(); client.GuildMemberUpdated += Client_GuildMemberUpdated; + + var _ = Task.Run(async () => + { + try + { + await Task.WhenAll(client.Guilds.Select(g => RescanUsers(g))).ConfigureAwait(false); + } + catch + { + // ignored + } + }); } private Task Client_GuildMemberUpdated(SocketGuildUser before, SocketGuildUser after) @@ -42,112 +52,30 @@ namespace NadekoBot.Modules.Utility.Services var _ = Task.Run(async () => { //if user wasn't streaming or didn't have a game status at all - if ((!before.Game.HasValue || before.Game.Value.StreamType == StreamType.NotStreaming) - && guildSettings.TryGetValue(after.Guild.Id, out var setting)) + if (guildSettings.TryGetValue(after.Guild.Id, out var setting)) { - await TryApplyRole(after, setting).ConfigureAwait(false); - } - - // try removing a role that was given to the user - // if user had a game status - // and he was streaming - // and he no longer has a game status, or has a game status which is not a stream - // and if he's scheduled for role removal, get the roleid to remove - else if (before.Game.HasValue && - before.Game.Value.StreamType != StreamType.NotStreaming && - (!after.Game.HasValue || after.Game.Value.StreamType == StreamType.NotStreaming) && - toRemove.TryRemove((after.Guild.Id, after.Id), out var roleId)) - { - try - { - //get the role to remove from the role id - var role = after.Guild.GetRole(roleId); - if (role == null) - return; - //check if user has the role which needs to be removed to avoid errors - if (after.Roles.Contains(role)) - await after.RemoveRoleAsync(role).ConfigureAwait(false); - } - catch (Exception ex) - { - _log.Warn("Failed removing the stream role from the user who stopped streaming."); - _log.Error(ex); - } + await RescanUser(after, setting).ConfigureAwait(false); } }); return Task.CompletedTask; } - - private async Task TryApplyRole(IGuildUser user, StreamRoleSettings setting) - { - // if the user has a game status now - // and that status is a streaming status - // and the feature is enabled - // and he's not blacklisted - // and keyword is either not set, or the game contains the keyword required, or he's whitelisted - if (user.Game.HasValue && - user.Game.Value.StreamType != StreamType.NotStreaming - && setting.Enabled - && !setting.Blacklist.Any(x => x.UserId == user.Id) - && (string.IsNullOrWhiteSpace(setting.Keyword) - || user.Game.Value.Name.Contains(setting.Keyword) - || setting.Whitelist.Any(x => x.UserId == user.Id))) - { - IRole fromRole; - IRole addRole; - - //get needed roles - fromRole = user.Guild.GetRole(setting.FromRoleId); - if (fromRole == null) - throw new StreamRoleNotFoundException(); - addRole = user.Guild.GetRole(setting.AddRoleId); - if (addRole == null) - throw new StreamRoleNotFoundException(); - - try - { - //check if user is in the fromrole - if (user.RoleIds.Contains(setting.FromRoleId)) - { - //check if he doesn't have addrole already, to avoid errors - if (!user.RoleIds.Contains(setting.AddRoleId)) - await user.AddRoleAsync(addRole).ConfigureAwait(false); - //schedule him for the role removal when he stops streaming - toRemove.TryAdd((addRole.Guild.Id, user.Id), addRole.Id); - } - } - catch (HttpException ex) when (ex.HttpCode == System.Net.HttpStatusCode.Forbidden) - { - StopStreamRole(user.Guild.Id); - _log.Warn("Error adding stream role(s). Disabling stream role feature."); - _log.Error(ex); - throw new StreamRolePermissionException(); - } - catch (Exception ex) - { - _log.Warn("Failed adding stream role."); - _log.Error(ex); - } - } - } - /// /// Adds or removes a user from a blacklist or a whitelist in the specified guild. /// - /// Id of the guild + /// Guild /// Add or rem action /// User's Id /// User's name#discrim /// Whether the operation was successful - public async Task ApplyListAction(StreamRoleListType listType, ulong guildId, AddRemove action, ulong userId, string userName) + public async Task ApplyListAction(StreamRoleListType listType, IGuild guild, AddRemove action, ulong userId, string userName) { userName.ThrowIfNull(nameof(userName)); bool success; using (var uow = _db.UnitOfWork) { - var streamRoleSettings = uow.GuildConfigs.GetStreamRoleSettings(guildId); + var streamRoleSettings = uow.GuildConfigs.GetStreamRoleSettings(guild.Id); if (listType == StreamRoleListType.Whitelist) { @@ -178,30 +106,34 @@ namespace NadekoBot.Modules.Utility.Services await uow.CompleteAsync().ConfigureAwait(false); } + if (success) + { + await RescanUsers(guild).ConfigureAwait(false); + } return success; } /// /// Sets keyword on a guild and updates the cache. /// - /// Guild Id + /// Guild Id /// Keyword to set /// The keyword set - public string SetKeyword(ulong guildId, string keyword) + public async Task SetKeyword(IGuild guild, string keyword) { keyword = keyword?.Trim()?.ToLowerInvariant(); using (var uow = _db.UnitOfWork) { - var streamRoleSettings = uow.GuildConfigs.GetStreamRoleSettings(guildId); + var streamRoleSettings = uow.GuildConfigs.GetStreamRoleSettings(guild.Id); streamRoleSettings.Keyword = keyword; - UpdateCache(guildId, streamRoleSettings); + UpdateCache(guild.Id, streamRoleSettings); uow.Complete(); - - return streamRoleSettings.Keyword; } + await RescanUsers(guild).ConfigureAwait(false); + return keyword; } /// @@ -251,9 +183,10 @@ namespace NadekoBot.Modules.Utility.Services UpdateCache(fromRole.Guild.Id, setting); - foreach (var usr in await fromRole.Guild.GetUsersAsync(CacheMode.CacheOnly).ConfigureAwait(false)) + foreach (var usr in await fromRole.GetMembersAsync()) { - await Task.WhenAll(TryApplyRole(usr, setting), Task.Delay(100)).ConfigureAwait(false); + if (usr is IGuildUser x) + await RescanUser(x, setting, addRole).ConfigureAwait(false); } } @@ -261,16 +194,94 @@ namespace NadekoBot.Modules.Utility.Services /// Stops the stream role feature on the specified guild. /// /// Guild's Id - public void StopStreamRole(ulong guildId) + public async Task StopStreamRole(IGuild guild, bool cleanup = false) { using (var uow = _db.UnitOfWork) { - var streamRoleSettings = uow.GuildConfigs.GetStreamRoleSettings(guildId); + var streamRoleSettings = uow.GuildConfigs.GetStreamRoleSettings(guild.Id); streamRoleSettings.Enabled = false; - uow.Complete(); + await uow.CompleteAsync().ConfigureAwait(false); } - guildSettings.TryRemove(guildId, out _); + if (guildSettings.TryRemove(guild.Id, out var setting) && cleanup) + await RescanUsers(guild).ConfigureAwait(false); + } + //todo multiple rescans at the same time? + private async Task RescanUser(IGuildUser user, StreamRoleSettings setting, IRole addRole = null) + { + if (user.Game.HasValue && + user.Game.Value.StreamType != StreamType.NotStreaming + && setting.Enabled + && !setting.Blacklist.Any(x => x.UserId == user.Id) + && user.RoleIds.Contains(setting.FromRoleId) + && (string.IsNullOrWhiteSpace(setting.Keyword) + || user.Game.Value.Name.Contains(setting.Keyword) + || setting.Whitelist.Any(x => x.UserId == user.Id))) + { + try + { + //check if he doesn't have addrole already, to avoid errors + if (!user.RoleIds.Contains(setting.AddRoleId)) + await user.AddRoleAsync(addRole).ConfigureAwait(false); + _log.Info("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.Warn("Error adding stream role(s). Forcibly disabling stream role feature."); + _log.Error(ex); + throw new StreamRolePermissionException(); + } + catch (Exception ex) + { + _log.Warn("Failed adding stream role."); + _log.Error(ex); + } + } + 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.Info("Removed stream role from a 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.Warn("Error removing stream role(s). Forcibly disabling stream role feature."); + _log.Error(ex); + throw new StreamRolePermissionException(); + } + _log.Info("Removed stream role from the user {0} in {1} server", user.ToString(), user.Guild.ToString()); + } + } + } + + 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) diff --git a/src/NadekoBot/Modules/Utility/StreamRoleCommands.cs b/src/NadekoBot/Modules/Utility/StreamRoleCommands.cs index 8237dce0..7749fbca 100644 --- a/src/NadekoBot/Modules/Utility/StreamRoleCommands.cs +++ b/src/NadekoBot/Modules/Utility/StreamRoleCommands.cs @@ -5,11 +5,13 @@ using NadekoBot.Common.Attributes; using NadekoBot.Modules.Utility.Services; using NadekoBot.Common.TypeReaders; using NadekoBot.Modules.Utility.Common; +using NadekoBot.Common; namespace NadekoBot.Modules.Utility { public partial class Utility { + [NoPublicBot] public class StreamRoleCommands : NadekoSubmodule { [NadekoCommand, Usage, Description, Aliases] @@ -29,7 +31,7 @@ namespace NadekoBot.Modules.Utility [RequireContext(ContextType.Guild)] public async Task StreamRole() { - this._service.StopStreamRole(Context.Guild.Id); + await this._service.StopStreamRole(Context.Guild).ConfigureAwait(false); await ReplyConfirmLocalized("stream_role_disabled").ConfigureAwait(false); } @@ -39,7 +41,7 @@ namespace NadekoBot.Modules.Utility [RequireContext(ContextType.Guild)] public async Task StreamRoleKeyword([Remainder]string keyword = null) { - string kw = this._service.SetKeyword(Context.Guild.Id, keyword); + string kw = await this._service.SetKeyword(Context.Guild, keyword).ConfigureAwait(false); if(string.IsNullOrWhiteSpace(keyword)) await ReplyConfirmLocalized("stream_role_kw_reset").ConfigureAwait(false); @@ -53,7 +55,7 @@ namespace NadekoBot.Modules.Utility [RequireContext(ContextType.Guild)] public async Task StreamRoleBlacklist(AddRemove action, [Remainder] IGuildUser user) { - var success = await this._service.ApplyListAction(StreamRoleListType.Blacklist, Context.Guild.Id, action, user.Id, user.ToString()) + var success = await this._service.ApplyListAction(StreamRoleListType.Blacklist, Context.Guild, action, user.Id, user.ToString()) .ConfigureAwait(false); if(action == AddRemove.Add) @@ -74,7 +76,7 @@ namespace NadekoBot.Modules.Utility [RequireContext(ContextType.Guild)] public async Task StreamRoleWhitelist(AddRemove action, [Remainder] IGuildUser user) { - var success = await this._service.ApplyListAction(StreamRoleListType.Whitelist, Context.Guild.Id, action, user.Id, user.ToString()) + var success = await this._service.ApplyListAction(StreamRoleListType.Whitelist, Context.Guild, action, user.Id, user.ToString()) .ConfigureAwait(false); if (action == AddRemove.Add) diff --git a/src/NadekoBot/_Extensions/Extensions.cs b/src/NadekoBot/_Extensions/Extensions.cs index 77522709..a21e66ea 100644 --- a/src/NadekoBot/_Extensions/Extensions.cs +++ b/src/NadekoBot/_Extensions/Extensions.cs @@ -126,8 +126,8 @@ namespace NadekoBot.Extensions public static double UnixTimestamp(this DateTime dt) => dt.ToUniversalTime().Subtract(new DateTime(1970, 1, 1, 0, 0, 0)).TotalSeconds; - public static IEnumerable Members(this IRole role) => - role.Guild.GetUsersAsync().GetAwaiter().GetResult().Where(u => u.RoleIds.Contains(role.Id)) ?? Enumerable.Empty(); + public static async Task> GetMembersAsync(this IRole role) => + (await role.Guild.GetUsersAsync(CacheMode.CacheOnly)).Where(u => u.RoleIds.Contains(role.Id)) ?? Enumerable.Empty(); public static string ToJson(this T any, Formatting formatting = Formatting.Indented) => JsonConvert.SerializeObject(any, formatting); From f1b348406d729d2b69e7222034c7a8b29fc2c45b Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Fri, 21 Jul 2017 07:10:17 +0200 Subject: [PATCH 209/346] Bugfixes --- .../Modules/Utility/Services/StreamRoleService.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/NadekoBot/Modules/Utility/Services/StreamRoleService.cs b/src/NadekoBot/Modules/Utility/Services/StreamRoleService.cs index 38ec4668..81018a77 100644 --- a/src/NadekoBot/Modules/Utility/Services/StreamRoleService.cs +++ b/src/NadekoBot/Modules/Utility/Services/StreamRoleService.cs @@ -105,6 +105,7 @@ namespace NadekoBot.Modules.Utility.Services } await uow.CompleteAsync().ConfigureAwait(false); + UpdateCache(guild.Id, streamRoleSettings); } if (success) { @@ -215,11 +216,15 @@ namespace NadekoBot.Modules.Utility.Services && !setting.Blacklist.Any(x => x.UserId == user.Id) && user.RoleIds.Contains(setting.FromRoleId) && (string.IsNullOrWhiteSpace(setting.Keyword) - || user.Game.Value.Name.Contains(setting.Keyword) + || user.Game.Value.Name.ToLowerInvariant().Contains(setting.Keyword.ToLowerInvariant()) || setting.Whitelist.Any(x => x.UserId == user.Id))) { try { + addRole = addRole ?? user.Guild.GetRole(setting.AddRoleId); + if (addRole == null) + throw new StreamRoleNotFoundException(); + //check if he doesn't have addrole already, to avoid errors if (!user.RoleIds.Contains(setting.AddRoleId)) await user.AddRoleAsync(addRole).ConfigureAwait(false); From 1aa86937c8bc1ad41254999d05c12103f8e07c58 Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Sat, 22 Jul 2017 07:57:17 +0200 Subject: [PATCH 210/346] small changes --- .../Administration/Services/GameVoiceChannelService.cs | 2 +- src/NadekoBot/NadekoBot.csproj | 4 ++-- src/NadekoBot/_Extensions/Extensions.cs | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/NadekoBot/Modules/Administration/Services/GameVoiceChannelService.cs b/src/NadekoBot/Modules/Administration/Services/GameVoiceChannelService.cs index 79b1f3ac..e97e0eb7 100644 --- a/src/NadekoBot/Modules/Administration/Services/GameVoiceChannelService.cs +++ b/src/NadekoBot/Modules/Administration/Services/GameVoiceChannelService.cs @@ -43,7 +43,7 @@ namespace NadekoBot.Modules.Administration.Services if (gUser == null) return; - var game = gUser.Game?.Name.TrimTo(50).ToLowerInvariant(); + var game = gUser.Game?.Name?.TrimTo(50).ToLowerInvariant(); if (oldState.VoiceChannel == newState.VoiceChannel || newState.VoiceChannel == null) diff --git a/src/NadekoBot/NadekoBot.csproj b/src/NadekoBot/NadekoBot.csproj index 810fa907..5e1db116 100644 --- a/src/NadekoBot/NadekoBot.csproj +++ b/src/NadekoBot/NadekoBot.csproj @@ -64,8 +64,8 @@ - - + + diff --git a/src/NadekoBot/_Extensions/Extensions.cs b/src/NadekoBot/_Extensions/Extensions.cs index a21e66ea..c901afaf 100644 --- a/src/NadekoBot/_Extensions/Extensions.cs +++ b/src/NadekoBot/_Extensions/Extensions.cs @@ -199,7 +199,7 @@ namespace NadekoBot.Extensions return await ownerPrivate.SendMessageAsync(message).ConfigureAwait(false); } - public static Image Merge(this IEnumerable> images) + public static Image Merge(this IEnumerable> images) { var imgs = images.ToArray(); From f773b0c6b625ab9f6011e9971cdb3fd179fec3ec Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Sat, 22 Jul 2017 18:12:08 +0200 Subject: [PATCH 211/346] .crca added. If you enable it on a custom reation, you can use the trigger word anywhere in the sentence in order to trigger that custom reaction. --- .../20170722074959_cr-ca.Designer.cs | 1682 +++++++++++++++++ .../Migrations/20170722074959_cr-ca.cs | 25 + .../NadekoSqliteContextModelSnapshot.cs | 2 + .../CustomReactions/CustomReactions.cs | 10 +- .../Services/CustomReactionsService.cs | 8 +- src/NadekoBot/Resources/CommandStrings.resx | 9 + .../Database/Models/CustomReaction.cs | 2 + .../_strings/ResponseStrings.en-US.json | 2 + 8 files changed, 1734 insertions(+), 6 deletions(-) create mode 100644 src/NadekoBot/Migrations/20170722074959_cr-ca.Designer.cs create mode 100644 src/NadekoBot/Migrations/20170722074959_cr-ca.cs diff --git a/src/NadekoBot/Migrations/20170722074959_cr-ca.Designer.cs b/src/NadekoBot/Migrations/20170722074959_cr-ca.Designer.cs new file mode 100644 index 00000000..df888de6 --- /dev/null +++ b/src/NadekoBot/Migrations/20170722074959_cr-ca.Designer.cs @@ -0,0 +1,1682 @@ +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using NadekoBot.Services.Database; +using NadekoBot.Services.Database.Models; + +namespace NadekoBot.Migrations +{ + [DbContext(typeof(NadekoContext))] + [Migration("20170722074959_cr-ca")] + partial class crca + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { + modelBuilder + .HasAnnotation("ProductVersion", "1.1.1"); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.AntiRaidSetting", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("Action"); + + b.Property("DateAdded"); + + b.Property("GuildConfigId"); + + b.Property("Seconds"); + + b.Property("UserThreshold"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId") + .IsUnique(); + + b.ToTable("AntiRaidSetting"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.AntiSpamIgnore", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AntiSpamSettingId"); + + b.Property("ChannelId"); + + b.Property("DateAdded"); + + b.HasKey("Id"); + + b.HasIndex("AntiSpamSettingId"); + + b.ToTable("AntiSpamIgnore"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.AntiSpamSetting", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("Action"); + + b.Property("DateAdded"); + + b.Property("GuildConfigId"); + + b.Property("MessageThreshold"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId") + .IsUnique(); + + b.ToTable("AntiSpamSetting"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.BlacklistItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("BotConfigId"); + + b.Property("DateAdded"); + + b.Property("ItemId"); + + b.Property("Type"); + + b.HasKey("Id"); + + b.HasIndex("BotConfigId"); + + b.ToTable("BlacklistItem"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.BlockedCmdOrMdl", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("BotConfigId"); + + b.Property("BotConfigId1"); + + b.Property("DateAdded"); + + b.Property("Name"); + + b.HasKey("Id"); + + b.HasIndex("BotConfigId"); + + b.HasIndex("BotConfigId1"); + + b.ToTable("BlockedCmdOrMdl"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.BotConfig", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("BetflipMultiplier"); + + b.Property("Betroll100Multiplier"); + + b.Property("Betroll67Multiplier"); + + b.Property("Betroll91Multiplier"); + + b.Property("BufferSize"); + + b.Property("CurrencyDropAmount"); + + b.Property("CurrencyDropAmountMax"); + + b.Property("CurrencyGenerationChance"); + + b.Property("CurrencyGenerationCooldown"); + + b.Property("CurrencyName"); + + b.Property("CurrencyPluralName"); + + b.Property("CurrencySign"); + + b.Property("CustomReactionsStartWith"); + + b.Property("DMHelpString"); + + b.Property("DateAdded"); + + b.Property("DefaultPrefix"); + + b.Property("ErrorColor"); + + b.Property("ForwardMessages"); + + b.Property("ForwardToAllOwners"); + + b.Property("HelpString"); + + b.Property("Locale"); + + b.Property("MigrationVersion"); + + b.Property("MinimumBetAmount"); + + b.Property("OkColor"); + + b.Property("PermissionVersion"); + + b.Property("RemindMessageFormat"); + + b.Property("RotatingStatuses"); + + b.Property("TriviaCurrencyReward"); + + b.HasKey("Id"); + + b.ToTable("BotConfig"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.ClashCaller", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("BaseDestroyed"); + + b.Property("CallUser"); + + b.Property("ClashWarId"); + + b.Property("DateAdded"); + + b.Property("SequenceNumber"); + + b.Property("Stars"); + + b.Property("TimeAdded"); + + b.HasKey("Id"); + + b.HasIndex("ClashWarId"); + + b.ToTable("ClashCallers"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.ClashWar", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ChannelId"); + + b.Property("DateAdded"); + + b.Property("EnemyClan"); + + b.Property("GuildId"); + + b.Property("Size"); + + b.Property("StartedAt"); + + b.Property("WarState"); + + b.HasKey("Id"); + + b.ToTable("ClashOfClans"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.CommandAlias", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("GuildConfigId"); + + b.Property("Mapping"); + + b.Property("Trigger"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.ToTable("CommandAlias"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.CommandCooldown", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("CommandName"); + + b.Property("DateAdded"); + + b.Property("GuildConfigId"); + + b.Property("Seconds"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.ToTable("CommandCooldown"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.CommandPrice", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("BotConfigId"); + + b.Property("CommandName"); + + b.Property("DateAdded"); + + b.Property("Price"); + + b.HasKey("Id"); + + b.HasIndex("BotConfigId"); + + b.HasIndex("Price") + .IsUnique(); + + b.ToTable("CommandPrice"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.ConvertUnit", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("InternalTrigger"); + + b.Property("Modifier"); + + b.Property("UnitType"); + + b.HasKey("Id"); + + b.ToTable("ConversionUnits"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.Currency", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("Amount"); + + b.Property("DateAdded"); + + b.Property("UserId"); + + b.HasKey("Id"); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("Currency"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.CurrencyTransaction", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("Amount"); + + b.Property("DateAdded"); + + b.Property("Reason"); + + b.Property("UserId"); + + b.HasKey("Id"); + + b.ToTable("CurrencyTransactions"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.CustomReaction", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AutoDeleteTrigger"); + + b.Property("ContainsAnywhere"); + + b.Property("DateAdded"); + + b.Property("DmResponse"); + + b.Property("GuildId"); + + b.Property("IsRegex"); + + b.Property("OwnerOnly"); + + b.Property("Response"); + + b.Property("Trigger"); + + b.HasKey("Id"); + + b.ToTable("CustomReactions"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.DiscordUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AvatarId"); + + b.Property("DateAdded"); + + b.Property("Discriminator"); + + b.Property("UserId"); + + b.Property("Username"); + + b.HasKey("Id"); + + b.HasAlternateKey("UserId"); + + b.ToTable("DiscordUser"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.Donator", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("Amount"); + + b.Property("DateAdded"); + + b.Property("Name"); + + b.Property("UserId"); + + b.HasKey("Id"); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("Donators"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.EightBallResponse", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("BotConfigId"); + + b.Property("DateAdded"); + + b.Property("Text"); + + b.HasKey("Id"); + + b.HasIndex("BotConfigId"); + + b.ToTable("EightBallResponses"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.FilterChannelId", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ChannelId"); + + b.Property("DateAdded"); + + b.Property("GuildConfigId"); + + b.Property("GuildConfigId1"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.HasIndex("GuildConfigId1"); + + b.ToTable("FilterChannelId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.FilteredWord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("GuildConfigId"); + + b.Property("Word"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.ToTable("FilteredWord"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.FollowedStream", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ChannelId"); + + b.Property("DateAdded"); + + b.Property("GuildConfigId"); + + b.Property("GuildId"); + + b.Property("Type"); + + b.Property("Username"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.ToTable("FollowedStream"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.GCChannelId", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ChannelId"); + + b.Property("DateAdded"); + + b.Property("GuildConfigId"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.ToTable("GCChannelId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.GuildConfig", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AutoAssignRoleId"); + + b.Property("AutoDeleteByeMessages"); + + b.Property("AutoDeleteByeMessagesTimer"); + + b.Property("AutoDeleteGreetMessages"); + + b.Property("AutoDeleteGreetMessagesTimer"); + + b.Property("AutoDeleteSelfAssignedRoleMessages"); + + b.Property("ByeMessageChannelId"); + + b.Property("ChannelByeMessageText"); + + b.Property("ChannelGreetMessageText"); + + b.Property("CleverbotEnabled"); + + b.Property("DateAdded"); + + b.Property("DefaultMusicVolume"); + + b.Property("DeleteMessageOnCommand"); + + b.Property("DmGreetMessageText"); + + b.Property("ExclusiveSelfAssignedRoles"); + + b.Property("FilterInvites"); + + b.Property("FilterWords"); + + b.Property("GameVoiceChannel"); + + b.Property("GreetMessageChannelId"); + + b.Property("GuildId"); + + b.Property("Locale"); + + b.Property("LogSettingId"); + + b.Property("MuteRoleName"); + + b.Property("PermissionRole"); + + b.Property("Prefix"); + + b.Property("RootPermissionId"); + + b.Property("SendChannelByeMessage"); + + b.Property("SendChannelGreetMessage"); + + b.Property("SendDmGreetMessage"); + + b.Property("TimeZoneId"); + + b.Property("VerboseErrors"); + + b.Property("VerbosePermissions"); + + b.Property("VoicePlusTextEnabled"); + + b.Property("WarningsInitialized"); + + b.HasKey("Id"); + + b.HasIndex("GuildId") + .IsUnique(); + + b.HasIndex("LogSettingId"); + + b.HasIndex("RootPermissionId"); + + b.ToTable("GuildConfigs"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.GuildRepeater", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ChannelId"); + + b.Property("DateAdded"); + + b.Property("GuildConfigId"); + + b.Property("GuildId"); + + b.Property("Interval"); + + b.Property("Message"); + + b.Property("StartTimeOfDay"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.ToTable("GuildRepeater"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.IgnoredLogChannel", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ChannelId"); + + b.Property("DateAdded"); + + b.Property("LogSettingId"); + + b.HasKey("Id"); + + b.HasIndex("LogSettingId"); + + b.ToTable("IgnoredLogChannels"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.IgnoredVoicePresenceChannel", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ChannelId"); + + b.Property("DateAdded"); + + b.Property("LogSettingId"); + + b.HasKey("Id"); + + b.HasIndex("LogSettingId"); + + b.ToTable("IgnoredVoicePresenceCHannels"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.LogSetting", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ChannelCreated"); + + b.Property("ChannelCreatedId"); + + b.Property("ChannelDestroyed"); + + b.Property("ChannelDestroyedId"); + + b.Property("ChannelId"); + + b.Property("ChannelUpdated"); + + b.Property("ChannelUpdatedId"); + + b.Property("DateAdded"); + + b.Property("IsLogging"); + + b.Property("LogOtherId"); + + b.Property("LogUserPresence"); + + b.Property("LogUserPresenceId"); + + b.Property("LogVoicePresence"); + + b.Property("LogVoicePresenceId"); + + b.Property("LogVoicePresenceTTSId"); + + b.Property("MessageDeleted"); + + b.Property("MessageDeletedId"); + + b.Property("MessageUpdated"); + + b.Property("MessageUpdatedId"); + + b.Property("UserBanned"); + + b.Property("UserBannedId"); + + b.Property("UserJoined"); + + b.Property("UserJoinedId"); + + b.Property("UserLeft"); + + b.Property("UserLeftId"); + + b.Property("UserMutedId"); + + b.Property("UserPresenceChannelId"); + + b.Property("UserUnbanned"); + + b.Property("UserUnbannedId"); + + b.Property("UserUpdated"); + + b.Property("UserUpdatedId"); + + b.Property("VoicePresenceChannelId"); + + b.HasKey("Id"); + + b.ToTable("LogSettings"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.ModulePrefix", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("BotConfigId"); + + b.Property("DateAdded"); + + b.Property("ModuleName"); + + b.Property("Prefix"); + + b.HasKey("Id"); + + b.HasIndex("BotConfigId"); + + b.ToTable("ModulePrefixes"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.MusicPlaylist", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("Author"); + + b.Property("AuthorId"); + + b.Property("DateAdded"); + + b.Property("Name"); + + b.HasKey("Id"); + + b.ToTable("MusicPlaylists"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.MutedUserId", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("GuildConfigId"); + + b.Property("UserId"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.ToTable("MutedUserId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.NsfwBlacklitedTag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("GuildConfigId"); + + b.Property("Tag"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.ToTable("NsfwBlacklitedTag"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.Permission", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("NextId"); + + b.Property("PrimaryTarget"); + + b.Property("PrimaryTargetId"); + + b.Property("SecondaryTarget"); + + b.Property("SecondaryTargetName"); + + b.Property("State"); + + b.HasKey("Id"); + + b.HasIndex("NextId") + .IsUnique(); + + b.ToTable("Permission"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.Permissionv2", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("GuildConfigId"); + + b.Property("Index"); + + b.Property("PrimaryTarget"); + + b.Property("PrimaryTargetId"); + + b.Property("SecondaryTarget"); + + b.Property("SecondaryTargetName"); + + b.Property("State"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.ToTable("Permissionv2"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.PlayingStatus", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("BotConfigId"); + + b.Property("DateAdded"); + + b.Property("Status"); + + b.HasKey("Id"); + + b.HasIndex("BotConfigId"); + + b.ToTable("PlayingStatus"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.PlaylistSong", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("MusicPlaylistId"); + + b.Property("Provider"); + + b.Property("ProviderType"); + + b.Property("Query"); + + b.Property("Title"); + + b.Property("Uri"); + + b.HasKey("Id"); + + b.HasIndex("MusicPlaylistId"); + + b.ToTable("PlaylistSong"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.Quote", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AuthorId"); + + b.Property("AuthorName") + .IsRequired(); + + b.Property("DateAdded"); + + b.Property("GuildId"); + + b.Property("Keyword") + .IsRequired(); + + b.Property("Text") + .IsRequired(); + + b.HasKey("Id"); + + b.ToTable("Quotes"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.RaceAnimal", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("BotConfigId"); + + b.Property("DateAdded"); + + b.Property("Icon"); + + b.Property("Name"); + + b.HasKey("Id"); + + b.HasIndex("BotConfigId"); + + b.ToTable("RaceAnimals"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.Reminder", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ChannelId"); + + b.Property("DateAdded"); + + b.Property("IsPrivate"); + + b.Property("Message"); + + b.Property("ServerId"); + + b.Property("UserId"); + + b.Property("When"); + + b.HasKey("Id"); + + b.ToTable("Reminders"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.RewardedUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AmountRewardedThisMonth"); + + b.Property("DateAdded"); + + b.Property("LastReward"); + + b.Property("PatreonUserId"); + + b.Property("UserId"); + + b.HasKey("Id"); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("RewardedUsers"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.SelfAssignedRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("GuildId"); + + b.Property("RoleId"); + + b.HasKey("Id"); + + b.HasIndex("GuildId", "RoleId") + .IsUnique(); + + b.ToTable("SelfAssignableRoles"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.ShopEntry", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AuthorId"); + + b.Property("DateAdded"); + + b.Property("GuildConfigId"); + + b.Property("Index"); + + b.Property("Name"); + + b.Property("Price"); + + b.Property("RoleId"); + + b.Property("RoleName"); + + b.Property("Type"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.ToTable("ShopEntry"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.ShopEntryItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("ShopEntryId"); + + b.Property("Text"); + + b.HasKey("Id"); + + b.HasIndex("ShopEntryId"); + + b.ToTable("ShopEntryItem"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.SlowmodeIgnoredRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("GuildConfigId"); + + b.Property("RoleId"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.ToTable("SlowmodeIgnoredRole"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.SlowmodeIgnoredUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("GuildConfigId"); + + b.Property("UserId"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.ToTable("SlowmodeIgnoredUser"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.StartupCommand", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("BotConfigId"); + + b.Property("ChannelId"); + + b.Property("ChannelName"); + + b.Property("CommandText"); + + b.Property("DateAdded"); + + b.Property("GuildId"); + + b.Property("GuildName"); + + b.Property("Index"); + + b.Property("VoiceChannelId"); + + b.Property("VoiceChannelName"); + + b.HasKey("Id"); + + b.HasIndex("BotConfigId"); + + b.ToTable("StartupCommand"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.StreamRoleBlacklistedUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("StreamRoleSettingsId"); + + b.Property("UserId"); + + b.Property("Username"); + + b.HasKey("Id"); + + b.HasIndex("StreamRoleSettingsId"); + + b.ToTable("StreamRoleBlacklistedUser"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.StreamRoleSettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AddRoleId"); + + b.Property("DateAdded"); + + b.Property("Enabled"); + + b.Property("FromRoleId"); + + b.Property("GuildConfigId"); + + b.Property("Keyword"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId") + .IsUnique(); + + b.ToTable("StreamRoleSettings"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.StreamRoleWhitelistedUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("StreamRoleSettingsId"); + + b.Property("UserId"); + + b.Property("Username"); + + b.HasKey("Id"); + + b.HasIndex("StreamRoleSettingsId"); + + b.ToTable("StreamRoleWhitelistedUser"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.UnmuteTimer", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("GuildConfigId"); + + b.Property("UnmuteAt"); + + b.Property("UserId"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.ToTable("UnmuteTimer"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.UserPokeTypes", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("UserId"); + + b.Property("type"); + + b.HasKey("Id"); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("PokeGame"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.VcRoleInfo", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("GuildConfigId"); + + b.Property("RoleId"); + + b.Property("VoiceChannelId"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.ToTable("VcRoleInfo"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.WaifuInfo", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AffinityId"); + + b.Property("ClaimerId"); + + b.Property("DateAdded"); + + b.Property("Price"); + + b.Property("WaifuId"); + + b.HasKey("Id"); + + b.HasIndex("AffinityId"); + + b.HasIndex("ClaimerId"); + + b.HasIndex("WaifuId") + .IsUnique(); + + b.ToTable("WaifuInfo"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.WaifuUpdate", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("NewId"); + + b.Property("OldId"); + + b.Property("UpdateType"); + + b.Property("UserId"); + + b.HasKey("Id"); + + b.HasIndex("NewId"); + + b.HasIndex("OldId"); + + b.HasIndex("UserId"); + + b.ToTable("WaifuUpdates"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.Warning", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("Forgiven"); + + b.Property("ForgivenBy"); + + b.Property("GuildId"); + + b.Property("Moderator"); + + b.Property("Reason"); + + b.Property("UserId"); + + b.HasKey("Id"); + + b.ToTable("Warnings"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.WarningPunishment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("Count"); + + b.Property("DateAdded"); + + b.Property("GuildConfigId"); + + b.Property("Punishment"); + + b.Property("Time"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.ToTable("WarningPunishment"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.AntiRaidSetting", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig", "GuildConfig") + .WithOne("AntiRaidSetting") + .HasForeignKey("NadekoBot.Services.Database.Models.AntiRaidSetting", "GuildConfigId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.AntiSpamIgnore", b => + { + b.HasOne("NadekoBot.Services.Database.Models.AntiSpamSetting") + .WithMany("IgnoredChannels") + .HasForeignKey("AntiSpamSettingId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.AntiSpamSetting", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig", "GuildConfig") + .WithOne("AntiSpamSetting") + .HasForeignKey("NadekoBot.Services.Database.Models.AntiSpamSetting", "GuildConfigId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.BlacklistItem", b => + { + b.HasOne("NadekoBot.Services.Database.Models.BotConfig") + .WithMany("Blacklist") + .HasForeignKey("BotConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.BlockedCmdOrMdl", b => + { + b.HasOne("NadekoBot.Services.Database.Models.BotConfig") + .WithMany("BlockedCommands") + .HasForeignKey("BotConfigId"); + + b.HasOne("NadekoBot.Services.Database.Models.BotConfig") + .WithMany("BlockedModules") + .HasForeignKey("BotConfigId1"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.ClashCaller", b => + { + b.HasOne("NadekoBot.Services.Database.Models.ClashWar", "ClashWar") + .WithMany("Bases") + .HasForeignKey("ClashWarId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.CommandAlias", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig") + .WithMany("CommandAliases") + .HasForeignKey("GuildConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.CommandCooldown", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig") + .WithMany("CommandCooldowns") + .HasForeignKey("GuildConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.CommandPrice", b => + { + b.HasOne("NadekoBot.Services.Database.Models.BotConfig") + .WithMany("CommandPrices") + .HasForeignKey("BotConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.EightBallResponse", b => + { + b.HasOne("NadekoBot.Services.Database.Models.BotConfig") + .WithMany("EightBallResponses") + .HasForeignKey("BotConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.FilterChannelId", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig") + .WithMany("FilterInvitesChannelIds") + .HasForeignKey("GuildConfigId"); + + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig") + .WithMany("FilterWordsChannelIds") + .HasForeignKey("GuildConfigId1"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.FilteredWord", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig") + .WithMany("FilteredWords") + .HasForeignKey("GuildConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.FollowedStream", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig") + .WithMany("FollowedStreams") + .HasForeignKey("GuildConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.GCChannelId", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig") + .WithMany("GenerateCurrencyChannelIds") + .HasForeignKey("GuildConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.GuildConfig", b => + { + b.HasOne("NadekoBot.Services.Database.Models.LogSetting", "LogSetting") + .WithMany() + .HasForeignKey("LogSettingId"); + + b.HasOne("NadekoBot.Services.Database.Models.Permission", "RootPermission") + .WithMany() + .HasForeignKey("RootPermissionId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.GuildRepeater", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig") + .WithMany("GuildRepeaters") + .HasForeignKey("GuildConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.IgnoredLogChannel", b => + { + b.HasOne("NadekoBot.Services.Database.Models.LogSetting", "LogSetting") + .WithMany("IgnoredChannels") + .HasForeignKey("LogSettingId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.IgnoredVoicePresenceChannel", b => + { + b.HasOne("NadekoBot.Services.Database.Models.LogSetting", "LogSetting") + .WithMany("IgnoredVoicePresenceChannelIds") + .HasForeignKey("LogSettingId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.ModulePrefix", b => + { + b.HasOne("NadekoBot.Services.Database.Models.BotConfig") + .WithMany("ModulePrefixes") + .HasForeignKey("BotConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.MutedUserId", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig") + .WithMany("MutedUsers") + .HasForeignKey("GuildConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.NsfwBlacklitedTag", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig") + .WithMany("NsfwBlacklistedTags") + .HasForeignKey("GuildConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.Permission", b => + { + b.HasOne("NadekoBot.Services.Database.Models.Permission", "Next") + .WithOne("Previous") + .HasForeignKey("NadekoBot.Services.Database.Models.Permission", "NextId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.Permissionv2", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig") + .WithMany("Permissions") + .HasForeignKey("GuildConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.PlayingStatus", b => + { + b.HasOne("NadekoBot.Services.Database.Models.BotConfig") + .WithMany("RotatingStatusMessages") + .HasForeignKey("BotConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.PlaylistSong", b => + { + b.HasOne("NadekoBot.Services.Database.Models.MusicPlaylist") + .WithMany("Songs") + .HasForeignKey("MusicPlaylistId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.RaceAnimal", b => + { + b.HasOne("NadekoBot.Services.Database.Models.BotConfig") + .WithMany("RaceAnimals") + .HasForeignKey("BotConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.ShopEntry", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig") + .WithMany("ShopEntries") + .HasForeignKey("GuildConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.ShopEntryItem", b => + { + b.HasOne("NadekoBot.Services.Database.Models.ShopEntry") + .WithMany("Items") + .HasForeignKey("ShopEntryId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.SlowmodeIgnoredRole", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig") + .WithMany("SlowmodeIgnoredRoles") + .HasForeignKey("GuildConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.SlowmodeIgnoredUser", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig") + .WithMany("SlowmodeIgnoredUsers") + .HasForeignKey("GuildConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.StartupCommand", b => + { + b.HasOne("NadekoBot.Services.Database.Models.BotConfig") + .WithMany("StartupCommands") + .HasForeignKey("BotConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.StreamRoleBlacklistedUser", b => + { + b.HasOne("NadekoBot.Services.Database.Models.StreamRoleSettings") + .WithMany("Blacklist") + .HasForeignKey("StreamRoleSettingsId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.StreamRoleSettings", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig", "GuildConfig") + .WithOne("StreamRole") + .HasForeignKey("NadekoBot.Services.Database.Models.StreamRoleSettings", "GuildConfigId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.StreamRoleWhitelistedUser", b => + { + b.HasOne("NadekoBot.Services.Database.Models.StreamRoleSettings") + .WithMany("Whitelist") + .HasForeignKey("StreamRoleSettingsId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.UnmuteTimer", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig") + .WithMany("UnmuteTimers") + .HasForeignKey("GuildConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.VcRoleInfo", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig") + .WithMany("VcRoleInfos") + .HasForeignKey("GuildConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.WaifuInfo", b => + { + b.HasOne("NadekoBot.Services.Database.Models.DiscordUser", "Affinity") + .WithMany() + .HasForeignKey("AffinityId"); + + b.HasOne("NadekoBot.Services.Database.Models.DiscordUser", "Claimer") + .WithMany() + .HasForeignKey("ClaimerId"); + + b.HasOne("NadekoBot.Services.Database.Models.DiscordUser", "Waifu") + .WithOne() + .HasForeignKey("NadekoBot.Services.Database.Models.WaifuInfo", "WaifuId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.WaifuUpdate", b => + { + b.HasOne("NadekoBot.Services.Database.Models.DiscordUser", "New") + .WithMany() + .HasForeignKey("NewId"); + + b.HasOne("NadekoBot.Services.Database.Models.DiscordUser", "Old") + .WithMany() + .HasForeignKey("OldId"); + + b.HasOne("NadekoBot.Services.Database.Models.DiscordUser", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.WarningPunishment", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig") + .WithMany("WarnPunishments") + .HasForeignKey("GuildConfigId"); + }); + } + } +} diff --git a/src/NadekoBot/Migrations/20170722074959_cr-ca.cs b/src/NadekoBot/Migrations/20170722074959_cr-ca.cs new file mode 100644 index 00000000..2655c82d --- /dev/null +++ b/src/NadekoBot/Migrations/20170722074959_cr-ca.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; +using Microsoft.EntityFrameworkCore.Migrations; + +namespace NadekoBot.Migrations +{ + public partial class crca : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "ContainsAnywhere", + table: "CustomReactions", + nullable: false, + defaultValue: false); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "ContainsAnywhere", + table: "CustomReactions"); + } + } +} diff --git a/src/NadekoBot/Migrations/NadekoSqliteContextModelSnapshot.cs b/src/NadekoBot/Migrations/NadekoSqliteContextModelSnapshot.cs index 46b5b476..a9d7a682 100644 --- a/src/NadekoBot/Migrations/NadekoSqliteContextModelSnapshot.cs +++ b/src/NadekoBot/Migrations/NadekoSqliteContextModelSnapshot.cs @@ -361,6 +361,8 @@ namespace NadekoBot.Migrations b.Property("AutoDeleteTrigger"); + b.Property("ContainsAnywhere"); + b.Property("DateAdded"); b.Property("DmResponse"); diff --git a/src/NadekoBot/Modules/CustomReactions/CustomReactions.cs b/src/NadekoBot/Modules/CustomReactions/CustomReactions.cs index ff062bac..a9311d55 100644 --- a/src/NadekoBot/Modules/CustomReactions/CustomReactions.cs +++ b/src/NadekoBot/Modules/CustomReactions/CustomReactions.cs @@ -270,7 +270,7 @@ namespace NadekoBot.Modules.CustomReactions } [NadekoCommand, Usage, Description, Aliases] - public async Task CrDm(int id) + public async Task CrCa(int id) { if ((Context.Guild == null && !_creds.IsOwner(Context.User)) || (Context.Guild != null && !((IGuildUser)Context.User).GuildPermissions.Administrator)) @@ -297,21 +297,21 @@ namespace NadekoBot.Modules.CustomReactions return; } - var setValue = reaction.DmResponse = !reaction.DmResponse; + var setValue = reaction.ContainsAnywhere = !reaction.ContainsAnywhere; using (var uow = _db.UnitOfWork) { - uow.CustomReactions.Get(id).DmResponse = setValue; + uow.CustomReactions.Get(id).ContainsAnywhere = setValue; uow.Complete(); } if (setValue) { - await ReplyConfirmLocalized("crdm_enabled", Format.Code(reaction.Id.ToString())).ConfigureAwait(false); + await ReplyConfirmLocalized("crca_enabled", Format.Code(reaction.Id.ToString())).ConfigureAwait(false); } else { - await ReplyConfirmLocalized("crdm_disabled", Format.Code(reaction.Id.ToString())).ConfigureAwait(false); + await ReplyConfirmLocalized("crca_disabled", Format.Code(reaction.Id.ToString())).ConfigureAwait(false); } } else diff --git a/src/NadekoBot/Modules/CustomReactions/Services/CustomReactionsService.cs b/src/NadekoBot/Modules/CustomReactions/Services/CustomReactionsService.cs index cb595124..e611e4d6 100644 --- a/src/NadekoBot/Modules/CustomReactions/Services/CustomReactionsService.cs +++ b/src/NadekoBot/Modules/CustomReactions/Services/CustomReactionsService.cs @@ -69,7 +69,13 @@ namespace NadekoBot.Modules.CustomReactions.Services var hasTarget = cr.Response.ToLowerInvariant().Contains("%target%"); var trigger = cr.TriggerWithContext(umsg, _client).Trim().ToLowerInvariant(); - return ((hasTarget && content.StartsWith(trigger + " ")) || (_bc.BotConfig.CustomReactionsStartWith && content.StartsWith(trigger + " ")) || content == trigger); + return ((cr.ContainsAnywhere && + (content.StartsWith(trigger + " ") + || content.EndsWith(" " + trigger) + || content.Contains(" " + trigger + " "))) + || (hasTarget && content.StartsWith(trigger + " ")) + || (_bc.BotConfig.CustomReactionsStartWith && content.StartsWith(trigger + " ")) + || content == trigger); }).ToArray(); if (rs.Length != 0) diff --git a/src/NadekoBot/Resources/CommandStrings.resx b/src/NadekoBot/Resources/CommandStrings.resx index 4b98b292..957a2efa 100644 --- a/src/NadekoBot/Resources/CommandStrings.resx +++ b/src/NadekoBot/Resources/CommandStrings.resx @@ -3249,6 +3249,15 @@ `{0}crdm 44` + + crca + + + Toggles whether the custom reaction will trigger if the triggering message contains the keyword (instead of only starting with it). + + + `{0}crca 44` + aliaslist cmdmaplist aliases diff --git a/src/NadekoBot/Services/Database/Models/CustomReaction.cs b/src/NadekoBot/Services/Database/Models/CustomReaction.cs index 25bb34fb..2f268849 100644 --- a/src/NadekoBot/Services/Database/Models/CustomReaction.cs +++ b/src/NadekoBot/Services/Database/Models/CustomReaction.cs @@ -17,6 +17,8 @@ namespace NadekoBot.Services.Database.Models public bool DmResponse { get; set; } public bool IsGlobal => !GuildId.HasValue; + + public bool ContainsAnywhere { get; set; } } public class ReactionResponse : DbEntity diff --git a/src/NadekoBot/_strings/ResponseStrings.en-US.json b/src/NadekoBot/_strings/ResponseStrings.en-US.json index 74303303..30a1d23f 100644 --- a/src/NadekoBot/_strings/ResponseStrings.en-US.json +++ b/src/NadekoBot/_strings/ResponseStrings.en-US.json @@ -734,6 +734,8 @@ "customreactions_crad_enabled": "Message triggering the custom reaction with id {0} will get automatically deleted.", "customreactions_crdm_disabled": "Response message for the custom reaction with id {0} won't be sent as a DM.", "customreactions_crdm_enabled": "Response message for the custom reaction with id {0} will be sent as a DM.", + "customreactions_crca_disabled": "Custom reaction with id {0} will no longer trigger unless it's trigger word is at the beggining of the sentence.", + "customreactions_crca_enabled": "Custom reaction with id {0} will now trigger if it's contained anywhere in the sentence.", "utility_aliases_none": "No alias found", "utility_alias_added": "Typing {0} will now be an alias of {1}.", "utility_alias_list": "List of aliases", From e9cf57d46fc92dc69bf925f497a3800c726e0324 Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Sun, 23 Jul 2017 09:52:45 +0200 Subject: [PATCH 212/346] .crdm was deleted by accident, it's back now --- .../CustomReactions/CustomReactions.cs | 53 ++++++++++++++++++- 1 file changed, 52 insertions(+), 1 deletion(-) diff --git a/src/NadekoBot/Modules/CustomReactions/CustomReactions.cs b/src/NadekoBot/Modules/CustomReactions/CustomReactions.cs index a9311d55..ffead6e5 100644 --- a/src/NadekoBot/Modules/CustomReactions/CustomReactions.cs +++ b/src/NadekoBot/Modules/CustomReactions/CustomReactions.cs @@ -272,7 +272,7 @@ namespace NadekoBot.Modules.CustomReactions [NadekoCommand, Usage, Description, Aliases] public async Task CrCa(int id) { - if ((Context.Guild == null && !_creds.IsOwner(Context.User)) || + if ((Context.Guild == null && !_creds.IsOwner(Context.User)) || (Context.Guild != null && !((IGuildUser)Context.User).GuildPermissions.Administrator)) { await ReplyErrorLocalized("insuff_perms").ConfigureAwait(false); @@ -320,6 +320,57 @@ namespace NadekoBot.Modules.CustomReactions } } + [NadekoCommand, Usage, Description, Aliases] + public async Task CrDm(int id) + { + if ((Context.Guild == null && !_creds.IsOwner(Context.User)) || + (Context.Guild != null && !((IGuildUser)Context.User).GuildPermissions.Administrator)) + { + await ReplyErrorLocalized("insuff_perms").ConfigureAwait(false); + return; + } + + CustomReaction[] reactions = new CustomReaction[0]; + + if (Context.Guild == null) + reactions = _service.GlobalReactions; + else + { + _service.GuildReactions.TryGetValue(Context.Guild.Id, out reactions); + } + if (reactions.Any()) + { + var reaction = reactions.FirstOrDefault(x => x.Id == id); + + if (reaction == null) + { + await ReplyErrorLocalized("no_found_id").ConfigureAwait(false); + return; + } + + var setValue = reaction.DmResponse = !reaction.DmResponse; + + using (var uow = _db.UnitOfWork) + { + uow.CustomReactions.Get(id).DmResponse = setValue; + uow.Complete(); + } + + if (setValue) + { + await ReplyConfirmLocalized("crdm_enabled", Format.Code(reaction.Id.ToString())).ConfigureAwait(false); + } + else + { + await ReplyConfirmLocalized("crdm_disabled", Format.Code(reaction.Id.ToString())).ConfigureAwait(false); + } + } + else + { + await ReplyErrorLocalized("no_found").ConfigureAwait(false); + } + } + [NadekoCommand, Usage, Description, Aliases] public async Task CrAd(int id) { From 263a95a6ad106f82d7b67ed49130b741f98a2024 Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Tue, 25 Jul 2017 18:31:30 +0200 Subject: [PATCH 213/346] cleanup --- .../Administration/AutoAssignRoleCommands.cs | 2 +- .../Administration/ProtectionCommands.cs | 12 +++++------- .../Administration/RatelimitCommands.cs | 4 ++-- .../Services/PlayingRotateService.cs | 16 ++++++++-------- .../Administration/Services/SelfService.cs | 1 - .../Modules/CustomReactions/CustomReactions.cs | 3 +-- .../Modules/Gambling/AnimalRacingCommands.cs | 3 +-- .../Modules/Games/CleverBotCommands.cs | 2 +- src/NadekoBot/Modules/Games/Games.cs | 8 ++++++++ src/NadekoBot/Modules/Games/HangmanCommands.cs | 10 +++++----- .../Modules/Permissions/CommandCostCommands.cs | 3 +-- .../Modules/Utility/BotConfigCommands.cs | 9 ++++++++- .../Modules/Utility/CommandMapCommands.cs | 11 ++++------- src/NadekoBot/NadekoBot.cs | 18 ++++++++++-------- src/NadekoBot/Services/CommandHandler.cs | 5 +++-- src/NadekoBot/Services/IBotConfigProvider.cs | 2 +- src/NadekoBot/Services/ILocalization.cs | 2 +- src/NadekoBot/Services/Impl/Localization.cs | 11 ++++++++--- 18 files changed, 68 insertions(+), 54 deletions(-) diff --git a/src/NadekoBot/Modules/Administration/AutoAssignRoleCommands.cs b/src/NadekoBot/Modules/Administration/AutoAssignRoleCommands.cs index a2f53f4e..54d2e244 100644 --- a/src/NadekoBot/Modules/Administration/AutoAssignRoleCommands.cs +++ b/src/NadekoBot/Modules/Administration/AutoAssignRoleCommands.cs @@ -37,7 +37,7 @@ namespace NadekoBot.Modules.Administration if (role == null) { conf.AutoAssignRoleId = 0; - _service.AutoAssignedRoles.TryRemove(Context.Guild.Id, out ulong throwaway); + _service.AutoAssignedRoles.TryRemove(Context.Guild.Id, out _); } else { diff --git a/src/NadekoBot/Modules/Administration/ProtectionCommands.cs b/src/NadekoBot/Modules/Administration/ProtectionCommands.cs index 8468f270..72e68bf9 100644 --- a/src/NadekoBot/Modules/Administration/ProtectionCommands.cs +++ b/src/NadekoBot/Modules/Administration/ProtectionCommands.cs @@ -60,9 +60,8 @@ namespace NadekoBot.Modules.Administration await ReplyErrorLocalized("raid_time", 2, 300).ConfigureAwait(false); return; } - - AntiRaidStats throwaway; - if (_service.AntiRaidGuilds.TryRemove(Context.Guild.Id, out throwaway)) + + if (_service.AntiRaidGuilds.TryRemove(Context.Guild.Id, out _)) { using (var uow = _db.UnitOfWork) { @@ -117,11 +116,10 @@ namespace NadekoBot.Modules.Administration { if (messageCount < 2 || messageCount > 10) return; - - AntiSpamStats throwaway; - if (_service.AntiSpamGuilds.TryRemove(Context.Guild.Id, out throwaway)) + + if (_service.AntiSpamGuilds.TryRemove(Context.Guild.Id, out var removed)) { - throwaway.UserStats.ForEach(x => x.Value.Dispose()); + removed.UserStats.ForEach(x => x.Value.Dispose()); using (var uow = _db.UnitOfWork) { var gc = uow.GuildConfigs.For(Context.Guild.Id, set => set.Include(x => x.AntiSpamSetting) diff --git a/src/NadekoBot/Modules/Administration/RatelimitCommands.cs b/src/NadekoBot/Modules/Administration/RatelimitCommands.cs index 7be7acd2..40b37884 100644 --- a/src/NadekoBot/Modules/Administration/RatelimitCommands.cs +++ b/src/NadekoBot/Modules/Administration/RatelimitCommands.cs @@ -30,9 +30,9 @@ namespace NadekoBot.Modules.Administration [RequireUserPermission(GuildPermission.ManageMessages)] public async Task Slowmode() { - if (_service.RatelimitingChannels.TryRemove(Context.Channel.Id, out Ratelimiter throwaway)) + if (_service.RatelimitingChannels.TryRemove(Context.Channel.Id, out Ratelimiter removed)) { - throwaway.CancelSource.Cancel(); + removed.CancelSource.Cancel(); await ReplyConfirmLocalized("slowmode_disabled").ConfigureAwait(false); } } diff --git a/src/NadekoBot/Modules/Administration/Services/PlayingRotateService.cs b/src/NadekoBot/Modules/Administration/Services/PlayingRotateService.cs index d7b5fe2e..9786d085 100644 --- a/src/NadekoBot/Modules/Administration/Services/PlayingRotateService.cs +++ b/src/NadekoBot/Modules/Administration/Services/PlayingRotateService.cs @@ -18,17 +18,19 @@ namespace NadekoBot.Modules.Administration.Services private readonly Logger _log; private readonly Replacer _rep; private readonly DbService _db; - public BotConfig BotConfig { get; private set; } //todo load whole botconifg, not just for this service when you have the time + private readonly IBotConfigProvider _bcp; + + public BotConfig BotConfig => _bcp.BotConfig; private class TimerState { public int Index { get; set; } } - public PlayingRotateService(DiscordSocketClient client, BotConfig bc, MusicService music, DbService db) + public PlayingRotateService(DiscordSocketClient client, IBotConfigProvider bcp, MusicService music, DbService db) { _client = client; - BotConfig = bc; + _bcp = bcp; _music = music; _db = db; _log = LogManager.GetCurrentClassLogger(); @@ -42,11 +44,9 @@ namespace NadekoBot.Modules.Administration.Services { try { - using (var uow = _db.UnitOfWork) - { - BotConfig = uow.BotConfig.GetOrCreate(); - } - var state = (TimerState)objState; + bcp.Reload(); + + var state = (TimerState)objState; if (!BotConfig.RotatingStatuses) return; if (state.Index >= BotConfig.RotatingStatusMessages.Count) diff --git a/src/NadekoBot/Modules/Administration/Services/SelfService.cs b/src/NadekoBot/Modules/Administration/Services/SelfService.cs index c5ad3ce9..93699858 100644 --- a/src/NadekoBot/Modules/Administration/Services/SelfService.cs +++ b/src/NadekoBot/Modules/Administration/Services/SelfService.cs @@ -15,7 +15,6 @@ namespace NadekoBot.Modules.Administration.Services { public class SelfService : ILateExecutor, INService { - //todo bot config public bool ForwardDMs => _bc.BotConfig.ForwardMessages; public bool ForwardDMsToAllOwners => _bc.BotConfig.ForwardToAllOwners; diff --git a/src/NadekoBot/Modules/CustomReactions/CustomReactions.cs b/src/NadekoBot/Modules/CustomReactions/CustomReactions.cs index ffead6e5..b466b158 100644 --- a/src/NadekoBot/Modules/CustomReactions/CustomReactions.cs +++ b/src/NadekoBot/Modules/CustomReactions/CustomReactions.cs @@ -433,8 +433,7 @@ namespace NadekoBot.Modules.CustomReactions } else { - uint throwaway; - if (_service.ReactionStats.TryRemove(trigger, out throwaway)) + if (_service.ReactionStats.TryRemove(trigger, out _)) { await ReplyErrorLocalized("stats_cleared", Format.Bold(trigger)).ConfigureAwait(false); } diff --git a/src/NadekoBot/Modules/Gambling/AnimalRacingCommands.cs b/src/NadekoBot/Modules/Gambling/AnimalRacingCommands.cs index f322a517..f4777bef 100644 --- a/src/NadekoBot/Modules/Gambling/AnimalRacingCommands.cs +++ b/src/NadekoBot/Modules/Gambling/AnimalRacingCommands.cs @@ -158,8 +158,7 @@ namespace NadekoBot.Modules.Gambling private void End() { - AnimalRace throwaway; - AnimalRaces.TryRemove(_serverId, out throwaway); + AnimalRaces.TryRemove(_serverId, out _); } private async Task StartRace() diff --git a/src/NadekoBot/Modules/Games/CleverBotCommands.cs b/src/NadekoBot/Modules/Games/CleverBotCommands.cs index a1534800..3373a98a 100644 --- a/src/NadekoBot/Modules/Games/CleverBotCommands.cs +++ b/src/NadekoBot/Modules/Games/CleverBotCommands.cs @@ -28,7 +28,7 @@ namespace NadekoBot.Modules.Games { var channel = (ITextChannel)Context.Channel; - if (_service.ChatterBotGuilds.TryRemove(channel.Guild.Id, out Lazy throwaway)) + if (_service.ChatterBotGuilds.TryRemove(channel.Guild.Id, out _)) { using (var uow = _db.UnitOfWork) { diff --git a/src/NadekoBot/Modules/Games/Games.cs b/src/NadekoBot/Modules/Games/Games.cs index 27ddd19e..c5cc0b13 100644 --- a/src/NadekoBot/Modules/Games/Games.cs +++ b/src/NadekoBot/Modules/Games/Games.cs @@ -11,6 +11,14 @@ using NadekoBot.Modules.Games.Services; namespace NadekoBot.Modules.Games { + /*todo more games + - Blackjack + - Shiritori + - Simple RPG adventure + - The nunchi game + - Wheel of fortune + - Connect 4 + */ public partial class Games : NadekoTopLevelModule { private readonly IImagesService _images; diff --git a/src/NadekoBot/Modules/Games/HangmanCommands.cs b/src/NadekoBot/Modules/Games/HangmanCommands.cs index f3e6a32a..5ebd979a 100644 --- a/src/NadekoBot/Modules/Games/HangmanCommands.cs +++ b/src/NadekoBot/Modules/Games/HangmanCommands.cs @@ -45,7 +45,7 @@ namespace NadekoBot.Modules.Games hm.OnEnded += g => { - HangmanGames.TryRemove(g.GameChannel.Id, out HangmanGame throwaway); + HangmanGames.TryRemove(g.GameChannel.Id, out _); }; try { @@ -54,8 +54,8 @@ namespace NadekoBot.Modules.Games catch (Exception ex) { try { await Context.Channel.SendErrorAsync(GetText("hangman_start_errored") + " " + ex.Message).ConfigureAwait(false); } catch { } - HangmanGames.TryRemove(Context.Channel.Id, out HangmanGame throwaway); - throwaway.Dispose(); + if(HangmanGames.TryRemove(Context.Channel.Id, out var removed)) + removed.Dispose(); return; } @@ -66,9 +66,9 @@ namespace NadekoBot.Modules.Games [RequireContext(ContextType.Guild)] public async Task HangmanStop() { - if (HangmanGames.TryRemove(Context.Channel.Id, out HangmanGame throwaway)) + if (HangmanGames.TryRemove(Context.Channel.Id, out var removed)) { - throwaway.Dispose(); + removed.Dispose(); await ReplyConfirmLocalized("hangman_stopped").ConfigureAwait(false); } } diff --git a/src/NadekoBot/Modules/Permissions/CommandCostCommands.cs b/src/NadekoBot/Modules/Permissions/CommandCostCommands.cs index 13651ca3..ed31eb9e 100644 --- a/src/NadekoBot/Modules/Permissions/CommandCostCommands.cs +++ b/src/NadekoBot/Modules/Permissions/CommandCostCommands.cs @@ -74,8 +74,7 @@ namespace NadekoBot.Modules.Permissions // else // { // bc.CommandCosts.RemoveAt(bc.CommandCosts.IndexOf(cmdPrice)); - // int throwaway; - // _commandCosts.TryRemove(cmdName, out throwaway); + // _commandCosts.TryRemove(cmdName, out _); // } // await uow.CompleteAsync().ConfigureAwait(false); diff --git a/src/NadekoBot/Modules/Utility/BotConfigCommands.cs b/src/NadekoBot/Modules/Utility/BotConfigCommands.cs index 6262bf98..cc91dd9f 100644 --- a/src/NadekoBot/Modules/Utility/BotConfigCommands.cs +++ b/src/NadekoBot/Modules/Utility/BotConfigCommands.cs @@ -10,8 +10,15 @@ namespace NadekoBot.Modules.Utility { public partial class Utility { - public class BotConfigCommands : NadekoSubmodule + public class BotConfigCommands : NadekoSubmodule { + private readonly IBotConfigProvider _service; + + public BotConfigCommands(IBotConfigProvider service) + { + _service = service; + } + [NadekoCommand, Usage, Description, Aliases] [OwnerOnly] public async Task BotConfigEdit() diff --git a/src/NadekoBot/Modules/Utility/CommandMapCommands.cs b/src/NadekoBot/Modules/Utility/CommandMapCommands.cs index 872c43b6..9903dedf 100644 --- a/src/NadekoBot/Modules/Utility/CommandMapCommands.cs +++ b/src/NadekoBot/Modules/Utility/CommandMapCommands.cs @@ -42,10 +42,8 @@ namespace NadekoBot.Modules.Utility if (string.IsNullOrWhiteSpace(mapping)) { - ConcurrentDictionary maps; - string throwaway; - if (!_service.AliasMaps.TryGetValue(Context.Guild.Id, out maps) || - !maps.TryRemove(trigger, out throwaway)) + if (!_service.AliasMaps.TryGetValue(Context.Guild.Id, out var maps) || + !maps.TryRemove(trigger, out _)) { await ReplyErrorLocalized("alias_remove_fail", Format.Code(trigger)).ConfigureAwait(false); return; @@ -112,9 +110,8 @@ namespace NadekoBot.Modules.Utility if (page < 0) return; - - ConcurrentDictionary maps; - if (!_service.AliasMaps.TryGetValue(Context.Guild.Id, out maps) || !maps.Any()) + + if (!_service.AliasMaps.TryGetValue(Context.Guild.Id, out var maps) || !maps.Any()) { await ReplyErrorLocalized("aliases_none").ConfigureAwait(false); return; diff --git a/src/NadekoBot/NadekoBot.cs b/src/NadekoBot/NadekoBot.cs index b49b0562..a92380ca 100644 --- a/src/NadekoBot/NadekoBot.cs +++ b/src/NadekoBot/NadekoBot.cs @@ -32,7 +32,7 @@ namespace NadekoBot public DiscordSocketClient Client { get; } public CommandService CommandService { get; } - public DbService Db { get; } + private readonly DbService _db; public ImmutableArray AllGuildConfigs { get; private set; } /* I don't know how to make this not be static @@ -64,7 +64,7 @@ namespace NadekoBot TerribleElevatedPermissionCheck(); Credentials = new BotCredentials(); - Db = new DbService(Credentials); + _db = new DbService(Credentials); Client = new DiscordSocketClient(new DiscordSocketConfig { MessageCacheSize = 10, @@ -83,7 +83,7 @@ namespace NadekoBot port = port ?? Credentials.ShardRunPort; _comClient = new ShardComClient(port.Value); - using (var uow = Db.UnitOfWork) + using (var uow = _db.UnitOfWork) { _botConfig = uow.BotConfig.GetOrCreate(); OkColor = new Color(Convert.ToUInt32(_botConfig.OkColor, 16)); @@ -120,20 +120,22 @@ namespace NadekoBot var startingGuildIdList = Client.Guilds.Select(x => (long)x.Id).ToList(); //this unit of work will be used for initialization of all modules too, to prevent multiple queries from running - using (var uow = Db.UnitOfWork) + using (var uow = _db.UnitOfWork) { AllGuildConfigs = uow.GuildConfigs.GetAllGuildConfigs(startingGuildIdList).ToImmutableArray(); - var localization = new Localization(_botConfig.Locale, AllGuildConfigs.ToDictionary(x => x.GuildId, x => x.Locale), Db); + IBotConfigProvider botConfigProvider = new BotConfigProvider(_db, _botConfig); + + //var localization = new Localization(_botConfig.Locale, AllGuildConfigs.ToDictionary(x => x.GuildId, x => x.Locale), Db); //initialize Services Services = new NServiceProvider.ServiceProviderBuilder() .AddManual(Credentials) - .AddManual(Db) - .AddManual(_botConfig) + .AddManual(_db) .AddManual(Client) .AddManual(CommandService) - .AddManual(localization) + .AddManual(botConfigProvider) + //.AddManual(localization) .AddManual>(AllGuildConfigs) //todo wrap this .AddManual(this) .AddManual(uow) diff --git a/src/NadekoBot/Services/CommandHandler.cs b/src/NadekoBot/Services/CommandHandler.cs index b1a25463..21309283 100644 --- a/src/NadekoBot/Services/CommandHandler.cs +++ b/src/NadekoBot/Services/CommandHandler.cs @@ -261,10 +261,11 @@ namespace NadekoBot.Services } } var prefix = GetPrefix(guild?.Id); + var isPrefixCommand = messageContent == ".prefix"; // execute the command and measure the time it took - if (messageContent.StartsWith(prefix)) + if (messageContent.StartsWith(prefix) || isPrefixCommand) { - var result = await ExecuteCommandAsync(new CommandContext(_client, usrMsg), messageContent, prefix.Length, _services, MultiMatchHandling.Best); + var result = await ExecuteCommandAsync(new CommandContext(_client, usrMsg), messageContent, isPrefixCommand ? 1 : prefix.Length, _services, MultiMatchHandling.Best); execTime = Environment.TickCount - execTime; if (result.Success) diff --git a/src/NadekoBot/Services/IBotConfigProvider.cs b/src/NadekoBot/Services/IBotConfigProvider.cs index 1e65f202..6ef54970 100644 --- a/src/NadekoBot/Services/IBotConfigProvider.cs +++ b/src/NadekoBot/Services/IBotConfigProvider.cs @@ -3,7 +3,7 @@ using NadekoBot.Services.Database.Models; namespace NadekoBot.Services { - public interface IBotConfigProvider : INService + public interface IBotConfigProvider { BotConfig BotConfig { get; } void Reload(); diff --git a/src/NadekoBot/Services/ILocalization.cs b/src/NadekoBot/Services/ILocalization.cs index b9fa898d..c3cfbe94 100644 --- a/src/NadekoBot/Services/ILocalization.cs +++ b/src/NadekoBot/Services/ILocalization.cs @@ -4,7 +4,7 @@ using Discord; namespace NadekoBot.Services { - public interface ILocalization + public interface ILocalization : INService { CultureInfo DefaultCultureInfo { get; } ConcurrentDictionary GuildCultureInfos { get; } diff --git a/src/NadekoBot/Services/Impl/Localization.cs b/src/NadekoBot/Services/Impl/Localization.cs index a1ac9043..d781054b 100644 --- a/src/NadekoBot/Services/Impl/Localization.cs +++ b/src/NadekoBot/Services/Impl/Localization.cs @@ -4,6 +4,7 @@ using System.Globalization; using System.Linq; using Discord; using NLog; +using NadekoBot.Services.Database.Models; namespace NadekoBot.Services.Impl { @@ -16,10 +17,15 @@ namespace NadekoBot.Services.Impl public CultureInfo DefaultCultureInfo { get; private set; } = CultureInfo.CurrentCulture; private Localization() { } - public Localization(string defaultCulture, IDictionary cultureInfoNames, DbService db) + public Localization(IBotConfigProvider bcp, IEnumerable gcs, DbService db) { _log = LogManager.GetCurrentClassLogger(); + + var cultureInfoNames = gcs.ToDictionary(x => x.GuildId, x => x.Locale); + var defaultCulture = bcp.BotConfig.Locale; + _db = db; + if (string.IsNullOrWhiteSpace(defaultCulture)) DefaultCultureInfo = new CultureInfo("en-US"); else @@ -74,8 +80,7 @@ namespace NadekoBot.Services.Impl public void RemoveGuildCulture(ulong guildId) { - CultureInfo throwaway; - if (GuildCultureInfos.TryRemove(guildId, out throwaway)) + if (GuildCultureInfos.TryRemove(guildId, out var _)) { using (var uow = _db.UnitOfWork) { From c738cb569eec1141748f40b6b05dc6fa12273ae4 Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Wed, 26 Jul 2017 18:08:44 +0200 Subject: [PATCH 214/346] Update ResponseStrings.ar.json (POEditor.com) From 5295704447c27cabd7ed4fa6feedba60c4e2209b Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Wed, 26 Jul 2017 18:08:47 +0200 Subject: [PATCH 215/346] Update ResponseStrings.zh-CN.json (POEditor.com) From 8e7935f89301857b15dcb865c381c7ed662e78e6 Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Wed, 26 Jul 2017 18:08:49 +0200 Subject: [PATCH 216/346] Update ResponseStrings.zh-TW.json (POEditor.com) --- .../_strings/ResponseStrings.zh-TW.json | 44 +++++++++---------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/src/NadekoBot/_strings/ResponseStrings.zh-TW.json b/src/NadekoBot/_strings/ResponseStrings.zh-TW.json index 999f8c26..50d61c97 100644 --- a/src/NadekoBot/_strings/ResponseStrings.zh-TW.json +++ b/src/NadekoBot/_strings/ResponseStrings.zh-TW.json @@ -66,7 +66,7 @@ "administration_bandm": "你已被 {0} 伺服器封鎖。\n原因:{1}", "administration_banned_pl": "成員已封鎖", "administration_banned_user": "成員已封鎖", - "administration_bot_name": "Bot 暱稱已改為 {0}", + "administration_bot_name": "Bot 的暱稱已變更為 {0}", "administration_bot_status": "Bot 狀態已改為 {0}", "administration_byedel_off": "自動刪除告別訊息功能已停用。", "administration_byedel_on": "告別訊息將於 {0} 秒後刪除。", @@ -283,7 +283,7 @@ "help_cmdlist_donate": "您可以透過 Patreon: <{0}> 或 Paypal: <{1}> 來贊助此專案", "help_cmd_and_alias": "指令和快捷", "help_commandlist_regen": "已重建指令清單。", - "help_commands_instr": "輸入`{0}h 指令名稱`來查閱該指令的詳細說明。 例如 `{0}h >8ball`", + "help_commands_instr": "輸入`{0}h 指令名稱`來查閱該指令的詳細說明。 例如 `{0}h {0}8ball`", "help_command_not_found": "我找不到该指令。請再次查詢前確定該指令是否存在。", "help_desc": "說明", "help_donate": "您可以透過:\nPatreon <{0}> 或\nPaypal <{1}>\n來贊助NadekoBot專案\n請不要忘記在訊息中留下您的ID。\n\n**感謝您**♥️", @@ -358,16 +358,16 @@ "games_category": "分類", "games_cleverbot_disabled": "已在此伺服器上停用Cleverbot。", "games_cleverbot_enabled": "已在此伺服器上啟用Cleverbot。", - "games_curgen_disabled": "停止在此頻道上生產貨幣。", - "games_curgen_enabled": "開始在此頻道上生產貨幣。", + "games_curgen_disabled": "停止在此頻道上生產代幣。", + "games_curgen_enabled": "開始在此頻道上生產代幣。", "games_curgen_pl": "發現了 {0} 個野生的 {1}!", "games_curgen_sn": "發現了一個野生的 {0}!", "games_failed_loading_question": "載入問題失敗。", "games_game_started": "遊戲開始", - "games_hangman_game_started": "Hangman遊戲開始了", - "games_hangman_running": "Hangman遊戲已經在此頻道上執行中。", - "games_hangman_start_errored": "Hangman啟動失敗。", - "games_hangman_types": "“{0}hangman” 單詞類型:", + "games_hangman_game_started": "猜單詞遊戲開始了", + "games_hangman_running": "猜單詞遊戲已經在此頻道上執行中。", + "games_hangman_start_errored": "猜單詞遊戲啟動失敗。", + "games_hangman_types": "“{0}猜單詞” 單詞類型列表:", "games_leaderboard": "排行榜", "games_not_enough": "您沒有足夠的 {0}", "games_no_results": "沒有結果", @@ -414,7 +414,7 @@ "music_no_search_results": "沒有搜尋結果。", "music_paused": "暫停播放音樂。", "music_player_queue": "播放清單 - 第 {0} / {1} 頁", - "music_playing_song": "正在播放", + "music_playing_song": "正在播放 #{0}", "music_playlists": "`#{0}` - **{1}** by *{2}* ({3}首歌)", "music_playlists_page": "已儲存的播放清單第 {0} 頁", "music_playlist_deleted": "播放清單已刪除。", @@ -590,7 +590,7 @@ "utiliity_joined": "已加入", "utility_activity_line": "`{0}.` {1} [{2:F2}/秒] - 共 {3}", "utility_activity_page": "活動頁面 #{0}", - "utility_activity_users_total": "共 {0} 個使用者。", + "utility_activity_users_total": "共 {0} 個成員。", "utility_author": "作者", "utility_botid": "Bot ID", "utility_calcops": "可在 {0}calc 使用的函式", @@ -657,7 +657,7 @@ "utility_server_info": "伺服器資訊", "utility_shard": "Shard", "utility_shard_stats": "Shard狀態", - "utility_shard_stats_txt": "Shard **#{0}** 的狀態與 {2} 伺服器為 {1} ", + "utility_shard_stats_txt": "{2} 伺服器 Shard **#{0}** 的狀態為 {1} - {3} 之前", "utility_showemojis": "**名稱:** {0} **連結:** {1}", "utility_showemojis_none": "找不到特殊的表情符號。", "utility_stats_songs": "正在播放 {0} 首歌,{1} 首已點播。", @@ -714,7 +714,7 @@ "administration_user_not_found": "找不到成員。", "administration_user_warned": "已警告成員 {0} 。", "administration_user_warned_and_punished": "成員 {0} 因警告多次故以 {1} 作為懲罰。", - "administration_warned_on": "在 {0} 伺服器上警告", + "administration_warned_on": "您在 {0} 伺服器上被警告", "administration_warned_on_by": "於 {0} {1},由 {2}", "administration_warnings_cleared": "已清除成員 {0} 上的所有警告。", "administration_warnings_none": "此頁沒有警告。", @@ -795,14 +795,14 @@ "administration_migration_error": "升級失敗,請參閱Bot的主畫面來獲得更多資訊。", "searches_hex_invalid": "無效的顏色碼。", "permissions_global_perms_reset": "全域權限已重設。", - "help_module": "", - "games_hangman_stopped": "", - "music_autoplaying": "", - "music_queue_stopped": "", - "music_removed_song_error": "", - "music_shuffling_playlist": "", - "music_songs_shuffle_enable": "", - "music_songs_shuffle_disable": "", - "music_song_skips_after": "", - "administration_warnings_list": "" + "help_module": "模組: {0}", + "games_hangman_stopped": "猜單詞遊戲已結束。", + "music_autoplaying": "自動播放中。", + "music_queue_stopped": "播放器已停止。使用 {0} 指令來開始播放。", + "music_removed_song_error": "該索引上的歌曲不存在", + "music_shuffling_playlist": "播放清單洗牌中", + "music_songs_shuffle_enable": "開始隨機播放清單。", + "music_songs_shuffle_disable": "停止隨機播放清單。", + "music_song_skips_after": "音樂將在 {0} 後跳過", + "administration_warnings_list": "伺服器上所有被警告成員的列表" } \ No newline at end of file From afda3e2046d88bbbd7cd7117b27620d4e77d523b Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Wed, 26 Jul 2017 18:08:52 +0200 Subject: [PATCH 217/346] Update ResponseStrings.cs-CZ.json (POEditor.com) From d86c8ed41b181b2531185bbfb5442e43d97a0754 Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Wed, 26 Jul 2017 18:08:55 +0200 Subject: [PATCH 218/346] Update ResponseStrings.da-DK.json (POEditor.com) From 81f00322620af76cd4d198c3bf77cc900a2bf5ec Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Wed, 26 Jul 2017 18:08:57 +0200 Subject: [PATCH 219/346] Update ResponseStrings.nl-NL.json (POEditor.com) --- src/NadekoBot/_strings/ResponseStrings.nl-NL.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/NadekoBot/_strings/ResponseStrings.nl-NL.json b/src/NadekoBot/_strings/ResponseStrings.nl-NL.json index 40db5939..23a7e229 100644 --- a/src/NadekoBot/_strings/ResponseStrings.nl-NL.json +++ b/src/NadekoBot/_strings/ResponseStrings.nl-NL.json @@ -396,7 +396,7 @@ "music_autoplay_enabled": "Autoplay aangezet.", "music_defvol_set": "Standaardvolume op {0}% gezet", "music_dir_queue_complete": "Map afspeellijst geladen/compleet.", - "music_fairplay": "fairplay", + "music_fairplay": "Fairplay", "music_finished_song": "Track afgelopen:", "music_fp_disabled": "Fairplay uitgeschakeld.", "music_fp_enabled": "Fairplay ingeschakeld.", @@ -414,7 +414,7 @@ "music_no_search_results": "Geen zoekresultaten.", "music_paused": "Muziek gepauzeerd.", "music_player_queue": "Speler afspeellijst - Pagina {0}/{1}", - "music_playing_song": "Track wordt afgespeeld", + "music_playing_song": "Track #{0} wordt afgespeeld", "music_playlists": "`#{0}` - **{1}** van *{2}* ({3} tracks) ", "music_playlists_page": "Pagina {0} van opgeslagen afspeellijsten\n", "music_playlist_deleted": "Afspeellijst verwijderd.", @@ -657,7 +657,7 @@ "utility_server_info": "Server Informatie", "utility_shard": "Shard", "utility_shard_stats": "Shard Statestieken", - "utility_shard_stats_txt": "Shard **#{0}** is in {1} status met {2} servers", + "utility_shard_stats_txt": "Shard **#{0}** is in {1} status met {2} servers - {3} geleden", "utility_showemojis": "*Naam:** {0} **Link:** {1}", "utility_showemojis_none": "Geen speciale emojis gevonden.", "utility_stats_songs": "{0} tracks aan het spelen, {1} in de wachtrij", From 44b3223c1b2409fcb4a79731a9a9e36d19b9db2d Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Wed, 26 Jul 2017 18:09:00 +0200 Subject: [PATCH 220/346] Update ResponseStrings.fr-FR.json (POEditor.com) --- src/NadekoBot/_strings/ResponseStrings.fr-FR.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/NadekoBot/_strings/ResponseStrings.fr-FR.json b/src/NadekoBot/_strings/ResponseStrings.fr-FR.json index 1009d984..38884372 100644 --- a/src/NadekoBot/_strings/ResponseStrings.fr-FR.json +++ b/src/NadekoBot/_strings/ResponseStrings.fr-FR.json @@ -283,7 +283,7 @@ "help_cmdlist_donate": "Vous pouvez supporter ce projet sur Patreon <{0}> ou via Paypal <{1}>", "help_cmd_and_alias": "Commandes et alias", "help_commandlist_regen": "Liste des commandes rafraîchie.", - "help_commands_instr": "Écrivez `{0}h NomDeLaCommande` pour voir l'aide spécifique à cette commande. Ex: `{0}h >8ball`", + "help_commands_instr": "Écrivez `{0}h NomDeLaCommande` pour voir l'aide spécifique à cette commande. Ex: `{0}h {0}8ball`", "help_command_not_found": "Impossible de trouver cette commande. Veuillez vérifier qu'elle existe avant de réessayer.", "help_desc": "Description", "help_donate": "Vous pouvez supporter le projet NadekoBot\nsur Patreon <{0}>\npar Paypal <{1}>\nN'oubliez pas de mettre votre nom discord ou ID dans le message.\n\n**Merci** ♥️", From 92bf9b88b48fd37fc9ee727cbbd7dac610e5960e Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Wed, 26 Jul 2017 18:09:03 +0200 Subject: [PATCH 221/346] Update ResponseStrings.de-DE.json (POEditor.com) --- .../_strings/ResponseStrings.de-DE.json | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/src/NadekoBot/_strings/ResponseStrings.de-DE.json b/src/NadekoBot/_strings/ResponseStrings.de-DE.json index 2f1a88c0..f155cb6e 100644 --- a/src/NadekoBot/_strings/ResponseStrings.de-DE.json +++ b/src/NadekoBot/_strings/ResponseStrings.de-DE.json @@ -691,7 +691,7 @@ "customreactions_crad_disabled": "Auslösende Nachricht der benutzerdefinierten Reaktion mit der ID {0} wird nicht automatisch gelöscht.", "customreactions_crad_enabled": "Auslösende Nachricht der benutzerdefinierten Reaktion mit der ID {0} wird automatisch gelöscht.", "customreactions_crdm_disabled": "Reaktionsnachricht für die benutzerdefinierte Reaktion mit der ID {0} wird nicht als DN gesendet.", - "customreactions_crdm_enabled": "Reaktionsnachricht für die benutzerdefinierte Reaktion mit der ID {0} wird nicht als DN gesendet.", + "customreactions_crdm_enabled": "Reaktionsnachricht für die benutzerdefinierte Reaktion mit der ID {0} wird als DN gesendet.", "utility_aliases_none": "Keinen Alias gefunden", "utility_alias_added": "{0} ist nun ein Alias für {1}", "utility_alias_list": "Liste der Aliasse", @@ -780,29 +780,29 @@ "administration_prefix_new": "Das Präfix auf diesem Server wurde von {0} zu {1} geändert", "administration_defprefix_current": "Standard bot Präfix ist {0}", "administration_defprefix_new": "Standard bot Präfix wurde von {0} zu {1} geändert", - "administration_bot_nick": "", - "administration_user_nick": "", - "administration_timezone_guild": "", - "administration_timezone_not_found": "", - "administration_timezones_available": "", - "music_song_not_found": "", - "searches_define_unknown": "", + "administration_bot_nick": "Bots Nickname wurde zu {0} geändert", + "administration_user_nick": "Nickname des Benutzer {0} wurde zu {1} geändert", + "administration_timezone_guild": "Die Zeitzone für die Gilde ist `{0}`", + "administration_timezone_not_found": "Zeitzone nicht gefunden. Benutze \"timezones\" Befehl um eine liste der Verfügbaren Zeitzonen zu sehen.", + "administration_timezones_available": "Verfügbare Zeitzonen", + "music_song_not_found": "Kein Lied gefunden", + "searches_define_unknown": "Konnte keine Definition für diesen Term finden.", "utility_repeater_initial": "", "utility_verbose_errors_enabled": "", "utility_verbose_errors_disabled": "", "permissions_perms_reset": "", "permissions_trigger": "", "administration_migration_error": "", - "searches_hex_invalid": "", + "searches_hex_invalid": "Ungültige Farbe angegeben", "permissions_global_perms_reset": "", - "help_module": "", + "help_module": "Modul: {0}", "games_hangman_stopped": "", "music_autoplaying": "", "music_queue_stopped": "", - "music_removed_song_error": "", + "music_removed_song_error": "Lied an diesem Index existiert nicht", "music_shuffling_playlist": "", - "music_songs_shuffle_enable": "", + "music_songs_shuffle_enable": "Lieder werden jetzt zufällig wiedergegeben.", "music_songs_shuffle_disable": "", - "music_song_skips_after": "", - "administration_warnings_list": "" + "music_song_skips_after": "Lieder werden übersprungen nach {0}", + "administration_warnings_list": "Liste aller gewarnten Benutzer auf diesem Server" } \ No newline at end of file From 2a137004a59e38f64384c348a8d0aeaaef205a8c Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Wed, 26 Jul 2017 18:09:05 +0200 Subject: [PATCH 222/346] Update ResponseStrings.he-IL.json (POEditor.com) From 2d1b5dda964f731e2b6d73d28a7b0d91707e01b6 Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Wed, 26 Jul 2017 18:09:08 +0200 Subject: [PATCH 223/346] Update ResponseStrings.id-ID.json (POEditor.com) --- .../_strings/ResponseStrings.id-ID.json | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/NadekoBot/_strings/ResponseStrings.id-ID.json b/src/NadekoBot/_strings/ResponseStrings.id-ID.json index b75efe0d..af888820 100644 --- a/src/NadekoBot/_strings/ResponseStrings.id-ID.json +++ b/src/NadekoBot/_strings/ResponseStrings.id-ID.json @@ -795,14 +795,14 @@ "administration_migration_error": "Error saat memindah, cek konsol bot untuk informasi selanjutnya.", "searches_hex_invalid": "Warna yang dipilih tidak sah.", "permissions_global_perms_reset": "Ijin global telah direset.", - "help_module": "", - "games_hangman_stopped": "", - "music_autoplaying": "", - "music_queue_stopped": "", - "music_removed_song_error": "", - "music_shuffling_playlist": "", - "music_songs_shuffle_enable": "", - "music_songs_shuffle_disable": "", - "music_song_skips_after": "", - "administration_warnings_list": "" + "help_module": "Modul : {0}", + "games_hangman_stopped": "Permainan Hangman diberhentikan.", + "music_autoplaying": "Bermain Otomatis.", + "music_queue_stopped": "Pemain diberhentikan. Gunakan perintah{0} untuk menyalakan.", + "music_removed_song_error": "Lagu di indeks itu tidak ada.", + "music_shuffling_playlist": "Mengacak lagu - lagu.", + "music_songs_shuffle_enable": "Lagu - lagu akan teracak mulai sekarang.", + "music_songs_shuffle_disable": "Lagu - lagu tidak akan teracak.", + "music_song_skips_after": "Lagu - lagu akan melompat setelah {0}", + "administration_warnings_list": "Daftar pengguna yang diperingatkan." } \ No newline at end of file From 89b810df54fb96ecf827a68d1ce2e0d582fa83d8 Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Wed, 26 Jul 2017 18:09:11 +0200 Subject: [PATCH 224/346] Update ResponseStrings.it-IT.json (POEditor.com) --- src/NadekoBot/_strings/ResponseStrings.it-IT.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/NadekoBot/_strings/ResponseStrings.it-IT.json b/src/NadekoBot/_strings/ResponseStrings.it-IT.json index 4531be81..ac53c9b2 100644 --- a/src/NadekoBot/_strings/ResponseStrings.it-IT.json +++ b/src/NadekoBot/_strings/ResponseStrings.it-IT.json @@ -778,13 +778,13 @@ "utility_quotes_notfound": "Non è stata trovata nessuna citazione corrispondente all'ID specificato.", "administration_prefix_current": "Il prefisso in questo server é {0}", "administration_prefix_new": "Prefisso in questo server cambiato da {0} a {1}", - "administration_defprefix_current": "", - "administration_defprefix_new": "", + "administration_defprefix_current": "Il prefisso base del bot é {0}", + "administration_defprefix_new": "Cambiato il prefisso base del bot da {0} a {1}", "administration_bot_nick": "Il soprannome del Bot é cambiato in {0}", "administration_user_nick": "Il soprannome dell'utente {0} é cambiato in {1}", "administration_timezone_guild": "Il fuso orario di questa gilda é '{0}'", "administration_timezone_not_found": "", - "administration_timezones_available": "", + "administration_timezones_available": "Fuso orari disponibili", "music_song_not_found": "Nessuna canzone trovata.", "searches_define_unknown": "Impossibile trovare la definizione per quel termine.", "utility_repeater_initial": "", @@ -804,5 +804,5 @@ "music_songs_shuffle_enable": "", "music_songs_shuffle_disable": "", "music_song_skips_after": "", - "administration_warnings_list": "" + "administration_warnings_list": "Lista di tutti gli utenti avvisati in questo server" } \ No newline at end of file From 6294dc867917427dc0937c0b0888ea216a9ab224 Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Wed, 26 Jul 2017 18:09:13 +0200 Subject: [PATCH 225/346] Update ResponseStrings.ja-JP.json (POEditor.com) From e10aee9c6929f5be378272913089110755f2d534 Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Wed, 26 Jul 2017 18:09:16 +0200 Subject: [PATCH 226/346] Update ResponseStrings.ko-KR.json (POEditor.com) --- src/NadekoBot/_strings/ResponseStrings.ko-KR.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/NadekoBot/_strings/ResponseStrings.ko-KR.json b/src/NadekoBot/_strings/ResponseStrings.ko-KR.json index 4f025452..e812bf90 100644 --- a/src/NadekoBot/_strings/ResponseStrings.ko-KR.json +++ b/src/NadekoBot/_strings/ResponseStrings.ko-KR.json @@ -638,7 +638,7 @@ "utility_quote_deleted": "인용구 #{0}이(가) 삭제되었습니다.", "utility_region": "지역", "utility_registered_on": "가입 날짜", - "utility_remind": "{2}마다 {0}에게 {1}을(를) 상기시킵니다. `({3:d.M.yyyy.} {4:HH:mm})`", + "utility_remind": "{2}후에 {0}에게 {1}을(를) 상기시킵니다. `({3:d.M.yyyy.} {4:HH:mm})`", "utility_remind_invalid_format": "유효하지 않은 시간 포맷입니다. 명령어 목록을 확인하세요.", "utility_remind_template": "새로운 상기 템플릿이 설정되었습니다.", "utility_repeater": "{0}을(를) {1}일 {2}시간 {3}분마다 반복합니다.", @@ -672,7 +672,7 @@ "games_no_votes_cast": "진행중인 투표가 없습니다.", "games_poll_already_running": "이 서버에서 이미 투표가 진행되고있습니다.", "games_poll_created": "📃 {0}님이 투표를 생성하였습니다.", - "games_poll_result": "`{0}.` {2}표를 획득한 {1}이(가) 되었습니다.", + "games_poll_result": "`{0}.` {1}이(가) {2}표를 획득하였습니다.", "games_poll_voted": "{0}님이 투표하였습니다.", "games_poll_vote_private": "해당 답변 번호와 함께 개인 메시지를 보내세요.", "games_poll_vote_public": "해당 답변 번호와 함께 여기에 메시지를 보내세요.", From ed623dd967d7501f62a4e1de84cc1a144d6f267e Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Wed, 26 Jul 2017 18:09:19 +0200 Subject: [PATCH 227/346] Update ResponseStrings.nb-NO.json (POEditor.com) From ffd570511a4410c12f75010bf37af0008e2b884e Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Wed, 26 Jul 2017 18:09:21 +0200 Subject: [PATCH 228/346] Update ResponseStrings.pl-PL.json (POEditor.com) From a0496454bdded13712588ec0c4cc152f42a563ef Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Wed, 26 Jul 2017 18:09:24 +0200 Subject: [PATCH 229/346] Update ResponseStrings.pt-BR.json (POEditor.com) --- .../_strings/ResponseStrings.pt-BR.json | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/NadekoBot/_strings/ResponseStrings.pt-BR.json b/src/NadekoBot/_strings/ResponseStrings.pt-BR.json index 16a05bd4..fb5ee165 100644 --- a/src/NadekoBot/_strings/ResponseStrings.pt-BR.json +++ b/src/NadekoBot/_strings/ResponseStrings.pt-BR.json @@ -795,14 +795,14 @@ "administration_migration_error": "Erro ao migrar, cheque o console do bot para mais informações.", "searches_hex_invalid": "Cor inválida especificada.", "permissions_global_perms_reset": "Permissões globais foram resetadas.", - "help_module": "", - "games_hangman_stopped": "", - "music_autoplaying": "", - "music_queue_stopped": "", - "music_removed_song_error": "", - "music_shuffling_playlist": "", - "music_songs_shuffle_enable": "", - "music_songs_shuffle_disable": "", - "music_song_skips_after": "", - "administration_warnings_list": "" + "help_module": "Módulo: {0}", + "games_hangman_stopped": "Jogo da forca foi terminado.", + "music_autoplaying": "Reproduzindo automaticamente.", + "music_queue_stopped": "Player foi pausado. Use o comando {0} para começar a tocar.", + "music_removed_song_error": "Não existe uma música neste índice", + "music_shuffling_playlist": "Embaralhando músicas", + "music_songs_shuffle_enable": "Músicas serão embaralhadas a partir de agora.", + "music_songs_shuffle_disable": "Músicas não serão mais embaralhadas.", + "music_song_skips_after": "Músicas serão puladas depois de {0}", + "administration_warnings_list": "Lista de todos os usuários advertidos neste server" } \ No newline at end of file From 97b84ef4696374ccf291c46cba1e2644fe3d5f2f Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Wed, 26 Jul 2017 18:09:26 +0200 Subject: [PATCH 230/346] Update ResponseStrings.ro-RO.json (POEditor.com) From f1e59d561d07b879b965bac29c62f72fe7f52eea Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Wed, 26 Jul 2017 18:09:29 +0200 Subject: [PATCH 231/346] Update ResponseStrings.ru-RU.json (POEditor.com) --- .../_strings/ResponseStrings.ru-RU.json | 44 +++++++++++++------ 1 file changed, 31 insertions(+), 13 deletions(-) diff --git a/src/NadekoBot/_strings/ResponseStrings.ru-RU.json b/src/NadekoBot/_strings/ResponseStrings.ru-RU.json index e2d15d70..f0ec42bc 100644 --- a/src/NadekoBot/_strings/ResponseStrings.ru-RU.json +++ b/src/NadekoBot/_strings/ResponseStrings.ru-RU.json @@ -151,7 +151,6 @@ "administration_old_nick": "Старое имя", "administration_old_topic": "Старый заголовок", "administration_perms": "Ошибка. Скорее всего мне не хватает прав.", - "administration_perms_reset": "Разрешения для этого сервера были сброшены.", "administration_prot_active": "Активные защиты от рейдов", "administration_prot_disable": "{0} был **отключён** на этом сервере.", "administration_prot_enable": "{0} включен", @@ -243,7 +242,6 @@ "administration_sbdm": "Вас забанили на сервере {0}. Причина: {1}", "administration_user_unbanned": "Пользователь разбанен.", "administration_migration_done": "Перемещение закончено!", - "administration_migration_error": "Ошибка при переносе файлов, проверьте консоль бота для получения дальнейшей информации.", "administration_presence_updates": "История присутвия пользователей", "administration_sb_user": "Пользователя выгнали", "gambling_awarded": "наградил {0} пользователю {1}", @@ -285,7 +283,7 @@ "help_cmdlist_donate": "Вы можете поддержать проект в patreon: <{0}> или через paypal: <{1}>", "help_cmd_and_alias": "Команды и альтернативные имена команд", "help_commandlist_regen": "Список команд создан.", - "help_commands_instr": "Напишите '{0}h ИмяКоманды', чтобы получить справку для этой команды. Например, '{0}h >8ball'", + "help_commands_instr": "Напишите `{0}h ИмяКоманды`, чтобы получить справку для этой команды. Например, `{0}h {0}8ball`", "help_command_not_found": "Эта команда не найдена. Пожалуйста, убедитесь, что команда существует.", "help_desc": "Описание", "help_donate": "Вы можете поддержать проект NadekoBot в \nPatreon <{0}> или\nPaypal <{1}>\nНе забудьте оставить ваше имя в Discord или id в Вашем сообщении.\n\n**Спасибо** ♥️", @@ -398,7 +396,7 @@ "music_autoplay_enabled": "Автовоспроизведение включено.", "music_defvol_set": "Громкость по умолчанию выставлена на {0}%", "music_dir_queue_complete": "Папка успешно добавлена в очередь воспроизведения.", - "music_fairplay": "справедливое воспроизведение", + "music_fairplay": "Справедливое воспроизведение", "music_finished_song": "Песня завершилась.", "music_fp_disabled": "Отключено честное воспроизведение.", "music_fp_enabled": "Включено честное воспроизведение.", @@ -416,7 +414,7 @@ "music_no_search_results": "Нет результатов поиска.", "music_paused": "Проигрывание музыки приостановлено.", "music_player_queue": "Очередь воспроизведения - Страница {0}/{1}", - "music_playing_song": "Проигрывается песня", + "music_playing_song": "Воспроизводится песня #{0}", "music_playlists": "'#{0}' - **{1}** *{2}* ({3} песен)", "music_playlists_page": "Страница {0} сохранённых плейлистов.", "music_playlist_deleted": "Плейлист удалён.", @@ -439,7 +437,6 @@ "music_rpl_enabled": "Включено повторение плейлиста.", "music_set_music_channel": "Проигрываемые, завершённые, приостановленные и удалённые песни будут выводится в этом канале.", "music_skipped_to": "Пропускаю до '{0}:{1}'", - "music_songs_shuffled": "Песни перемешаны", "music_song_moved": "Песня перемещена", "music_time_format": "{0}ч {1}м {2}с", "music_to_position": "К моменту", @@ -605,9 +602,6 @@ "utility_convert_not_found": "Нельзя перевести {0} в {1}: единицы измерения не найдены", "utility_convert_type_error": "Нельзя перевести {0} в {1}: единицы измерения не эквивалентны.", "utility_created_at": "Создано", - "utility_csc_join": "Присоедился к межсерверному каналу.", - "utility_csc_leave": "Покинул межсерверный канал.", - "utility_csc_token": "Ваш CSC токен:", "utility_custom_emojis": "Серверные emoji", "utility_error": "Ошибка", "utility_features": "Признаки", @@ -626,7 +620,7 @@ "utility_messages": "Сообщения", "utility_message_repeater": "Повторяемые сообщения", "utility_name": "Имя", - "utility_nickname": "Кличка", + "utility_nickname": "Имя", "utility_nobody_playing_game": "Никто не играет в эту игру", "utility_no_active_repeaters": "Нет активных повторяемых сообщений", "utility_no_roles_on_page": "На этой странице нет ролей.", @@ -663,7 +657,7 @@ "utility_server_info": "Информация о сервере", "utility_shard": "Shard", "utility_shard_stats": "Статистика Shard-а", - "utility_shard_stats_txt": "Shard **#{0}** находится в состоянии {1} с {2} серверами.", + "utility_shard_stats_txt": "Shard **#{0}** в состоянии {1} на {2} серверах — {3} назад", "utility_showemojis": "**Имя:** {0} **Link:** {1}", "utility_showemojis_none": "Серверные emoji не найдены.", "utility_stats_songs": "Проигрывается {0} песен, {1} в очереди", @@ -754,7 +748,6 @@ "gambling_shop_role": "Вы получите роль {0}", "gambling_type": "Вид", "utility_clpa_next_update": "Следующее обновление в {0}", - "administration_global_perms_reset": "Глобальные разрешения были сброшены.", "administration_gvc_disabled": "Функция Игрового Голосового Канала отключена на этом сервере.", "administration_gvc_enabled": "{0} теперь является Игровым Голосовом Каналом.", "administration_not_in_voice": "Вы не находитесь в голосовом канале на этом сервере.", @@ -786,5 +779,30 @@ "administration_prefix_current": "Префикс для этого сервера — {0}", "administration_prefix_new": "Префикс для этого сервера изменен с {0} на {1}", "administration_defprefix_current": "Стандартный префикс для бота — {0}", - "administration_defprefix_new": "Стандартный префикс изменен с {0} на {1}" + "administration_defprefix_new": "Стандартный префикс изменен с {0} на {1}", + "administration_bot_nick": "Имя бота было изменено на {0}", + "administration_user_nick": "Имя пользователя {0} изменено на {1}", + "administration_timezone_guild": "Часовой пояс для этого сервера — '{0}'", + "administration_timezone_not_found": "Часовой пояс не найден. Используйте команду \"timezones\", чтобы просмотреть список доступных часовых поясов.", + "administration_timezones_available": "Доступные часовые пояса", + "music_song_not_found": "Песен не найдено.", + "searches_define_unknown": "Не могу найти определение для этого термина.", + "utility_repeater_initial": "Первоначальное повторяющееся сообщение будет отправлено через {0} часов {1} минут.", + "utility_verbose_errors_enabled": "Неверно введенные команды теперь будут выводить ошибки.", + "utility_verbose_errors_disabled": "Неверно введенные команды больше не будут выводить ошибки.", + "permissions_perms_reset": "Разрешения для этого сервера были сброшены.", + "permissions_trigger": "Разрешение #{0} {1} запрещает это действие.", + "administration_migration_error": "Ошибка при переносе данных, проверьте консоль для получения дополнительной информации.", + "searches_hex_invalid": "Задан неверный цвет.", + "permissions_global_perms_reset": "Глобальные разрешения были сброшены.", + "help_module": "Модуль: {0}", + "games_hangman_stopped": "Игра в Виселицу остановлена.", + "music_autoplaying": "Автовоспроизведение.", + "music_queue_stopped": "Воспроизведение приостановлено. Используйте команду {0}, чтобы начать воспроизведение.", + "music_removed_song_error": "Песни по этому индексу не существует", + "music_shuffling_playlist": "Воспроизвожу песни в случайном порядке", + "music_songs_shuffle_enable": "Песни будут воспроизводиться в случайном порядке.", + "music_songs_shuffle_disable": "Песни больше не будут воспроизводиться в случайном порядке.", + "music_song_skips_after": "Песни будут пропущены после {0}", + "administration_warnings_list": "Список всех предупреждённых пользователей на сервере" } \ No newline at end of file From bf7585cd83bc474945a384a042a1d651bcc95647 Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Wed, 26 Jul 2017 18:19:39 +0200 Subject: [PATCH 232/346] slight cleanup --- .../Modules/Utility/Common/RepeatRunner.cs | 9 +++------ src/NadekoBot/Modules/Utility/RepeatCommands.cs | 4 ++-- .../Utility/Services/MessageRepeaterService.cs | 16 +++++++++++----- 3 files changed, 16 insertions(+), 13 deletions(-) diff --git a/src/NadekoBot/Modules/Utility/Common/RepeatRunner.cs b/src/NadekoBot/Modules/Utility/Common/RepeatRunner.cs index b2633505..bf21d317 100644 --- a/src/NadekoBot/Modules/Utility/Common/RepeatRunner.cs +++ b/src/NadekoBot/Modules/Utility/Common/RepeatRunner.cs @@ -22,18 +22,15 @@ namespace NadekoBot.Modules.Utility.Common private IUserMessage oldMsg = null; private Timer _t; - public RepeatRunner(DiscordSocketClient client, Repeater repeater) + public RepeatRunner(DiscordSocketClient client, SocketGuild guild, Repeater repeater) { _log = LogManager.GetCurrentClassLogger(); Repeater = repeater; - - //todo 40 @.@ fix all of this - Guild = client.GetGuild(repeater.GuildId); + Guild = guild; InitialInterval = Repeater.Interval; - if (Guild != null) - Run(); + Run(); } private void Run() diff --git a/src/NadekoBot/Modules/Utility/RepeatCommands.cs b/src/NadekoBot/Modules/Utility/RepeatCommands.cs index 68489a87..921c0746 100644 --- a/src/NadekoBot/Modules/Utility/RepeatCommands.cs +++ b/src/NadekoBot/Modules/Utility/RepeatCommands.cs @@ -131,7 +131,7 @@ namespace NadekoBot.Modules.Utility await uow.CompleteAsync().ConfigureAwait(false); } - var rep = new RepeatRunner(_client, toAdd); + var rep = new RepeatRunner(_client, (SocketGuild)Context.Guild, toAdd); _service.Repeaters.AddOrUpdate(Context.Guild.Id, new ConcurrentQueue(new[] {rep}), (key, old) => { @@ -179,7 +179,7 @@ namespace NadekoBot.Modules.Utility await uow.CompleteAsync().ConfigureAwait(false); } - var rep = new RepeatRunner(_client, toAdd); + var rep = new RepeatRunner(_client, (SocketGuild)Context.Guild, toAdd); _service.Repeaters.AddOrUpdate(Context.Guild.Id, new ConcurrentQueue(new[] { rep }), (key, old) => { diff --git a/src/NadekoBot/Modules/Utility/Services/MessageRepeaterService.cs b/src/NadekoBot/Modules/Utility/Services/MessageRepeaterService.cs index 0df59c76..4e937eb4 100644 --- a/src/NadekoBot/Modules/Utility/Services/MessageRepeaterService.cs +++ b/src/NadekoBot/Modules/Utility/Services/MessageRepeaterService.cs @@ -9,7 +9,6 @@ using NadekoBot.Services.Database.Models; namespace NadekoBot.Modules.Utility.Services { - //todo 50 rewrite public class MessageRepeaterService : INService { //messagerepeater @@ -24,10 +23,17 @@ namespace NadekoBot.Modules.Utility.Services await bot.Ready.Task.ConfigureAwait(false); Repeaters = new ConcurrentDictionary>(gcs - .ToDictionary(gc => gc.GuildId, - gc => new ConcurrentQueue(gc.GuildRepeaters - .Select(gr => new RepeatRunner(client, gr)) - .Where(x => x.Guild != null)))); + .Select(gc => + { + var guild = client.GetGuild(gc.GuildId); + if (guild == null) + return (0, null); + return (gc.GuildId, new ConcurrentQueue(gc.GuildRepeaters + .Select(gr => new RepeatRunner(client, guild, gr)) + .Where(x => x.Guild != null))); + }) + .Where(x => x.Item2 != null) + .ToDictionary(x => x.Item1, x => x.Item2)); RepeaterReady = true; }); } From fece28b66b4bb591d0191614bc9215f40889194d Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Thu, 27 Jul 2017 18:43:15 +0200 Subject: [PATCH 233/346] Acrophobia completely rewritten. Works the same as before, only much more maintainable. It won't repost itself after 10 messages anymore though. --- .../Modules/Games/AcropobiaCommands.cs | 427 ++++++++---------- .../Games/Common/Acrophobia/Acrophobia.cs | 177 ++++++++ .../Games/Common/Acrophobia/AcrophobiaUser.cs | 28 ++ .../Modules/Games/PlantAndPickCommands.cs | 3 +- 4 files changed, 400 insertions(+), 235 deletions(-) create mode 100644 src/NadekoBot/Modules/Games/Common/Acrophobia/Acrophobia.cs create mode 100644 src/NadekoBot/Modules/Games/Common/Acrophobia/AcrophobiaUser.cs diff --git a/src/NadekoBot/Modules/Games/AcropobiaCommands.cs b/src/NadekoBot/Modules/Games/AcropobiaCommands.cs index e66578fa..198666ad 100644 --- a/src/NadekoBot/Modules/Games/AcropobiaCommands.cs +++ b/src/NadekoBot/Modules/Games/AcropobiaCommands.cs @@ -14,6 +14,7 @@ using NadekoBot.Common; using NadekoBot.Common.Attributes; using NadekoBot.Common.Collections; using NadekoBot.Services.Impl; +using NadekoBot.Modules.Games.Common.Acrophobia; namespace NadekoBot.Modules.Games { @@ -25,7 +26,7 @@ namespace NadekoBot.Modules.Games private readonly DiscordSocketClient _client; //channelId, game - public static ConcurrentDictionary AcrophobiaGames { get; } = new ConcurrentDictionary(); + public static ConcurrentDictionary AcrophobiaGames { get; } = new ConcurrentDictionary(); public AcropobiaCommands(DiscordSocketClient client) { @@ -34,291 +35,251 @@ namespace NadekoBot.Modules.Games [NadekoCommand, Usage, Description, Aliases] [RequireContext(ContextType.Guild)] - public async Task Acro(int time = 60) + public async Task Acro(int submissionTime = 30) { - if (time < 10 || time > 120) + if (submissionTime < 10 || submissionTime > 120) return; var channel = (ITextChannel)Context.Channel; - var game = new AcrophobiaGame(_client, _strings, channel, time); + var game = new Acrophobia(submissionTime); if (AcrophobiaGames.TryAdd(channel.Id, game)) { try { - await game.Run(); + game.OnStarted += Game_OnStarted; + game.OnEnded += Game_OnEnded; + game.OnVotingStarted += Game_OnVotingStarted; + game.OnUserVoted += Game_OnUserVoted; + _client.MessageReceived += _client_MessageReceived; + await game.Run().ConfigureAwait(false); } finally { - game.EnsureStopped(); + _client.MessageReceived -= _client_MessageReceived; AcrophobiaGames.TryRemove(channel.Id, out game); + game.Dispose(); } } else { await ReplyErrorLocalized("acro_running").ConfigureAwait(false); } - } - } - public enum AcroPhase - { - Submitting, - Idle, // used to wait for some other actions while transitioning through phases - Voting - } - - //todo 85 Isolate, this shouldn't print or anything like that. - public class AcrophobiaGame - { - private readonly ITextChannel _channel; - private readonly int _time; - private readonly NadekoRandom _rng; - private readonly ImmutableArray _startingLetters; - private readonly CancellationTokenSource _source; - private AcroPhase phase { get; set; } = AcroPhase.Submitting; - - private readonly ConcurrentDictionary _submissions = new ConcurrentDictionary(); - public IReadOnlyDictionary Submissions => _submissions; - - private readonly ConcurrentHashSet _usersWhoSubmitted = new ConcurrentHashSet(); - private readonly ConcurrentHashSet _usersWhoVoted = new ConcurrentHashSet(); - - private int _spamCount; - - //text, votes - private readonly ConcurrentDictionary _votes = new ConcurrentDictionary(); - private readonly Logger _log; - private readonly DiscordSocketClient _client; - private readonly NadekoStrings _strings; - - public AcrophobiaGame(DiscordSocketClient client, NadekoStrings strings, ITextChannel channel, int time) - { - _log = LogManager.GetCurrentClassLogger(); - _client = client; - _strings = strings; - - _channel = channel; - _time = time; - _source = new CancellationTokenSource(); - - _rng = new NadekoRandom(); - var wordCount = _rng.Next(3, 6); - - var lettersArr = new char[wordCount]; - - for (int i = 0; i < wordCount; i++) + Task _client_MessageReceived(SocketMessage msg) { - var randChar = (char)_rng.Next(65, 91); - lettersArr[i] = randChar == 'X' ? (char)_rng.Next(65, 88) : randChar; + if (msg.Channel.Id != Context.Channel.Id) + return Task.CompletedTask; + + var _ = Task.Run(async () => + { + try + { + var success = await game.UserInput(msg.Author.Id, msg.Author.ToString(), msg.Content) + .ConfigureAwait(false); + if (success) + await msg.DeleteAsync().ConfigureAwait(false); + } + catch { } + }); + + return Task.CompletedTask; } - _startingLetters = lettersArr.ToImmutableArray(); } - private EmbedBuilder GetEmbed() + private Task Game_OnStarted(Acrophobia game) { - var i = 0; - return phase == AcroPhase.Submitting - - ? new EmbedBuilder().WithOkColor() + var embed = new EmbedBuilder().WithOkColor() .WithTitle(GetText("acrophobia")) - .WithDescription(GetText("acro_started", Format.Bold(string.Join(".", _startingLetters)))) - .WithFooter(efb => efb.WithText(GetText("acro_started_footer", _time))) + .WithDescription(GetText("acro_started", Format.Bold(string.Join(".", game.StartingLetters)))) + .WithFooter(efb => efb.WithText(GetText("acro_started_footer", game.SubmissionPhaseLength))); - : new EmbedBuilder() - .WithOkColor() - .WithTitle(GetText("acrophobia") + " - " + GetText("submissions_closed")) - .WithDescription(GetText("acro_nym_was", Format.Bold(string.Join(".", _startingLetters)) + "\n" + -$@"-- -{_submissions.Aggregate("",(agg, cur) => agg + $"`{++i}.` **{cur.Key.ToLowerInvariant().ToTitleCase()}**\n")} ---")) - .WithFooter(efb => efb.WithText(GetText("acro_vote"))); + return Context.Channel.EmbedAsync(embed); } - public async Task Run() + private Task Game_OnUserVoted(string user) { - _client.MessageReceived += PotentialAcro; - var embed = GetEmbed(); + return Context.Channel.SendConfirmAsync( + GetText("acrophobia"), + GetText("acro_vote_cast", Format.Bold(user))); + } - //SUBMISSIONS PHASE - await _channel.EmbedAsync(embed).ConfigureAwait(false); - try - { - await Task.Delay(_time * 1000, _source.Token).ConfigureAwait(false); - phase = AcroPhase.Idle; - } - catch (OperationCanceledException) + private async Task Game_OnVotingStarted(Acrophobia game, ImmutableArray> submissions) + { + if (submissions.Length == 0) { + await Context.Channel.SendErrorAsync(GetText("acrophobia"), GetText("acro_ended_no_sub")); return; } - - //var i = 0; - if (_submissions.Count == 0) + if (submissions.Length == 1) { - await _channel.SendErrorAsync(GetText("acrophobia"), GetText("acro_ended_no_sub")); - return; - } - if (_submissions.Count == 1) - { - await _channel.EmbedAsync(new EmbedBuilder().WithOkColor() + await Context.Channel.EmbedAsync(new EmbedBuilder().WithOkColor() .WithDescription( GetText("acro_winner_only", - Format.Bold(_submissions.First().Value.ToString()))) - .WithFooter(efb => efb.WithText(_submissions.First().Key.ToLowerInvariant().ToTitleCase()))) + Format.Bold(submissions.First().Key.UserName))) + .WithFooter(efb => efb.WithText(submissions.First().Key.Input))) .ConfigureAwait(false); return; } - var submissionClosedEmbed = GetEmbed(); - await _channel.EmbedAsync(submissionClosedEmbed).ConfigureAwait(false); - //VOTING PHASE - phase = AcroPhase.Voting; - try - { - //30 secondds for voting - await Task.Delay(30000, _source.Token).ConfigureAwait(false); - phase = AcroPhase.Idle; - } - catch (OperationCanceledException) - { - return; - } - await End().ConfigureAwait(false); + var i = 0; + var embed = new EmbedBuilder() + .WithOkColor() + .WithTitle(GetText("acrophobia") + " - " + GetText("submissions_closed")) + .WithDescription(GetText("acro_nym_was", Format.Bold(string.Join(".", game.StartingLetters)) + "\n" + +$@"-- +{submissions.Aggregate("", (agg, cur) => agg + $"`{++i}.` **{cur.Key.Input}**\n")} +--")) + .WithFooter(efb => efb.WithText(GetText("acro_vote"))); + + await Context.Channel.EmbedAsync(embed).ConfigureAwait(false); } - private Task PotentialAcro(SocketMessage arg) + private async Task Game_OnEnded(Acrophobia game, ImmutableArray> votes) { - var _ = Task.Run(async () => + if (!votes.Any() || votes.All(x => x.Value == 0)) { - try - { - var msg = arg as SocketUserMessage; - if (msg == null || msg.Author.IsBot || msg.Channel.Id != _channel.Id) - return; - - ++_spamCount; - - var guildUser = (IGuildUser)msg.Author; - - var input = msg.Content.ToUpperInvariant().Trim(); - - if (phase == AcroPhase.Submitting) - { - if (_spamCount > 10) - { - _spamCount = 0; - try { await _channel.EmbedAsync(GetEmbed()).ConfigureAwait(false); } - catch { } - } - var inputWords = input.Split(' '); //get all words - - if (inputWords.Length != _startingLetters.Length) // number of words must be the same as the number of the starting letters - return; - - for (int i = 0; i < _startingLetters.Length; i++) - { - var letter = _startingLetters[i]; - - if (!inputWords[i].StartsWith(letter.ToString())) // all first letters must match - return; - } - - - if (!_usersWhoSubmitted.Add(guildUser.Id)) - return; - //try adding it to the list of answers - if (!_submissions.TryAdd(input, guildUser)) - { - _usersWhoSubmitted.TryRemove(guildUser.Id); - return; - } - - // all good. valid input. answer recorded - await _channel.SendConfirmAsync(GetText("acrophobia"), - GetText("acro_submit", guildUser.Mention, - _submissions.Count)); - try - { - await msg.DeleteAsync(); - } - catch - { - await msg.DeleteAsync(); //try twice - } - } - else if (phase == AcroPhase.Voting) - { - if (_spamCount > 10) - { - _spamCount = 0; - try { await _channel.EmbedAsync(GetEmbed()).ConfigureAwait(false); } - catch { } - } - - //if (submissions.TryGetValue(input, out usr) && usr.Id != guildUser.Id) - //{ - // if (!usersWhoVoted.Add(guildUser.Id)) - // return; - // votes.AddOrUpdate(input, 1, (key, old) => ++old); - // await channel.SendConfirmAsync("Acrophobia", $"{guildUser.Mention} cast their vote!").ConfigureAwait(false); - // await msg.DeleteAsync().ConfigureAwait(false); - // return; - //} - - int num; - if (int.TryParse(input, out num) && num > 0 && num <= _submissions.Count) - { - var kvp = _submissions.Skip(num - 1).First(); - var usr = kvp.Value; - //can't vote for yourself, can't vote multiple times - if (usr.Id == guildUser.Id || !_usersWhoVoted.Add(guildUser.Id)) - return; - _votes.AddOrUpdate(kvp.Key, 1, (key, old) => ++old); - await _channel.SendConfirmAsync(GetText("acrophobia"), - GetText("acro_vote_cast", Format.Bold(guildUser.ToString()))).ConfigureAwait(false); - await msg.DeleteAsync().ConfigureAwait(false); - } - - } - } - catch (Exception ex) - { - _log.Warn(ex); - } - }); - return Task.CompletedTask; - } - - public async Task End() - { - if (!_votes.Any()) - { - await _channel.SendErrorAsync(GetText("acrophobia"), GetText("acro_no_votes_cast")).ConfigureAwait(false); + await Context.Channel.SendErrorAsync(GetText("acrophobia"), GetText("acro_no_votes_cast")).ConfigureAwait(false); return; } - var table = _votes.OrderByDescending(v => v.Value); + var table = votes.OrderByDescending(v => v.Value); var winner = table.First(); var embed = new EmbedBuilder().WithOkColor() .WithTitle(GetText("acrophobia")) - .WithDescription(GetText("acro_winner", Format.Bold(_submissions[winner.Key].ToString()), + .WithDescription(GetText("acro_winner", Format.Bold(winner.Key.UserName), Format.Bold(winner.Value.ToString()))) - .WithFooter(efb => efb.WithText(winner.Key.ToLowerInvariant().ToTitleCase())); + .WithFooter(efb => efb.WithText(winner.Key.Input)); - await _channel.EmbedAsync(embed).ConfigureAwait(false); + await Context.Channel.EmbedAsync(embed).ConfigureAwait(false); } - - public void EnsureStopped() - { - _client.MessageReceived -= PotentialAcro; - if (!_source.IsCancellationRequested) - _source.Cancel(); - } - - private string GetText(string key, params object[] replacements) - => _strings.GetText(key, - _channel.Guild.Id, - typeof(Games).Name.ToLowerInvariant(), - replacements); } + + //public enum AcroPhase + //{ + // Submitting, + // Idle, // used to wait for some other actions while transitioning through phases + // Voting + //} + + ////todo 85 Isolate, this shouldn't print or anything like that. + //public class OldAcrophobiaGame + //{ + // private readonly ITextChannel _channel; + // private readonly int _time; + // private readonly NadekoRandom _rng; + // private readonly ImmutableArray _startingLetters; + // private readonly CancellationTokenSource _source; + // private AcroPhase phase { get; set; } = AcroPhase.Submitting; + + // private readonly ConcurrentDictionary _submissions = new ConcurrentDictionary(); + // public IReadOnlyDictionary Submissions => _submissions; + + // private readonly ConcurrentHashSet _usersWhoSubmitted = new ConcurrentHashSet(); + // private readonly ConcurrentHashSet _usersWhoVoted = new ConcurrentHashSet(); + + // private int _spamCount; + + // //text, votes + // private readonly ConcurrentDictionary _votes = new ConcurrentDictionary(); + // private readonly Logger _log; + // private readonly DiscordSocketClient _client; + // private readonly NadekoStrings _strings; + + // public OldAcrophobiaGame(DiscordSocketClient client, NadekoStrings strings, ITextChannel channel, int time) + // { + // _log = LogManager.GetCurrentClassLogger(); + // _client = client; + // _strings = strings; + + // _channel = channel; + // _time = time; + // _source = new CancellationTokenSource(); + + // _rng = new NadekoRandom(); + // var wordCount = _rng.Next(3, 6); + + // var lettersArr = new char[wordCount]; + + // for (int i = 0; i < wordCount; i++) + // { + // var randChar = (char)_rng.Next(65, 91); + // lettersArr[i] = randChar == 'X' ? (char)_rng.Next(65, 88) : randChar; + // } + // _startingLetters = lettersArr.ToImmutableArray(); + // } + + // private Task PotentialAcro(SocketMessage arg) + // { + // var _ = Task.Run(async () => + // { + // try + // { + // var msg = arg as SocketUserMessage; + // if (msg == null || msg.Author.IsBot || msg.Channel.Id != _channel.Id) + // return; + + // ++_spamCount; + + // var guildUser = (IGuildUser)msg.Author; + + // var input = msg.Content.ToUpperInvariant().Trim(); + + // if (phase == AcroPhase.Submitting) + // { + // // all good. valid input. answer recorded + // await _channel.SendConfirmAsync(GetText("acrophobia"), + // GetText("acro_submit", guildUser.Mention, + // _submissions.Count)); + // try + // { + // await msg.DeleteAsync(); + // } + // catch + // { + // await msg.DeleteAsync(); //try twice + // } + // } + // else if (phase == AcroPhase.Voting) + // { + // if (_spamCount > 10) + // { + // _spamCount = 0; + // try { await _channel.EmbedAsync(GetEmbed()).ConfigureAwait(false); } + // catch { } + // } + + // //if (submissions.TryGetValue(input, out usr) && usr.Id != guildUser.Id) + // //{ + // // if (!usersWhoVoted.Add(guildUser.Id)) + // // return; + // // votes.AddOrUpdate(input, 1, (key, old) => ++old); + // // await channel.SendConfirmAsync("Acrophobia", $"{guildUser.Mention} cast their vote!").ConfigureAwait(false); + // // await msg.DeleteAsync().ConfigureAwait(false); + // // return; + // //} + + // int num; + // if (int.TryParse(input, out num) && num > 0 && num <= _submissions.Count) + // { + // var kvp = _submissions.Skip(num - 1).First(); + // var usr = kvp.Value; + // //can't vote for yourself, can't vote multiple times + // if (usr.Id == guildUser.Id || !_usersWhoVoted.Add(guildUser.Id)) + // return; + // _votes.AddOrUpdate(kvp.Key, 1, (key, old) => ++old); + + // } + + // } + // } + // catch (Exception ex) + // { + // _log.Warn(ex); + // } + // }); + // return Task.CompletedTask; + // } + + //} } } \ No newline at end of file diff --git a/src/NadekoBot/Modules/Games/Common/Acrophobia/Acrophobia.cs b/src/NadekoBot/Modules/Games/Common/Acrophobia/Acrophobia.cs new file mode 100644 index 00000000..daeaf8a6 --- /dev/null +++ b/src/NadekoBot/Modules/Games/Common/Acrophobia/Acrophobia.cs @@ -0,0 +1,177 @@ +using NadekoBot.Common; +using NadekoBot.Extensions; +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace NadekoBot.Modules.Games.Common.Acrophobia +{ + /// + /// Platform-agnostic acrophobia game + /// + public class Acrophobia : IDisposable + { + private const int VotingPhaseLength = 30; + + public enum Phase + { + Submission, + Voting, + Ended + } + + public enum UserInputResult + { + Submitted, + SubmissionFailed, + Voted, + VotingFailed, + Failed + } + + public int SubmissionPhaseLength { get; } + + public Phase CurrentPhase { get; private set; } = Phase.Submission; + public ImmutableArray StartingLetters { get; private set; } + + private readonly Dictionary submissions = new Dictionary(); + private readonly SemaphoreSlim locker = new SemaphoreSlim(1, 1); + private readonly NadekoRandom _rng; + + public event Func OnStarted = delegate { return Task.CompletedTask; }; + public event Func>, Task> OnVotingStarted = delegate { return Task.CompletedTask; }; + public event Func OnUserVoted = delegate { return Task.CompletedTask; }; + public event Func>, Task> OnEnded = delegate { return Task.CompletedTask; }; + + private readonly HashSet _usersWhoVoted = new HashSet(); + + public Acrophobia(int submissionPhaseLength = 30) + { + _rng = new NadekoRandom(); + SubmissionPhaseLength = submissionPhaseLength; + InitializeStartingLetters(); + } + + public async Task Run() + { + await OnStarted(this).ConfigureAwait(false); + await Task.Delay(SubmissionPhaseLength * 1000); + await locker.WaitAsync().ConfigureAwait(false); + try + { + if (submissions.Count == 0) + { + CurrentPhase = Phase.Ended; + await OnVotingStarted(this, ImmutableArray.Create>()).ConfigureAwait(false); + return; + } + if (submissions.Count == 1) + { + CurrentPhase = Phase.Ended; + await OnVotingStarted(this, submissions.ToArray().ToImmutableArray()).ConfigureAwait(false); + return; + } + + CurrentPhase = Phase.Voting; + + await OnVotingStarted(this, submissions.ToArray().ToImmutableArray()).ConfigureAwait(false); + } + finally { locker.Release(); } + + await Task.Delay(VotingPhaseLength * 1000); + await locker.WaitAsync().ConfigureAwait(false); + try + { + CurrentPhase = Phase.Ended; + await OnEnded(this, submissions.ToArray().ToImmutableArray()).ConfigureAwait(false) ; + } + finally { locker.Release(); } + } + + private void InitializeStartingLetters() + { + var wordCount = _rng.Next(3, 6); + + var lettersArr = new char[wordCount]; + + for (int i = 0; i < wordCount; i++) + { + var randChar = (char)_rng.Next(65, 91); + lettersArr[i] = randChar == 'X' ? (char)_rng.Next(65, 88) : randChar; + } + StartingLetters = lettersArr.ToImmutableArray(); + } + + public async Task UserInput(ulong userId, string userName, string input) + { + var user = new AcrophobiaUser(userId, userName, input.ToLowerInvariant().ToTitleCase()); + + await locker.WaitAsync(); + try + { + switch (CurrentPhase) + { + case Phase.Submission: + if (submissions.ContainsKey(user) || !IsValidAnswer(input)) + break; + + submissions.Add(user, 0); + return true; + case Phase.Voting: + AcrophobiaUser toVoteFor; + if (!int.TryParse(input, out var index) + || --index < 0 + || index >= submissions.Count + || (toVoteFor = submissions.ToArray()[index].Key).UserId == user.UserId + || !_usersWhoVoted.Add(userId)) + break; + ++submissions[toVoteFor]; + var _ = Task.Run(() => OnUserVoted(userName)); + return true; + default: + break; + } + return false; + } + finally + { + locker.Release(); + } + } + + private bool IsValidAnswer(string input) + { + input = input.ToUpperInvariant(); + + var inputWords = input.Split(' '); + + if (inputWords.Length != StartingLetters.Length) // number of words must be the same as the number of the starting letters + return false; + + for (int i = 0; i < StartingLetters.Length; i++) + { + var letter = StartingLetters[i]; + + if (!inputWords[i].StartsWith(letter.ToString())) // all first letters must match + return false; + } + + return true; + } + + public void Dispose() + { + this.CurrentPhase = Phase.Ended; + OnStarted = null; + OnEnded = null; + OnUserVoted = null; + OnVotingStarted = null; + _usersWhoVoted.Clear(); + submissions.Clear(); + locker.Dispose(); + } + } +} diff --git a/src/NadekoBot/Modules/Games/Common/Acrophobia/AcrophobiaUser.cs b/src/NadekoBot/Modules/Games/Common/Acrophobia/AcrophobiaUser.cs new file mode 100644 index 00000000..8801e700 --- /dev/null +++ b/src/NadekoBot/Modules/Games/Common/Acrophobia/AcrophobiaUser.cs @@ -0,0 +1,28 @@ +namespace NadekoBot.Modules.Games.Common.Acrophobia +{ + public class AcrophobiaUser + { + public string UserName { get; } + public ulong UserId { get; } + public string Input { get; } + + public AcrophobiaUser(ulong userId, string userName, string input) + { + this.UserName = userName; + this.UserId = userId; + this.Input = input; + } + + public override int GetHashCode() + { + return UserId.GetHashCode(); + } + + public override bool Equals(object obj) + { + return obj is AcrophobiaUser x + ? x.UserId == this.UserId + : false; + } + } +} diff --git a/src/NadekoBot/Modules/Games/PlantAndPickCommands.cs b/src/NadekoBot/Modules/Games/PlantAndPickCommands.cs index 1ebb7357..3bbe2c79 100644 --- a/src/NadekoBot/Modules/Games/PlantAndPickCommands.cs +++ b/src/NadekoBot/Modules/Games/PlantAndPickCommands.cs @@ -75,8 +75,7 @@ namespace NadekoBot.Modules.Games } var imgData = _games.GetRandomCurrencyImage(); - - //todo 81 upload all currency images to transfer.sh and use that one as cdn + var msgToSend = GetText("planted", Format.Bold(Context.User.ToString()), amount + _bc.BotConfig.CurrencySign, From 53661b3337503781d7730e25e2815f0f0a40b076 Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Thu, 27 Jul 2017 18:44:45 +0200 Subject: [PATCH 234/346] Removed old stuff --- .../Modules/Games/AcropobiaCommands.cs | 128 ------------------ 1 file changed, 128 deletions(-) diff --git a/src/NadekoBot/Modules/Games/AcropobiaCommands.cs b/src/NadekoBot/Modules/Games/AcropobiaCommands.cs index 198666ad..c9deb585 100644 --- a/src/NadekoBot/Modules/Games/AcropobiaCommands.cs +++ b/src/NadekoBot/Modules/Games/AcropobiaCommands.cs @@ -153,133 +153,5 @@ $@"-- await Context.Channel.EmbedAsync(embed).ConfigureAwait(false); } } - - //public enum AcroPhase - //{ - // Submitting, - // Idle, // used to wait for some other actions while transitioning through phases - // Voting - //} - - ////todo 85 Isolate, this shouldn't print or anything like that. - //public class OldAcrophobiaGame - //{ - // private readonly ITextChannel _channel; - // private readonly int _time; - // private readonly NadekoRandom _rng; - // private readonly ImmutableArray _startingLetters; - // private readonly CancellationTokenSource _source; - // private AcroPhase phase { get; set; } = AcroPhase.Submitting; - - // private readonly ConcurrentDictionary _submissions = new ConcurrentDictionary(); - // public IReadOnlyDictionary Submissions => _submissions; - - // private readonly ConcurrentHashSet _usersWhoSubmitted = new ConcurrentHashSet(); - // private readonly ConcurrentHashSet _usersWhoVoted = new ConcurrentHashSet(); - - // private int _spamCount; - - // //text, votes - // private readonly ConcurrentDictionary _votes = new ConcurrentDictionary(); - // private readonly Logger _log; - // private readonly DiscordSocketClient _client; - // private readonly NadekoStrings _strings; - - // public OldAcrophobiaGame(DiscordSocketClient client, NadekoStrings strings, ITextChannel channel, int time) - // { - // _log = LogManager.GetCurrentClassLogger(); - // _client = client; - // _strings = strings; - - // _channel = channel; - // _time = time; - // _source = new CancellationTokenSource(); - - // _rng = new NadekoRandom(); - // var wordCount = _rng.Next(3, 6); - - // var lettersArr = new char[wordCount]; - - // for (int i = 0; i < wordCount; i++) - // { - // var randChar = (char)_rng.Next(65, 91); - // lettersArr[i] = randChar == 'X' ? (char)_rng.Next(65, 88) : randChar; - // } - // _startingLetters = lettersArr.ToImmutableArray(); - // } - - // private Task PotentialAcro(SocketMessage arg) - // { - // var _ = Task.Run(async () => - // { - // try - // { - // var msg = arg as SocketUserMessage; - // if (msg == null || msg.Author.IsBot || msg.Channel.Id != _channel.Id) - // return; - - // ++_spamCount; - - // var guildUser = (IGuildUser)msg.Author; - - // var input = msg.Content.ToUpperInvariant().Trim(); - - // if (phase == AcroPhase.Submitting) - // { - // // all good. valid input. answer recorded - // await _channel.SendConfirmAsync(GetText("acrophobia"), - // GetText("acro_submit", guildUser.Mention, - // _submissions.Count)); - // try - // { - // await msg.DeleteAsync(); - // } - // catch - // { - // await msg.DeleteAsync(); //try twice - // } - // } - // else if (phase == AcroPhase.Voting) - // { - // if (_spamCount > 10) - // { - // _spamCount = 0; - // try { await _channel.EmbedAsync(GetEmbed()).ConfigureAwait(false); } - // catch { } - // } - - // //if (submissions.TryGetValue(input, out usr) && usr.Id != guildUser.Id) - // //{ - // // if (!usersWhoVoted.Add(guildUser.Id)) - // // return; - // // votes.AddOrUpdate(input, 1, (key, old) => ++old); - // // await channel.SendConfirmAsync("Acrophobia", $"{guildUser.Mention} cast their vote!").ConfigureAwait(false); - // // await msg.DeleteAsync().ConfigureAwait(false); - // // return; - // //} - - // int num; - // if (int.TryParse(input, out num) && num > 0 && num <= _submissions.Count) - // { - // var kvp = _submissions.Skip(num - 1).First(); - // var usr = kvp.Value; - // //can't vote for yourself, can't vote multiple times - // if (usr.Id == guildUser.Id || !_usersWhoVoted.Add(guildUser.Id)) - // return; - // _votes.AddOrUpdate(kvp.Key, 1, (key, old) => ++old); - - // } - - // } - // } - // catch (Exception ex) - // { - // _log.Warn(ex); - // } - // }); - // return Task.CompletedTask; - // } - - //} } } \ No newline at end of file From d5978a0d66d0af779b84dac2fc6cbf8263953e60 Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Fri, 28 Jul 2017 12:28:08 +0200 Subject: [PATCH 235/346] .hangman completely rewritten. Should work almost the same, with some minor improvements (such as showing the category, and you can now guess the whole word at once) --- .../Modules/Games/AcropobiaCommands.cs | 6 - .../Exceptions/TermNotFoundException.cs | 11 + .../Modules/Games/Common/Hangman/Hangman.cs | 169 ++++++++++++++ .../Games/Common/Hangman/HangmanGame.cs | 214 ------------------ .../{IHangmanObject.cs => HangmanObject.cs} | 0 .../Modules/Games/Common/Hangman/Phase.cs | 14 ++ .../Modules/Games/Common/Hangman/TermPool.cs | 52 +++++ .../Modules/Games/Common/Hangman/TermType.cs | 18 ++ .../Modules/Games/HangmanCommands.cs | 91 ++++++-- 9 files changed, 339 insertions(+), 236 deletions(-) create mode 100644 src/NadekoBot/Modules/Games/Common/Hangman/Exceptions/TermNotFoundException.cs create mode 100644 src/NadekoBot/Modules/Games/Common/Hangman/Hangman.cs delete mode 100644 src/NadekoBot/Modules/Games/Common/Hangman/HangmanGame.cs rename src/NadekoBot/Modules/Games/Common/Hangman/{IHangmanObject.cs => HangmanObject.cs} (100%) create mode 100644 src/NadekoBot/Modules/Games/Common/Hangman/Phase.cs create mode 100644 src/NadekoBot/Modules/Games/Common/Hangman/TermPool.cs create mode 100644 src/NadekoBot/Modules/Games/Common/Hangman/TermType.cs diff --git a/src/NadekoBot/Modules/Games/AcropobiaCommands.cs b/src/NadekoBot/Modules/Games/AcropobiaCommands.cs index c9deb585..f933d3bf 100644 --- a/src/NadekoBot/Modules/Games/AcropobiaCommands.cs +++ b/src/NadekoBot/Modules/Games/AcropobiaCommands.cs @@ -2,18 +2,12 @@ using Discord.Commands; using Discord.WebSocket; using NadekoBot.Extensions; -using NLog; -using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; -using System.Threading; using System.Threading.Tasks; -using NadekoBot.Common; using NadekoBot.Common.Attributes; -using NadekoBot.Common.Collections; -using NadekoBot.Services.Impl; using NadekoBot.Modules.Games.Common.Acrophobia; namespace NadekoBot.Modules.Games diff --git a/src/NadekoBot/Modules/Games/Common/Hangman/Exceptions/TermNotFoundException.cs b/src/NadekoBot/Modules/Games/Common/Hangman/Exceptions/TermNotFoundException.cs new file mode 100644 index 00000000..01573bf1 --- /dev/null +++ b/src/NadekoBot/Modules/Games/Common/Hangman/Exceptions/TermNotFoundException.cs @@ -0,0 +1,11 @@ +using System; + +namespace NadekoBot.Modules.Games.Common.Hangman.Exceptions +{ + public class TermNotFoundException : Exception + { + public TermNotFoundException() : base("Term of that type couldn't be found") + { + } + } +} diff --git a/src/NadekoBot/Modules/Games/Common/Hangman/Hangman.cs b/src/NadekoBot/Modules/Games/Common/Hangman/Hangman.cs new file mode 100644 index 00000000..94f14835 --- /dev/null +++ b/src/NadekoBot/Modules/Games/Common/Hangman/Hangman.cs @@ -0,0 +1,169 @@ +using NadekoBot.Extensions; +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace NadekoBot.Modules.Games.Common.Hangman +{ + public class Hangman : IDisposable + { + public string TermType { get; } + public HangmanObject Term { get; } + + public string ScrambledWord => "`" + String.Concat(Term.Word.Select(c => + { + if (c == ' ') + return " \u2000"; + if (!(char.IsLetter(c) || char.IsDigit(c))) + return $" {c}"; + + c = char.ToLowerInvariant(c); + return _previousGuesses.Contains(c) ? $" {c}" : " ◯"; + })) + "`"; + + private Phase _currentPhase = Phase.Active; + public Phase CurrentPhase + { + get => _currentPhase; + set + { + if (value == Phase.Ended) + _endingCompletionSource.TrySetResult(true); + + _currentPhase = value; + } + } + + private readonly SemaphoreSlim _locker = new SemaphoreSlim(1, 1); + + private readonly HashSet _recentUsers = new HashSet(); + + public uint Errors { get; private set; } = 0; + public uint MaxErrors { get; } = 6; + + public event Func OnGameEnded = delegate { return Task.CompletedTask; }; + public event Func OnLetterAlreadyUsed = delegate { return Task.CompletedTask; }; + public event Func OnGuessFailed = delegate { return Task.CompletedTask; }; + public event Func OnGuessSucceeded = delegate { return Task.CompletedTask; }; + + private readonly HashSet _previousGuesses = new HashSet(); + public ImmutableArray PreviousGuesses => _previousGuesses.ToImmutableArray(); + + private readonly TaskCompletionSource _endingCompletionSource = new TaskCompletionSource(); + + public Task EndedTask => _endingCompletionSource.Task; + + public Hangman(TermType type) + { + this.TermType = type.ToString().Replace('_', ' ').ToTitleCase(); + this.Term = TermPool.GetTerm(type); + } + + private void AddError() + { + Errors++; + if (Errors > MaxErrors) + { + CurrentPhase = Phase.Ended; + var _ = OnGameEnded(this, null); + } + } + + public string GetHangman() => $@". ┌─────┐ +.┃...............┋ +.┃...............┋ +.┃{(Errors > 0 ? ".............😲" : "")} +.┃{(Errors > 1 ? "............./" : "")} {(Errors > 2 ? "|" : "")} {(Errors > 3 ? "\\" : "")} +.┃{(Errors > 4 ? "............../" : "")} {(Errors > 5 ? "\\" : "")} +/-\"; + + public async Task Input(ulong userId, string userName, string input) + { + if (CurrentPhase == Phase.Ended) + return; + + if (string.IsNullOrWhiteSpace(input)) + return; + + input = input.Trim().ToLowerInvariant(); + + await _locker.WaitAsync().ConfigureAwait(false); + try + { + if (CurrentPhase == Phase.Ended) + return; + + if (input.Length > 1) // tried to guess the whole word + { + if (input != Term.Word) // failed + return; + + CurrentPhase = Phase.Ended; + var _ = OnGameEnded?.Invoke(this, userName); + return; + } + + var ch = input[0]; + + if (!(char.IsLetterOrDigit(ch))) + return; + + if (!_recentUsers.Add(userId)) // don't let a single user spam guesses + return; + + if (!_previousGuesses.Add(ch)) // that latter was already guessed + { + var _ = OnLetterAlreadyUsed?.Invoke(this, userName, ch); + AddError(); + } + else if (!Term.Word.Contains(ch)) // guessed letter doesn't exist + { + var _ = OnGuessFailed?.Invoke(this, userName, ch); + AddError(); + } + else if (Term.Word.All(x => _previousGuesses.IsSupersetOf(Term.Word.ToLowerInvariant() + .Where(c => char.IsLetterOrDigit(c))))) + { + var _ = OnGameEnded.Invoke(this, userName); //if all letters are guessed + } + else //guessed but not last letter + { + var _ = OnGuessSucceeded?.Invoke(this, userName, ch); + _recentUsers.Remove(userId); // he can guess again right away + return; + } + + var clearSpam = Task.Run(async () => + { + await Task.Delay(3000).ConfigureAwait(false); // remove the user from the spamlist after 5 seconds + _recentUsers.Remove(userId); + }); + } + finally { _locker.Release(); } + } + + public async Task Stop() + { + await _locker.WaitAsync().ConfigureAwait(false); + try + { + CurrentPhase = Phase.Ended; + } + finally { _locker.Release(); } + } + + public void Dispose() + { + OnGameEnded = null; + OnGuessFailed = null; + OnGuessSucceeded = null; + OnLetterAlreadyUsed = null; + _previousGuesses.Clear(); + _recentUsers.Clear(); + _locker.Dispose(); + } + } +} diff --git a/src/NadekoBot/Modules/Games/Common/Hangman/HangmanGame.cs b/src/NadekoBot/Modules/Games/Common/Hangman/HangmanGame.cs deleted file mode 100644 index f0f5c820..00000000 --- a/src/NadekoBot/Modules/Games/Common/Hangman/HangmanGame.cs +++ /dev/null @@ -1,214 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Threading.Tasks; -using Discord; -using Discord.WebSocket; -using NadekoBot.Common; -using NadekoBot.Extensions; -using Newtonsoft.Json; -using NLog; - -namespace NadekoBot.Modules.Games.Common.Hangman -{ - public class HangmanTermPool - { - const string termsPath = "data/hangman3.json"; - public static IReadOnlyDictionary data { get; } - static HangmanTermPool() - { - try - { - data = JsonConvert.DeserializeObject>(File.ReadAllText(termsPath)); - } - catch (Exception) - { - //ignored - } - } - - public static HangmanObject GetTerm(string type) - { - if (string.IsNullOrWhiteSpace(type)) - throw new ArgumentNullException(nameof(type)); - - type = type.Trim(); - - var rng = new NadekoRandom(); - - if (type == "All") { - var keys = data.Keys.ToArray(); - type = keys[rng.Next(0, keys.Length)]; - } - - HangmanObject[] termTypes; - data.TryGetValue(type, out termTypes); - - if (termTypes == null || termTypes.Length == 0) - return null; - - return termTypes[rng.Next(0, termTypes.Length)]; - } - } - - public class HangmanGame: IDisposable - { - private readonly Logger _log; - private readonly DiscordSocketClient _client; - - public IMessageChannel GameChannel { get; } - public HashSet Guesses { get; } = new HashSet(); - public HangmanObject Term { get; private set; } - public uint Errors { get; private set; } = 0; - public uint MaxErrors { get; } = 6; - public uint MessagesSinceLastPost { get; private set; } = 0; - public string ScrambledWord => "`" + String.Concat(Term.Word.Select(c => - { - if (c == ' ') - return " \u2000"; - if (!(char.IsLetter(c) || char.IsDigit(c))) - return $" {c}"; - - c = char.ToUpperInvariant(c); - return Guesses.Contains(c) ? $" {c}" : " ◯"; - })) + "`"; - - public bool GuessedAll => Guesses.IsSupersetOf(Term.Word.ToUpperInvariant() - .Where(c => char.IsLetter(c) || char.IsDigit(c))); - - public string TermType { get; } - - public event Action OnEnded; - - public HangmanGame(DiscordSocketClient client, IMessageChannel channel, string type) - { - _log = LogManager.GetCurrentClassLogger(); - _client = client; - - this.GameChannel = channel; - this.TermType = type.ToTitleCase(); - } - - public void Start() - { - this.Term = HangmanTermPool.GetTerm(TermType); - - if (this.Term == null) - throw new KeyNotFoundException("Can't find a term with that type. Use hangmanlist command."); - // start listening for answers when game starts - _client.MessageReceived += PotentialGuess; - } - - public async Task End() - { - _client.MessageReceived -= PotentialGuess; - OnEnded(this); - var toSend = "Game ended. You **" + (Errors >= MaxErrors ? "LOSE" : "WIN") + "**!\n" + GetHangman(); - var embed = new EmbedBuilder().WithTitle("Hangman Game") - .WithDescription(toSend) - .AddField(efb => efb.WithName("It was").WithValue(Term.Word)) - .WithFooter(efb => efb.WithText(string.Join(" ", Guesses))); - if(Uri.IsWellFormedUriString(Term.ImageUrl, UriKind.Absolute)) - embed.WithImageUrl(Term.ImageUrl); - - if (Errors >= MaxErrors) - await GameChannel.EmbedAsync(embed.WithErrorColor()).ConfigureAwait(false); - else - await GameChannel.EmbedAsync(embed.WithOkColor()).ConfigureAwait(false); - } - - private Task PotentialGuess(SocketMessage msg) - { - var _ = Task.Run(async () => - { - try - { - if (!(msg is SocketUserMessage)) - return; - - if (msg.Channel != GameChannel) - return; // message's channel has to be the same as game's - if (msg.Content.Length == 1) // message must be 1 char long - { - if (++MessagesSinceLastPost > 10) - { - MessagesSinceLastPost = 0; - try - { - await GameChannel.SendConfirmAsync("Hangman Game", - ScrambledWord + "\n" + GetHangman(), - footer: string.Join(" ", Guesses)).ConfigureAwait(false); - } - catch { } - } - - if (!(char.IsLetter(msg.Content[0]) || char.IsDigit(msg.Content[0])))// and a letter or a digit - return; - - var guess = char.ToUpperInvariant(msg.Content[0]); - if (Guesses.Contains(guess)) - { - MessagesSinceLastPost = 0; - ++Errors; - if (Errors < MaxErrors) - await GameChannel.SendErrorAsync("Hangman Game", $"{msg.Author} Letter `{guess}` has already been used.\n" + ScrambledWord + "\n" + GetHangman(), - footer: string.Join(" ", Guesses)).ConfigureAwait(false); - else - await End().ConfigureAwait(false); - return; - } - - Guesses.Add(guess); - - if (Term.Word.ToUpperInvariant().Contains(guess)) - { - if (GuessedAll) - { - try { await GameChannel.SendConfirmAsync("Hangman Game", $"{msg.Author} guessed a letter `{guess}`!").ConfigureAwait(false); } catch { } - - await End().ConfigureAwait(false); - return; - } - MessagesSinceLastPost = 0; - try - { - await GameChannel.SendConfirmAsync("Hangman Game", $"{msg.Author} guessed a letter `{guess}`!\n" + ScrambledWord + "\n" + GetHangman(), - footer: string.Join(" ", Guesses)).ConfigureAwait(false); - } - catch { } - - } - else - { - MessagesSinceLastPost = 0; - ++Errors; - if (Errors < MaxErrors) - await GameChannel.SendErrorAsync("Hangman Game", $"{msg.Author} Letter `{guess}` does not exist.\n" + ScrambledWord + "\n" + GetHangman(), - footer: string.Join(" ", Guesses)).ConfigureAwait(false); - else - await End().ConfigureAwait(false); - } - - } - } - catch (Exception ex) { _log.Warn(ex); } - }); - return Task.CompletedTask; - } - - public string GetHangman() => $@". ┌─────┐ -.┃...............┋ -.┃...............┋ -.┃{(Errors > 0 ? ".............😲" : "")} -.┃{(Errors > 1 ? "............./" : "")} {(Errors > 2 ? "|" : "")} {(Errors > 3 ? "\\" : "")} -.┃{(Errors > 4 ? "............../" : "")} {(Errors > 5 ? "\\" : "")} -/-\"; - - public void Dispose() - { - _client.MessageReceived -= PotentialGuess; - OnEnded = null; - } - } -} \ No newline at end of file diff --git a/src/NadekoBot/Modules/Games/Common/Hangman/IHangmanObject.cs b/src/NadekoBot/Modules/Games/Common/Hangman/HangmanObject.cs similarity index 100% rename from src/NadekoBot/Modules/Games/Common/Hangman/IHangmanObject.cs rename to src/NadekoBot/Modules/Games/Common/Hangman/HangmanObject.cs diff --git a/src/NadekoBot/Modules/Games/Common/Hangman/Phase.cs b/src/NadekoBot/Modules/Games/Common/Hangman/Phase.cs new file mode 100644 index 00000000..b23b856b --- /dev/null +++ b/src/NadekoBot/Modules/Games/Common/Hangman/Phase.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace NadekoBot.Modules.Games.Common.Hangman +{ + public enum Phase + { + Active, + Ended, + } +} diff --git a/src/NadekoBot/Modules/Games/Common/Hangman/TermPool.cs b/src/NadekoBot/Modules/Games/Common/Hangman/TermPool.cs new file mode 100644 index 00000000..4647e740 --- /dev/null +++ b/src/NadekoBot/Modules/Games/Common/Hangman/TermPool.cs @@ -0,0 +1,52 @@ +using NadekoBot.Common; +using NadekoBot.Extensions; +using NadekoBot.Modules.Games.Common.Hangman.Exceptions; +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.IO; +using System.Linq; + +namespace NadekoBot.Modules.Games.Common.Hangman +{ + public class TermPool + { + const string termsPath = "data/hangman3.json"; + public static IReadOnlyDictionary data { get; } = new Dictionary(); + static TermPool() + { + try + { + data = JsonConvert.DeserializeObject>(File.ReadAllText(termsPath)); + } + catch (Exception) + { + //ignored + } + } + + private static readonly ImmutableArray _termTypes = Enum.GetValues(typeof(TermType)) + .Cast() + .ToImmutableArray(); + + public static HangmanObject GetTerm(TermType type) + { + var rng = new NadekoRandom(); + + if (type == TermType.Random) + { + var keys = data.Keys.ToArray(); + + type = _termTypes[rng.Next(0, _termTypes.Length - 1)]; // - 1 because last one is 'all' + } + if (!data.TryGetValue(type.ToString(), out var termTypes) || termTypes.Length == 0) + throw new TermNotFoundException(); + + var obj = termTypes[rng.Next(0, termTypes.Length)]; + + obj.Word = obj.Word.Trim().ToLowerInvariant(); + return obj; + } + } +} diff --git a/src/NadekoBot/Modules/Games/Common/Hangman/TermType.cs b/src/NadekoBot/Modules/Games/Common/Hangman/TermType.cs new file mode 100644 index 00000000..f3c09af6 --- /dev/null +++ b/src/NadekoBot/Modules/Games/Common/Hangman/TermType.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace NadekoBot.Modules.Games.Common.Hangman +{ + [Flags] + public enum TermType + { + Countries = 0, + Movies = 1, + Animals = 2, + Things = 4, + Random = 8, + } +} diff --git a/src/NadekoBot/Modules/Games/HangmanCommands.cs b/src/NadekoBot/Modules/Games/HangmanCommands.cs index 5ebd979a..9d28d49a 100644 --- a/src/NadekoBot/Modules/Games/HangmanCommands.cs +++ b/src/NadekoBot/Modules/Games/HangmanCommands.cs @@ -23,43 +23,102 @@ namespace NadekoBot.Modules.Games } //channelId, game - public static ConcurrentDictionary HangmanGames { get; } = new ConcurrentDictionary(); + public static ConcurrentDictionary HangmanGames { get; } = new ConcurrentDictionary(); + [NadekoCommand, Usage, Description, Aliases] [RequireContext(ContextType.Guild)] public async Task Hangmanlist() { - await Context.Channel.SendConfirmAsync(Format.Code(GetText("hangman_types", Prefix)) + "\n" + string.Join(", ", HangmanTermPool.data.Keys)); + await Context.Channel.SendConfirmAsync(Format.Code(GetText("hangman_types", Prefix)) + "\n" + string.Join(", ", TermPool.data.Keys)); } [NadekoCommand, Usage, Description, Aliases] [RequireContext(ContextType.Guild)] - public async Task Hangman([Remainder]string type = "All") + public async Task Hangman([Remainder]TermType type = TermType.Random) { - var hm = new HangmanGame(_client, Context.Channel, type); + var hm = new Hangman(type); if (!HangmanGames.TryAdd(Context.Channel.Id, hm)) { + hm.Dispose(); await ReplyErrorLocalized("hangman_running").ConfigureAwait(false); return; } + hm.OnGameEnded += Hm_OnGameEnded; + hm.OnGuessFailed += Hm_OnGuessFailed; + hm.OnGuessSucceeded += Hm_OnGuessSucceeded; + hm.OnLetterAlreadyUsed += Hm_OnLetterAlreadyUsed; + _client.MessageReceived += _client_MessageReceived; - hm.OnEnded += g => - { - HangmanGames.TryRemove(g.GameChannel.Id, out _); - }; try { - hm.Start(); + await Context.Channel.SendConfirmAsync(GetText("hangman_game_started") + $" ({hm.TermType})", + hm.ScrambledWord + "\n" + hm.GetHangman()) + .ConfigureAwait(false); } - catch (Exception ex) + catch { } + + await hm.EndedTask.ConfigureAwait(false); + + + Task _client_MessageReceived(SocketMessage msg) { - try { await Context.Channel.SendErrorAsync(GetText("hangman_start_errored") + " " + ex.Message).ConfigureAwait(false); } catch { } - if(HangmanGames.TryRemove(Context.Channel.Id, out var removed)) - removed.Dispose(); - return; + var _ = Task.Run(() => + { + if (Context.Channel.Id == msg.Channel.Id) + return hm.Input(msg.Author.Id, msg.Author.ToString(), msg.Content); + else + return Task.CompletedTask; + }); + return Task.CompletedTask; + } + } + + Task Hm_OnGameEnded(Hangman game, string winner) + { + HangmanGames.TryRemove(Context.Channel.Id, out _); + + if (winner == null) + { + var loseEmbed = new EmbedBuilder().WithTitle($"Hangman Game ({game.TermType}) - Ended") + .WithDescription(Format.Bold("You lose.")) + .AddField(efb => efb.WithName("It was").WithValue(game.Term.Word.ToTitleCase())) + .WithFooter(efb => efb.WithText(string.Join(" ", game.PreviousGuesses))) + .WithErrorColor(); + + if (Uri.IsWellFormedUriString(game.Term.ImageUrl, UriKind.Absolute)) + loseEmbed.WithImageUrl(game.Term.ImageUrl); + + return Context.Channel.EmbedAsync(loseEmbed); } - await Context.Channel.SendConfirmAsync(GetText("hangman_game_started"), hm.ScrambledWord + "\n" + hm.GetHangman()); + var winEmbed = new EmbedBuilder().WithTitle($"Hangman Game ({game.TermType}) - Ended") + .WithDescription(Format.Bold($"{winner} Won.")) + .AddField(efb => efb.WithName("It was").WithValue(game.Term.Word.ToTitleCase())) + .WithFooter(efb => efb.WithText(string.Join(" ", game.PreviousGuesses))) + .WithOkColor(); + + if (Uri.IsWellFormedUriString(game.Term.ImageUrl, UriKind.Absolute)) + winEmbed.WithImageUrl(game.Term.ImageUrl); + + return Context.Channel.EmbedAsync(winEmbed); + } + + private Task Hm_OnLetterAlreadyUsed(Hangman game, string user, char guess) + { + return Context.Channel.SendErrorAsync($"Hangman Game ({game.TermType})", $"{user} Letter `{guess}` has already been used. You can guess again in 3 seconds.\n" + game.ScrambledWord + "\n" + game.GetHangman(), + footer: string.Join(" ", game.PreviousGuesses)); + } + + private Task Hm_OnGuessSucceeded(Hangman game, string user, char guess) + { + return Context.Channel.SendConfirmAsync($"Hangman Game ({game.TermType})", $"{user} guessed a letter `{guess}`!\n" + game.ScrambledWord + "\n" + game.GetHangman()); + } + + private Task Hm_OnGuessFailed(Hangman game, string user, char guess) + { + return Context.Channel.SendErrorAsync($"Hangman Game ({game.TermType})", $"{user} Letter `{guess}` does not exist. You can guess again in 3 seconds.\n" + game.ScrambledWord + "\n" + game.GetHangman(), + footer: string.Join(" ", game.PreviousGuesses)); } [NadekoCommand, Usage, Description, Aliases] @@ -74,4 +133,4 @@ namespace NadekoBot.Modules.Games } } } -} +} \ No newline at end of file From f06ee47516b3f9a1cd7e210d1af2fd26afedc7bd Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Fri, 28 Jul 2017 12:38:08 +0200 Subject: [PATCH 236/346] cleanup --- src/NadekoBot/Modules/Games/Common/Hangman/Hangman.cs | 1 + src/NadekoBot/Modules/Games/Common/Hangman/TermPool.cs | 1 - src/NadekoBot/Modules/Games/HangmanCommands.cs | 7 ++++--- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/NadekoBot/Modules/Games/Common/Hangman/Hangman.cs b/src/NadekoBot/Modules/Games/Common/Hangman/Hangman.cs index 94f14835..61df5560 100644 --- a/src/NadekoBot/Modules/Games/Common/Hangman/Hangman.cs +++ b/src/NadekoBot/Modules/Games/Common/Hangman/Hangman.cs @@ -53,6 +53,7 @@ namespace NadekoBot.Modules.Games.Common.Hangman public ImmutableArray PreviousGuesses => _previousGuesses.ToImmutableArray(); private readonly TaskCompletionSource _endingCompletionSource = new TaskCompletionSource(); + private bool disposed = false; public Task EndedTask => _endingCompletionSource.Task; diff --git a/src/NadekoBot/Modules/Games/Common/Hangman/TermPool.cs b/src/NadekoBot/Modules/Games/Common/Hangman/TermPool.cs index 4647e740..3074aef7 100644 --- a/src/NadekoBot/Modules/Games/Common/Hangman/TermPool.cs +++ b/src/NadekoBot/Modules/Games/Common/Hangman/TermPool.cs @@ -1,5 +1,4 @@ using NadekoBot.Common; -using NadekoBot.Extensions; using NadekoBot.Modules.Games.Common.Hangman.Exceptions; using Newtonsoft.Json; using System; diff --git a/src/NadekoBot/Modules/Games/HangmanCommands.cs b/src/NadekoBot/Modules/Games/HangmanCommands.cs index 9d28d49a..ce76cc37 100644 --- a/src/NadekoBot/Modules/Games/HangmanCommands.cs +++ b/src/NadekoBot/Modules/Games/HangmanCommands.cs @@ -60,6 +60,9 @@ namespace NadekoBot.Modules.Games await hm.EndedTask.ConfigureAwait(false); + _client.MessageReceived -= _client_MessageReceived; + HangmanGames.TryRemove(Context.Channel.Id, out _); + hm.Dispose(); Task _client_MessageReceived(SocketMessage msg) { @@ -76,8 +79,6 @@ namespace NadekoBot.Modules.Games Task Hm_OnGameEnded(Hangman game, string winner) { - HangmanGames.TryRemove(Context.Channel.Id, out _); - if (winner == null) { var loseEmbed = new EmbedBuilder().WithTitle($"Hangman Game ({game.TermType}) - Ended") @@ -127,7 +128,7 @@ namespace NadekoBot.Modules.Games { if (HangmanGames.TryRemove(Context.Channel.Id, out var removed)) { - removed.Dispose(); + await removed.Stop().ConfigureAwait(false); await ReplyConfirmLocalized("hangman_stopped").ConfigureAwait(false); } } From 3097ef88a7d931baa8d5190cf8b631375f004615 Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Mon, 31 Jul 2017 08:50:04 +0200 Subject: [PATCH 237/346] Fixed hangman error --- src/NadekoBot/Modules/Games/Common/Hangman/Hangman.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/NadekoBot/Modules/Games/Common/Hangman/Hangman.cs b/src/NadekoBot/Modules/Games/Common/Hangman/Hangman.cs index 61df5560..94f14835 100644 --- a/src/NadekoBot/Modules/Games/Common/Hangman/Hangman.cs +++ b/src/NadekoBot/Modules/Games/Common/Hangman/Hangman.cs @@ -53,7 +53,6 @@ namespace NadekoBot.Modules.Games.Common.Hangman public ImmutableArray PreviousGuesses => _previousGuesses.ToImmutableArray(); private readonly TaskCompletionSource _endingCompletionSource = new TaskCompletionSource(); - private bool disposed = false; public Task EndedTask => _endingCompletionSource.Task; From 82aac891dd7f45ed8000c8f58b3e422ba605fd05 Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Tue, 1 Aug 2017 00:11:36 +0200 Subject: [PATCH 238/346] animal racing rewritten to be isolated. Please hunt bugs. --- .../Modules/Gambling/AnimalRacingCommands.cs | 395 ++++-------------- .../Common/AnimalRacing/AnimalRace.cs | 161 +++++++ .../Common/AnimalRacing/AnimalRacingUser.cs | 32 ++ .../Exceptions/AlreadyJoinedException.cs | 9 + .../Exceptions/AlreadyStartedException.cs | 9 + .../Exceptions/AnimalRaceFullException.cs | 8 + .../Exceptions/NotEnoughFundsException.cs | 9 + .../Modules/Games/Common/Hangman/TermPool.cs | 8 +- src/NadekoBot/Modules/Games/Common/Poll.cs | 3 +- .../Modules/Games/HangmanCommands.cs | 2 +- .../_strings/ResponseStrings.en-US.json | 23 - 11 files changed, 326 insertions(+), 333 deletions(-) create mode 100644 src/NadekoBot/Modules/Gambling/Common/AnimalRacing/AnimalRace.cs create mode 100644 src/NadekoBot/Modules/Gambling/Common/AnimalRacing/AnimalRacingUser.cs create mode 100644 src/NadekoBot/Modules/Gambling/Common/AnimalRacing/Exceptions/AlreadyJoinedException.cs create mode 100644 src/NadekoBot/Modules/Gambling/Common/AnimalRacing/Exceptions/AlreadyStartedException.cs create mode 100644 src/NadekoBot/Modules/Gambling/Common/AnimalRacing/Exceptions/AnimalRaceFullException.cs create mode 100644 src/NadekoBot/Modules/Gambling/Common/AnimalRacing/Exceptions/NotEnoughFundsException.cs diff --git a/src/NadekoBot/Modules/Gambling/AnimalRacingCommands.cs b/src/NadekoBot/Modules/Gambling/AnimalRacingCommands.cs index f4777bef..2b9ee3ce 100644 --- a/src/NadekoBot/Modules/Gambling/AnimalRacingCommands.cs +++ b/src/NadekoBot/Modules/Gambling/AnimalRacingCommands.cs @@ -3,16 +3,13 @@ using Discord.Commands; using Discord.WebSocket; using NadekoBot.Extensions; using NadekoBot.Services; -using NLog; using System; using System.Collections.Concurrent; -using System.Collections.Generic; using System.Linq; -using System.Threading; using System.Threading.Tasks; -using NadekoBot.Common; using NadekoBot.Common.Attributes; -using NadekoBot.Services.Impl; +using NadekoBot.Modules.Gambling.Common.AnimalRacing.Exceptions; +using NadekoBot.Modules.Gambling.Common.AnimalRacing; namespace NadekoBot.Modules.Gambling { @@ -24,7 +21,7 @@ namespace NadekoBot.Modules.Gambling private readonly IBotConfigProvider _bc; private readonly CurrencyService _cs; private readonly DiscordSocketClient _client; - + public static ConcurrentDictionary AnimalRaces { get; } = new ConcurrentDictionary(); @@ -35,326 +32,118 @@ namespace NadekoBot.Modules.Gambling _client = client; } + private IUserMessage raceMessage = null; + [NadekoCommand, Usage, Description, Aliases] [RequireContext(ContextType.Guild)] - public async Task Race() + public Task Race() { - var ar = new AnimalRace(Context.Guild.Id, (ITextChannel)Context.Channel, Prefix, - _bc, _cs, _client,_localization, _strings); + var ar = new AnimalRace(_cs, _bc.BotConfig.RaceAnimals.Shuffle().ToArray()); + if (!AnimalRaces.TryAdd(Context.Guild.Id, ar)) + return Context.Channel.SendErrorAsync(GetText("animal_race"), GetText("animal_race_already_started")); + ar.Initialize(); - if (ar.Fail) - await ReplyErrorLocalized("race_failed_starting").ConfigureAwait(false); + ar.OnStartingFailed += Ar_OnStartingFailed; + ar.OnStateUpdate += Ar_OnStateUpdate; + ar.OnEnded += Ar_OnEnded; + ar.OnStarted += Ar_OnStarted; + + return Context.Channel.SendConfirmAsync(GetText("animal_race"), GetText("animal_race_starting"), + footer: GetText("animal_race_join_instr", Prefix)); + } + + private Task Ar_OnStarted(AnimalRace race) + { + if(race.Users.Length == race.MaxUsers) + return Context.Channel.SendConfirmAsync(GetText("animal_race"), GetText("animal_race_full")); + else + return Context.Channel.SendConfirmAsync(GetText("animal_race"), GetText("animal_race_starting_with_x", race.Users.Length)); + } + + private Task Ar_OnEnded(AnimalRace race) + { + AnimalRaces.TryRemove(Context.Guild.Id, out _); + var winner = race.FinishedUsers[0]; + if (race.FinishedUsers[0].Bet > 0) + { + return Context.Channel.SendConfirmAsync(GetText("animal_race"), + GetText("animal_race_won_money", Format.Bold(winner.Username), + winner.Animal.Icon, (race.FinishedUsers[0].Bet * (race.Users.Length - 1)) + _bc.BotConfig.CurrencySign)); + } + else + { + return Context.Channel.SendConfirmAsync(GetText("animal_race"), + GetText("animal_race_won", Format.Bold(winner.Username), winner.Animal.Icon)); + } + } + + private async Task Ar_OnStateUpdate(AnimalRace race) + { + var text = $@"|🏁🏁🏁🏁🏁🏁🏁🏁🏁🏁🏁🏁🏁🏁🏁🔚| +{String.Join("\n", race.Users.Select(p => + { + var index = race.FinishedUsers.IndexOf(p); + var extra = (index == -1 ? "" : $"#{index + 1} {(index == 0 ? "🏆" : "")}"); + return $"{(int)(p.Progress / 60f * 100),-2}%|{new string('‣', p.Progress) + p.Animal.Icon + extra}"; + }))} +|🏁🏁🏁🏁🏁🏁🏁🏁🏁🏁🏁🏁🏁🏁🏁🔚|"; + + if (raceMessage == null) + raceMessage = await Context.Channel.SendConfirmAsync(text) + .ConfigureAwait(false); + else + await raceMessage.ModifyAsync(x => x.Embed = new EmbedBuilder() + .WithTitle(GetText("animal_race")) + .WithDescription(text) + .WithOkColor() + .Build()) + .ConfigureAwait(false); + } + + private Task Ar_OnStartingFailed(AnimalRace race) + { + AnimalRaces.TryRemove(Context.Guild.Id, out _); + return ReplyErrorLocalized("animal_race_failed"); } [NadekoCommand, Usage, Description, Aliases] [RequireContext(ContextType.Guild)] public async Task JoinRace(int amount = 0) { - - if (amount < 0) - amount = 0; - - - AnimalRace ar; - if (!AnimalRaces.TryGetValue(Context.Guild.Id, out ar)) + if (!AnimalRaces.TryGetValue(Context.Guild.Id, out var ar)) { await ReplyErrorLocalized("race_not_exist").ConfigureAwait(false); return; } - await ar.JoinRace(Context.User as IGuildUser, amount); - } - - //todo 85 needs to be completely isolated, shouldn't use any services in the constructor, - //then move the rest either to the module itself, or the service - public class AnimalRace - { - - private ConcurrentQueue animals { get; } - - public bool Fail { get; set; } - - private readonly List _participants = new List(); - private readonly ulong _serverId; - private int _messagesSinceGameStarted; - private readonly string _prefix; - - private readonly Logger _log; - - private readonly ITextChannel _raceChannel; - private readonly IBotConfigProvider _bc; - private readonly CurrencyService _cs; - private readonly DiscordSocketClient _client; - private readonly ILocalization _localization; - private readonly NadekoStrings _strings; - - public bool Started { get; private set; } - - public AnimalRace(ulong serverId, ITextChannel channel, string prefix, IBotConfigProvider bc, - CurrencyService cs, DiscordSocketClient client, ILocalization localization, - NadekoStrings strings) + try { - _prefix = prefix; - _bc = bc; - _cs = cs; - _log = LogManager.GetCurrentClassLogger(); - _serverId = serverId; - _raceChannel = channel; - _client = client; - _localization = localization; - _strings = strings; - - if (!AnimalRaces.TryAdd(serverId, this)) - { - Fail = true; - return; - } - - animals = new ConcurrentQueue(_bc.BotConfig.RaceAnimals.Select(ra => ra.Icon).Shuffle()); - - - var cancelSource = new CancellationTokenSource(); - var token = cancelSource.Token; - var fullgame = CheckForFullGameAsync(token); - Task.Run(async () => - { - try - { - try - { - await _raceChannel.SendConfirmAsync(GetText("animal_race"), GetText("animal_race_starting"), - footer: GetText("animal_race_join_instr", _prefix)); - } - catch (Exception ex) - { - _log.Warn(ex); - } - var t = await Task.WhenAny(Task.Delay(20000, token), fullgame); - Started = true; - cancelSource.Cancel(); - if (t == fullgame) - { - try { await _raceChannel.SendConfirmAsync(GetText("animal_race"), GetText("animal_race_full") ); } catch (Exception ex) { _log.Warn(ex); } - } - else if (_participants.Count > 1) - { - try { await _raceChannel.SendConfirmAsync(GetText("animal_race"), GetText("animal_race_starting_with_x", _participants.Count)); } catch (Exception ex) { _log.Warn(ex); } - } - else - { - try { await _raceChannel.SendErrorAsync(GetText("animal_race"), GetText("animal_race_failed")); } catch (Exception ex) { _log.Warn(ex); } - var p = _participants.FirstOrDefault(); - - if (p != null && p.AmountBet > 0) - await _cs.AddAsync(p.User, "BetRace", p.AmountBet, false).ConfigureAwait(false); - End(); - return; - } - await Task.Run(StartRace); - End(); - } - catch { try { End(); } catch { } } - }); - } - - private void End() - { - AnimalRaces.TryRemove(_serverId, out _); - } - - private async Task StartRace() - { - var rng = new NadekoRandom(); - Participant winner = null; - IUserMessage msg = null; - var place = 1; - try - { - _client.MessageReceived += Client_MessageReceived; - - while (!_participants.All(p => p.Total >= 60)) - { - //update the state - _participants.ForEach(p => - { - p.Total += 1 + rng.Next(0, 10); - }); - - - _participants - .OrderByDescending(p => p.Total) - .ForEach(p => - { - if (p.Total > 60) - { - if (winner == null) - { - winner = p; - } - p.Total = 60; - if (p.Place == 0) - p.Place = place++; - } - }); - - - //draw the state - - var text = $@"|🏁🏁🏁🏁🏁🏁🏁🏁🏁🏁🏁🏁🏁🏁🏁🔚| -{String.Join("\n", _participants.Select(p => $"{(int)(p.Total / 60f * 100),-2}%|{p.ToString()}"))} -|🏁🏁🏁🏁🏁🏁🏁🏁🏁🏁🏁🏁🏁🏁🏁🔚|"; - if (msg == null || _messagesSinceGameStarted >= 10) // also resend the message if channel was spammed - { - if (msg != null) - try { await msg.DeleteAsync(); } catch { } - _messagesSinceGameStarted = 0; - try { msg = await _raceChannel.SendMessageAsync(text).ConfigureAwait(false); } catch (Exception ex) { _log.Warn(ex); } - } - else - { - try { await msg.ModifyAsync(m => m.Content = text).ConfigureAwait(false); } catch (Exception ex) { _log.Warn(ex); } - } - - await Task.Delay(2500); - } - } - catch - { - // ignored - } - finally - { - _client.MessageReceived -= Client_MessageReceived; - } - - if (winner != null) - { - if (winner.AmountBet > 0) - { - var wonAmount = winner.AmountBet * (_participants.Count - 1); - - await _cs.AddAsync(winner.User, "Won a Race", wonAmount, true) - .ConfigureAwait(false); - await _raceChannel.SendConfirmAsync(GetText("animal_race"), - Format.Bold(GetText("animal_race_won_money", winner.User.Mention, - winner.Animal, wonAmount + _bc.BotConfig.CurrencySign))) - .ConfigureAwait(false); - } - else - { - await _raceChannel.SendConfirmAsync(GetText("animal_race"), - Format.Bold(GetText("animal_race_won", winner.User.Mention, winner.Animal))).ConfigureAwait(false); - } - } - - } - - private Task Client_MessageReceived(SocketMessage imsg) - { - var _ = Task.Run(() => - { - var msg = imsg as SocketUserMessage; - if (msg == null) - return Task.CompletedTask; - if ((msg.Author.Id == _client.CurrentUser.Id) || !(imsg.Channel is ITextChannel) || imsg.Channel != _raceChannel) - return Task.CompletedTask; - Interlocked.Increment(ref _messagesSinceGameStarted); - return Task.CompletedTask; - }); - return Task.CompletedTask; - } - - private async Task CheckForFullGameAsync(CancellationToken cancelToken) - { - while (animals.Count > 0) - { - await Task.Delay(100, cancelToken); - } - } - - public async Task JoinRace(IGuildUser u, int amount = 0) - { - string animal; - if (!animals.TryDequeue(out animal)) - { - await _raceChannel.SendErrorAsync(GetText("animal_race_no_race")).ConfigureAwait(false); - return; - } - var p = new Participant(u, animal, amount); - if (_participants.Contains(p)) - { - await _raceChannel.SendErrorAsync(GetText("animal_race_already_in")).ConfigureAwait(false); - return; - } - if (Started) - { - await _raceChannel.SendErrorAsync(GetText("animal_race_already_started")).ConfigureAwait(false); - return; - } + var user = await ar.JoinRace(Context.User.Id, Context.User.ToString(), amount) + .ConfigureAwait(false); if (amount > 0) - if (!await _cs.RemoveAsync(u, "BetRace", amount, false).ConfigureAwait(false)) - { - await _raceChannel.SendErrorAsync(GetText("not_enough", _bc.BotConfig.CurrencySign)).ConfigureAwait(false); - return; - } - _participants.Add(p); - string confStr; - if (amount > 0) - confStr = GetText("animal_race_join_bet", u.Mention, p.Animal, amount + _bc.BotConfig.CurrencySign); + await Context.Channel.SendConfirmAsync(GetText("animal_race_join_bet", Context.User.Mention, user.Animal.Icon, amount + _bc.BotConfig.CurrencySign)).ConfigureAwait(false); else - confStr = GetText("animal_race_join", u.Mention, p.Animal); - await _raceChannel.SendConfirmAsync(GetText("animal_race"), Format.Bold(confStr)).ConfigureAwait(false); + await Context.Channel.SendConfirmAsync(GetText("animal_race_join", Context.User.Mention, user.Animal.Icon)).ConfigureAwait(false); } - - private string GetText(string text) - => _strings.GetText(text, - _localization.GetCultureInfo(_raceChannel.Guild), - typeof(Gambling).Name.ToLowerInvariant()); - - private string GetText(string text, params object[] replacements) - => _strings.GetText(text, - _localization.GetCultureInfo(_raceChannel.Guild), - typeof(Gambling).Name.ToLowerInvariant(), - replacements); - } - - public class Participant - { - public IGuildUser User { get; } - public string Animal { get; } - public int AmountBet { get; } - - public float Coeff { get; set; } - public int Total { get; set; } - - public int Place { get; set; } - - public Participant(IGuildUser u, string a, int amount) + catch (ArgumentOutOfRangeException) { - User = u; - Animal = a; - AmountBet = amount; + //ignore if user inputed an invalid amount } - - public override int GetHashCode() => User.GetHashCode(); - - public override bool Equals(object obj) + catch (AlreadyJoinedException) { - var p = obj as Participant; - return p != null && p.User == User; + // just ignore this } - - public override string ToString() + catch (AlreadyStartedException) { - var str = new string('‣', Total) + Animal; - if (Place == 0) - return str; - - str += $"`#{Place}`"; - - if (Place == 1) - str += "🏆"; - - return str; + //ignore + } + catch (AnimalRaceFullException) + { + await Context.Channel.SendConfirmAsync(GetText("animal_race"), GetText("animal_race_full")) + .ConfigureAwait(false); + } + catch (NotEnoughFundsException) + { + await Context.Channel.SendErrorAsync(GetText("not_enough", _bc.BotConfig.CurrencySign)).ConfigureAwait(false); } } } diff --git a/src/NadekoBot/Modules/Gambling/Common/AnimalRacing/AnimalRace.cs b/src/NadekoBot/Modules/Gambling/Common/AnimalRacing/AnimalRace.cs new file mode 100644 index 00000000..bf1c51ac --- /dev/null +++ b/src/NadekoBot/Modules/Gambling/Common/AnimalRacing/AnimalRace.cs @@ -0,0 +1,161 @@ +using NadekoBot.Common; +using NadekoBot.Extensions; +using NadekoBot.Modules.Gambling.Common.AnimalRacing.Exceptions; +using NadekoBot.Services; +using NadekoBot.Services.Database.Models; +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace NadekoBot.Modules.Gambling.Common.AnimalRacing +{ + public class AnimalRace : IDisposable + { + public enum Phase + { + WaitingForPlayers, + Running, + Ended, + } + + private const int _startingDelayMiliseconds = 20_000; + + public Phase CurrentPhase = Phase.WaitingForPlayers; + + public event Func OnStarted = delegate { return Task.CompletedTask; }; + public event Func OnStartingFailed = delegate { return Task.CompletedTask; }; + public event Func OnStateUpdate = delegate { return Task.CompletedTask; }; + public event Func OnEnded = delegate { return Task.CompletedTask; }; + + public ImmutableArray Users => _users.ToImmutableArray(); + public List FinishedUsers { get; } = new List(); + + private readonly SemaphoreSlim _locker = new SemaphoreSlim(1, 1); + private readonly HashSet _users = new HashSet(); + private readonly CurrencyService _currency; + private readonly Queue _animalsQueue; + public int MaxUsers { get; } + + public AnimalRace(CurrencyService currency, RaceAnimal[] availableAnimals) + { + this._currency = currency; + this._animalsQueue = new Queue(availableAnimals); + this.MaxUsers = availableAnimals.Length; + + if (this._animalsQueue.Count == 0) + CurrentPhase = Phase.Ended; + } + + public void Initialize() //lame name + { + var _t = Task.Run(async () => + { + await Task.Delay(_startingDelayMiliseconds).ConfigureAwait(false); + + await _locker.WaitAsync().ConfigureAwait(false); + try + { + if (CurrentPhase != Phase.WaitingForPlayers) + return; + + await Start().ConfigureAwait(false); + } + finally { _locker.Release(); } + }); + } + + public async Task JoinRace(ulong userId, string userName, int bet = 0) + { + if (bet < 0) + throw new ArgumentOutOfRangeException(nameof(bet)); + + var user = new AnimalRacingUser(userName, userId, bet); + + await _locker.WaitAsync().ConfigureAwait(false); + try + { + if (_users.Count == MaxUsers) + throw new AnimalRaceFullException(); + + if (CurrentPhase != Phase.WaitingForPlayers) + throw new AlreadyStartedException(); + + if (!await _currency.RemoveAsync(userId, "BetRace", bet).ConfigureAwait(false)) + throw new NotEnoughFundsException(); + + if (_users.Contains(user)) + throw new AlreadyJoinedException(); + + var animal = _animalsQueue.Dequeue(); + user.Animal = animal; + _users.Add(user); + + if (_animalsQueue.Count == 0) //start if no more spots left + await Start().ConfigureAwait(false); + + return user; + } + finally { _locker.Release(); } + } + + private async Task Start() + { + CurrentPhase = Phase.Running; + if (_users.Count <= 1) + { + foreach (var user in _users) + { + if(user.Bet > 0) + await _currency.AddAsync(user.UserId, "Race refund", user.Bet).ConfigureAwait(false); + } + + var _sf = OnStartingFailed?.Invoke(this); + CurrentPhase = Phase.Ended; + return; + } + + var _ = OnStarted?.Invoke(this); + var _t = Task.Run(async () => + { + var rng = new NadekoRandom(); + while (!_users.All(x => x.Progress >= 60)) + { + foreach (var user in _users) + { + user.Progress += rng.Next(1, 11); + if (user.Progress >= 60) + user.Progress = 60; + } + + var finished = _users.Where(x => x.Progress >= 60 && !FinishedUsers.Contains(x)) + .Shuffle(); + + FinishedUsers.AddRange(finished); + + var _ignore = OnStateUpdate?.Invoke(this); + await Task.Delay(2500).ConfigureAwait(false); + } + + if (FinishedUsers[0].Bet > 0) + await _currency.AddAsync(FinishedUsers[0].UserId, "Won a Race", FinishedUsers[0].Bet * (_users.Count - 1)) + .ConfigureAwait(false); + + var _ended = OnEnded?.Invoke(this); + }); + } + + public void Dispose() + { + CurrentPhase = Phase.Ended; + OnStarted = null; + OnEnded = null; + OnStartingFailed = null; + OnStateUpdate = null; + _locker.Dispose(); + _users.Clear(); + } + } +} \ No newline at end of file diff --git a/src/NadekoBot/Modules/Gambling/Common/AnimalRacing/AnimalRacingUser.cs b/src/NadekoBot/Modules/Gambling/Common/AnimalRacing/AnimalRacingUser.cs new file mode 100644 index 00000000..ea9bc453 --- /dev/null +++ b/src/NadekoBot/Modules/Gambling/Common/AnimalRacing/AnimalRacingUser.cs @@ -0,0 +1,32 @@ +using NadekoBot.Services.Database.Models; + +namespace NadekoBot.Modules.Gambling.Common.AnimalRacing +{ + public class AnimalRacingUser + { + public int Bet { get; } + public string Username { get; } + public ulong UserId { get; } + public RaceAnimal Animal { get; set; } + public int Progress { get; set; } + + public AnimalRacingUser(string username, ulong userId, int bet) + { + this.Bet = bet; + this.Username = username; + this.UserId = userId; + } + + public override bool Equals(object obj) + { + return obj is AnimalRacingUser x + ? x.UserId == this.UserId + : false; + } + + public override int GetHashCode() + { + return this.UserId.GetHashCode(); + } + } +} diff --git a/src/NadekoBot/Modules/Gambling/Common/AnimalRacing/Exceptions/AlreadyJoinedException.cs b/src/NadekoBot/Modules/Gambling/Common/AnimalRacing/Exceptions/AlreadyJoinedException.cs new file mode 100644 index 00000000..56469bf8 --- /dev/null +++ b/src/NadekoBot/Modules/Gambling/Common/AnimalRacing/Exceptions/AlreadyJoinedException.cs @@ -0,0 +1,9 @@ +using System; + +namespace NadekoBot.Modules.Gambling.Common.AnimalRacing.Exceptions +{ + public class AlreadyJoinedException : Exception + { + + } +} diff --git a/src/NadekoBot/Modules/Gambling/Common/AnimalRacing/Exceptions/AlreadyStartedException.cs b/src/NadekoBot/Modules/Gambling/Common/AnimalRacing/Exceptions/AlreadyStartedException.cs new file mode 100644 index 00000000..ab54a87a --- /dev/null +++ b/src/NadekoBot/Modules/Gambling/Common/AnimalRacing/Exceptions/AlreadyStartedException.cs @@ -0,0 +1,9 @@ +using System; + +namespace NadekoBot.Modules.Gambling.Common.AnimalRacing.Exceptions +{ + public class AlreadyStartedException : Exception + { + + } +} diff --git a/src/NadekoBot/Modules/Gambling/Common/AnimalRacing/Exceptions/AnimalRaceFullException.cs b/src/NadekoBot/Modules/Gambling/Common/AnimalRacing/Exceptions/AnimalRaceFullException.cs new file mode 100644 index 00000000..9c3e8f69 --- /dev/null +++ b/src/NadekoBot/Modules/Gambling/Common/AnimalRacing/Exceptions/AnimalRaceFullException.cs @@ -0,0 +1,8 @@ +using System; + +namespace NadekoBot.Modules.Gambling.Common.AnimalRacing.Exceptions +{ + public class AnimalRaceFullException : Exception + { + } +} diff --git a/src/NadekoBot/Modules/Gambling/Common/AnimalRacing/Exceptions/NotEnoughFundsException.cs b/src/NadekoBot/Modules/Gambling/Common/AnimalRacing/Exceptions/NotEnoughFundsException.cs new file mode 100644 index 00000000..1fb01093 --- /dev/null +++ b/src/NadekoBot/Modules/Gambling/Common/AnimalRacing/Exceptions/NotEnoughFundsException.cs @@ -0,0 +1,9 @@ +using System; + +namespace NadekoBot.Modules.Gambling.Common.AnimalRacing.Exceptions +{ + public class NotEnoughFundsException : Exception + { + + } +} diff --git a/src/NadekoBot/Modules/Games/Common/Hangman/TermPool.cs b/src/NadekoBot/Modules/Games/Common/Hangman/TermPool.cs index 3074aef7..94423b54 100644 --- a/src/NadekoBot/Modules/Games/Common/Hangman/TermPool.cs +++ b/src/NadekoBot/Modules/Games/Common/Hangman/TermPool.cs @@ -12,12 +12,12 @@ namespace NadekoBot.Modules.Games.Common.Hangman public class TermPool { const string termsPath = "data/hangman3.json"; - public static IReadOnlyDictionary data { get; } = new Dictionary(); + public static IReadOnlyDictionary Data { get; } = new Dictionary(); static TermPool() { try { - data = JsonConvert.DeserializeObject>(File.ReadAllText(termsPath)); + Data = JsonConvert.DeserializeObject>(File.ReadAllText(termsPath)); } catch (Exception) { @@ -35,11 +35,11 @@ namespace NadekoBot.Modules.Games.Common.Hangman if (type == TermType.Random) { - var keys = data.Keys.ToArray(); + var keys = Data.Keys.ToArray(); type = _termTypes[rng.Next(0, _termTypes.Length - 1)]; // - 1 because last one is 'all' } - if (!data.TryGetValue(type.ToString(), out var termTypes) || termTypes.Length == 0) + if (!Data.TryGetValue(type.ToString(), out var termTypes) || termTypes.Length == 0) throw new TermNotFoundException(); var obj = termTypes[rng.Next(0, termTypes.Length)]; diff --git a/src/NadekoBot/Modules/Games/Common/Poll.cs b/src/NadekoBot/Modules/Games/Common/Poll.cs index 816e2101..cf051a07 100644 --- a/src/NadekoBot/Modules/Games/Common/Poll.cs +++ b/src/NadekoBot/Modules/Games/Common/Poll.cs @@ -11,12 +11,11 @@ using NadekoBot.Services.Impl; namespace NadekoBot.Modules.Games.Common { - //todo 75 rewrite public class Poll { private readonly IUserMessage _originalMessage; private readonly IGuild _guild; - private string[] answers { get; } + private readonly string[] answers; private readonly ConcurrentDictionary _participants = new ConcurrentDictionary(); private readonly string _question; private readonly DiscordSocketClient _client; diff --git a/src/NadekoBot/Modules/Games/HangmanCommands.cs b/src/NadekoBot/Modules/Games/HangmanCommands.cs index ce76cc37..f7098f8e 100644 --- a/src/NadekoBot/Modules/Games/HangmanCommands.cs +++ b/src/NadekoBot/Modules/Games/HangmanCommands.cs @@ -29,7 +29,7 @@ namespace NadekoBot.Modules.Games [RequireContext(ContextType.Guild)] public async Task Hangmanlist() { - await Context.Channel.SendConfirmAsync(Format.Code(GetText("hangman_types", Prefix)) + "\n" + string.Join(", ", TermPool.data.Keys)); + await Context.Channel.SendConfirmAsync(Format.Code(GetText("hangman_types", Prefix)) + "\n" + string.Join(", ", TermPool.Data.Keys)); } [NadekoCommand, Usage, Description, Aliases] diff --git a/src/NadekoBot/_strings/ResponseStrings.en-US.json b/src/NadekoBot/_strings/ResponseStrings.en-US.json index 30a1d23f..5e1b3ead 100644 --- a/src/NadekoBot/_strings/ResponseStrings.en-US.json +++ b/src/NadekoBot/_strings/ResponseStrings.en-US.json @@ -1,27 +1,4 @@ { - "clashofclans_base_already_claimed": "That base is already claimed or destroyed.", - "clashofclans_base_already_destroyed": "That base is already destroyed.", - "clashofclans_base_already_unclaimed": "That base is not claimed.", - "clashofclans_base_destroyed": "**DESTROYED** base #{0} in a war against {1}", - "clashofclans_base_unclaimed": "{0} has **UNCLAIMED** base #{1} in a war against {2}", - "clashofclans_claimed_base": "{0} claimed a base #{1} in a war against {2}", - "clashofclans_claimed_other": "@{0} You already claimed base #{1}. You can't claim a new one.", - "clashofclans_claim_expired": "Claim from @{0} in a war against {1} has expired.", - "clashofclans_enemy": "Enemy", - "clashofclans_info_about_war": "Info about war against {0}", - "clashofclans_invalid_base_number": "Invalid base number.", - "clashofclans_invalid_size": "Not a valid war size.", - "clashofclans_list_active_wars": "List of active wars", - "clashofclans_not_claimed": "not claimed", - "clashofclans_not_partic": "You are not participating in that war.", - "clashofclans_not_partic_or_destroyed": "@{0} You are either not participating in that war, or that base is already destroyed.", - "clashofclans_no_active_wars": "No active war.", - "clashofclans_size": "Size", - "clashofclans_war_already_started": "War against {0} has already started.", - "clashofclans_war_created": "War against {0} created.", - "clashofclans_war_ended": "War against {0} ended.", - "clashofclans_war_not_exist": "That war does not exist.", - "clashofclans_war_started": "War against {0} started!", "customreactions_all_stats_cleared": "All custom reaction stats cleared.", "customreactions_deleted": "Custom Reaction deleted", "customreactions_insuff_perms": "Insufficient permissions. Requires Bot ownership for global custom reactions, and Administrator for server custom reactions.", From e0be610ec0f83c6467591b498ca86143c853eb1a Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Wed, 2 Aug 2017 22:07:19 +0200 Subject: [PATCH 239/346] .wheel command added (wheel of fortune gambling) --- .../Administration/ProtectionCommands.cs | 5 +- .../Common/WheelOfFortune/WheelOfFortune.cs | 45 +++++++++++ .../Gambling/WheelOfFortuneCommands.cs | 81 +++++++++++++++++++ .../Utility/Services/ConverterService.cs | 3 +- src/NadekoBot/Resources/CommandStrings.resx | 9 +++ 5 files changed, 141 insertions(+), 2 deletions(-) create mode 100644 src/NadekoBot/Modules/Gambling/Common/WheelOfFortune/WheelOfFortune.cs create mode 100644 src/NadekoBot/Modules/Gambling/WheelOfFortuneCommands.cs diff --git a/src/NadekoBot/Modules/Administration/ProtectionCommands.cs b/src/NadekoBot/Modules/Administration/ProtectionCommands.cs index 72e68bf9..ea97a089 100644 --- a/src/NadekoBot/Modules/Administration/ProtectionCommands.cs +++ b/src/NadekoBot/Modules/Administration/ProtectionCommands.cs @@ -112,10 +112,13 @@ namespace NadekoBot.Modules.Administration [NadekoCommand, Usage, Description, Aliases] [RequireContext(ContextType.Guild)] [RequireUserPermission(GuildPermission.Administrator)] - public async Task AntiSpam(int messageCount = 3, PunishmentAction action = PunishmentAction.Mute) + public async Task AntiSpam(int messageCount = 3, PunishmentAction action = PunishmentAction.Mute, int time = 0) { if (messageCount < 2 || messageCount > 10) return; + + if (time < 0 || time > 60 * 12) + return; if (_service.AntiSpamGuilds.TryRemove(Context.Guild.Id, out var removed)) { diff --git a/src/NadekoBot/Modules/Gambling/Common/WheelOfFortune/WheelOfFortune.cs b/src/NadekoBot/Modules/Gambling/Common/WheelOfFortune/WheelOfFortune.cs new file mode 100644 index 00000000..f99a969a --- /dev/null +++ b/src/NadekoBot/Modules/Gambling/Common/WheelOfFortune/WheelOfFortune.cs @@ -0,0 +1,45 @@ +using NadekoBot.Common; +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace NadekoBot.Modules.Gambling.Common.WheelOfFortune +{ + public class WheelOfFortune + { + private static readonly NadekoRandom _rng = new NadekoRandom(); + + private static readonly ImmutableArray _emojis = new string[] { + "⬆", + "↖", + "⬅", + "↙", + "⬇", + "↘", + "➡", + "↗" }.ToImmutableArray(); + + public static readonly ImmutableArray Multipliers = new float[] { + 1.7f, + 1.5f, + 0.2f, + 0.1f, + 0.3f, + 0.5f, + 1.2f, + 2.4f, + }.ToImmutableArray(); + + public int Result { get; } + public string Emoji => _emojis[Result]; + public float Multiplier => Multipliers[Result]; + + public WheelOfFortune() + { + this.Result = _rng.Next(0, 8); + } + } +} diff --git a/src/NadekoBot/Modules/Gambling/WheelOfFortuneCommands.cs b/src/NadekoBot/Modules/Gambling/WheelOfFortuneCommands.cs new file mode 100644 index 00000000..cd3de369 --- /dev/null +++ b/src/NadekoBot/Modules/Gambling/WheelOfFortuneCommands.cs @@ -0,0 +1,81 @@ +using Discord; +using Discord.Commands; +using NadekoBot.Common.Attributes; +using NadekoBot.Extensions; +using NadekoBot.Modules.Gambling.Common.WheelOfFortune; +using NadekoBot.Services; +using System.Threading.Tasks; +using Wof = NadekoBot.Modules.Gambling.Common.WheelOfFortune.WheelOfFortune; + +namespace NadekoBot.Modules.Gambling +{ + public partial class Gambling + { + public class WheelOfFortuneCommands : NadekoSubmodule + { + private readonly CurrencyService _cs; + private readonly IBotConfigProvider _bc; + + public WheelOfFortuneCommands(CurrencyService cs, IBotConfigProvider bc) + { + _cs = cs; + _bc = bc; + } + + [NadekoCommand, Usage, Description, Aliases] + [RequireContext(ContextType.Guild)] + public async Task WheelOfFortune(int bet) + { + const int minBet = 10; + if (bet < minBet) + { + await ReplyErrorLocalized("min_bet_limit", minBet + _bc.BotConfig.CurrencySign).ConfigureAwait(false); + return; + } + + if (!await _cs.RemoveAsync(Context.User.Id, "Wheel Of Fortune - bet", bet).ConfigureAwait(false)) + { + await ReplyErrorLocalized("not_enough", _bc.BotConfig.CurrencySign).ConfigureAwait(false); + return; + } + + var wof = new WheelOfFortune(); + + var amount = (int)(bet * wof.Multiplier); + + if (amount > 0) + await _cs.AddAsync(Context.User.Id, "Wheel Of Fortune - won", amount).ConfigureAwait(false); + + await Context.Channel.SendConfirmAsync( +Format.Bold($@"{Context.User.ToString()} won: {amount + _bc.BotConfig.CurrencySign} + + 『{Wof.Multipliers[1]}』 『{Wof.Multipliers[0]}』 『{Wof.Multipliers[7]}』 + +『{Wof.Multipliers[2]}』 {wof.Emoji} 『{Wof.Multipliers[6]}』 + + 『{Wof.Multipliers[3]}』 『{Wof.Multipliers[4]}』 『{Wof.Multipliers[5]}』")).ConfigureAwait(false); + } + + //[NadekoCommand, Usage, Description, Aliases] + //[RequireContext(ContextType.Guild)] + //public async Task WofTest(int length = 1000) + //{ + // var mults = new Dictionary(); + // for (int i = 0; i < length; i++) + // { + // var x = new Wof(); + // if (mults.ContainsKey(x.Multiplier)) + // ++mults[x.Multiplier]; + // else + // mults.Add(x.Multiplier, 1); + // } + + // var payout = mults.Sum(x => x.Key * x.Value); + // await Context.Channel.SendMessageAsync($"Total bet: {length}\n" + + // $"Paid out: {payout}\n" + + // $"Total Payout: {payout / length:F3}x") + // .ConfigureAwait(false); + //} + } + } +} \ No newline at end of file diff --git a/src/NadekoBot/Modules/Utility/Services/ConverterService.cs b/src/NadekoBot/Modules/Utility/Services/ConverterService.cs index d2573421..49bfd43e 100644 --- a/src/NadekoBot/Modules/Utility/Services/ConverterService.cs +++ b/src/NadekoBot/Modules/Utility/Services/ConverterService.cs @@ -96,7 +96,8 @@ namespace NadekoBot.Modules.Utility.Services using (var uow = _db.UnitOfWork) { - uow.ConverterUnits.RemoveRange(toRemove.ToArray()); + if(toRemove.Any()) + uow.ConverterUnits.RemoveRange(toRemove.ToArray()); uow.ConverterUnits.Add(baseType); uow.ConverterUnits.AddRange(range); diff --git a/src/NadekoBot/Resources/CommandStrings.resx b/src/NadekoBot/Resources/CommandStrings.resx index 957a2efa..6900c091 100644 --- a/src/NadekoBot/Resources/CommandStrings.resx +++ b/src/NadekoBot/Resources/CommandStrings.resx @@ -1314,6 +1314,15 @@ `{0}br 5` + + wheeloffortune wheel + + + Bets a certain amount of currency on the wheel of fortune. Wheel can stop on one of many different multipliers. Won amount is rounded down to the nearest whole number. + + + `{0}wheel 5` + leaderboard lb From f3984c824ec09fb6271dc0d07462d877a3cf0119 Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Thu, 3 Aug 2017 00:29:38 +0200 Subject: [PATCH 240/346] Nunchi game added. A bit confusing now. Will be polished further tomorrow. --- .../Modules/Games/Common/Nunchi/Nunchi.cs | 170 ++++++++++++++++++ src/NadekoBot/Modules/Games/Games.cs | 1 - src/NadekoBot/Modules/Games/NunchiCommands.cs | 121 +++++++++++++ src/NadekoBot/Resources/CommandStrings.resx | 9 + .../_strings/ResponseStrings.en-US.json | 10 ++ 5 files changed, 310 insertions(+), 1 deletion(-) create mode 100644 src/NadekoBot/Modules/Games/Common/Nunchi/Nunchi.cs create mode 100644 src/NadekoBot/Modules/Games/NunchiCommands.cs diff --git a/src/NadekoBot/Modules/Games/Common/Nunchi/Nunchi.cs b/src/NadekoBot/Modules/Games/Common/Nunchi/Nunchi.cs new file mode 100644 index 00000000..0cf517db --- /dev/null +++ b/src/NadekoBot/Modules/Games/Common/Nunchi/Nunchi.cs @@ -0,0 +1,170 @@ +using NadekoBot.Common; +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace NadekoBot.Modules.Games.Common.Nunchi +{ + public class Nunchi : IDisposable + { + public enum Phase + { + Joining, + Playing, + Ended, + } + + public int CurrentNumber { get; private set; } = new NadekoRandom().Next(0, 100); + public Phase CurrentPhase { get; private set; } = Phase.Joining; + + public event Func OnGameStarted; + public event Func OnRoundStarted; + public event Func OnUserGuessed; + public event Func OnRoundEnded; // tuple of the user who failed + public event Func OnGameEnded; // name of the user who won + + private readonly SemaphoreSlim _locker = new SemaphoreSlim(1, 1); + + private HashSet<(ulong Id, string Name)> _participants = new HashSet<(ulong Id, string Name)>(); + private HashSet<(ulong Id, string Name)> _passed = new HashSet<(ulong Id, string Name)>(); + + public ImmutableArray<(ulong Id, string Name)> Participants => _participants.ToImmutableArray(); + public int ParticipantCount => _participants.Count; + + private const int _killTimeout = 20 * 1000; + private Timer _killTimer; + + public Nunchi(ulong creatorId, string creatorName) + { + _participants.Add((creatorId, creatorName)); + } + + public async Task Join(ulong userId, string userName) + { + await _locker.WaitAsync().ConfigureAwait(false); + try + { + if (CurrentPhase != Phase.Joining) + return false; + + return _participants.Add((userId, userName)); + } + finally { _locker.Release(); } + } + + public async Task Initialize() + { + CurrentPhase = Phase.Joining; + await Task.Delay(30000).ConfigureAwait(false); + await _locker.WaitAsync().ConfigureAwait(false); + try + { + if (_participants.Count < 2) + { + CurrentPhase = Phase.Ended; + return false; + } + + _killTimer = new Timer(async state => + { + await _locker.WaitAsync().ConfigureAwait(false); + try + { + if (CurrentPhase != Phase.Playing) + return; + + //if some players took too long to type a number, boot them all out and start a new round + _participants = new HashSet<(ulong, string)>(_passed); + EndRound(); + } + finally { _locker.Release(); } + }, null, _killTimeout, _killTimeout); + + CurrentPhase = Phase.Playing; + var _ = OnGameStarted?.Invoke(this); + var __ = OnRoundStarted?.Invoke(this); + return true; + } + finally { _locker.Release(); } + } + + public async Task Input(ulong userId, string userName, int input) + { + await _locker.WaitAsync().ConfigureAwait(false); + try + { + if (CurrentPhase != Phase.Playing) + return; + + var userTuple = (Id: userId, Name: userName); + + // if the user is not a member of the race, + // or he already successfully typed the number + // ignore the input + if (!_participants.Contains(userTuple) || !_passed.Add(userTuple)) + return; + + //if the number is correct + if (CurrentNumber == input - 1) + { + //increment current number + ++CurrentNumber; + if (_passed.Count == _participants.Count - 1) + { + // if only n players are left, and n - 1 type the correct number, round is over + + // if only 2 players are left, game is over + if (_participants.Count == 2) + { + CurrentPhase = Phase.Ended; + var _ = OnGameEnded?.Invoke(this, userTuple.Name); + } + else // else just start the new round without the user who was the last + { + var failure = _participants.Except(_passed).First(); + EndRound(failure); + } + } + var __ = OnUserGuessed?.Invoke(this); + } + else + { + //if the user failed + + EndRound(userTuple); + } + } + finally { _locker.Release(); } + } + + private void EndRound((ulong, string)? failure = null) + { + _killTimer.Change(_killTimeout, _killTimeout); + CurrentNumber = new NadekoRandom().Next(0, 100); // reset the counter + _passed.Clear(); // reset all users who passed (new round starts) + if(failure != null) + _participants.Remove(failure.Value); // remove the dude who failed from the list of players + + var __ = OnRoundEnded?.Invoke(this, failure); + if (_participants.Count <= 1) // means we have a winner or everyone was booted out + { + CurrentPhase = Phase.Ended; + var _ = OnGameEnded?.Invoke(this, _participants.Count > 0 ? _participants.First().Name : null); + return; + } + var ___ = OnRoundStarted?.Invoke(this); + } + + public void Dispose() + { + OnGameEnded = null; + OnGameStarted = null; + OnRoundEnded = null; + OnRoundStarted = null; + OnUserGuessed = null; + } + } +} \ No newline at end of file diff --git a/src/NadekoBot/Modules/Games/Games.cs b/src/NadekoBot/Modules/Games/Games.cs index c5cc0b13..1b8898c9 100644 --- a/src/NadekoBot/Modules/Games/Games.cs +++ b/src/NadekoBot/Modules/Games/Games.cs @@ -16,7 +16,6 @@ namespace NadekoBot.Modules.Games - Shiritori - Simple RPG adventure - The nunchi game - - Wheel of fortune - Connect 4 */ public partial class Games : NadekoTopLevelModule diff --git a/src/NadekoBot/Modules/Games/NunchiCommands.cs b/src/NadekoBot/Modules/Games/NunchiCommands.cs new file mode 100644 index 00000000..9f2aa58a --- /dev/null +++ b/src/NadekoBot/Modules/Games/NunchiCommands.cs @@ -0,0 +1,121 @@ +using Discord; +using Discord.Commands; +using Discord.WebSocket; +using NadekoBot.Common.Attributes; +using NadekoBot.Modules.Games.Common.Nunchi; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace NadekoBot.Modules.Games +{ + public partial class Games + { + public class NunchiCommands : NadekoSubmodule + { + public static readonly ConcurrentDictionary Games = new ConcurrentDictionary(); + private readonly DiscordSocketClient _client; + + public NunchiCommands(DiscordSocketClient client) + { + _client = client; + } + + [NadekoCommand, Usage, Description, Aliases] + [RequireContext(ContextType.Guild)] + public async Task Nunchi() + { + var newNunchi = new Nunchi(Context.User.Id, Context.User.ToString()); + Nunchi nunchi; + + //if a game was already active + if ((nunchi = Games.GetOrAdd(Context.Guild.Id, newNunchi)) != newNunchi) + { + // join it + if (!await nunchi.Join(Context.User.Id, Context.User.ToString())) + { + // if you failed joining, that means game is running or just ended + // await ReplyErrorLocalized("nunchi_already_started").ConfigureAwait(false); + return; + } + + await ReplyConfirmLocalized("nunchi_joined", nunchi.ParticipantCount).ConfigureAwait(false); + return; + } + + + try { await ReplyConfirmLocalized("nunchi_created").ConfigureAwait(false); } catch { } + + nunchi.OnGameEnded += Nunchi_OnGameEnded; + //nunchi.OnGameStarted += Nunchi_OnGameStarted; + nunchi.OnRoundEnded += Nunchi_OnRoundEnded; + nunchi.OnUserGuessed += Nunchi_OnUserGuessed; + nunchi.OnRoundStarted += Nunchi_OnRoundStarted; + _client.MessageReceived += _client_MessageReceived; + + var success = await nunchi.Initialize().ConfigureAwait(false); + if (!success) + { + if (Games.TryRemove(Context.Guild.Id, out var game)) + game.Dispose(); + await ReplyErrorLocalized("nunchi_failed_to_start").ConfigureAwait(false); + } + + async Task _client_MessageReceived(SocketMessage arg) + { + if (arg.Channel.Id != Context.Channel.Id) + return; + + if (!int.TryParse(arg.Content, out var number)) + return; + try + { + await nunchi.Input(arg.Author.Id, arg.Author.ToString(), number).ConfigureAwait(false); + } + catch (Exception ex) + { + Console.WriteLine(ex); + } + } + } + + private Task Nunchi_OnRoundStarted(Nunchi arg) + { + return ReplyConfirmLocalized("nunchi_round_started", Format.Bold(arg.CurrentNumber.ToString())); + } + + private Task Nunchi_OnUserGuessed(Nunchi arg) + { + return ReplyConfirmLocalized("nunchi_next_number", Format.Bold(arg.CurrentNumber.ToString())); + } + + private Task Nunchi_OnRoundEnded(Nunchi arg1, (ulong Id, string Name)? arg2) + { + if(arg2.HasValue) + return ReplyConfirmLocalized("nunchi_round_ended", Format.Bold(arg2.Value.Name)); + else + return ReplyConfirmLocalized("nunchi_round_ended_boot", + Format.Bold("\n" + string.Join("\n, ", arg1.Participants.Select(x => x.Name)))); // this won't work if there are too many users + } + + private Task Nunchi_OnGameStarted(Nunchi arg) + { + return ReplyConfirmLocalized("nunchi_started", Format.Bold(arg.ParticipantCount.ToString())); + } + + private Task Nunchi_OnGameEnded(Nunchi arg1, string arg2) + { + if (Games.TryRemove(Context.Guild.Id, out var game)) + game.Dispose(); + + if(arg2 == null) + return ReplyConfirmLocalized("nunchi_ended_no_winner", Format.Bold(arg2)); + else + return ReplyConfirmLocalized("nunchi_ended", Format.Bold(arg2)); + } + } + } +} \ No newline at end of file diff --git a/src/NadekoBot/Resources/CommandStrings.resx b/src/NadekoBot/Resources/CommandStrings.resx index 6900c091..51fe44fb 100644 --- a/src/NadekoBot/Resources/CommandStrings.resx +++ b/src/NadekoBot/Resources/CommandStrings.resx @@ -1269,6 +1269,15 @@ `{0}jr` or `{0}jr 5` + + nunchi + + + Creates or joins a nunchi game. Minimum 3 users required. + + + `{0}nunchi` + raffle diff --git a/src/NadekoBot/_strings/ResponseStrings.en-US.json b/src/NadekoBot/_strings/ResponseStrings.en-US.json index 5e1b3ead..c96f9a68 100644 --- a/src/NadekoBot/_strings/ResponseStrings.en-US.json +++ b/src/NadekoBot/_strings/ResponseStrings.en-US.json @@ -363,6 +363,16 @@ "games_leaderboard": "Leaderboard", "games_not_enough": "You don't have enough {0}", "games_no_results": "No results", + "games_nunchi_joined": "Joined nunchi game. {0} users joined so far.", + "games_nunchi_ended": "Nunchi game ended. {0} won", + "games_nunchi_ended_no_winner": "Nunchi game ended with no winner.", + "games_nunchi_started": "Nunchi game started with {0} participants.", + "games_nunchi_round_ended": "Nunchi round ended. {0} is out of the game.", + "games_nunchi_round_ended_boot": "Nunchi round ended due to timeout of some users. These users are still in the game: {0}", + "games_nunchi_round_started": "Nunchi round started. Start counting from the number {0}.", + "games_nunchi_next_number": "Number registered. Last number was {0}.", + "games_nunchi_failed_to_start": "Nunchi failed to start because there were not enough participants.", + "games_nunchi_created": "Nunchi game created. Waiting for users to join.", "games_picked": "picked {0}", "games_planted": "{0} planted {1}", "games_trivia_already_running": "Trivia game is already running on this server.", From d9a446d87408960f80b6f3d52d7720befa73d719 Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Thu, 3 Aug 2017 20:19:09 +0200 Subject: [PATCH 241/346] Nunchi is much more fair and forgiving now. There are 5 seconds delays between rounds, and only one player can fail per round (multiple users can still get booted if they're inactive) --- .../Modules/Games/Common/Nunchi/Nunchi.cs | 15 +++++++++++++-- src/NadekoBot/Modules/Games/NunchiCommands.cs | 18 +++++++++--------- src/NadekoBot/Resources/CommandStrings.resx | 2 +- 3 files changed, 23 insertions(+), 12 deletions(-) diff --git a/src/NadekoBot/Modules/Games/Common/Nunchi/Nunchi.cs b/src/NadekoBot/Modules/Games/Common/Nunchi/Nunchi.cs index 0cf517db..dd489bf0 100644 --- a/src/NadekoBot/Modules/Games/Common/Nunchi/Nunchi.cs +++ b/src/NadekoBot/Modules/Games/Common/Nunchi/Nunchi.cs @@ -14,6 +14,7 @@ namespace NadekoBot.Modules.Games.Common.Nunchi { Joining, Playing, + WaitingForNextRound, Ended, } @@ -35,6 +36,7 @@ namespace NadekoBot.Modules.Games.Common.Nunchi public int ParticipantCount => _participants.Count; private const int _killTimeout = 20 * 1000; + private const int _nextRoundTimeout = 5 * 1000; private Timer _killTimer; public Nunchi(ulong creatorId, string creatorName) @@ -62,7 +64,7 @@ namespace NadekoBot.Modules.Games.Common.Nunchi await _locker.WaitAsync().ConfigureAwait(false); try { - if (_participants.Count < 2) + if (_participants.Count < 3) { CurrentPhase = Phase.Ended; return false; @@ -119,6 +121,7 @@ namespace NadekoBot.Modules.Games.Common.Nunchi // if only 2 players are left, game is over if (_participants.Count == 2) { + _killTimer.Change(Timeout.Infinite, Timeout.Infinite); CurrentPhase = Phase.Ended; var _ = OnGameEnded?.Invoke(this, userTuple.Name); } @@ -151,11 +154,19 @@ namespace NadekoBot.Modules.Games.Common.Nunchi var __ = OnRoundEnded?.Invoke(this, failure); if (_participants.Count <= 1) // means we have a winner or everyone was booted out { + _killTimer.Change(Timeout.Infinite, Timeout.Infinite); CurrentPhase = Phase.Ended; var _ = OnGameEnded?.Invoke(this, _participants.Count > 0 ? _participants.First().Name : null); return; } - var ___ = OnRoundStarted?.Invoke(this); + CurrentPhase = Phase.WaitingForNextRound; + var throwawayDelay = Task.Run(async () => + { + await Task.Delay(_nextRoundTimeout).ConfigureAwait(false); + CurrentPhase = Phase.Playing; + var ___ = OnRoundStarted?.Invoke(this); + }); + } public void Dispose() diff --git a/src/NadekoBot/Modules/Games/NunchiCommands.cs b/src/NadekoBot/Modules/Games/NunchiCommands.cs index 9f2aa58a..3f4fdc7d 100644 --- a/src/NadekoBot/Modules/Games/NunchiCommands.cs +++ b/src/NadekoBot/Modules/Games/NunchiCommands.cs @@ -47,7 +47,7 @@ namespace NadekoBot.Modules.Games } - try { await ReplyConfirmLocalized("nunchi_created").ConfigureAwait(false); } catch { } + try { await ConfirmLocalized("nunchi_created").ConfigureAwait(false); } catch { } nunchi.OnGameEnded += Nunchi_OnGameEnded; //nunchi.OnGameStarted += Nunchi_OnGameStarted; @@ -61,7 +61,7 @@ namespace NadekoBot.Modules.Games { if (Games.TryRemove(Context.Guild.Id, out var game)) game.Dispose(); - await ReplyErrorLocalized("nunchi_failed_to_start").ConfigureAwait(false); + await ConfirmLocalized("nunchi_failed_to_start").ConfigureAwait(false); } async Task _client_MessageReceived(SocketMessage arg) @@ -84,26 +84,26 @@ namespace NadekoBot.Modules.Games private Task Nunchi_OnRoundStarted(Nunchi arg) { - return ReplyConfirmLocalized("nunchi_round_started", Format.Bold(arg.CurrentNumber.ToString())); + return ConfirmLocalized("nunchi_round_started", Format.Bold(arg.CurrentNumber.ToString())); } private Task Nunchi_OnUserGuessed(Nunchi arg) { - return ReplyConfirmLocalized("nunchi_next_number", Format.Bold(arg.CurrentNumber.ToString())); + return ConfirmLocalized("nunchi_next_number", Format.Bold(arg.CurrentNumber.ToString())); } private Task Nunchi_OnRoundEnded(Nunchi arg1, (ulong Id, string Name)? arg2) { if(arg2.HasValue) - return ReplyConfirmLocalized("nunchi_round_ended", Format.Bold(arg2.Value.Name)); + return ConfirmLocalized("nunchi_round_ended", Format.Bold(arg2.Value.Name)); else - return ReplyConfirmLocalized("nunchi_round_ended_boot", + return ConfirmLocalized("nunchi_round_ended_boot", Format.Bold("\n" + string.Join("\n, ", arg1.Participants.Select(x => x.Name)))); // this won't work if there are too many users } private Task Nunchi_OnGameStarted(Nunchi arg) { - return ReplyConfirmLocalized("nunchi_started", Format.Bold(arg.ParticipantCount.ToString())); + return ConfirmLocalized("nunchi_started", Format.Bold(arg.ParticipantCount.ToString())); } private Task Nunchi_OnGameEnded(Nunchi arg1, string arg2) @@ -112,9 +112,9 @@ namespace NadekoBot.Modules.Games game.Dispose(); if(arg2 == null) - return ReplyConfirmLocalized("nunchi_ended_no_winner", Format.Bold(arg2)); + return ConfirmLocalized("nunchi_ended_no_winner", Format.Bold(arg2)); else - return ReplyConfirmLocalized("nunchi_ended", Format.Bold(arg2)); + return ConfirmLocalized("nunchi_ended", Format.Bold(arg2)); } } } diff --git a/src/NadekoBot/Resources/CommandStrings.resx b/src/NadekoBot/Resources/CommandStrings.resx index 51fe44fb..6336b76f 100644 --- a/src/NadekoBot/Resources/CommandStrings.resx +++ b/src/NadekoBot/Resources/CommandStrings.resx @@ -1273,7 +1273,7 @@ nunchi - Creates or joins a nunchi game. Minimum 3 users required. + Creates or joins an existing nunchi game. Users have to count up by 1 from the starting number shown by the bot. If someone makes a mistake (types an incorrent number, or repeats the same number) they are out of the game and a new round starts without them. Minimum 3 users required. `{0}nunchi` From 94e4c89564f1c98bbdaf9954de6ca3658aed0519 Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Thu, 3 Aug 2017 20:19:43 +0200 Subject: [PATCH 242/346] Updated commandlist --- docs/Commands List.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/Commands List.md b/docs/Commands List.md index 4c9001a8..0d784a0e 100644 --- a/docs/Commands List.md +++ b/docs/Commands List.md @@ -126,6 +126,7 @@ Commands and aliases | Description | Usage `.listcustreactg` `.lcrg` | Lists global or server custom reactions (20 commands per page) grouped by trigger, and show a number of responses for each. Running the command in DM will list global custom reactions, while running it in server will list that server's custom reactions. | `.lcrg 1` `.showcustreact` `.scr` | Shows a custom reaction's response on a given ID. | `.scr 1` `.delcustreact` `.dcr` | Deletes a custom reaction on a specific index. If ran in DM, it is bot owner only and deletes a global custom reaction. If ran in a server, it requires Administration privileges and removes server custom reaction. | `.dcr 5` +`.crca` | Toggles whether the custom reaction will trigger if the triggering message contains the keyword (instead of only starting with it). | `.crca 44` `.crdm` | Toggles whether the response message of the custom reaction will be sent as a direct message. | `.crdm 44` `.crad` | Toggles whether the message triggering the custom reaction will be automatically deleted. | `.crad 59` `.crstatsclear` | Resets the counters on `.crstats`. You can specify a trigger to clear stats only for that trigger. **Bot owner only** | `.crstatsclear` or `.crstatsclear rng` @@ -167,6 +168,7 @@ Commands and aliases | Description | Usage `.affinity` | Sets your affinity towards someone you want to be claimed by. Setting affinity will reduce their `.claim` on you by 20%. You can leave second argument empty to clear your affinity. 30 minutes cooldown. | `.affinity @MyHusband` or `.affinity` `.waifus` `.waifulb` | Shows top 9 waifus. You can specify another page to show other waifus. | `.waifus` or `.waifulb 3` `.waifuinfo` `.waifustats` | Shows waifu stats for a target person. Defaults to you if no user is provided. | `.waifuinfo @MyCrush` or `.waifuinfo` +`.wheeloffortune` `.wheel` | Bets a certain amount of currency on the wheel of fortune. Wheel can stop on one of many different multipliers. Won amount is rounded down to the nearest whole number. | `.wheel 5` ###### [Back to ToC](#table-of-contents) @@ -184,6 +186,7 @@ Commands and aliases | Description | Usage `.hangmanlist` | Shows a list of hangman term types. | `.hangmanlist` `.hangman` | Starts a game of hangman in the channel. Use `.hangmanlist` to see a list of available term types. Defaults to 'all'. | `.hangman` or `.hangman movies` `.hangmanstop` | Stops the active hangman game on this channel if it exists. | `.hangmanstop` +`.nunchi` | Creates or joins an existing nunchi game. Users have to count up by 1 from the starting number shown by the bot. If someone makes a mistake (types an incorrent number, or repeats the same number) they are out of the game and a new round starts without them. Minimum 3 users required. | `.nunchi` `.pick` | Picks the currency planted in this channel. 60 seconds cooldown. | `.pick` `.plant` | Spend an amount of currency to plant it in this channel. Default is 1. (If bot is restarted or crashes, the currency will be lost) | `.plant` or `.plant 5` `.gencurrency` `.gc` | Toggles currency generation on this channel. Every posted message will have chance to spawn currency. Chance is specified by the Bot Owner. (default is 2%) **Requires ManageMessages server permission.** | `.gc` From ec7f69f1c08beafa2a8a152adec8eca73323f163 Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Fri, 4 Aug 2017 14:36:07 +0200 Subject: [PATCH 243/346] connect4 game added. --- .../Modules/Games/Common/Connect4/Connect4.cs | 356 ++++++++++++++++++ .../Modules/Games/Connect4Commands.cs | 180 +++++++++ src/NadekoBot/Modules/Games/NunchiCommands.cs | 51 +-- .../Searches/Common/SearchImageCacher.cs | 10 +- .../Searches/Services/SearchesService.cs | 1 - src/NadekoBot/Resources/CommandStrings.resx | 9 + .../_strings/ResponseStrings.en-US.json | 5 + 7 files changed, 584 insertions(+), 28 deletions(-) create mode 100644 src/NadekoBot/Modules/Games/Common/Connect4/Connect4.cs create mode 100644 src/NadekoBot/Modules/Games/Connect4Commands.cs diff --git a/src/NadekoBot/Modules/Games/Common/Connect4/Connect4.cs b/src/NadekoBot/Modules/Games/Common/Connect4/Connect4.cs new file mode 100644 index 00000000..0b247301 --- /dev/null +++ b/src/NadekoBot/Modules/Games/Common/Connect4/Connect4.cs @@ -0,0 +1,356 @@ +using NadekoBot.Common; +using System; +using System.Collections.Immutable; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace NadekoBot.Modules.Games.Common.Connect4 +{ + //todo: diagonal checking + public class Connect4Game : IDisposable + { + public enum Phase + { + Joining, // waiting for second player to join + P1Move, + P2Move, + Ended, + } + + public enum Field //temporary most likely + { + Empty, + P1, + P2, + } + + public enum Result + { + Draw, + CurrentPlayerWon, + OtherPlayerWon, + } + + public const int NumberOfColumns = 6; + public const int NumberOfRows = 7; + + public Phase CurrentPhase { get; private set; } = Phase.Joining; + + //state is bottom to top, left to right + private readonly Field[] _gameState = new Field[NumberOfRows * NumberOfColumns]; + private readonly (ulong UserId, string Username)?[] _players = new(ulong, string)?[2]; + + public ImmutableArray GameState => _gameState.ToImmutableArray(); + public ImmutableArray<(ulong UserId, string Username)?> Players => _players.ToImmutableArray(); + + public string CurrentPlayer => CurrentPhase == Phase.P1Move + ? _players[0].Value.Username + : _players[1].Value.Username; + + public string OtherPlayer => CurrentPhase == Phase.P2Move + ? _players[0].Value.Username + : _players[1].Value.Username; + + //public event Func OnGameStarted; + public event Func OnGameStateUpdated; + public event Func OnGameFailedToStart; + public event Func OnGameEnded; + + private readonly SemaphoreSlim _locker = new SemaphoreSlim(1, 1); + private readonly NadekoRandom _rng; + + private Timer _playerTimeoutTimer; + + /* rows = 4, columns = 3, total = 12 + * [][][][][][] + * [][][][][][] + * [][][][][][] + * [][][][][][] + * [][][][][][] + * [][][][][][] + * [][][][][][] + * */ + + public Connect4Game(ulong userId, string userName) + { + _players[0] = (userId, userName); + + _rng = new NadekoRandom(); + for (int i = 0; i < NumberOfColumns * NumberOfRows; i++) + { + _gameState[i] = Field.Empty; + } + } + + public void Initialize() + { + var _ = Task.Run(async () => + { + await Task.Delay(15000).ConfigureAwait(false); + await _locker.WaitAsync().ConfigureAwait(false); + try + { + if (_players[1] == null) + { + var __ = OnGameFailedToStart?.Invoke(this); + CurrentPhase = Phase.Ended; + return; + } + } + finally { _locker.Release(); } + }); + } + + public async Task Join(ulong userId, string userName) + { + await _locker.WaitAsync().ConfigureAwait(false); + try + { + if (CurrentPhase != Phase.Joining) //can't join if its not a joining phase + return false; + + if (_players[0].Value.UserId == userId) // same user can't join own game + return false; + + if (_rng.Next(0, 2) == 0) //rolling from 0-1, if number is 0, join as first player + { + _players[1] = _players[0]; + _players[0] = (userId, userName); + } + else //else join as a second player + _players[1] = (userId, userName); + + CurrentPhase = Phase.P1Move; //start the game + _playerTimeoutTimer = new Timer(async state => + { + await _locker.WaitAsync().ConfigureAwait(false); + try + { + EndGame(Result.OtherPlayerWon); + } + finally { _locker.Release(); } + }, null, TimeSpan.FromSeconds(30), TimeSpan.FromSeconds(30)); + var __ = OnGameStateUpdated?.Invoke(this); + + return true; + } + finally { _locker.Release(); } + } + + public async Task Input(ulong userId, string userName, int inputCol) + { + await _locker.WaitAsync().ConfigureAwait(false); + try + { + inputCol -= 1; + if (CurrentPhase == Phase.Ended || CurrentPhase == Phase.Joining) + return false; + + if (!((_players[0].Value.UserId == userId && CurrentPhase == Phase.P1Move) + || (_players[1].Value.UserId == userId && CurrentPhase == Phase.P2Move))) + return false; + + if (inputCol < 0 || inputCol > NumberOfColumns) //invalid input + return false; + + if (IsColumnFull(inputCol)) //can't play there event? + return false; + + var start = NumberOfRows * inputCol; + for (int i = start; i < start + NumberOfRows; i++) + { + if (_gameState[i] == Field.Empty) + { + _gameState[i] = GetPlayerPiece(userId); + break; + } + } + + //check winnning condition + // ok, i'll go from [0-2] in rows (and through all columns) and check upward if 4 are connected + + for (int i = 0; i < NumberOfRows - 3; i++) + { + if (CurrentPhase == Phase.Ended) + break; + + for (int j = 0; j < NumberOfColumns; j++) + { + if (CurrentPhase == Phase.Ended) + break; + + var first = _gameState[i + j * NumberOfRows]; + if (first != Field.Empty) + { + //Console.WriteLine(i + j * NumberOfRows); + for (int k = 1; k < 4; k++) + { + var next = _gameState[i + k + j * NumberOfRows]; + if (next == first) + { + //Console.WriteLine(i + k + j * NumberOfRows); + if (k == 3) + EndGame(Result.CurrentPlayerWon); + else + continue; + } + else break; + } + } + } + } + + // i'll go [0-1] in columns (and through all rows) and check to the right if 4 are connected + for (int i = 0; i < NumberOfColumns - 3; i++) + { + if (CurrentPhase == Phase.Ended) + break; + + for (int j = 0; j < NumberOfRows; j++) + { + if (CurrentPhase == Phase.Ended) + break; + + var first = _gameState[j + i * NumberOfRows]; + if (first != Field.Empty) + { + for (int k = 1; k < 4; k++) + { + var next = _gameState[j + (i + k) * NumberOfRows]; + if (next == first) + if (k == 3) + EndGame(Result.CurrentPlayerWon); + else + continue; + else break; + } + } + } + } + + //need to check diagonal now + for (int col = 0; col < NumberOfColumns; col++) + { + if (CurrentPhase == Phase.Ended) + break; + + for (int row = 0; row < NumberOfRows; row++) + { + if (CurrentPhase == Phase.Ended) + break; + + var first = _gameState[row + col * NumberOfRows]; + + if (first != Field.Empty) + { + var same = 1; + + //top left + for (int i = 1; i < 4; i++) + { + //while going top left, rows are increasing, columns are decreasing + var curRow = row + i; + var curCol = col - i; + + //check if current values are in range + if (curRow >= NumberOfRows || curRow < 0) + break; + if (curCol < 0 || curCol >= NumberOfColumns) + break; + + var cur = _gameState[curRow + curCol * NumberOfRows]; + if (cur == first) + same++; + else break; + } + + if (same == 4) + { + EndGame(Result.CurrentPlayerWon); + break; + } + + //top right + for (int i = 1; i < 4; i++) + { + //while going top right, rows are increasing, columns are increasing + var curRow = row + i; + var curCol = col + i; + + //check if current values are in range + if (curRow >= NumberOfRows || curRow < 0) + break; + if (curCol < 0 || curCol >= NumberOfColumns) + break; + + var cur = _gameState[curRow + curCol * NumberOfRows]; + if (cur == first) + same++; + else break; + } + + if (same == 4) + { + EndGame(Result.CurrentPlayerWon); + break; + } + } + } + } + + //check draw? if it's even possible + if (_gameState.All(x => x != Field.Empty)) + { + EndGame(Result.Draw); + } + + if (CurrentPhase != Phase.Ended) + { + if (CurrentPhase == Phase.P1Move) + CurrentPhase = Phase.P2Move; + else + CurrentPhase = Phase.P1Move; + + ResetTimer(); + } + var _ = OnGameStateUpdated?.Invoke(this); + return true; + } + finally { _locker.Release(); } + } + + private void ResetTimer() + { + _playerTimeoutTimer.Change(TimeSpan.FromSeconds(30), TimeSpan.FromSeconds(30)); + } + + private void EndGame(Result result) + { + var _ = OnGameEnded?.Invoke(this, result); + CurrentPhase = Phase.Ended; + } + + private Field GetPlayerPiece(ulong userId) => _players[0].Value.UserId == userId + ? Field.P1 + : Field.P2; + + //column is full if there are no empty fields + private bool IsColumnFull(int column) + { + var start = NumberOfRows * column; + for (int i = start; i < start + NumberOfRows; i++) + { + if (_gameState[i] == Field.Empty) + return false; + } + return true; + } + + public void Dispose() + { + OnGameFailedToStart = null; + OnGameStateUpdated = null; + } + } +} diff --git a/src/NadekoBot/Modules/Games/Connect4Commands.cs b/src/NadekoBot/Modules/Games/Connect4Commands.cs new file mode 100644 index 00000000..81d03b4f --- /dev/null +++ b/src/NadekoBot/Modules/Games/Connect4Commands.cs @@ -0,0 +1,180 @@ +using Discord; +using Discord.Commands; +using Discord.WebSocket; +using NadekoBot.Common.Attributes; +using NadekoBot.Extensions; +using NadekoBot.Modules.Games.Common.Connect4; +using System.Collections.Concurrent; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace NadekoBot.Modules.Games +{ + public partial class Games + { + public class Connect4Commands : NadekoSubmodule + { + public static ConcurrentDictionary Games = new ConcurrentDictionary(); + private readonly DiscordSocketClient _client; + + //private readonly string[] numbers = new string[] { "⓪", " ①", "②", "③", "④", "⑤", "⑥", "⑦", "⑧", "⑨" }; + private readonly string[] numbers = new string[] { ":one:", ":two:", ":three:", ":four:", ":five:", ":six:", ":seven:", ":eight:"}; + + public Connect4Commands(DiscordSocketClient client) + { + _client = client; + } + + [NadekoCommand, Usage, Description, Aliases] + [RequireContext(ContextType.Guild)] + public async Task Connect4() + { + var newGame = new Connect4Game(Context.User.Id, Context.User.ToString()); + Connect4Game game; + if ((game = Games.GetOrAdd(Context.Channel.Id, newGame)) != newGame) + { + //means game already exists, try to join + var joined = await game.Join(Context.User.Id, Context.User.ToString()).ConfigureAwait(false); + return; + } + + game.OnGameStateUpdated += Game_OnGameStateUpdated; + game.OnGameFailedToStart += Game_OnGameFailedToStart; + game.OnGameEnded += Game_OnGameEnded; + _client.MessageReceived += _client_MessageReceived; + + game.Initialize(); + + await ReplyConfirmLocalized("connect4_created").ConfigureAwait(false); + + Task _client_MessageReceived(SocketMessage arg) + { + if (Context.Channel.Id != arg.Channel.Id) + return Task.CompletedTask; + + var _ = Task.Run(async () => + { + bool success = false; + if (int.TryParse(arg.Content, out var col)) + { + success = await game.Input(arg.Author.Id, arg.Author.ToString(), col).ConfigureAwait(false); + } + + if (success) + try { await arg.DeleteAsync().ConfigureAwait(false); } catch { } + else + { + if (game.CurrentPhase == Connect4Game.Phase.Joining + || game.CurrentPhase == Connect4Game.Phase.Ended) + { + return; + } + RepostCounter++; + if (RepostCounter == 0) + try { msg = await Context.Channel.SendMessageAsync("", embed: (Embed)msg.Embeds.First()); } catch { } + } + }); + return Task.CompletedTask; + } + + Task Game_OnGameFailedToStart(Connect4Game arg) + { + if (Games.TryRemove(Context.Channel.Id, out var toDispose)) + { + _client.MessageReceived -= _client_MessageReceived; + toDispose.Dispose(); + } + return ErrorLocalized("connect4_failed_to_start"); + } + + Task Game_OnGameEnded(Connect4Game arg, Connect4Game.Result result) + { + if (Games.TryRemove(Context.Channel.Id, out var toDispose)) + { + _client.MessageReceived -= _client_MessageReceived; + toDispose.Dispose(); + } + + string title; + if (result == Connect4Game.Result.CurrentPlayerWon) + { + title = GetText("connect4_won", Format.Bold(arg.CurrentPlayer), Format.Bold(arg.OtherPlayer)); + } + else if (result == Connect4Game.Result.OtherPlayerWon) + { + title = GetText("connect4_won", Format.Bold(arg.OtherPlayer), Format.Bold(arg.CurrentPlayer)); + } + else + title = GetText("connect4_draw"); + + return msg.ModifyAsync(x => x.Embed = new EmbedBuilder() + .WithTitle(title) + .WithDescription(GetGameStateText(game)) + .WithOkColor() + .Build()); + } + } + + private IUserMessage msg; + + private int _repostCounter = 0; + private int RepostCounter + { + get => _repostCounter; + set + { + if (value < 0 || value > 7) + _repostCounter = 0; + else _repostCounter = value; + } + } + + private async Task Game_OnGameStateUpdated(Connect4Game game) + { + var embed = new EmbedBuilder() + .WithTitle($"{game.CurrentPlayer} vs {game.OtherPlayer}") + .WithDescription(GetGameStateText(game)) + .WithOkColor(); + + + if (msg == null) + msg = await Context.Channel.EmbedAsync(embed).ConfigureAwait(false); + else + await msg.ModifyAsync(x => x.Embed = embed.Build()).ConfigureAwait(false); + } + + private string GetGameStateText(Connect4Game game) + { + var sb = new StringBuilder(); + + if (game.CurrentPhase == Connect4Game.Phase.P1Move || + game.CurrentPhase == Connect4Game.Phase.P2Move) + sb.AppendLine(GetText("connect4_player_to_move", Format.Bold(game.CurrentPlayer))); + + for (int i = Connect4Game.NumberOfRows; i > 0; i--) + { + for (int j = 0; j < Connect4Game.NumberOfColumns; j++) + { + //Console.WriteLine(i + (j * Connect4Game.NumberOfRows) - 1); + var cur = game.GameState[i + (j * Connect4Game.NumberOfRows) - 1]; + + if (cur == Connect4Game.Field.Empty) + sb.Append("⚫"); //black circle + else if (cur == Connect4Game.Field.P1) + sb.Append("🔴"); //red circle + else + sb.Append("🔵"); //blue circle + } + sb.AppendLine(); + } + + for (int i = 0; i < Connect4Game.NumberOfColumns; i++) + { + sb.Append(/*new string(' ', 1 + ((i + 1) / 2)) + */numbers[i]); + } + return sb.ToString(); + } + } + } +} diff --git a/src/NadekoBot/Modules/Games/NunchiCommands.cs b/src/NadekoBot/Modules/Games/NunchiCommands.cs index 3f4fdc7d..e5b3af96 100644 --- a/src/NadekoBot/Modules/Games/NunchiCommands.cs +++ b/src/NadekoBot/Modules/Games/NunchiCommands.cs @@ -64,21 +64,39 @@ namespace NadekoBot.Modules.Games await ConfirmLocalized("nunchi_failed_to_start").ConfigureAwait(false); } - async Task _client_MessageReceived(SocketMessage arg) + Task _client_MessageReceived(SocketMessage arg) { - if (arg.Channel.Id != Context.Channel.Id) - return; + var _ = Task.Run(async () => + { + if (arg.Channel.Id != Context.Channel.Id) + return; - if (!int.TryParse(arg.Content, out var number)) - return; - try + if (!int.TryParse(arg.Content, out var number)) + return; + try + { + await nunchi.Input(arg.Author.Id, arg.Author.ToString(), number).ConfigureAwait(false); + } + catch (Exception ex) + { + Console.WriteLine(ex); + } + }); + return Task.CompletedTask; + } + + Task Nunchi_OnGameEnded(Nunchi arg1, string arg2) + { + if (Games.TryRemove(Context.Guild.Id, out var game)) { - await nunchi.Input(arg.Author.Id, arg.Author.ToString(), number).ConfigureAwait(false); - } - catch (Exception ex) - { - Console.WriteLine(ex); + _client.MessageReceived -= _client_MessageReceived; + game.Dispose(); } + + if (arg2 == null) + return ConfirmLocalized("nunchi_ended_no_winner", Format.Bold(arg2)); + else + return ConfirmLocalized("nunchi_ended", Format.Bold(arg2)); } } @@ -105,17 +123,6 @@ namespace NadekoBot.Modules.Games { return ConfirmLocalized("nunchi_started", Format.Bold(arg.ParticipantCount.ToString())); } - - private Task Nunchi_OnGameEnded(Nunchi arg1, string arg2) - { - if (Games.TryRemove(Context.Guild.Id, out var game)) - game.Dispose(); - - if(arg2 == null) - return ConfirmLocalized("nunchi_ended_no_winner", Format.Bold(arg2)); - else - return ConfirmLocalized("nunchi_ended", Format.Bold(arg2)); - } } } } \ No newline at end of file diff --git a/src/NadekoBot/Modules/Searches/Common/SearchImageCacher.cs b/src/NadekoBot/Modules/Searches/Common/SearchImageCacher.cs index cc446af0..18bc2064 100644 --- a/src/NadekoBot/Modules/Searches/Common/SearchImageCacher.cs +++ b/src/NadekoBot/Modules/Searches/Common/SearchImageCacher.cs @@ -101,19 +101,19 @@ namespace NadekoBot.Modules.Searches.Common website = $"https://e621.net/post/index.json?limit=1000&tags={tag}"; break; case DapiSearchType.Danbooru: - website = $"http://danbooru.donmai.us/posts.json?limit=200&tags={tag}"; + website = $"http://danbooru.donmai.us/posts.json?limit=100&tags={tag}"; break; case DapiSearchType.Gelbooru: - website = $"http://gelbooru.com/index.php?page=dapi&s=post&q=index&limit=1000&tags={tag}"; + website = $"http://gelbooru.com/index.php?page=dapi&s=post&q=index&limit=100&tags={tag}"; break; case DapiSearchType.Rule34: website = $"https://rule34.xxx/index.php?page=dapi&s=post&q=index&limit=100&tags={tag}"; break; case DapiSearchType.Konachan: - website = $"https://konachan.com/post.json?s=post&q=index&limit=1000&tags={tag}"; + website = $"https://konachan.com/post.json?s=post&q=index&limit=100&tags={tag}"; break; case DapiSearchType.Yandere: - website = $"https://yande.re/post.json?limit=1000&tags={tag}"; + website = $"https://yande.re/post.json?limit=100&tags={tag}"; break; } @@ -137,7 +137,7 @@ namespace NadekoBot.Modules.Searches.Common private async Task LoadXmlAsync(string website, DapiSearchType type) { - var list = new List(1000); + var list = new List(); using (var http = new HttpClient()) { using (var reader = XmlReader.Create(await http.GetStreamAsync(website), new XmlReaderSettings() diff --git a/src/NadekoBot/Modules/Searches/Services/SearchesService.cs b/src/NadekoBot/Modules/Searches/Services/SearchesService.cs index c8819b11..396937a9 100644 --- a/src/NadekoBot/Modules/Searches/Services/SearchesService.cs +++ b/src/NadekoBot/Modules/Searches/Services/SearchesService.cs @@ -10,7 +10,6 @@ using System.Collections.Generic; using System.IO; using System.Threading.Tasks; using NadekoBot.Modules.Searches.Common; -using NadekoBot.Common.Collections; using NadekoBot.Services.Database.Models; using System.Linq; using Microsoft.EntityFrameworkCore; diff --git a/src/NadekoBot/Resources/CommandStrings.resx b/src/NadekoBot/Resources/CommandStrings.resx index 6336b76f..6a77668d 100644 --- a/src/NadekoBot/Resources/CommandStrings.resx +++ b/src/NadekoBot/Resources/CommandStrings.resx @@ -1278,6 +1278,15 @@ `{0}nunchi` + + connect4 con4 + + + Creates or joins an existing connect4 game. 2 players are required for the game. Objective of the game is to get 4 of your pieces next to each other in a vertical, horizontal or diagonal line. + + + `{0}connect4` + raffle diff --git a/src/NadekoBot/_strings/ResponseStrings.en-US.json b/src/NadekoBot/_strings/ResponseStrings.en-US.json index c96f9a68..8928ba6c 100644 --- a/src/NadekoBot/_strings/ResponseStrings.en-US.json +++ b/src/NadekoBot/_strings/ResponseStrings.en-US.json @@ -349,6 +349,11 @@ "games_category": "Category", "games_cleverbot_disabled": "Disabled cleverbot on this server.", "games_cleverbot_enabled": "Enabled cleverbot on this server.", + "games_connect4_created": "Created a Connect4 game. Waiting for a player to join.", + "games_connect4_player_to_move": "Player to move: {0}", + "games_connect4_failed_to_start": "Connect4 game failed to start because nobody joined.", + "games_connect4_draw": "Connect4 game ended in a draw.", + "games_connect4_won": "{0} won the game of Connect4 against {1}.", "games_curgen_disabled": "Currency generation has been disabled on this channel.", "games_curgen_enabled": "Currency generation has been enabled on this channel.", "games_curgen_pl": "{0} random {1} appeared!", From 47125ed6874f64506f0a7ac1f7d51fa361316979 Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Fri, 4 Aug 2017 18:12:26 +0200 Subject: [PATCH 244/346] connect4 bugfixes --- src/NadekoBot/Modules/Games/Common/Connect4/Connect4.cs | 8 ++++++++ src/NadekoBot/Modules/Games/Connect4Commands.cs | 4 ++++ src/NadekoBot/Modules/Utility/StreamRoleCommands.cs | 1 - 3 files changed, 12 insertions(+), 1 deletion(-) diff --git a/src/NadekoBot/Modules/Games/Common/Connect4/Connect4.cs b/src/NadekoBot/Modules/Games/Common/Connect4/Connect4.cs index 0b247301..c892d7e0 100644 --- a/src/NadekoBot/Modules/Games/Common/Connect4/Connect4.cs +++ b/src/NadekoBot/Modules/Games/Common/Connect4/Connect4.cs @@ -85,6 +85,8 @@ namespace NadekoBot.Modules.Games.Common.Connect4 public void Initialize() { + if (CurrentPhase != Phase.Joining) + return; var _ = Task.Run(async () => { await Task.Delay(15000).ConfigureAwait(false); @@ -267,6 +269,7 @@ namespace NadekoBot.Modules.Games.Common.Connect4 if (same == 4) { + Console.WriteLine($"Won top left diagonal starting from {row + col * NumberOfRows}"); EndGame(Result.CurrentPlayerWon); break; } @@ -292,6 +295,7 @@ namespace NadekoBot.Modules.Games.Common.Connect4 if (same == 4) { + Console.WriteLine($"Won top right diagonal starting from {row + col * NumberOfRows}"); EndGame(Result.CurrentPlayerWon); break; } @@ -327,6 +331,8 @@ namespace NadekoBot.Modules.Games.Common.Connect4 private void EndGame(Result result) { + if (CurrentPhase == Phase.Ended) + return; var _ = OnGameEnded?.Invoke(this, result); CurrentPhase = Phase.Ended; } @@ -351,6 +357,8 @@ namespace NadekoBot.Modules.Games.Common.Connect4 { OnGameFailedToStart = null; OnGameStateUpdated = null; + OnGameEnded = null; + _playerTimeoutTimer?.Change(Timeout.Infinite, Timeout.Infinite); } } } diff --git a/src/NadekoBot/Modules/Games/Connect4Commands.cs b/src/NadekoBot/Modules/Games/Connect4Commands.cs index 81d03b4f..5a1183c2 100644 --- a/src/NadekoBot/Modules/Games/Connect4Commands.cs +++ b/src/NadekoBot/Modules/Games/Connect4Commands.cs @@ -34,6 +34,10 @@ namespace NadekoBot.Modules.Games Connect4Game game; if ((game = Games.GetOrAdd(Context.Channel.Id, newGame)) != newGame) { + if (game.CurrentPhase != Connect4Game.Phase.Joining) + return; + + newGame.Dispose(); //means game already exists, try to join var joined = await game.Join(Context.User.Id, Context.User.ToString()).ConfigureAwait(false); return; diff --git a/src/NadekoBot/Modules/Utility/StreamRoleCommands.cs b/src/NadekoBot/Modules/Utility/StreamRoleCommands.cs index 7749fbca..e6f38947 100644 --- a/src/NadekoBot/Modules/Utility/StreamRoleCommands.cs +++ b/src/NadekoBot/Modules/Utility/StreamRoleCommands.cs @@ -11,7 +11,6 @@ namespace NadekoBot.Modules.Utility { public partial class Utility { - [NoPublicBot] public class StreamRoleCommands : NadekoSubmodule { [NadekoCommand, Usage, Description, Aliases] From dea9a935a4b28e5b259a236f17833cf4281104fc Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Fri, 4 Aug 2017 19:43:25 +0200 Subject: [PATCH 245/346] Small nunchi fix, global nadeko improvement --- .../Administration/Services/LogCommandService.cs | 2 ++ .../Modules/Games/Common/Nunchi/Nunchi.cs | 6 +++--- src/NadekoBot/Modules/Games/NunchiCommands.cs | 8 ++++---- src/NadekoBot/Services/Impl/StatsService.cs | 2 +- src/NadekoBot/Services/ServiceProvider.cs | 15 +++++++++++++-- src/NadekoBot/_strings/ResponseStrings.en-US.json | 2 +- 6 files changed, 24 insertions(+), 11 deletions(-) diff --git a/src/NadekoBot/Modules/Administration/Services/LogCommandService.cs b/src/NadekoBot/Modules/Administration/Services/LogCommandService.cs index b185fff7..37b2781e 100644 --- a/src/NadekoBot/Modules/Administration/Services/LogCommandService.cs +++ b/src/NadekoBot/Modules/Administration/Services/LogCommandService.cs @@ -12,9 +12,11 @@ using NadekoBot.Services; using NadekoBot.Services.Database.Models; using NadekoBot.Services.Impl; using NLog; +using NadekoBot.Common; namespace NadekoBot.Modules.Administration.Services { + [NoPublicBot] public class LogCommandService : INService { diff --git a/src/NadekoBot/Modules/Games/Common/Nunchi/Nunchi.cs b/src/NadekoBot/Modules/Games/Common/Nunchi/Nunchi.cs index dd489bf0..1fd1cbcc 100644 --- a/src/NadekoBot/Modules/Games/Common/Nunchi/Nunchi.cs +++ b/src/NadekoBot/Modules/Games/Common/Nunchi/Nunchi.cs @@ -22,7 +22,7 @@ namespace NadekoBot.Modules.Games.Common.Nunchi public Phase CurrentPhase { get; private set; } = Phase.Joining; public event Func OnGameStarted; - public event Func OnRoundStarted; + public event Func OnRoundStarted; public event Func OnUserGuessed; public event Func OnRoundEnded; // tuple of the user who failed public event Func OnGameEnded; // name of the user who won @@ -87,7 +87,7 @@ namespace NadekoBot.Modules.Games.Common.Nunchi CurrentPhase = Phase.Playing; var _ = OnGameStarted?.Invoke(this); - var __ = OnRoundStarted?.Invoke(this); + var __ = OnRoundStarted?.Invoke(this, CurrentNumber); return true; } finally { _locker.Release(); } @@ -164,7 +164,7 @@ namespace NadekoBot.Modules.Games.Common.Nunchi { await Task.Delay(_nextRoundTimeout).ConfigureAwait(false); CurrentPhase = Phase.Playing; - var ___ = OnRoundStarted?.Invoke(this); + var ___ = OnRoundStarted?.Invoke(this, CurrentNumber); }); } diff --git a/src/NadekoBot/Modules/Games/NunchiCommands.cs b/src/NadekoBot/Modules/Games/NunchiCommands.cs index e5b3af96..834e2c02 100644 --- a/src/NadekoBot/Modules/Games/NunchiCommands.cs +++ b/src/NadekoBot/Modules/Games/NunchiCommands.cs @@ -5,9 +5,7 @@ using NadekoBot.Common.Attributes; using NadekoBot.Modules.Games.Common.Nunchi; using System; using System.Collections.Concurrent; -using System.Collections.Generic; using System.Linq; -using System.Text; using System.Threading.Tasks; namespace NadekoBot.Modules.Games @@ -100,9 +98,11 @@ namespace NadekoBot.Modules.Games } } - private Task Nunchi_OnRoundStarted(Nunchi arg) + private Task Nunchi_OnRoundStarted(Nunchi arg, int cur) { - return ConfirmLocalized("nunchi_round_started", Format.Bold(arg.CurrentNumber.ToString())); + return ConfirmLocalized("nunchi_round_started", + Format.Bold(arg.ParticipantCount.ToString()), + Format.Bold(cur.ToString())); } private Task Nunchi_OnUserGuessed(Nunchi arg) diff --git a/src/NadekoBot/Services/Impl/StatsService.cs b/src/NadekoBot/Services/Impl/StatsService.cs index eb82d0ce..2d31c6fc 100644 --- a/src/NadekoBot/Services/Impl/StatsService.cs +++ b/src/NadekoBot/Services/Impl/StatsService.cs @@ -20,7 +20,7 @@ namespace NadekoBot.Services.Impl private readonly IBotCredentials _creds; private readonly DateTime _started; - public const string BotVersion = "1.6"; + public const string BotVersion = "1.6.1"; public string Author => "Kwoth#2560"; public string Library => "Discord.Net"; diff --git a/src/NadekoBot/Services/ServiceProvider.cs b/src/NadekoBot/Services/ServiceProvider.cs index 32a8419d..6efe0a2b 100644 --- a/src/NadekoBot/Services/ServiceProvider.cs +++ b/src/NadekoBot/Services/ServiceProvider.cs @@ -7,6 +7,10 @@ using System.Reflection; using System.Linq; using System.Diagnostics; using NLog; +#if GLOBAL_NADEKO +using NadekoBot.Common; +#endif + namespace NadekoBot.Services { @@ -42,11 +46,18 @@ namespace NadekoBot.Services { var allTypes = assembly.GetTypes(); var services = new Queue(allTypes - .Where(x => x.GetInterfaces().Contains(typeof(INService)) && !x.GetTypeInfo().IsInterface && !x.GetTypeInfo().IsAbstract) + .Where(x => x.GetInterfaces().Contains(typeof(INService)) + && !x.GetTypeInfo().IsInterface && !x.GetTypeInfo().IsAbstract + +#if GLOBAL_NADEKO + && x.GetTypeInfo().GetCustomAttribute() == null +#endif + ) .ToArray()); var interfaces = new HashSet(allTypes - .Where(x => x.GetInterfaces().Contains(typeof(INService)) && x.GetTypeInfo().IsInterface)); + .Where(x => x.GetInterfaces().Contains(typeof(INService)) + && x.GetTypeInfo().IsInterface)); var alreadyFailed = new Dictionary(); diff --git a/src/NadekoBot/_strings/ResponseStrings.en-US.json b/src/NadekoBot/_strings/ResponseStrings.en-US.json index 8928ba6c..5eb34cd7 100644 --- a/src/NadekoBot/_strings/ResponseStrings.en-US.json +++ b/src/NadekoBot/_strings/ResponseStrings.en-US.json @@ -374,7 +374,7 @@ "games_nunchi_started": "Nunchi game started with {0} participants.", "games_nunchi_round_ended": "Nunchi round ended. {0} is out of the game.", "games_nunchi_round_ended_boot": "Nunchi round ended due to timeout of some users. These users are still in the game: {0}", - "games_nunchi_round_started": "Nunchi round started. Start counting from the number {0}.", + "games_nunchi_round_started": "Nunchi round started with {0} users. Start counting from the number {0}.", "games_nunchi_next_number": "Number registered. Last number was {0}.", "games_nunchi_failed_to_start": "Nunchi failed to start because there were not enough participants.", "games_nunchi_created": "Nunchi game created. Waiting for users to join.", From 958eca2935994c7ff3fc8b6942712b48b14caa10 Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Sat, 5 Aug 2017 09:52:32 +0200 Subject: [PATCH 246/346] Nunchi fix, shop fix --- src/NadekoBot/Modules/Gambling/FlowerShopCommands.cs | 2 +- src/NadekoBot/_strings/ResponseStrings.en-US.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/NadekoBot/Modules/Gambling/FlowerShopCommands.cs b/src/NadekoBot/Modules/Gambling/FlowerShopCommands.cs index 9c285906..660b0d33 100644 --- a/src/NadekoBot/Modules/Gambling/FlowerShopCommands.cs +++ b/src/NadekoBot/Modules/Gambling/FlowerShopCommands.cs @@ -69,7 +69,7 @@ namespace NadekoBot.Modules.Gambling for (int i = 0; i < theseEntries.Length; i++) { - var entry = entries[i]; + var entry = theseEntries[i]; embed.AddField(efb => efb.WithName($"#{curPage * 9 + i + 1} - {entry.Price}{_bc.BotConfig.CurrencySign}").WithValue(EntryToString(entry)).WithIsInline(true)); } return embed; diff --git a/src/NadekoBot/_strings/ResponseStrings.en-US.json b/src/NadekoBot/_strings/ResponseStrings.en-US.json index 5eb34cd7..13edd204 100644 --- a/src/NadekoBot/_strings/ResponseStrings.en-US.json +++ b/src/NadekoBot/_strings/ResponseStrings.en-US.json @@ -374,7 +374,7 @@ "games_nunchi_started": "Nunchi game started with {0} participants.", "games_nunchi_round_ended": "Nunchi round ended. {0} is out of the game.", "games_nunchi_round_ended_boot": "Nunchi round ended due to timeout of some users. These users are still in the game: {0}", - "games_nunchi_round_started": "Nunchi round started with {0} users. Start counting from the number {0}.", + "games_nunchi_round_started": "Nunchi round started with {0} users. Start counting from the number {1}.", "games_nunchi_next_number": "Number registered. Last number was {0}.", "games_nunchi_failed_to_start": "Nunchi failed to start because there were not enough participants.", "games_nunchi_created": "Nunchi game created. Waiting for users to join.", From 38125509e5f337256a80aba645ed893098127806 Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Sat, 5 Aug 2017 10:12:20 +0200 Subject: [PATCH 247/346] fixed .weather min/max temperature --- src/NadekoBot/Modules/Searches/Common/WeatherModels.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/NadekoBot/Modules/Searches/Common/WeatherModels.cs b/src/NadekoBot/Modules/Searches/Common/WeatherModels.cs index 7c7922e8..c7552a90 100644 --- a/src/NadekoBot/Modules/Searches/Common/WeatherModels.cs +++ b/src/NadekoBot/Modules/Searches/Common/WeatherModels.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using Newtonsoft.Json; +using System.Collections.Generic; namespace NadekoBot.Modules.Searches.Common { @@ -21,7 +22,9 @@ namespace NadekoBot.Modules.Searches.Common public double Temp { get; set; } public float Pressure { get; set; } public float Humidity { get; set; } + [JsonProperty("temp_min")] public double TempMin { get; set; } + [JsonProperty("temp_max")] public double TempMax { get; set; } } From ce602b5b355eac59eef4de9ae3cad7118d437018 Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Sat, 5 Aug 2017 10:13:05 +0200 Subject: [PATCH 248/346] Version upped --- src/NadekoBot/Services/Impl/StatsService.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/NadekoBot/Services/Impl/StatsService.cs b/src/NadekoBot/Services/Impl/StatsService.cs index 2d31c6fc..4b04efd2 100644 --- a/src/NadekoBot/Services/Impl/StatsService.cs +++ b/src/NadekoBot/Services/Impl/StatsService.cs @@ -20,7 +20,7 @@ namespace NadekoBot.Services.Impl private readonly IBotCredentials _creds; private readonly DateTime _started; - public const string BotVersion = "1.6.1"; + public const string BotVersion = "1.6.2"; public string Author => "Kwoth#2560"; public string Library => "Discord.Net"; From d12d70df1b950dd066cb725f409be62a88e00676 Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Sat, 5 Aug 2017 10:46:38 +0200 Subject: [PATCH 249/346] Fixed connect4 weird wins --- src/NadekoBot/Modules/Games/Common/Connect4/Connect4.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/NadekoBot/Modules/Games/Common/Connect4/Connect4.cs b/src/NadekoBot/Modules/Games/Common/Connect4/Connect4.cs index c892d7e0..0c6bc173 100644 --- a/src/NadekoBot/Modules/Games/Common/Connect4/Connect4.cs +++ b/src/NadekoBot/Modules/Games/Common/Connect4/Connect4.cs @@ -274,6 +274,8 @@ namespace NadekoBot.Modules.Games.Common.Connect4 break; } + same = 1; + //top right for (int i = 1; i < 4; i++) { From 79d3fca7e4f5d9db7a5a4bd8c04697f8b27e11c0 Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Sat, 5 Aug 2017 10:51:46 +0200 Subject: [PATCH 250/346] Fixed incorrect usage for .ttt --- src/NadekoBot/Resources/CommandStrings.resx | 4 ++-- src/NadekoBot/Services/Impl/StatsService.cs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/NadekoBot/Resources/CommandStrings.resx b/src/NadekoBot/Resources/CommandStrings.resx index 6a77668d..994ad981 100644 --- a/src/NadekoBot/Resources/CommandStrings.resx +++ b/src/NadekoBot/Resources/CommandStrings.resx @@ -3022,7 +3022,7 @@ Sets a price for a command. Running that command will take currency from users. Set 0 to remove the price. - `{0}cmdcost 0 !!q` or `{0}cmdcost 1 >8ball` + `{0}cmdcost 0 !!q` or `{0}cmdcost 1 {0}8ball` startevent @@ -3166,7 +3166,7 @@ Starts a game of tic tac toe. Another user must run the command in the same channel in order to accept the challenge. Use numbers 1-9 to play. 15 seconds per move. - >ttt + {0}ttt timezones diff --git a/src/NadekoBot/Services/Impl/StatsService.cs b/src/NadekoBot/Services/Impl/StatsService.cs index 4b04efd2..bde8aa07 100644 --- a/src/NadekoBot/Services/Impl/StatsService.cs +++ b/src/NadekoBot/Services/Impl/StatsService.cs @@ -20,7 +20,7 @@ namespace NadekoBot.Services.Impl private readonly IBotCredentials _creds; private readonly DateTime _started; - public const string BotVersion = "1.6.2"; + public const string BotVersion = "1.6.3"; public string Author => "Kwoth#2560"; public string Library => "Discord.Net"; From 540209706d667b7f6796ba32cb4b070d0caa62be Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Sat, 5 Aug 2017 12:38:53 +0200 Subject: [PATCH 251/346] Added .qn and fixed .hangman bugs --- .../Modules/Games/Common/Connect4/Connect4.cs | 17 +++++++------ .../Modules/Games/Common/Hangman/Hangman.cs | 5 ++-- .../Modules/Music/Common/MusicPlayer.cs | 10 ++++++++ .../Modules/Music/Common/MusicQueue.cs | 21 ++++++++++++++++ src/NadekoBot/Modules/Music/Music.cs | 24 +++++++++++++------ src/NadekoBot/Resources/CommandStrings.resx | 9 +++++++ 6 files changed, 68 insertions(+), 18 deletions(-) diff --git a/src/NadekoBot/Modules/Games/Common/Connect4/Connect4.cs b/src/NadekoBot/Modules/Games/Common/Connect4/Connect4.cs index 0c6bc173..0ba9acb8 100644 --- a/src/NadekoBot/Modules/Games/Common/Connect4/Connect4.cs +++ b/src/NadekoBot/Modules/Games/Common/Connect4/Connect4.cs @@ -62,15 +62,14 @@ namespace NadekoBot.Modules.Games.Common.Connect4 private Timer _playerTimeoutTimer; - /* rows = 4, columns = 3, total = 12 - * [][][][][][] - * [][][][][][] - * [][][][][][] - * [][][][][][] - * [][][][][][] - * [][][][][][] - * [][][][][][] - * */ + /* [ ][ ][ ][ ][ ][ ] + * [ ][ ][ ][ ][ ][ ] + * [ ][ ][ ][ ][ ][ ] + * [ ][ ][ ][ ][ ][ ] + * [ ][ ][ ][ ][ ][ ] + * [ ][ ][ ][ ][ ][ ] + * [ ][ ][ ][ ][ ][ ] + */ public Connect4Game(ulong userId, string userName) { diff --git a/src/NadekoBot/Modules/Games/Common/Hangman/Hangman.cs b/src/NadekoBot/Modules/Games/Common/Hangman/Hangman.cs index 94f14835..6c91edc1 100644 --- a/src/NadekoBot/Modules/Games/Common/Hangman/Hangman.cs +++ b/src/NadekoBot/Modules/Games/Common/Hangman/Hangman.cs @@ -67,8 +67,8 @@ namespace NadekoBot.Modules.Games.Common.Hangman Errors++; if (Errors > MaxErrors) { - CurrentPhase = Phase.Ended; var _ = OnGameEnded(this, null); + CurrentPhase = Phase.Ended; } } @@ -101,8 +101,8 @@ namespace NadekoBot.Modules.Games.Common.Hangman if (input != Term.Word) // failed return; - CurrentPhase = Phase.Ended; var _ = OnGameEnded?.Invoke(this, userName); + CurrentPhase = Phase.Ended; return; } @@ -128,6 +128,7 @@ namespace NadekoBot.Modules.Games.Common.Hangman .Where(c => char.IsLetterOrDigit(c))))) { var _ = OnGameEnded.Invoke(this, userName); //if all letters are guessed + CurrentPhase = Phase.Ended; } else //guessed but not last letter { diff --git a/src/NadekoBot/Modules/Music/Common/MusicPlayer.cs b/src/NadekoBot/Modules/Music/Common/MusicPlayer.cs index 00e38c98..b578904f 100644 --- a/src/NadekoBot/Modules/Music/Common/MusicPlayer.cs +++ b/src/NadekoBot/Modules/Music/Common/MusicPlayer.cs @@ -405,6 +405,16 @@ namespace NadekoBot.Modules.Music.Common } } + public int EnqueueNext(SongInfo song) + { + lock (locker) + { + if (Exited) + return -1; + return Queue.AddNext(song); + } + } + public void SetIndex(int index) { if (index < 0) diff --git a/src/NadekoBot/Modules/Music/Common/MusicQueue.cs b/src/NadekoBot/Modules/Music/Common/MusicQueue.cs index 4927b21e..81241adf 100644 --- a/src/NadekoBot/Modules/Music/Common/MusicQueue.cs +++ b/src/NadekoBot/Modules/Music/Common/MusicQueue.cs @@ -78,6 +78,27 @@ namespace NadekoBot.Modules.Music.Common } } + public int AddNext(SongInfo song) + { + song.ThrowIfNull(nameof(song)); + lock (locker) + { + if (MaxQueueSize != 0 && Songs.Count >= MaxQueueSize) + throw new QueueFullException(); + var curSong = Current.Song; + if (curSong == null) + { + Songs.AddLast(song); + return Songs.Count; + } + + var songlist = Songs.ToList(); + songlist.Insert(CurrentIndex + 1, song); + Songs = new LinkedList(songlist); + return CurrentIndex + 1; + } + } + public void Next(int skipCount = 1) { lock(locker) diff --git a/src/NadekoBot/Modules/Music/Music.cs b/src/NadekoBot/Modules/Music/Music.cs index 6b5499f3..5f1a99b3 100644 --- a/src/NadekoBot/Modules/Music/Music.cs +++ b/src/NadekoBot/Modules/Music/Music.cs @@ -98,7 +98,7 @@ namespace NadekoBot.Modules.Music // return Task.CompletedTask; //} - private async Task InternalQueue(MusicPlayer mp, SongInfo songInfo, bool silent) + private async Task InternalQueue(MusicPlayer mp, SongInfo songInfo, bool silent, bool queueFirst = false) { if (songInfo == null) { @@ -110,8 +110,9 @@ namespace NadekoBot.Modules.Music int index; try { - _log.Info("Added"); - index = mp.Enqueue(songInfo); + index = queueFirst + ? mp.EnqueueNext(songInfo) + : mp.Enqueue(songInfo); } catch (QueueFullException) { @@ -175,13 +176,22 @@ namespace NadekoBot.Modules.Music [RequireContext(ContextType.Guild)] public async Task Queue([Remainder] string query) { - _log.Info("Getting player"); var mp = await _service.GetOrCreatePlayer(Context); - _log.Info("Resolving song"); var songInfo = await _service.ResolveSong(query, Context.User.ToString()); - _log.Info("Queueing song"); try { await InternalQueue(mp, songInfo, false); } catch (QueueFullException) { return; } - _log.Info("--------------"); + if ((await Context.Guild.GetCurrentUserAsync()).GetPermissions((IGuildChannel)Context.Channel).ManageMessages) + { + Context.Message.DeleteAfter(10); + } + } + + [NadekoCommand, Usage, Description, Aliases] + [RequireContext(ContextType.Guild)] + public async Task QueueNext([Remainder] string query) + { + var mp = await _service.GetOrCreatePlayer(Context); + var songInfo = await _service.ResolveSong(query, Context.User.ToString()); + try { await InternalQueue(mp, songInfo, false, true); } catch (QueueFullException) { return; } if ((await Context.Guild.GetCurrentUserAsync()).GetPermissions((IGuildChannel)Context.Channel).ManageMessages) { Context.Message.DeleteAfter(10); diff --git a/src/NadekoBot/Resources/CommandStrings.resx b/src/NadekoBot/Resources/CommandStrings.resx index 994ad981..ab6beaa3 100644 --- a/src/NadekoBot/Resources/CommandStrings.resx +++ b/src/NadekoBot/Resources/CommandStrings.resx @@ -1539,6 +1539,15 @@ `{0}q Dream Of Venice` + + queuenext qn + + + Works the same as `{0}queue` command, except it enqueues the new song after the current one. **You must be in a voice channel**. + + + `{0}qn Dream Of Venice` + queuesearch qs yqs From 1358878773bd2ba3bcf08389dd55ce8e675bafd3 Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Sun, 6 Aug 2017 11:49:27 +0200 Subject: [PATCH 252/346] Fixed index for .qn --- src/NadekoBot/Modules/Games/Games.cs | 2 -- src/NadekoBot/Modules/Music/Common/MusicPlayer.cs | 2 +- src/NadekoBot/Modules/Music/Music.cs | 2 +- 3 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/NadekoBot/Modules/Games/Games.cs b/src/NadekoBot/Modules/Games/Games.cs index 1b8898c9..b1db0acd 100644 --- a/src/NadekoBot/Modules/Games/Games.cs +++ b/src/NadekoBot/Modules/Games/Games.cs @@ -15,8 +15,6 @@ namespace NadekoBot.Modules.Games - Blackjack - Shiritori - Simple RPG adventure - - The nunchi game - - Connect 4 */ public partial class Games : NadekoTopLevelModule { diff --git a/src/NadekoBot/Modules/Music/Common/MusicPlayer.cs b/src/NadekoBot/Modules/Music/Common/MusicPlayer.cs index b578904f..aed23c0e 100644 --- a/src/NadekoBot/Modules/Music/Common/MusicPlayer.cs +++ b/src/NadekoBot/Modules/Music/Common/MusicPlayer.cs @@ -401,7 +401,7 @@ namespace NadekoBot.Modules.Music.Common if (Exited) return -1; Queue.Add(song); - return Queue.Count; + return Queue.Count - 1; } } diff --git a/src/NadekoBot/Modules/Music/Music.cs b/src/NadekoBot/Modules/Music/Music.cs index 5f1a99b3..5589984a 100644 --- a/src/NadekoBot/Modules/Music/Music.cs +++ b/src/NadekoBot/Modules/Music/Music.cs @@ -126,7 +126,7 @@ namespace NadekoBot.Modules.Music try { var embed = new EmbedBuilder().WithOkColor() - .WithAuthor(eab => eab.WithName(GetText("queued_song") + " #" + (index)).WithMusicIcon()) + .WithAuthor(eab => eab.WithName(GetText("queued_song") + " #" + (index + 1)).WithMusicIcon()) .WithDescription($"{songInfo.PrettyName}\n{GetText("queue")} ") .WithFooter(ef => ef.WithText(songInfo.PrettyProvider)); From 10fdd36e8700bea40f378c32a7de8e10d28d6d57 Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Sun, 6 Aug 2017 12:02:13 +0200 Subject: [PATCH 253/346] fixed sfi and sfw not ignoring server admin when message is edited. #1444 --- .../Modules/Permissions/Services/FilterService.cs | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/NadekoBot/Modules/Permissions/Services/FilterService.cs b/src/NadekoBot/Modules/Permissions/Services/FilterService.cs index 886f85e7..e15926a9 100644 --- a/src/NadekoBot/Modules/Permissions/Services/FilterService.cs +++ b/src/NadekoBot/Modules/Permissions/Services/FilterService.cs @@ -61,12 +61,15 @@ namespace NadekoBot.Modules.Permissions.Services _client.MessageUpdated += (oldData, newMsg, channel) => { - var _ = Task.Run(async () => + var _ = Task.Run(() => { var guild = (channel as ITextChannel)?.Guild; var usrMsg = newMsg as IUserMessage; - return (await FilterInvites(guild, usrMsg)) || (await FilterWords(guild, usrMsg)); + if (guild == null || usrMsg == null) + return Task.CompletedTask; + + return TryBlockEarly(guild, usrMsg); }); return Task.CompletedTask; }; @@ -116,9 +119,9 @@ namespace NadekoBot.Modules.Permissions.Services if (usrMsg is null) return false; - if ((InviteFilteringChannels.Contains(usrMsg.Channel.Id) || - InviteFilteringServers.Contains(guild.Id)) && - usrMsg.Content.IsDiscordInvite()) + if ((InviteFilteringChannels.Contains(usrMsg.Channel.Id) + || InviteFilteringServers.Contains(guild.Id)) + && usrMsg.Content.IsDiscordInvite()) { try { From 464118f792a4edf29dd6c67d6f8a77b20288d4f2 Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Sun, 6 Aug 2017 14:31:28 +0200 Subject: [PATCH 254/346] having .crca enabled with %target% will replace target with everything that comes after the trigger wherever it is in the triggering message --- .../CustomReactions/Extensions/Extensions.cs | 57 +++++++++++++++++-- .../Services/CustomReactionsService.cs | 4 +- 2 files changed, 52 insertions(+), 9 deletions(-) diff --git a/src/NadekoBot/Modules/CustomReactions/Extensions/Extensions.cs b/src/NadekoBot/Modules/CustomReactions/Extensions/Extensions.cs index 41d58d26..79591b7c 100644 --- a/src/NadekoBot/Modules/CustomReactions/Extensions/Extensions.cs +++ b/src/NadekoBot/Modules/CustomReactions/Extensions/Extensions.cs @@ -58,11 +58,23 @@ namespace NadekoBot.Modules.CustomReactions.Extensions return str; } - private static async Task ResolveResponseStringAsync(this string str, IUserMessage ctx, DiscordSocketClient client, string resolvedTrigger) + private static async Task ResolveResponseStringAsync(this string str, IUserMessage ctx, DiscordSocketClient client, string resolvedTrigger, bool containsAnywhere) { + var substringIndex = resolvedTrigger.Length; + if (containsAnywhere) + { + var pos = ctx.Content.GetWordPosition(resolvedTrigger); + if (pos == WordPosition.Start) + substringIndex += 1; + else if (pos == WordPosition.End) + substringIndex = ctx.Content.Length; + else if (pos == WordPosition.Middle) + substringIndex += ctx.Content.IndexOf(resolvedTrigger); + } + var rep = new ReplacementBuilder() .WithDefault(ctx.Author, ctx.Channel, (ctx.Channel as ITextChannel)?.Guild, client) - .WithOverride("%target%", () => ctx.Content.Substring(resolvedTrigger.Length).Trim()) + .WithOverride("%target%", () => ctx.Content.Substring(substringIndex)) .Build(); str = rep.Replace(str); @@ -77,8 +89,8 @@ namespace NadekoBot.Modules.CustomReactions.Extensions public static string TriggerWithContext(this CustomReaction cr, IUserMessage ctx, DiscordSocketClient client) => cr.Trigger.ResolveTriggerString(ctx, client); - public static Task ResponseWithContextAsync(this CustomReaction cr, IUserMessage ctx, DiscordSocketClient client) - => cr.Response.ResolveResponseStringAsync(ctx, client, cr.Trigger.ResolveTriggerString(ctx, client)); + public static Task ResponseWithContextAsync(this CustomReaction cr, IUserMessage ctx, DiscordSocketClient client, bool containsAnywhere) + => cr.Response.ResolveResponseStringAsync(ctx, client, cr.Trigger.ResolveTriggerString(ctx, client), containsAnywhere); public static async Task Send(this CustomReaction cr, IUserMessage ctx, DiscordSocketClient client, CustomReactionsService crs) { @@ -88,16 +100,49 @@ namespace NadekoBot.Modules.CustomReactions.Extensions if (CREmbed.TryParse(cr.Response, out CREmbed crembed)) { + var trigger = cr.Trigger.ResolveTriggerString(ctx, client); + var substringIndex = trigger.Length; + if (cr.ContainsAnywhere) + { + var pos = ctx.Content.GetWordPosition(trigger); + if (pos == WordPosition.Start) + substringIndex += 1; + else if (pos == WordPosition.End) + substringIndex = ctx.Content.Length; + else if (pos == WordPosition.Middle) + substringIndex += ctx.Content.IndexOf(trigger); + } + var rep = new ReplacementBuilder() .WithDefault(ctx.Author, ctx.Channel, (ctx.Channel as ITextChannel)?.Guild, client) - .WithOverride("%target%", () => ctx.Content.Substring(cr.Trigger.ResolveTriggerString(ctx, client).Length).Trim()) + .WithOverride("%target%", () => ctx.Content.Substring(substringIndex).Trim()) .Build(); rep.Replace(crembed); return await channel.EmbedAsync(crembed.ToEmbed(), crembed.PlainText?.SanitizeMentions() ?? ""); } - return await channel.SendMessageAsync((await cr.ResponseWithContextAsync(ctx, client)).SanitizeMentions()); + return await channel.SendMessageAsync((await cr.ResponseWithContextAsync(ctx, client, cr.ContainsAnywhere)).SanitizeMentions()); + } + + public static WordPosition GetWordPosition(this string str, string word) + { + if (str.StartsWith(word + " ")) + return WordPosition.Start; + else if (str.EndsWith(" " + word)) + return WordPosition.End; + else if (str.Contains(" " + word + " ")) + return WordPosition.Middle; + else + return WordPosition.None; } } + + public enum WordPosition + { + None, + Start, + Middle, + End, + } } diff --git a/src/NadekoBot/Modules/CustomReactions/Services/CustomReactionsService.cs b/src/NadekoBot/Modules/CustomReactions/Services/CustomReactionsService.cs index e611e4d6..2944ab6c 100644 --- a/src/NadekoBot/Modules/CustomReactions/Services/CustomReactionsService.cs +++ b/src/NadekoBot/Modules/CustomReactions/Services/CustomReactionsService.cs @@ -70,9 +70,7 @@ namespace NadekoBot.Modules.CustomReactions.Services var hasTarget = cr.Response.ToLowerInvariant().Contains("%target%"); var trigger = cr.TriggerWithContext(umsg, _client).Trim().ToLowerInvariant(); return ((cr.ContainsAnywhere && - (content.StartsWith(trigger + " ") - || content.EndsWith(" " + trigger) - || content.Contains(" " + trigger + " "))) + (content.GetWordPosition(trigger) != WordPosition.None)) || (hasTarget && content.StartsWith(trigger + " ")) || (_bc.BotConfig.CustomReactionsStartWith && content.StartsWith(trigger + " ")) || content == trigger); From 57dd324f3ec80dfea403f9b9a559db1139078599 Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Sun, 6 Aug 2017 15:59:29 +0200 Subject: [PATCH 255/346] You can no longer give your max role to other users with .sr --- src/NadekoBot/Modules/Administration/Administration.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/NadekoBot/Modules/Administration/Administration.cs b/src/NadekoBot/Modules/Administration/Administration.cs index 84941722..5c943b27 100644 --- a/src/NadekoBot/Modules/Administration/Administration.cs +++ b/src/NadekoBot/Modules/Administration/Administration.cs @@ -56,7 +56,7 @@ namespace NadekoBot.Modules.Administration { var guser = (IGuildUser)Context.User; var maxRole = guser.GetRoles().Max(x => x.Position); - if ((Context.User.Id != Context.Guild.OwnerId) && (maxRole < role.Position || maxRole <= usr.GetRoles().Max(x => x.Position))) + if ((Context.User.Id != Context.Guild.OwnerId) && (maxRole <= role.Position || maxRole <= usr.GetRoles().Max(x => x.Position))) return; try { From e2d7ed343c1709e2439e6ae26b76dc321a31f01e Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Sun, 6 Aug 2017 16:07:48 +0200 Subject: [PATCH 256/346] Beam renamed to mixer. Version upped. --- .../Modules/Administration/MigrationCommands.cs | 2 +- .../Searches/Services/StreamNotificationService.cs | 12 ++++++------ .../Modules/Searches/StreamNotificationCommands.cs | 4 ++-- src/NadekoBot/Resources/CommandStrings.resx | 12 ++++++------ .../Services/Database/Models/FollowedStream.cs | 2 +- src/NadekoBot/Services/Impl/StatsService.cs | 2 +- 6 files changed, 17 insertions(+), 17 deletions(-) diff --git a/src/NadekoBot/Modules/Administration/MigrationCommands.cs b/src/NadekoBot/Modules/Administration/MigrationCommands.cs index fe14afe6..b2d6d2ea 100644 --- a/src/NadekoBot/Modules/Administration/MigrationCommands.cs +++ b/src/NadekoBot/Modules/Administration/MigrationCommands.cs @@ -212,7 +212,7 @@ namespace NadekoBot.Modules.Administration type = FollowedStream.FollowedStreamType.Twitch; break; case StreamNotificationConfig0_9.StreamType.Beam: - type = FollowedStream.FollowedStreamType.Beam; + type = FollowedStream.FollowedStreamType.Mixer; break; case StreamNotificationConfig0_9.StreamType.Hitbox: type = FollowedStream.FollowedStreamType.Hitbox; diff --git a/src/NadekoBot/Modules/Searches/Services/StreamNotificationService.cs b/src/NadekoBot/Modules/Searches/Services/StreamNotificationService.cs index 18665738..af651fcf 100644 --- a/src/NadekoBot/Modules/Searches/Services/StreamNotificationService.cs +++ b/src/NadekoBot/Modules/Searches/Services/StreamNotificationService.cs @@ -126,8 +126,8 @@ namespace NadekoBot.Modules.Searches.Services }; _cachedStatuses.AddOrUpdate(twitchUrl, result, (key, old) => result); return result; - case FollowedStream.FollowedStreamType.Beam: - var beamUrl = $"https://beam.pro/api/v1/channels/{stream.Username.ToLowerInvariant()}"; + case FollowedStream.FollowedStreamType.Mixer: + var beamUrl = $"https://mixer.com/api/v1/channels/{stream.Username.ToLowerInvariant()}"; if (checkCache && _cachedStatuses.TryGetValue(beamUrl, out result)) return result; using (var http = new HttpClient()) @@ -179,11 +179,11 @@ namespace NadekoBot.Modules.Searches.Services public string GetLink(FollowedStream fs) { if (fs.Type == FollowedStream.FollowedStreamType.Hitbox) - return $"http://www.hitbox.tv/{fs.Username}/"; + return $"https://www.hitbox.tv/{fs.Username}/"; if (fs.Type == FollowedStream.FollowedStreamType.Twitch) - return $"http://www.twitch.tv/{fs.Username}/"; - if (fs.Type == FollowedStream.FollowedStreamType.Beam) - return $"https://beam.pro/{fs.Username}/"; + return $"https://www.twitch.tv/{fs.Username}/"; + if (fs.Type == FollowedStream.FollowedStreamType.Mixer) + return $"https://www.mixer.com/{fs.Username}/"; return "??"; } } diff --git a/src/NadekoBot/Modules/Searches/StreamNotificationCommands.cs b/src/NadekoBot/Modules/Searches/StreamNotificationCommands.cs index 890ba1d2..2fb88ce6 100644 --- a/src/NadekoBot/Modules/Searches/StreamNotificationCommands.cs +++ b/src/NadekoBot/Modules/Searches/StreamNotificationCommands.cs @@ -41,8 +41,8 @@ namespace NadekoBot.Modules.Searches [NadekoCommand, Usage, Description, Aliases] [RequireContext(ContextType.Guild)] [RequireUserPermission(GuildPermission.ManageMessages)] - public async Task Beam([Remainder] string username) => - await TrackStream((ITextChannel)Context.Channel, username, FollowedStream.FollowedStreamType.Beam) + public async Task Mixer([Remainder] string username) => + await TrackStream((ITextChannel)Context.Channel, username, FollowedStream.FollowedStreamType.Mixer) .ConfigureAwait(false); [NadekoCommand, Usage, Description, Aliases] diff --git a/src/NadekoBot/Resources/CommandStrings.resx b/src/NadekoBot/Resources/CommandStrings.resx index ab6beaa3..64b261ca 100644 --- a/src/NadekoBot/Resources/CommandStrings.resx +++ b/src/NadekoBot/Resources/CommandStrings.resx @@ -1827,14 +1827,14 @@ `{0}twitch SomeStreamer` - - beam bm + + mixer bm - + Notifies this channel when a certain user starts streaming. - - `{0}beam SomeStreamer` + + `{0}mixer SomeStreamer` removestream rms @@ -1843,7 +1843,7 @@ Removes notifications of a certain streamer from a certain platform on this channel. - `{0}rms Twitch SomeGuy` or `{0}rms Beam SomeOtherGuy` + `{0}rms Twitch SomeGuy` or `{0}rms mixer SomeOtherGuy` liststreams ls diff --git a/src/NadekoBot/Services/Database/Models/FollowedStream.cs b/src/NadekoBot/Services/Database/Models/FollowedStream.cs index 59c4ad21..f653d3f9 100644 --- a/src/NadekoBot/Services/Database/Models/FollowedStream.cs +++ b/src/NadekoBot/Services/Database/Models/FollowedStream.cs @@ -9,7 +9,7 @@ public enum FollowedStreamType { - Twitch, Hitbox, Beam + Twitch, Hitbox, Mixer } public override int GetHashCode() => diff --git a/src/NadekoBot/Services/Impl/StatsService.cs b/src/NadekoBot/Services/Impl/StatsService.cs index bde8aa07..fe56b94a 100644 --- a/src/NadekoBot/Services/Impl/StatsService.cs +++ b/src/NadekoBot/Services/Impl/StatsService.cs @@ -20,7 +20,7 @@ namespace NadekoBot.Services.Impl private readonly IBotCredentials _creds; private readonly DateTime _started; - public const string BotVersion = "1.6.3"; + public const string BotVersion = "1.6.4"; public string Author => "Kwoth#2560"; public string Library => "Discord.Net"; From f02ac7cd78492a9eb288111403c040adb9a4582a Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Sun, 6 Aug 2017 17:34:52 +0200 Subject: [PATCH 257/346] renamed hitbox to smashcast --- .../Modules/Administration/MigrationCommands.cs | 2 +- .../Searches/Services/StreamNotificationService.cs | 4 ++-- .../Modules/Searches/StreamNotificationCommands.cs | 4 ++-- src/NadekoBot/Resources/CommandStrings.resx | 10 +++++----- .../Services/Database/Models/FollowedStream.cs | 2 +- 5 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/NadekoBot/Modules/Administration/MigrationCommands.cs b/src/NadekoBot/Modules/Administration/MigrationCommands.cs index b2d6d2ea..1228690a 100644 --- a/src/NadekoBot/Modules/Administration/MigrationCommands.cs +++ b/src/NadekoBot/Modules/Administration/MigrationCommands.cs @@ -215,7 +215,7 @@ namespace NadekoBot.Modules.Administration type = FollowedStream.FollowedStreamType.Mixer; break; case StreamNotificationConfig0_9.StreamType.Hitbox: - type = FollowedStream.FollowedStreamType.Hitbox; + type = FollowedStream.FollowedStreamType.Smashcast; break; default: break; diff --git a/src/NadekoBot/Modules/Searches/Services/StreamNotificationService.cs b/src/NadekoBot/Modules/Searches/Services/StreamNotificationService.cs index af651fcf..679e59a5 100644 --- a/src/NadekoBot/Modules/Searches/Services/StreamNotificationService.cs +++ b/src/NadekoBot/Modules/Searches/Services/StreamNotificationService.cs @@ -86,7 +86,7 @@ namespace NadekoBot.Modules.Searches.Services StreamStatus result; switch (stream.Type) { - case FollowedStream.FollowedStreamType.Hitbox: + case FollowedStream.FollowedStreamType.Smashcast: var hitboxUrl = $"https://api.hitbox.tv/media/status/{stream.Username.ToLowerInvariant()}"; if (checkCache && _cachedStatuses.TryGetValue(hitboxUrl, out result)) return result; @@ -178,7 +178,7 @@ namespace NadekoBot.Modules.Searches.Services public string GetLink(FollowedStream fs) { - if (fs.Type == FollowedStream.FollowedStreamType.Hitbox) + if (fs.Type == FollowedStream.FollowedStreamType.Smashcast) return $"https://www.hitbox.tv/{fs.Username}/"; if (fs.Type == FollowedStream.FollowedStreamType.Twitch) return $"https://www.twitch.tv/{fs.Username}/"; diff --git a/src/NadekoBot/Modules/Searches/StreamNotificationCommands.cs b/src/NadekoBot/Modules/Searches/StreamNotificationCommands.cs index 2fb88ce6..0dab151a 100644 --- a/src/NadekoBot/Modules/Searches/StreamNotificationCommands.cs +++ b/src/NadekoBot/Modules/Searches/StreamNotificationCommands.cs @@ -27,8 +27,8 @@ namespace NadekoBot.Modules.Searches [NadekoCommand, Usage, Description, Aliases] [RequireContext(ContextType.Guild)] [RequireUserPermission(GuildPermission.ManageMessages)] - public async Task Hitbox([Remainder] string username) => - await TrackStream((ITextChannel)Context.Channel, username, FollowedStream.FollowedStreamType.Hitbox) + public async Task Smashcast([Remainder] string username) => + await TrackStream((ITextChannel)Context.Channel, username, FollowedStream.FollowedStreamType.Smashcast) .ConfigureAwait(false); [NadekoCommand, Usage, Description, Aliases] diff --git a/src/NadekoBot/Resources/CommandStrings.resx b/src/NadekoBot/Resources/CommandStrings.resx index 64b261ca..18def6af 100644 --- a/src/NadekoBot/Resources/CommandStrings.resx +++ b/src/NadekoBot/Resources/CommandStrings.resx @@ -1809,14 +1809,14 @@ `{0}lolban` - - hitbox hb + + smashcast hb - + Notifies this channel when a certain user starts streaming. - - `{0}hitbox SomeStreamer` + + `{0}smashcast SomeStreamer` twitch tw diff --git a/src/NadekoBot/Services/Database/Models/FollowedStream.cs b/src/NadekoBot/Services/Database/Models/FollowedStream.cs index f653d3f9..b49bc430 100644 --- a/src/NadekoBot/Services/Database/Models/FollowedStream.cs +++ b/src/NadekoBot/Services/Database/Models/FollowedStream.cs @@ -9,7 +9,7 @@ public enum FollowedStreamType { - Twitch, Hitbox, Mixer + Twitch, Smashcast, Mixer } public override int GetHashCode() => From 0bf6459e6ad53364ed45e3d8fe4dd4b1ef7177cd Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Sun, 6 Aug 2017 17:43:15 +0200 Subject: [PATCH 258/346] if .tesar is enabled, .iam will remove all other self assignable roles except only one. #1402 --- .../SelfAssignedRolesCommands.cs | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/src/NadekoBot/Modules/Administration/SelfAssignedRolesCommands.cs b/src/NadekoBot/Modules/Administration/SelfAssignedRolesCommands.cs index 271ef2c0..79e76a1d 100644 --- a/src/NadekoBot/Modules/Administration/SelfAssignedRolesCommands.cs +++ b/src/NadekoBot/Modules/Administration/SelfAssignedRolesCommands.cs @@ -195,18 +195,23 @@ namespace NadekoBot.Modules.Administration var roleIds = roles.Select(x => x.RoleId).ToArray(); if (conf.ExclusiveSelfAssignedRoles) { - var sameRoleId = guildUser.RoleIds.FirstOrDefault(r => roleIds.Contains(r)); + var sameRoles = guildUser.RoleIds.Where(r => roleIds.Contains(r)); - if (sameRoleId != default(ulong)) + foreach (var roleId in sameRoles) { - var sameRole = Context.Guild.GetRole(sameRoleId); + var sameRole = Context.Guild.GetRole(roleId); if (sameRole != null) { - await guildUser.RemoveRoleAsync(sameRole).ConfigureAwait(false); - await Task.Delay(500).ConfigureAwait(false); + try + { + await guildUser.RemoveRoleAsync(sameRole).ConfigureAwait(false); + await Task.Delay(300).ConfigureAwait(false); + } + catch (Exception ex) + { + _log.Warn(ex); + } } - //await ReplyErrorLocalized("self_assign_already_excl", Format.Bold(sameRole?.Name)).ConfigureAwait(false); - //return; } } try From 6c5ea68032c7e3b856f7bb064f1d3d62bb149ce0 Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Sun, 6 Aug 2017 18:01:10 +0200 Subject: [PATCH 259/346] .antispam should now update when you use it with new parameters. Use no parameters to disable it if it exists, or run it with default values if it doesn't --- .../Administration/ProtectionCommands.cs | 37 +++++++++++++------ 1 file changed, 26 insertions(+), 11 deletions(-) diff --git a/src/NadekoBot/Modules/Administration/ProtectionCommands.cs b/src/NadekoBot/Modules/Administration/ProtectionCommands.cs index ea97a089..c9d021ad 100644 --- a/src/NadekoBot/Modules/Administration/ProtectionCommands.cs +++ b/src/NadekoBot/Modules/Administration/ProtectionCommands.cs @@ -34,8 +34,8 @@ namespace NadekoBot.Modules.Administration if (string.IsNullOrWhiteSpace(ignoredString)) ignoredString = "none"; return GetText("spam_stats", - Format.Bold(stats.AntiSpamSettings.MessageThreshold.ToString()), - Format.Bold(stats.AntiSpamSettings.Action.ToString()), + Format.Bold(stats.AntiSpamSettings.MessageThreshold.ToString()), + Format.Bold(stats.AntiSpamSettings.Action.ToString()), ignoredString); } @@ -60,7 +60,7 @@ namespace NadekoBot.Modules.Administration await ReplyErrorLocalized("raid_time", 2, 300).ConfigureAwait(false); return; } - + if (_service.AntiRaidGuilds.TryRemove(Context.Guild.Id, out _)) { using (var uow = _db.UnitOfWork) @@ -112,14 +112,9 @@ namespace NadekoBot.Modules.Administration [NadekoCommand, Usage, Description, Aliases] [RequireContext(ContextType.Guild)] [RequireUserPermission(GuildPermission.Administrator)] - public async Task AntiSpam(int messageCount = 3, PunishmentAction action = PunishmentAction.Mute, int time = 0) + [Priority(1)] + public async Task AntiSpam() { - if (messageCount < 2 || messageCount > 10) - return; - - if (time < 0 || time > 60 * 12) - return; - if (_service.AntiSpamGuilds.TryRemove(Context.Guild.Id, out var removed)) { removed.UserStats.ForEach(x => x.Value.Dispose()); @@ -135,6 +130,21 @@ namespace NadekoBot.Modules.Administration return; } + await AntiSpam(3).ConfigureAwait(false); + } + + [NadekoCommand, Usage, Description, Aliases] + [RequireContext(ContextType.Guild)] + [RequireUserPermission(GuildPermission.Administrator)] + [Priority(0)] + public async Task AntiSpam(int messageCount, PunishmentAction action = PunishmentAction.Mute, int time = 0) + { + if (messageCount < 2 || messageCount > 10) + return; + + if (time < 0 || time > 60 * 12) + return; + try { await _mute.GetMuteRole(Context.Guild).ConfigureAwait(false); @@ -155,7 +165,12 @@ namespace NadekoBot.Modules.Administration } }; - _service.AntiSpamGuilds.AddOrUpdate(Context.Guild.Id, stats, (key, old) => stats); + _service.AntiSpamGuilds.AddOrUpdate(Context.Guild.Id, stats, (key, old) => + { + stats.AntiSpamSettings.MessageThreshold = messageCount; + stats.AntiSpamSettings.Action = action; + return stats; + }); using (var uow = _db.UnitOfWork) { From 1552d2c8925045c3f88b4e1e55ebddceafeffa80 Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Fri, 11 Aug 2017 14:02:48 +0200 Subject: [PATCH 260/346] Fixed extra space in %target%, closes #1483 --- src/NadekoBot/Modules/CustomReactions/Extensions/Extensions.cs | 2 +- src/NadekoBot/Modules/Games/Common/Connect4/Connect4.cs | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/NadekoBot/Modules/CustomReactions/Extensions/Extensions.cs b/src/NadekoBot/Modules/CustomReactions/Extensions/Extensions.cs index 79591b7c..f24effb1 100644 --- a/src/NadekoBot/Modules/CustomReactions/Extensions/Extensions.cs +++ b/src/NadekoBot/Modules/CustomReactions/Extensions/Extensions.cs @@ -74,7 +74,7 @@ namespace NadekoBot.Modules.CustomReactions.Extensions var rep = new ReplacementBuilder() .WithDefault(ctx.Author, ctx.Channel, (ctx.Channel as ITextChannel)?.Guild, client) - .WithOverride("%target%", () => ctx.Content.Substring(substringIndex)) + .WithOverride("%target%", () => ctx.Content.Substring(substringIndex).Trim()) .Build(); str = rep.Replace(str); diff --git a/src/NadekoBot/Modules/Games/Common/Connect4/Connect4.cs b/src/NadekoBot/Modules/Games/Common/Connect4/Connect4.cs index 0ba9acb8..ae475e5e 100644 --- a/src/NadekoBot/Modules/Games/Common/Connect4/Connect4.cs +++ b/src/NadekoBot/Modules/Games/Common/Connect4/Connect4.cs @@ -7,7 +7,6 @@ using System.Threading.Tasks; namespace NadekoBot.Modules.Games.Common.Connect4 { - //todo: diagonal checking public class Connect4Game : IDisposable { public enum Phase From a98be21181059402c1759f689b6099c16f4d3552 Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Fri, 11 Aug 2017 22:53:31 +0200 Subject: [PATCH 261/346] small error fix --- src/NadekoBot/Modules/Searches/Services/SearchesService.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/NadekoBot/Modules/Searches/Services/SearchesService.cs b/src/NadekoBot/Modules/Searches/Services/SearchesService.cs index 396937a9..368c5618 100644 --- a/src/NadekoBot/Modules/Searches/Services/SearchesService.cs +++ b/src/NadekoBot/Modules/Searches/Services/SearchesService.cs @@ -35,7 +35,7 @@ namespace NadekoBot.Modules.Searches.Services public List WowJokes { get; } = new List(); public List MagicItems { get; } = new List(); - private readonly ConcurrentDictionary _imageCacher = new ConcurrentDictionary(); + private readonly ConcurrentDictionary _imageCacher = new ConcurrentDictionary(); private readonly ConcurrentDictionary> _blacklistedTags = new ConcurrentDictionary>(); @@ -133,7 +133,7 @@ namespace NadekoBot.Modules.Searches.Services { throw new TagBlacklistedException(); } - var cacher = _imageCacher.GetOrAdd(guild, (key) => new SearchImageCacher()); + var cacher = _imageCacher.GetOrAdd(guild ?? 0, (key) => new SearchImageCacher()); return cacher.GetImage(tag, isExplicit, type); } From 70906ed5cbe13a13df476395ea5c21785a704938 Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Sun, 13 Aug 2017 04:24:20 +0200 Subject: [PATCH 262/346] Added betflip and betroll multiplier to .bce, closes #1498 --- src/NadekoBot/Common/BotConfigEditType.cs | 4 ++++ .../Services/Impl/BotConfigProvider.cs | 24 +++++++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/src/NadekoBot/Common/BotConfigEditType.cs b/src/NadekoBot/Common/BotConfigEditType.cs index 32b1415c..5eadbe9d 100644 --- a/src/NadekoBot/Common/BotConfigEditType.cs +++ b/src/NadekoBot/Common/BotConfigEditType.cs @@ -2,6 +2,10 @@ { public enum BotConfigEditType { + BetflipMultiplier, + Betroll100Multiplier, + Betroll67Multiplier, + Betroll91Multiplier, CurrencyGenerationChance, CurrencyGenerationCooldown, CurrencyName, diff --git a/src/NadekoBot/Services/Impl/BotConfigProvider.cs b/src/NadekoBot/Services/Impl/BotConfigProvider.cs index c18a0e1a..0ed97550 100644 --- a/src/NadekoBot/Services/Impl/BotConfigProvider.cs +++ b/src/NadekoBot/Services/Impl/BotConfigProvider.cs @@ -98,6 +98,30 @@ namespace NadekoBot.Services.Impl else return false; break; + case BotConfigEditType.Betroll100Multiplier: + if (float.TryParse(newValue, out var br100) && br100 > 0) + bc.Betroll100Multiplier = br100; + else + return false; + break; + case BotConfigEditType.Betroll91Multiplier: + if (int.TryParse(newValue, out var br91) && br91 > 0) + bc.Betroll91Multiplier = br91; + else + return false; + break; + case BotConfigEditType.Betroll67Multiplier: + if (int.TryParse(newValue, out var br67) && br67 > 0) + bc.Betroll67Multiplier = br67; + else + return false; + break; + case BotConfigEditType.BetflipMultiplier: + if (int.TryParse(newValue, out var bf) && bf > 0) + bc.BetflipMultiplier = bf; + else + return false; + break; default: return false; } From 7a1895bf31c578bcb3f13bbf7b3573b12870a93b Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Mon, 14 Aug 2017 05:19:37 +0200 Subject: [PATCH 263/346] possible fix for flowerreaction event on public nadeko --- .../Gambling/CurrencyEventsCommands.cs | 40 +++++++++++++++---- src/NadekoBot/Services/CurrencyService.cs | 21 ++++++++++ .../Discord/SocketMessageEventWrapper.cs | 39 +++++++++++------- 3 files changed, 78 insertions(+), 22 deletions(-) diff --git a/src/NadekoBot/Modules/Gambling/CurrencyEventsCommands.cs b/src/NadekoBot/Modules/Gambling/CurrencyEventsCommands.cs index a250a80d..6fbee42e 100644 --- a/src/NadekoBot/Modules/Gambling/CurrencyEventsCommands.cs +++ b/src/NadekoBot/Modules/Gambling/CurrencyEventsCommands.cs @@ -11,6 +11,8 @@ using NadekoBot.Common; using NadekoBot.Common.Attributes; using NadekoBot.Common.Collections; using NLog; +using System.Collections.Concurrent; +using System.Collections.Generic; namespace NadekoBot.Modules.Gambling { @@ -137,14 +139,14 @@ namespace NadekoBot.Modules.Gambling desc, footer: footer) .ConfigureAwait(false); - await new FlowerReactionEvent(_client, _cs).Start(msg, context, amount); + await new FlowerReactionEvent(_client, _cs, amount).Start(msg, context); } } } public abstract class CurrencyEvent { - public abstract Task Start(IUserMessage msg, ICommandContext channel, int amount); + public abstract Task Start(IUserMessage msg, ICommandContext channel); } public class FlowerReactionEvent : CurrencyEvent @@ -160,14 +162,39 @@ namespace NadekoBot.Modules.Gambling private CancellationTokenSource Source { get; } private CancellationToken CancelToken { get; } - public FlowerReactionEvent(DiscordSocketClient client, CurrencyService cs) + private readonly ConcurrentQueue _toGiveTo = new ConcurrentQueue(); + private readonly int _amount; + + public FlowerReactionEvent(DiscordSocketClient client, CurrencyService cs, int amount) { _log = LogManager.GetCurrentClassLogger(); _client = client; _cs = cs; _botUser = client.CurrentUser; + _amount = amount; Source = new CancellationTokenSource(); CancelToken = Source.Token; + + var _ = Task.Run(async () => + { + + var users = new List(); + while (!CancelToken.IsCancellationRequested) + { + await Task.Delay(1000).ConfigureAwait(false); + while (_toGiveTo.TryDequeue(out var usrId)) + { + users.Add(usrId); + } + + if (users.Count > 0) + { + await _cs.AddToManyAsync("", _amount, users.ToArray()).ConfigureAwait(false); + } + + users.Clear(); + } + }, CancelToken); } private async Task End() @@ -191,7 +218,7 @@ namespace NadekoBot.Modules.Gambling return Task.CompletedTask; } - public override async Task Start(IUserMessage umsg, ICommandContext context, int amount) + public override async Task Start(IUserMessage umsg, ICommandContext context) { StartingMessage = umsg; _client.MessageDeleted += MessageDeletedEventHandler; @@ -206,7 +233,7 @@ namespace NadekoBot.Modules.Gambling catch { return; } } } - using (StartingMessage.OnReaction(_client, async (r) => + using (StartingMessage.OnReaction(_client, (r) => { try { @@ -215,8 +242,7 @@ namespace NadekoBot.Modules.Gambling if (r.Emote.Name == "🌸" && r.User.IsSpecified && ((DateTime.UtcNow - r.User.Value.CreatedAt).TotalDays > 5) && _flowerReactionAwardedUsers.Add(r.User.Value.Id)) { - await _cs.AddAsync(r.User.Value, "Flower Reaction Event", amount, false) - .ConfigureAwait(false); + _toGiveTo.Enqueue(r.UserId); } } catch diff --git a/src/NadekoBot/Services/CurrencyService.cs b/src/NadekoBot/Services/CurrencyService.cs index d7306a81..5b8fb44f 100644 --- a/src/NadekoBot/Services/CurrencyService.cs +++ b/src/NadekoBot/Services/CurrencyService.cs @@ -4,6 +4,7 @@ using Discord; using NadekoBot.Extensions; using NadekoBot.Services.Database.Models; using NadekoBot.Services.Database; +using NadekoBot.Services; namespace NadekoBot.Services { @@ -60,6 +61,26 @@ namespace NadekoBot.Services return true; } + public async Task AddToManyAsync(string reason, long amount, params ulong[] userIds) + { + using (var uow = _db.UnitOfWork) + { + foreach (var userId in userIds) + { + var transaction = new CurrencyTransaction() + { + UserId = userId, + Reason = reason, + Amount = amount, + }; + uow.Currency.TryUpdateState(userId, amount); + uow.CurrencyTransactions.Add(transaction); + } + + await uow.CompleteAsync(); + } + } + public async Task AddAsync(IUser author, string reason, long amount, bool sendMessage) { await AddAsync(author.Id, reason, amount); diff --git a/src/NadekoBot/Services/Discord/SocketMessageEventWrapper.cs b/src/NadekoBot/Services/Discord/SocketMessageEventWrapper.cs index 4903fd66..3c3aabf9 100644 --- a/src/NadekoBot/Services/Discord/SocketMessageEventWrapper.cs +++ b/src/NadekoBot/Services/Discord/SocketMessageEventWrapper.cs @@ -24,36 +24,45 @@ namespace NadekoBot.Services.Discord private Task Discord_ReactionsCleared(Cacheable msg, ISocketMessageChannel channel) { - try + Task.Run(() => { - if (msg.Id == Message.Id) - OnReactionsCleared?.Invoke(); - } - catch { } + try + { + if (msg.Id == Message.Id) + OnReactionsCleared?.Invoke(); + } + catch { } + }); return Task.CompletedTask; } private Task Discord_ReactionRemoved(Cacheable msg, ISocketMessageChannel channel, SocketReaction reaction) { - try + Task.Run(() => { - if (msg.Id == Message.Id) - OnReactionRemoved?.Invoke(reaction); - } - catch { } + try + { + if (msg.Id == Message.Id) + OnReactionRemoved?.Invoke(reaction); + } + catch { } + }); return Task.CompletedTask; } private Task Discord_ReactionAdded(Cacheable msg, ISocketMessageChannel channel, SocketReaction reaction) { - try + Task.Run(() => { - if (msg.Id == Message.Id) - OnReactionAdded?.Invoke(reaction); - } - catch { } + try + { + if (msg.Id == Message.Id) + OnReactionAdded?.Invoke(reaction); + } + catch { } + }); return Task.CompletedTask; } From d74a23d215d7bd523f185279d75a2926f21601eb Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Mon, 14 Aug 2017 07:25:32 +0200 Subject: [PATCH 264/346] You can now gift items to waifus with '.gift' command --- .../20170814044636_waifu-items.Designer.cs | 1711 +++++++++++++++++ .../Migrations/20170814044636_waifu-items.cs | 46 + .../NadekoSqliteContextModelSnapshot.cs | 29 + .../Modules/Gambling/WaifuClaimCommands.cs | 65 +- src/NadekoBot/Resources/CommandStrings.resx | 90 +- .../Services/Database/Models/Waifu.cs | 2 + .../Services/Database/Models/WaifuItem.cs | 87 + .../Repositories/Impl/WaifuRepository.cs | 2 + .../_strings/ResponseStrings.en-US.json | 3 + 9 files changed, 1953 insertions(+), 82 deletions(-) create mode 100644 src/NadekoBot/Migrations/20170814044636_waifu-items.Designer.cs create mode 100644 src/NadekoBot/Migrations/20170814044636_waifu-items.cs create mode 100644 src/NadekoBot/Services/Database/Models/WaifuItem.cs diff --git a/src/NadekoBot/Migrations/20170814044636_waifu-items.Designer.cs b/src/NadekoBot/Migrations/20170814044636_waifu-items.Designer.cs new file mode 100644 index 00000000..c76dcc33 --- /dev/null +++ b/src/NadekoBot/Migrations/20170814044636_waifu-items.Designer.cs @@ -0,0 +1,1711 @@ +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using NadekoBot.Services.Database; +using NadekoBot.Services.Database.Models; + +namespace NadekoBot.Migrations +{ + [DbContext(typeof(NadekoContext))] + [Migration("20170814044636_waifu-items")] + partial class waifuitems + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { + modelBuilder + .HasAnnotation("ProductVersion", "1.1.1"); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.AntiRaidSetting", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("Action"); + + b.Property("DateAdded"); + + b.Property("GuildConfigId"); + + b.Property("Seconds"); + + b.Property("UserThreshold"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId") + .IsUnique(); + + b.ToTable("AntiRaidSetting"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.AntiSpamIgnore", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AntiSpamSettingId"); + + b.Property("ChannelId"); + + b.Property("DateAdded"); + + b.HasKey("Id"); + + b.HasIndex("AntiSpamSettingId"); + + b.ToTable("AntiSpamIgnore"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.AntiSpamSetting", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("Action"); + + b.Property("DateAdded"); + + b.Property("GuildConfigId"); + + b.Property("MessageThreshold"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId") + .IsUnique(); + + b.ToTable("AntiSpamSetting"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.BlacklistItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("BotConfigId"); + + b.Property("DateAdded"); + + b.Property("ItemId"); + + b.Property("Type"); + + b.HasKey("Id"); + + b.HasIndex("BotConfigId"); + + b.ToTable("BlacklistItem"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.BlockedCmdOrMdl", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("BotConfigId"); + + b.Property("BotConfigId1"); + + b.Property("DateAdded"); + + b.Property("Name"); + + b.HasKey("Id"); + + b.HasIndex("BotConfigId"); + + b.HasIndex("BotConfigId1"); + + b.ToTable("BlockedCmdOrMdl"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.BotConfig", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("BetflipMultiplier"); + + b.Property("Betroll100Multiplier"); + + b.Property("Betroll67Multiplier"); + + b.Property("Betroll91Multiplier"); + + b.Property("BufferSize"); + + b.Property("CurrencyDropAmount"); + + b.Property("CurrencyDropAmountMax"); + + b.Property("CurrencyGenerationChance"); + + b.Property("CurrencyGenerationCooldown"); + + b.Property("CurrencyName"); + + b.Property("CurrencyPluralName"); + + b.Property("CurrencySign"); + + b.Property("CustomReactionsStartWith"); + + b.Property("DMHelpString"); + + b.Property("DateAdded"); + + b.Property("DefaultPrefix"); + + b.Property("ErrorColor"); + + b.Property("ForwardMessages"); + + b.Property("ForwardToAllOwners"); + + b.Property("HelpString"); + + b.Property("Locale"); + + b.Property("MigrationVersion"); + + b.Property("MinimumBetAmount"); + + b.Property("OkColor"); + + b.Property("PermissionVersion"); + + b.Property("RemindMessageFormat"); + + b.Property("RotatingStatuses"); + + b.Property("TriviaCurrencyReward"); + + b.HasKey("Id"); + + b.ToTable("BotConfig"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.ClashCaller", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("BaseDestroyed"); + + b.Property("CallUser"); + + b.Property("ClashWarId"); + + b.Property("DateAdded"); + + b.Property("SequenceNumber"); + + b.Property("Stars"); + + b.Property("TimeAdded"); + + b.HasKey("Id"); + + b.HasIndex("ClashWarId"); + + b.ToTable("ClashCallers"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.ClashWar", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ChannelId"); + + b.Property("DateAdded"); + + b.Property("EnemyClan"); + + b.Property("GuildId"); + + b.Property("Size"); + + b.Property("StartedAt"); + + b.Property("WarState"); + + b.HasKey("Id"); + + b.ToTable("ClashOfClans"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.CommandAlias", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("GuildConfigId"); + + b.Property("Mapping"); + + b.Property("Trigger"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.ToTable("CommandAlias"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.CommandCooldown", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("CommandName"); + + b.Property("DateAdded"); + + b.Property("GuildConfigId"); + + b.Property("Seconds"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.ToTable("CommandCooldown"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.CommandPrice", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("BotConfigId"); + + b.Property("CommandName"); + + b.Property("DateAdded"); + + b.Property("Price"); + + b.HasKey("Id"); + + b.HasIndex("BotConfigId"); + + b.HasIndex("Price") + .IsUnique(); + + b.ToTable("CommandPrice"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.ConvertUnit", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("InternalTrigger"); + + b.Property("Modifier"); + + b.Property("UnitType"); + + b.HasKey("Id"); + + b.ToTable("ConversionUnits"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.Currency", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("Amount"); + + b.Property("DateAdded"); + + b.Property("UserId"); + + b.HasKey("Id"); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("Currency"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.CurrencyTransaction", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("Amount"); + + b.Property("DateAdded"); + + b.Property("Reason"); + + b.Property("UserId"); + + b.HasKey("Id"); + + b.ToTable("CurrencyTransactions"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.CustomReaction", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AutoDeleteTrigger"); + + b.Property("ContainsAnywhere"); + + b.Property("DateAdded"); + + b.Property("DmResponse"); + + b.Property("GuildId"); + + b.Property("IsRegex"); + + b.Property("OwnerOnly"); + + b.Property("Response"); + + b.Property("Trigger"); + + b.HasKey("Id"); + + b.ToTable("CustomReactions"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.DiscordUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AvatarId"); + + b.Property("DateAdded"); + + b.Property("Discriminator"); + + b.Property("UserId"); + + b.Property("Username"); + + b.HasKey("Id"); + + b.HasAlternateKey("UserId"); + + b.ToTable("DiscordUser"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.Donator", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("Amount"); + + b.Property("DateAdded"); + + b.Property("Name"); + + b.Property("UserId"); + + b.HasKey("Id"); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("Donators"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.EightBallResponse", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("BotConfigId"); + + b.Property("DateAdded"); + + b.Property("Text"); + + b.HasKey("Id"); + + b.HasIndex("BotConfigId"); + + b.ToTable("EightBallResponses"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.FilterChannelId", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ChannelId"); + + b.Property("DateAdded"); + + b.Property("GuildConfigId"); + + b.Property("GuildConfigId1"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.HasIndex("GuildConfigId1"); + + b.ToTable("FilterChannelId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.FilteredWord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("GuildConfigId"); + + b.Property("Word"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.ToTable("FilteredWord"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.FollowedStream", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ChannelId"); + + b.Property("DateAdded"); + + b.Property("GuildConfigId"); + + b.Property("GuildId"); + + b.Property("Type"); + + b.Property("Username"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.ToTable("FollowedStream"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.GCChannelId", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ChannelId"); + + b.Property("DateAdded"); + + b.Property("GuildConfigId"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.ToTable("GCChannelId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.GuildConfig", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AutoAssignRoleId"); + + b.Property("AutoDeleteByeMessages"); + + b.Property("AutoDeleteByeMessagesTimer"); + + b.Property("AutoDeleteGreetMessages"); + + b.Property("AutoDeleteGreetMessagesTimer"); + + b.Property("AutoDeleteSelfAssignedRoleMessages"); + + b.Property("ByeMessageChannelId"); + + b.Property("ChannelByeMessageText"); + + b.Property("ChannelGreetMessageText"); + + b.Property("CleverbotEnabled"); + + b.Property("DateAdded"); + + b.Property("DefaultMusicVolume"); + + b.Property("DeleteMessageOnCommand"); + + b.Property("DmGreetMessageText"); + + b.Property("ExclusiveSelfAssignedRoles"); + + b.Property("FilterInvites"); + + b.Property("FilterWords"); + + b.Property("GameVoiceChannel"); + + b.Property("GreetMessageChannelId"); + + b.Property("GuildId"); + + b.Property("Locale"); + + b.Property("LogSettingId"); + + b.Property("MuteRoleName"); + + b.Property("PermissionRole"); + + b.Property("Prefix"); + + b.Property("RootPermissionId"); + + b.Property("SendChannelByeMessage"); + + b.Property("SendChannelGreetMessage"); + + b.Property("SendDmGreetMessage"); + + b.Property("TimeZoneId"); + + b.Property("VerboseErrors"); + + b.Property("VerbosePermissions"); + + b.Property("VoicePlusTextEnabled"); + + b.Property("WarningsInitialized"); + + b.HasKey("Id"); + + b.HasIndex("GuildId") + .IsUnique(); + + b.HasIndex("LogSettingId"); + + b.HasIndex("RootPermissionId"); + + b.ToTable("GuildConfigs"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.GuildRepeater", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ChannelId"); + + b.Property("DateAdded"); + + b.Property("GuildConfigId"); + + b.Property("GuildId"); + + b.Property("Interval"); + + b.Property("Message"); + + b.Property("StartTimeOfDay"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.ToTable("GuildRepeater"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.IgnoredLogChannel", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ChannelId"); + + b.Property("DateAdded"); + + b.Property("LogSettingId"); + + b.HasKey("Id"); + + b.HasIndex("LogSettingId"); + + b.ToTable("IgnoredLogChannels"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.IgnoredVoicePresenceChannel", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ChannelId"); + + b.Property("DateAdded"); + + b.Property("LogSettingId"); + + b.HasKey("Id"); + + b.HasIndex("LogSettingId"); + + b.ToTable("IgnoredVoicePresenceCHannels"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.LogSetting", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ChannelCreated"); + + b.Property("ChannelCreatedId"); + + b.Property("ChannelDestroyed"); + + b.Property("ChannelDestroyedId"); + + b.Property("ChannelId"); + + b.Property("ChannelUpdated"); + + b.Property("ChannelUpdatedId"); + + b.Property("DateAdded"); + + b.Property("IsLogging"); + + b.Property("LogOtherId"); + + b.Property("LogUserPresence"); + + b.Property("LogUserPresenceId"); + + b.Property("LogVoicePresence"); + + b.Property("LogVoicePresenceId"); + + b.Property("LogVoicePresenceTTSId"); + + b.Property("MessageDeleted"); + + b.Property("MessageDeletedId"); + + b.Property("MessageUpdated"); + + b.Property("MessageUpdatedId"); + + b.Property("UserBanned"); + + b.Property("UserBannedId"); + + b.Property("UserJoined"); + + b.Property("UserJoinedId"); + + b.Property("UserLeft"); + + b.Property("UserLeftId"); + + b.Property("UserMutedId"); + + b.Property("UserPresenceChannelId"); + + b.Property("UserUnbanned"); + + b.Property("UserUnbannedId"); + + b.Property("UserUpdated"); + + b.Property("UserUpdatedId"); + + b.Property("VoicePresenceChannelId"); + + b.HasKey("Id"); + + b.ToTable("LogSettings"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.ModulePrefix", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("BotConfigId"); + + b.Property("DateAdded"); + + b.Property("ModuleName"); + + b.Property("Prefix"); + + b.HasKey("Id"); + + b.HasIndex("BotConfigId"); + + b.ToTable("ModulePrefixes"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.MusicPlaylist", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("Author"); + + b.Property("AuthorId"); + + b.Property("DateAdded"); + + b.Property("Name"); + + b.HasKey("Id"); + + b.ToTable("MusicPlaylists"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.MutedUserId", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("GuildConfigId"); + + b.Property("UserId"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.ToTable("MutedUserId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.NsfwBlacklitedTag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("GuildConfigId"); + + b.Property("Tag"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.ToTable("NsfwBlacklitedTag"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.Permission", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("NextId"); + + b.Property("PrimaryTarget"); + + b.Property("PrimaryTargetId"); + + b.Property("SecondaryTarget"); + + b.Property("SecondaryTargetName"); + + b.Property("State"); + + b.HasKey("Id"); + + b.HasIndex("NextId") + .IsUnique(); + + b.ToTable("Permission"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.Permissionv2", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("GuildConfigId"); + + b.Property("Index"); + + b.Property("PrimaryTarget"); + + b.Property("PrimaryTargetId"); + + b.Property("SecondaryTarget"); + + b.Property("SecondaryTargetName"); + + b.Property("State"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.ToTable("Permissionv2"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.PlayingStatus", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("BotConfigId"); + + b.Property("DateAdded"); + + b.Property("Status"); + + b.HasKey("Id"); + + b.HasIndex("BotConfigId"); + + b.ToTable("PlayingStatus"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.PlaylistSong", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("MusicPlaylistId"); + + b.Property("Provider"); + + b.Property("ProviderType"); + + b.Property("Query"); + + b.Property("Title"); + + b.Property("Uri"); + + b.HasKey("Id"); + + b.HasIndex("MusicPlaylistId"); + + b.ToTable("PlaylistSong"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.Quote", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AuthorId"); + + b.Property("AuthorName") + .IsRequired(); + + b.Property("DateAdded"); + + b.Property("GuildId"); + + b.Property("Keyword") + .IsRequired(); + + b.Property("Text") + .IsRequired(); + + b.HasKey("Id"); + + b.ToTable("Quotes"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.RaceAnimal", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("BotConfigId"); + + b.Property("DateAdded"); + + b.Property("Icon"); + + b.Property("Name"); + + b.HasKey("Id"); + + b.HasIndex("BotConfigId"); + + b.ToTable("RaceAnimals"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.Reminder", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ChannelId"); + + b.Property("DateAdded"); + + b.Property("IsPrivate"); + + b.Property("Message"); + + b.Property("ServerId"); + + b.Property("UserId"); + + b.Property("When"); + + b.HasKey("Id"); + + b.ToTable("Reminders"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.RewardedUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AmountRewardedThisMonth"); + + b.Property("DateAdded"); + + b.Property("LastReward"); + + b.Property("PatreonUserId"); + + b.Property("UserId"); + + b.HasKey("Id"); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("RewardedUsers"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.SelfAssignedRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("GuildId"); + + b.Property("RoleId"); + + b.HasKey("Id"); + + b.HasIndex("GuildId", "RoleId") + .IsUnique(); + + b.ToTable("SelfAssignableRoles"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.ShopEntry", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AuthorId"); + + b.Property("DateAdded"); + + b.Property("GuildConfigId"); + + b.Property("Index"); + + b.Property("Name"); + + b.Property("Price"); + + b.Property("RoleId"); + + b.Property("RoleName"); + + b.Property("Type"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.ToTable("ShopEntry"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.ShopEntryItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("ShopEntryId"); + + b.Property("Text"); + + b.HasKey("Id"); + + b.HasIndex("ShopEntryId"); + + b.ToTable("ShopEntryItem"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.SlowmodeIgnoredRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("GuildConfigId"); + + b.Property("RoleId"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.ToTable("SlowmodeIgnoredRole"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.SlowmodeIgnoredUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("GuildConfigId"); + + b.Property("UserId"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.ToTable("SlowmodeIgnoredUser"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.StartupCommand", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("BotConfigId"); + + b.Property("ChannelId"); + + b.Property("ChannelName"); + + b.Property("CommandText"); + + b.Property("DateAdded"); + + b.Property("GuildId"); + + b.Property("GuildName"); + + b.Property("Index"); + + b.Property("VoiceChannelId"); + + b.Property("VoiceChannelName"); + + b.HasKey("Id"); + + b.HasIndex("BotConfigId"); + + b.ToTable("StartupCommand"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.StreamRoleBlacklistedUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("StreamRoleSettingsId"); + + b.Property("UserId"); + + b.Property("Username"); + + b.HasKey("Id"); + + b.HasIndex("StreamRoleSettingsId"); + + b.ToTable("StreamRoleBlacklistedUser"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.StreamRoleSettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AddRoleId"); + + b.Property("DateAdded"); + + b.Property("Enabled"); + + b.Property("FromRoleId"); + + b.Property("GuildConfigId"); + + b.Property("Keyword"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId") + .IsUnique(); + + b.ToTable("StreamRoleSettings"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.StreamRoleWhitelistedUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("StreamRoleSettingsId"); + + b.Property("UserId"); + + b.Property("Username"); + + b.HasKey("Id"); + + b.HasIndex("StreamRoleSettingsId"); + + b.ToTable("StreamRoleWhitelistedUser"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.UnmuteTimer", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("GuildConfigId"); + + b.Property("UnmuteAt"); + + b.Property("UserId"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.ToTable("UnmuteTimer"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.UserPokeTypes", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("UserId"); + + b.Property("type"); + + b.HasKey("Id"); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("PokeGame"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.VcRoleInfo", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("GuildConfigId"); + + b.Property("RoleId"); + + b.Property("VoiceChannelId"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.ToTable("VcRoleInfo"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.WaifuInfo", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AffinityId"); + + b.Property("ClaimerId"); + + b.Property("DateAdded"); + + b.Property("Price"); + + b.Property("WaifuId"); + + b.HasKey("Id"); + + b.HasIndex("AffinityId"); + + b.HasIndex("ClaimerId"); + + b.HasIndex("WaifuId") + .IsUnique(); + + b.ToTable("WaifuInfo"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.WaifuItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("Item"); + + b.Property("ItemEmoji"); + + b.Property("Price"); + + b.Property("WaifuInfoId"); + + b.HasKey("Id"); + + b.HasIndex("WaifuInfoId"); + + b.ToTable("WaifuItem"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.WaifuUpdate", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("NewId"); + + b.Property("OldId"); + + b.Property("UpdateType"); + + b.Property("UserId"); + + b.HasKey("Id"); + + b.HasIndex("NewId"); + + b.HasIndex("OldId"); + + b.HasIndex("UserId"); + + b.ToTable("WaifuUpdates"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.Warning", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("Forgiven"); + + b.Property("ForgivenBy"); + + b.Property("GuildId"); + + b.Property("Moderator"); + + b.Property("Reason"); + + b.Property("UserId"); + + b.HasKey("Id"); + + b.ToTable("Warnings"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.WarningPunishment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("Count"); + + b.Property("DateAdded"); + + b.Property("GuildConfigId"); + + b.Property("Punishment"); + + b.Property("Time"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.ToTable("WarningPunishment"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.AntiRaidSetting", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig", "GuildConfig") + .WithOne("AntiRaidSetting") + .HasForeignKey("NadekoBot.Services.Database.Models.AntiRaidSetting", "GuildConfigId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.AntiSpamIgnore", b => + { + b.HasOne("NadekoBot.Services.Database.Models.AntiSpamSetting") + .WithMany("IgnoredChannels") + .HasForeignKey("AntiSpamSettingId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.AntiSpamSetting", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig", "GuildConfig") + .WithOne("AntiSpamSetting") + .HasForeignKey("NadekoBot.Services.Database.Models.AntiSpamSetting", "GuildConfigId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.BlacklistItem", b => + { + b.HasOne("NadekoBot.Services.Database.Models.BotConfig") + .WithMany("Blacklist") + .HasForeignKey("BotConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.BlockedCmdOrMdl", b => + { + b.HasOne("NadekoBot.Services.Database.Models.BotConfig") + .WithMany("BlockedCommands") + .HasForeignKey("BotConfigId"); + + b.HasOne("NadekoBot.Services.Database.Models.BotConfig") + .WithMany("BlockedModules") + .HasForeignKey("BotConfigId1"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.ClashCaller", b => + { + b.HasOne("NadekoBot.Services.Database.Models.ClashWar", "ClashWar") + .WithMany("Bases") + .HasForeignKey("ClashWarId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.CommandAlias", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig") + .WithMany("CommandAliases") + .HasForeignKey("GuildConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.CommandCooldown", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig") + .WithMany("CommandCooldowns") + .HasForeignKey("GuildConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.CommandPrice", b => + { + b.HasOne("NadekoBot.Services.Database.Models.BotConfig") + .WithMany("CommandPrices") + .HasForeignKey("BotConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.EightBallResponse", b => + { + b.HasOne("NadekoBot.Services.Database.Models.BotConfig") + .WithMany("EightBallResponses") + .HasForeignKey("BotConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.FilterChannelId", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig") + .WithMany("FilterInvitesChannelIds") + .HasForeignKey("GuildConfigId"); + + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig") + .WithMany("FilterWordsChannelIds") + .HasForeignKey("GuildConfigId1"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.FilteredWord", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig") + .WithMany("FilteredWords") + .HasForeignKey("GuildConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.FollowedStream", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig") + .WithMany("FollowedStreams") + .HasForeignKey("GuildConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.GCChannelId", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig") + .WithMany("GenerateCurrencyChannelIds") + .HasForeignKey("GuildConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.GuildConfig", b => + { + b.HasOne("NadekoBot.Services.Database.Models.LogSetting", "LogSetting") + .WithMany() + .HasForeignKey("LogSettingId"); + + b.HasOne("NadekoBot.Services.Database.Models.Permission", "RootPermission") + .WithMany() + .HasForeignKey("RootPermissionId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.GuildRepeater", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig") + .WithMany("GuildRepeaters") + .HasForeignKey("GuildConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.IgnoredLogChannel", b => + { + b.HasOne("NadekoBot.Services.Database.Models.LogSetting", "LogSetting") + .WithMany("IgnoredChannels") + .HasForeignKey("LogSettingId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.IgnoredVoicePresenceChannel", b => + { + b.HasOne("NadekoBot.Services.Database.Models.LogSetting", "LogSetting") + .WithMany("IgnoredVoicePresenceChannelIds") + .HasForeignKey("LogSettingId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.ModulePrefix", b => + { + b.HasOne("NadekoBot.Services.Database.Models.BotConfig") + .WithMany("ModulePrefixes") + .HasForeignKey("BotConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.MutedUserId", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig") + .WithMany("MutedUsers") + .HasForeignKey("GuildConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.NsfwBlacklitedTag", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig") + .WithMany("NsfwBlacklistedTags") + .HasForeignKey("GuildConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.Permission", b => + { + b.HasOne("NadekoBot.Services.Database.Models.Permission", "Next") + .WithOne("Previous") + .HasForeignKey("NadekoBot.Services.Database.Models.Permission", "NextId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.Permissionv2", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig") + .WithMany("Permissions") + .HasForeignKey("GuildConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.PlayingStatus", b => + { + b.HasOne("NadekoBot.Services.Database.Models.BotConfig") + .WithMany("RotatingStatusMessages") + .HasForeignKey("BotConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.PlaylistSong", b => + { + b.HasOne("NadekoBot.Services.Database.Models.MusicPlaylist") + .WithMany("Songs") + .HasForeignKey("MusicPlaylistId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.RaceAnimal", b => + { + b.HasOne("NadekoBot.Services.Database.Models.BotConfig") + .WithMany("RaceAnimals") + .HasForeignKey("BotConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.ShopEntry", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig") + .WithMany("ShopEntries") + .HasForeignKey("GuildConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.ShopEntryItem", b => + { + b.HasOne("NadekoBot.Services.Database.Models.ShopEntry") + .WithMany("Items") + .HasForeignKey("ShopEntryId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.SlowmodeIgnoredRole", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig") + .WithMany("SlowmodeIgnoredRoles") + .HasForeignKey("GuildConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.SlowmodeIgnoredUser", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig") + .WithMany("SlowmodeIgnoredUsers") + .HasForeignKey("GuildConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.StartupCommand", b => + { + b.HasOne("NadekoBot.Services.Database.Models.BotConfig") + .WithMany("StartupCommands") + .HasForeignKey("BotConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.StreamRoleBlacklistedUser", b => + { + b.HasOne("NadekoBot.Services.Database.Models.StreamRoleSettings") + .WithMany("Blacklist") + .HasForeignKey("StreamRoleSettingsId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.StreamRoleSettings", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig", "GuildConfig") + .WithOne("StreamRole") + .HasForeignKey("NadekoBot.Services.Database.Models.StreamRoleSettings", "GuildConfigId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.StreamRoleWhitelistedUser", b => + { + b.HasOne("NadekoBot.Services.Database.Models.StreamRoleSettings") + .WithMany("Whitelist") + .HasForeignKey("StreamRoleSettingsId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.UnmuteTimer", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig") + .WithMany("UnmuteTimers") + .HasForeignKey("GuildConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.VcRoleInfo", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig") + .WithMany("VcRoleInfos") + .HasForeignKey("GuildConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.WaifuInfo", b => + { + b.HasOne("NadekoBot.Services.Database.Models.DiscordUser", "Affinity") + .WithMany() + .HasForeignKey("AffinityId"); + + b.HasOne("NadekoBot.Services.Database.Models.DiscordUser", "Claimer") + .WithMany() + .HasForeignKey("ClaimerId"); + + b.HasOne("NadekoBot.Services.Database.Models.DiscordUser", "Waifu") + .WithOne() + .HasForeignKey("NadekoBot.Services.Database.Models.WaifuInfo", "WaifuId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.WaifuItem", b => + { + b.HasOne("NadekoBot.Services.Database.Models.WaifuInfo") + .WithMany("Items") + .HasForeignKey("WaifuInfoId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.WaifuUpdate", b => + { + b.HasOne("NadekoBot.Services.Database.Models.DiscordUser", "New") + .WithMany() + .HasForeignKey("NewId"); + + b.HasOne("NadekoBot.Services.Database.Models.DiscordUser", "Old") + .WithMany() + .HasForeignKey("OldId"); + + b.HasOne("NadekoBot.Services.Database.Models.DiscordUser", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.WarningPunishment", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig") + .WithMany("WarnPunishments") + .HasForeignKey("GuildConfigId"); + }); + } + } +} diff --git a/src/NadekoBot/Migrations/20170814044636_waifu-items.cs b/src/NadekoBot/Migrations/20170814044636_waifu-items.cs new file mode 100644 index 00000000..3054342f --- /dev/null +++ b/src/NadekoBot/Migrations/20170814044636_waifu-items.cs @@ -0,0 +1,46 @@ +using System; +using System.Collections.Generic; +using Microsoft.EntityFrameworkCore.Migrations; + +namespace NadekoBot.Migrations +{ + public partial class waifuitems : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "WaifuItem", + columns: table => new + { + Id = table.Column(nullable: false) + .Annotation("Sqlite:Autoincrement", true), + DateAdded = table.Column(nullable: true), + Item = table.Column(nullable: false), + ItemEmoji = table.Column(nullable: true), + Price = table.Column(nullable: false), + WaifuInfoId = table.Column(nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_WaifuItem", x => x.Id); + table.ForeignKey( + name: "FK_WaifuItem_WaifuInfo_WaifuInfoId", + column: x => x.WaifuInfoId, + principalTable: "WaifuInfo", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + }); + + migrationBuilder.CreateIndex( + name: "IX_WaifuItem_WaifuInfoId", + table: "WaifuItem", + column: "WaifuInfoId"); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "WaifuItem"); + } + } +} diff --git a/src/NadekoBot/Migrations/NadekoSqliteContextModelSnapshot.cs b/src/NadekoBot/Migrations/NadekoSqliteContextModelSnapshot.cs index a9d7a682..f387dcc2 100644 --- a/src/NadekoBot/Migrations/NadekoSqliteContextModelSnapshot.cs +++ b/src/NadekoBot/Migrations/NadekoSqliteContextModelSnapshot.cs @@ -1297,6 +1297,28 @@ namespace NadekoBot.Migrations b.ToTable("WaifuInfo"); }); + modelBuilder.Entity("NadekoBot.Services.Database.Models.WaifuItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("Item"); + + b.Property("ItemEmoji"); + + b.Property("Price"); + + b.Property("WaifuInfoId"); + + b.HasKey("Id"); + + b.HasIndex("WaifuInfoId"); + + b.ToTable("WaifuItem"); + }); + modelBuilder.Entity("NadekoBot.Services.Database.Models.WaifuUpdate", b => { b.Property("Id") @@ -1654,6 +1676,13 @@ namespace NadekoBot.Migrations .OnDelete(DeleteBehavior.Cascade); }); + modelBuilder.Entity("NadekoBot.Services.Database.Models.WaifuItem", b => + { + b.HasOne("NadekoBot.Services.Database.Models.WaifuInfo") + .WithMany("Items") + .HasForeignKey("WaifuInfoId"); + }); + modelBuilder.Entity("NadekoBot.Services.Database.Models.WaifuUpdate", b => { b.HasOne("NadekoBot.Services.Database.Models.DiscordUser", "New") diff --git a/src/NadekoBot/Modules/Gambling/WaifuClaimCommands.cs b/src/NadekoBot/Modules/Gambling/WaifuClaimCommands.cs index baa1781b..fbe9a101 100644 --- a/src/NadekoBot/Modules/Gambling/WaifuClaimCommands.cs +++ b/src/NadekoBot/Modules/Gambling/WaifuClaimCommands.cs @@ -459,11 +459,74 @@ namespace NadekoBot.Modules.Gambling .AddField(efb => efb.WithName(GetText("likes")).WithValue(w.Affinity?.ToString() ?? nobody).WithIsInline(true)) .AddField(efb => efb.WithName(GetText("changes_of_heart")).WithValue($"{affInfo.Count} - \"the {affInfo.Title}\"").WithIsInline(true)) .AddField(efb => efb.WithName(GetText("divorces")).WithValue(divorces.ToString()).WithIsInline(true)) - .AddField(efb => efb.WithName($"Waifus ({claims.Count})").WithValue(claims.Count == 0 ? nobody : string.Join("\n", claims.OrderBy(x => rng.Next()).Take(30).Select(x => x.Waifu))).WithIsInline(true)); + .AddField(efb => efb.WithName(GetText("gifts")).WithValue(!w.Items.Any() ? "-" : string.Join("\n", w.Items.OrderBy(x => x.Price).GroupBy(x => x.ItemEmoji).Select(x => $"{x.Key} x{x.Count()}"))).WithIsInline(true)) + .AddField(efb => efb.WithName($"Waifus ({claims.Count})").WithValue(claims.Count == 0 ? nobody : string.Join("\n", claims.OrderBy(x => rng.Next()).Take(30).Select(x => x.Waifu))).WithIsInline(false)); await Context.Channel.EmbedAsync(embed).ConfigureAwait(false); } + [NadekoCommand, Usage, Description, Aliases] + [RequireContext(ContextType.Guild)] + [Priority(1)] + public async Task WaifuGift() + { + var embed = new EmbedBuilder() + .WithTitle(GetText("waifu_gift_shop")) + .WithOkColor(); + + Enum.GetValues(typeof(WaifuItem.ItemName)) + .Cast() + .Select(x => WaifuItem.GetItem(x)) + .ForEach(x => embed.AddField(f => f.WithName(x.ItemEmoji + " " + x.Item).WithValue(x.Price).WithIsInline(true))); + + await Context.Channel.EmbedAsync(embed).ConfigureAwait(false); + } + + [NadekoCommand, Usage, Description, Aliases] + [RequireContext(ContextType.Guild)] + [Priority(0)] + public async Task WaifuGift(WaifuItem.ItemName item, [Remainder] IUser waifu) + { + var itemObj = WaifuItem.GetItem(item); + + using (var uow = _db.UnitOfWork) + { + var w = uow.Waifus.ByWaifuUserId(waifu.Id); + + //try to buy the item first + + if (!await _cs.RemoveAsync(Context.User.Id, "Bought waifu item", itemObj.Price, uow)) + { + await ReplyErrorLocalized("not_enough", _bc.BotConfig.CurrencySign).ConfigureAwait(false); + return; + } + if (w == null) + { + uow.Waifus.Add(w = new WaifuInfo() + { + Affinity = null, + Claimer = null, + Price = 1, + Waifu = uow.DiscordUsers.GetOrCreate(waifu), + }); + + w.Waifu.Username = waifu.Username; + w.Waifu.Discriminator = waifu.Discriminator; + } + w.Items.Add(itemObj); + if (w.Claimer?.UserId == Context.User.Id) + { + w.Price += itemObj.Price; + } + else + w.Price += itemObj.Price / 2; + + await uow.CompleteAsync().ConfigureAwait(false); + } + + await ReplyConfirmLocalized("waifu_gift", Format.Bold(item.ToString() + " " +itemObj.ItemEmoji), Format.Bold(waifu.ToString())).ConfigureAwait(false); + } + public struct WaifuProfileTitle { diff --git a/src/NadekoBot/Resources/CommandStrings.resx b/src/NadekoBot/Resources/CommandStrings.resx index 18def6af..cf106686 100644 --- a/src/NadekoBot/Resources/CommandStrings.resx +++ b/src/NadekoBot/Resources/CommandStrings.resx @@ -2259,87 +2259,6 @@ `{0}butts` or `{0}ass` - - createwar cw - - - Creates a new war by specifying a size (>10 and multiple of 5) and enemy clan name. - - - `{0}cw 15 The Enemy Clan` - - - startwar sw - - - Starts a war with a given number. - - - `{0}sw 15` - - - listwar lw - - - Shows the active war claims by a number. Shows all wars in a short way if no number is specified. - - - `{0}lw [war_number]` or `{0}lw` - - - basecall - - - Claims a certain base from a certain war. You can supply a name in the third optional argument to claim in someone else's place. - - - `{0}basecall [war_number] [base_number] [optional_other_name]` - - - callfinish cf - - - Finish your claim with 3 stars if you destroyed a base. First argument is the war number, optional second argument is a base number if you want to finish for someone else. - - - `{0}cf 1` or `{0}cf 1 5` - - - callfinish2 cf2 - - - Finish your claim with 2 stars if you destroyed a base. First argument is the war number, optional second argument is a base number if you want to finish for someone else. - - - `{0}cf2 1` or `{0}cf2 1 5` - - - callfinish1 cf1 - - - Finish your claim with 1 star if you destroyed a base. First argument is the war number, optional second argument is a base number if you want to finish for someone else. - - - `{0}cf1 1` or `{0}cf1 1 5` - - - uncall - - - Removes your claim from a certain war. Optional second argument denotes a person in whose place to unclaim - - - `{0}uc [war_number] [optional_other_name]` - - - endwar ew - - - Ends the war with a given index. - - - `{0}ew [war_number]` - translate trans @@ -3087,6 +3006,15 @@ `{0}claim 50 @Himesama` + + waifugift gift gifts + + + Gift an item to someone. This will increase their waifu value by 50% of the gifted item's value if they don't have affinity set towards you, or 100% if they do. Provide no arguments to see a list of items that you can gift. + + + `{0}gifts` or `{0}gift Rose @Himesama` + waifus waifulb diff --git a/src/NadekoBot/Services/Database/Models/Waifu.cs b/src/NadekoBot/Services/Database/Models/Waifu.cs index 88c2778a..5be73b46 100644 --- a/src/NadekoBot/Services/Database/Models/Waifu.cs +++ b/src/NadekoBot/Services/Database/Models/Waifu.cs @@ -1,4 +1,5 @@ using NadekoBot.Extensions; +using System.Collections.Generic; namespace NadekoBot.Services.Database.Models { @@ -14,6 +15,7 @@ namespace NadekoBot.Services.Database.Models public DiscordUser Affinity { get; set; } public int Price { get; set; } + public List Items { get; set; } = new List(); public override string ToString() { diff --git a/src/NadekoBot/Services/Database/Models/WaifuItem.cs b/src/NadekoBot/Services/Database/Models/WaifuItem.cs new file mode 100644 index 00000000..2184391f --- /dev/null +++ b/src/NadekoBot/Services/Database/Models/WaifuItem.cs @@ -0,0 +1,87 @@ +using System; + +namespace NadekoBot.Services.Database.Models +{ + public class WaifuItem : DbEntity + { + public string ItemEmoji { get; set; } + public int Price { get; set; } + public ItemName Item { get; set; } + + public enum ItemName + { + Cookie, + Rose, + LoveLetter, + Chocolate, + Rice, + MovieTicket, + Book, + Lipstick, + Laptop, + Violin, + Ring, + Helicopter, + } + + public WaifuItem() + { + + } + + public WaifuItem(string itemEmoji, int price, ItemName item) + { + ItemEmoji = itemEmoji; + Price = price; + Item = item; + } + + public static WaifuItem GetItem(ItemName itemName) + { + switch (itemName) + { + case ItemName.Cookie: + return new WaifuItem("🍪", 10, itemName); + case ItemName.Rose: + return new WaifuItem("🌹", 50, itemName); + case ItemName.LoveLetter: + return new WaifuItem("💌", 100, itemName); + case ItemName.Chocolate: + return new WaifuItem("🍫", 200, itemName); + case ItemName.Rice: + return new WaifuItem("🍚", 400, itemName); + case ItemName.MovieTicket: + return new WaifuItem("🎟", 800, itemName); + case ItemName.Book: + return new WaifuItem("📔", 1500, itemName); + case ItemName.Lipstick: + return new WaifuItem("💄", 3000, itemName); + case ItemName.Laptop: + return new WaifuItem("💻", 5000, itemName); + case ItemName.Violin: + return new WaifuItem("🎻", 7500, itemName); + case ItemName.Ring: + return new WaifuItem("💍", 10000, itemName); + case ItemName.Helicopter: + return new WaifuItem("🚁", 20000, itemName); + default: + throw new ArgumentException(nameof(itemName)); + } + } + } +} + + +/* +🍪 Cookie 10 +🌹 Rose 50 +💌 Love Letter 100 +🍫 Chocolate 200 +🍚 Rice 400 +🎟 Movie Ticket 800 +📔 Book 1.5k +💄 Lipstick 3k +💻 Laptop 5k +🎻 Violin 7.5k +💍 Ring 10k +*/ diff --git a/src/NadekoBot/Services/Database/Repositories/Impl/WaifuRepository.cs b/src/NadekoBot/Services/Database/Repositories/Impl/WaifuRepository.cs index 3b9a4dc6..f08473b6 100644 --- a/src/NadekoBot/Services/Database/Repositories/Impl/WaifuRepository.cs +++ b/src/NadekoBot/Services/Database/Repositories/Impl/WaifuRepository.cs @@ -17,6 +17,7 @@ namespace NadekoBot.Services.Database.Repositories.Impl return _set.Include(wi => wi.Waifu) .Include(wi => wi.Affinity) .Include(wi => wi.Claimer) + .Include(wi => wi.Items) .FirstOrDefault(wi => wi.Waifu.UserId == userId); } @@ -25,6 +26,7 @@ namespace NadekoBot.Services.Database.Repositories.Impl return _set.Include(wi => wi.Waifu) .Include(wi => wi.Affinity) .Include(wi => wi.Claimer) + .Include(wi => wi.Items) .Where(wi => wi.Claimer != null && wi.Claimer.UserId == userId) .ToList(); } diff --git a/src/NadekoBot/_strings/ResponseStrings.en-US.json b/src/NadekoBot/_strings/ResponseStrings.en-US.json index 13edd204..84a236d4 100644 --- a/src/NadekoBot/_strings/ResponseStrings.en-US.json +++ b/src/NadekoBot/_strings/ResponseStrings.en-US.json @@ -327,6 +327,9 @@ "gambling_waifu_recent_divorce": "You divorced recently. You must wait {0} hours and {1} minutes to divorce again.", "gambling_nobody": "Nobody", "gambling_waifu_divorced_notlike": "You have divorced a waifu who doesn't like you. You received {0} back.", + "gambling_waifu_gift": "Gifted {0} to {1}", + "gambling_waifu_gift_shop": "Waifu gift shop", + "gambling_gifts": "Gifts", "games_8ball": "8ball", "games_acrophobia": "Acrophobia", "games_acro_ended_no_sub": "Game ended with no submissions.", From e50e71014efd54427c0db573617423dd63e60b5c Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Tue, 15 Aug 2017 22:59:48 +0200 Subject: [PATCH 265/346] Upped version --- src/NadekoBot/Modules/Gambling/WaifuClaimCommands.cs | 3 +++ src/NadekoBot/NadekoBot.csproj | 4 ++++ src/NadekoBot/Services/Impl/StatsService.cs | 2 +- 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/src/NadekoBot/Modules/Gambling/WaifuClaimCommands.cs b/src/NadekoBot/Modules/Gambling/WaifuClaimCommands.cs index fbe9a101..3019155e 100644 --- a/src/NadekoBot/Modules/Gambling/WaifuClaimCommands.cs +++ b/src/NadekoBot/Modules/Gambling/WaifuClaimCommands.cs @@ -487,6 +487,9 @@ namespace NadekoBot.Modules.Gambling [Priority(0)] public async Task WaifuGift(WaifuItem.ItemName item, [Remainder] IUser waifu) { + if (waifu.Id == Context.User.Id) + return; + var itemObj = WaifuItem.GetItem(item); using (var uow = _db.UnitOfWork) diff --git a/src/NadekoBot/NadekoBot.csproj b/src/NadekoBot/NadekoBot.csproj index 5e1db116..224f9f30 100644 --- a/src/NadekoBot/NadekoBot.csproj +++ b/src/NadekoBot/NadekoBot.csproj @@ -91,4 +91,8 @@ + + + + diff --git a/src/NadekoBot/Services/Impl/StatsService.cs b/src/NadekoBot/Services/Impl/StatsService.cs index fe56b94a..335db6b8 100644 --- a/src/NadekoBot/Services/Impl/StatsService.cs +++ b/src/NadekoBot/Services/Impl/StatsService.cs @@ -20,7 +20,7 @@ namespace NadekoBot.Services.Impl private readonly IBotCredentials _creds; private readonly DateTime _started; - public const string BotVersion = "1.6.4"; + public const string BotVersion = "1.7"; public string Author => "Kwoth#2560"; public string Library => "Discord.Net"; From fe3770270e96015ff47595034c4ba1d5bd692cfb Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Tue, 15 Aug 2017 23:54:54 +0200 Subject: [PATCH 266/346] Added .songautodelete/.sad command. --- .../Modules/Music/Common/MusicPlayer.cs | 18 +++++++++++++++--- src/NadekoBot/Modules/Music/Music.cs | 17 +++++++++++++++++ src/NadekoBot/Resources/CommandStrings.resx | 9 +++++++++ .../_strings/ResponseStrings.en-US.json | 2 ++ 4 files changed, 43 insertions(+), 3 deletions(-) diff --git a/src/NadekoBot/Modules/Music/Common/MusicPlayer.cs b/src/NadekoBot/Modules/Music/Common/MusicPlayer.cs index aed23c0e..aa61a9ce 100644 --- a/src/NadekoBot/Modules/Music/Common/MusicPlayer.cs +++ b/src/NadekoBot/Modules/Music/Common/MusicPlayer.cs @@ -91,6 +91,7 @@ namespace NadekoBot.Modules.Music.Common _fairPlay = value; } } + public bool AutoDelete { get; set; } public uint MaxPlaytimeSeconds { get; set; } @@ -256,10 +257,17 @@ namespace NadekoBot.Modules.Music.Common int queueCount; bool stopped; + int currentIndex; lock (locker) { queueCount = Queue.Count; stopped = Stopped; + currentIndex = Queue.CurrentIndex; + } + + if (AutoDelete && !RepeatCurrentSong && !RepeatPlaylist && data.Song != null) + { + Queue.RemoveSong(data.Song); } if (!manualIndex && (!RepeatCurrentSong || manualSkip)) @@ -279,7 +287,8 @@ namespace NadekoBot.Modules.Music.Common { _log.Info("Loading related song"); await _musicService.TryQueueRelatedSongAsync(data.Song, OutputTextChannel, VoiceChannel); - Queue.Next(); + if(!AutoDelete) + Queue.Next(); } catch { @@ -327,8 +336,9 @@ namespace NadekoBot.Modules.Music.Common _log.Info("Next song"); lock (locker) { - if(!Stopped) - Queue.Next(); + if (!Stopped) + if(!AutoDelete) + Queue.Next(); } } } @@ -423,6 +433,8 @@ namespace NadekoBot.Modules.Music.Common { if (Exited) return; + if (AutoDelete && index >= Queue.CurrentIndex && index > 0) + index--; Queue.CurrentIndex = index; manualIndex = true; Stopped = false; diff --git a/src/NadekoBot/Modules/Music/Music.cs b/src/NadekoBot/Modules/Music/Music.cs index 5589984a..a5b0295e 100644 --- a/src/NadekoBot/Modules/Music/Music.cs +++ b/src/NadekoBot/Modules/Music/Music.cs @@ -578,6 +578,23 @@ namespace NadekoBot.Modules.Music } } + [NadekoCommand, Usage, Description, Aliases] + [RequireContext(ContextType.Guild)] + public async Task SongAutoDelete() + { + var mp = await _service.GetOrCreatePlayer(Context); + var val = mp.AutoDelete = !mp.AutoDelete; + + if (val) + { + await ReplyConfirmLocalized("sad_enabled").ConfigureAwait(false); + } + else + { + await ReplyConfirmLocalized("sad_disabled").ConfigureAwait(false); + } + } + [NadekoCommand, Usage, Description, Aliases] [RequireContext(ContextType.Guild)] public async Task SoundCloudQueue([Remainder] string query) diff --git a/src/NadekoBot/Resources/CommandStrings.resx b/src/NadekoBot/Resources/CommandStrings.resx index cf106686..743a1689 100644 --- a/src/NadekoBot/Resources/CommandStrings.resx +++ b/src/NadekoBot/Resources/CommandStrings.resx @@ -2826,6 +2826,15 @@ `{0}fp` + + songautodelete sad + + + Toggles whether the song should be automatically removed from the music queue when it finishes playing. + + + `{0}sad` + define def diff --git a/src/NadekoBot/_strings/ResponseStrings.en-US.json b/src/NadekoBot/_strings/ResponseStrings.en-US.json index 84a236d4..42c5998f 100644 --- a/src/NadekoBot/_strings/ResponseStrings.en-US.json +++ b/src/NadekoBot/_strings/ResponseStrings.en-US.json @@ -411,6 +411,8 @@ "music_finished_song": "Finished song", "music_fp_disabled": "Fair play disabled.", "music_fp_enabled": "Fair play enabled.", + "music_sad_disabled": "Songs will be deleted from the music queue when they finish playing.", + "music_sad_enabled": "Songs will no longer be deleted from the music queue when they finish playing.", "music_from_position": "From position", "music_id": "Id", "music_invalid_input": "Invalid input.", From 03a86b0be9b676c0779ce198883762e0d122f326 Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Wed, 16 Aug 2017 01:12:18 +0200 Subject: [PATCH 267/346] Antispam can now take an extra parameter at the end which is the time of the mute. It will be ignored if punishment type isn't mute --- ...70815222316_mute-time-antispam.Designer.cs | 1713 +++++++++++++++++ .../20170815222316_mute-time-antispam.cs | 25 + .../NadekoSqliteContextModelSnapshot.cs | 2 + .../Administration/ProtectionCommands.cs | 12 +- .../Services/ProtectionService.cs | 11 +- .../Database/Models/AntiProtection.cs | 1 + 6 files changed, 1759 insertions(+), 5 deletions(-) create mode 100644 src/NadekoBot/Migrations/20170815222316_mute-time-antispam.Designer.cs create mode 100644 src/NadekoBot/Migrations/20170815222316_mute-time-antispam.cs diff --git a/src/NadekoBot/Migrations/20170815222316_mute-time-antispam.Designer.cs b/src/NadekoBot/Migrations/20170815222316_mute-time-antispam.Designer.cs new file mode 100644 index 00000000..fcab240e --- /dev/null +++ b/src/NadekoBot/Migrations/20170815222316_mute-time-antispam.Designer.cs @@ -0,0 +1,1713 @@ +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using NadekoBot.Services.Database; +using NadekoBot.Services.Database.Models; + +namespace NadekoBot.Migrations +{ + [DbContext(typeof(NadekoContext))] + [Migration("20170815222316_mute-time-antispam")] + partial class mutetimeantispam + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { + modelBuilder + .HasAnnotation("ProductVersion", "1.1.1"); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.AntiRaidSetting", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("Action"); + + b.Property("DateAdded"); + + b.Property("GuildConfigId"); + + b.Property("Seconds"); + + b.Property("UserThreshold"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId") + .IsUnique(); + + b.ToTable("AntiRaidSetting"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.AntiSpamIgnore", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AntiSpamSettingId"); + + b.Property("ChannelId"); + + b.Property("DateAdded"); + + b.HasKey("Id"); + + b.HasIndex("AntiSpamSettingId"); + + b.ToTable("AntiSpamIgnore"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.AntiSpamSetting", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("Action"); + + b.Property("DateAdded"); + + b.Property("GuildConfigId"); + + b.Property("MessageThreshold"); + + b.Property("MuteTime"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId") + .IsUnique(); + + b.ToTable("AntiSpamSetting"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.BlacklistItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("BotConfigId"); + + b.Property("DateAdded"); + + b.Property("ItemId"); + + b.Property("Type"); + + b.HasKey("Id"); + + b.HasIndex("BotConfigId"); + + b.ToTable("BlacklistItem"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.BlockedCmdOrMdl", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("BotConfigId"); + + b.Property("BotConfigId1"); + + b.Property("DateAdded"); + + b.Property("Name"); + + b.HasKey("Id"); + + b.HasIndex("BotConfigId"); + + b.HasIndex("BotConfigId1"); + + b.ToTable("BlockedCmdOrMdl"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.BotConfig", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("BetflipMultiplier"); + + b.Property("Betroll100Multiplier"); + + b.Property("Betroll67Multiplier"); + + b.Property("Betroll91Multiplier"); + + b.Property("BufferSize"); + + b.Property("CurrencyDropAmount"); + + b.Property("CurrencyDropAmountMax"); + + b.Property("CurrencyGenerationChance"); + + b.Property("CurrencyGenerationCooldown"); + + b.Property("CurrencyName"); + + b.Property("CurrencyPluralName"); + + b.Property("CurrencySign"); + + b.Property("CustomReactionsStartWith"); + + b.Property("DMHelpString"); + + b.Property("DateAdded"); + + b.Property("DefaultPrefix"); + + b.Property("ErrorColor"); + + b.Property("ForwardMessages"); + + b.Property("ForwardToAllOwners"); + + b.Property("HelpString"); + + b.Property("Locale"); + + b.Property("MigrationVersion"); + + b.Property("MinimumBetAmount"); + + b.Property("OkColor"); + + b.Property("PermissionVersion"); + + b.Property("RemindMessageFormat"); + + b.Property("RotatingStatuses"); + + b.Property("TriviaCurrencyReward"); + + b.HasKey("Id"); + + b.ToTable("BotConfig"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.ClashCaller", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("BaseDestroyed"); + + b.Property("CallUser"); + + b.Property("ClashWarId"); + + b.Property("DateAdded"); + + b.Property("SequenceNumber"); + + b.Property("Stars"); + + b.Property("TimeAdded"); + + b.HasKey("Id"); + + b.HasIndex("ClashWarId"); + + b.ToTable("ClashCallers"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.ClashWar", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ChannelId"); + + b.Property("DateAdded"); + + b.Property("EnemyClan"); + + b.Property("GuildId"); + + b.Property("Size"); + + b.Property("StartedAt"); + + b.Property("WarState"); + + b.HasKey("Id"); + + b.ToTable("ClashOfClans"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.CommandAlias", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("GuildConfigId"); + + b.Property("Mapping"); + + b.Property("Trigger"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.ToTable("CommandAlias"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.CommandCooldown", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("CommandName"); + + b.Property("DateAdded"); + + b.Property("GuildConfigId"); + + b.Property("Seconds"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.ToTable("CommandCooldown"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.CommandPrice", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("BotConfigId"); + + b.Property("CommandName"); + + b.Property("DateAdded"); + + b.Property("Price"); + + b.HasKey("Id"); + + b.HasIndex("BotConfigId"); + + b.HasIndex("Price") + .IsUnique(); + + b.ToTable("CommandPrice"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.ConvertUnit", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("InternalTrigger"); + + b.Property("Modifier"); + + b.Property("UnitType"); + + b.HasKey("Id"); + + b.ToTable("ConversionUnits"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.Currency", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("Amount"); + + b.Property("DateAdded"); + + b.Property("UserId"); + + b.HasKey("Id"); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("Currency"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.CurrencyTransaction", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("Amount"); + + b.Property("DateAdded"); + + b.Property("Reason"); + + b.Property("UserId"); + + b.HasKey("Id"); + + b.ToTable("CurrencyTransactions"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.CustomReaction", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AutoDeleteTrigger"); + + b.Property("ContainsAnywhere"); + + b.Property("DateAdded"); + + b.Property("DmResponse"); + + b.Property("GuildId"); + + b.Property("IsRegex"); + + b.Property("OwnerOnly"); + + b.Property("Response"); + + b.Property("Trigger"); + + b.HasKey("Id"); + + b.ToTable("CustomReactions"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.DiscordUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AvatarId"); + + b.Property("DateAdded"); + + b.Property("Discriminator"); + + b.Property("UserId"); + + b.Property("Username"); + + b.HasKey("Id"); + + b.HasAlternateKey("UserId"); + + b.ToTable("DiscordUser"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.Donator", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("Amount"); + + b.Property("DateAdded"); + + b.Property("Name"); + + b.Property("UserId"); + + b.HasKey("Id"); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("Donators"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.EightBallResponse", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("BotConfigId"); + + b.Property("DateAdded"); + + b.Property("Text"); + + b.HasKey("Id"); + + b.HasIndex("BotConfigId"); + + b.ToTable("EightBallResponses"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.FilterChannelId", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ChannelId"); + + b.Property("DateAdded"); + + b.Property("GuildConfigId"); + + b.Property("GuildConfigId1"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.HasIndex("GuildConfigId1"); + + b.ToTable("FilterChannelId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.FilteredWord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("GuildConfigId"); + + b.Property("Word"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.ToTable("FilteredWord"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.FollowedStream", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ChannelId"); + + b.Property("DateAdded"); + + b.Property("GuildConfigId"); + + b.Property("GuildId"); + + b.Property("Type"); + + b.Property("Username"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.ToTable("FollowedStream"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.GCChannelId", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ChannelId"); + + b.Property("DateAdded"); + + b.Property("GuildConfigId"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.ToTable("GCChannelId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.GuildConfig", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AutoAssignRoleId"); + + b.Property("AutoDeleteByeMessages"); + + b.Property("AutoDeleteByeMessagesTimer"); + + b.Property("AutoDeleteGreetMessages"); + + b.Property("AutoDeleteGreetMessagesTimer"); + + b.Property("AutoDeleteSelfAssignedRoleMessages"); + + b.Property("ByeMessageChannelId"); + + b.Property("ChannelByeMessageText"); + + b.Property("ChannelGreetMessageText"); + + b.Property("CleverbotEnabled"); + + b.Property("DateAdded"); + + b.Property("DefaultMusicVolume"); + + b.Property("DeleteMessageOnCommand"); + + b.Property("DmGreetMessageText"); + + b.Property("ExclusiveSelfAssignedRoles"); + + b.Property("FilterInvites"); + + b.Property("FilterWords"); + + b.Property("GameVoiceChannel"); + + b.Property("GreetMessageChannelId"); + + b.Property("GuildId"); + + b.Property("Locale"); + + b.Property("LogSettingId"); + + b.Property("MuteRoleName"); + + b.Property("PermissionRole"); + + b.Property("Prefix"); + + b.Property("RootPermissionId"); + + b.Property("SendChannelByeMessage"); + + b.Property("SendChannelGreetMessage"); + + b.Property("SendDmGreetMessage"); + + b.Property("TimeZoneId"); + + b.Property("VerboseErrors"); + + b.Property("VerbosePermissions"); + + b.Property("VoicePlusTextEnabled"); + + b.Property("WarningsInitialized"); + + b.HasKey("Id"); + + b.HasIndex("GuildId") + .IsUnique(); + + b.HasIndex("LogSettingId"); + + b.HasIndex("RootPermissionId"); + + b.ToTable("GuildConfigs"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.GuildRepeater", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ChannelId"); + + b.Property("DateAdded"); + + b.Property("GuildConfigId"); + + b.Property("GuildId"); + + b.Property("Interval"); + + b.Property("Message"); + + b.Property("StartTimeOfDay"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.ToTable("GuildRepeater"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.IgnoredLogChannel", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ChannelId"); + + b.Property("DateAdded"); + + b.Property("LogSettingId"); + + b.HasKey("Id"); + + b.HasIndex("LogSettingId"); + + b.ToTable("IgnoredLogChannels"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.IgnoredVoicePresenceChannel", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ChannelId"); + + b.Property("DateAdded"); + + b.Property("LogSettingId"); + + b.HasKey("Id"); + + b.HasIndex("LogSettingId"); + + b.ToTable("IgnoredVoicePresenceCHannels"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.LogSetting", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ChannelCreated"); + + b.Property("ChannelCreatedId"); + + b.Property("ChannelDestroyed"); + + b.Property("ChannelDestroyedId"); + + b.Property("ChannelId"); + + b.Property("ChannelUpdated"); + + b.Property("ChannelUpdatedId"); + + b.Property("DateAdded"); + + b.Property("IsLogging"); + + b.Property("LogOtherId"); + + b.Property("LogUserPresence"); + + b.Property("LogUserPresenceId"); + + b.Property("LogVoicePresence"); + + b.Property("LogVoicePresenceId"); + + b.Property("LogVoicePresenceTTSId"); + + b.Property("MessageDeleted"); + + b.Property("MessageDeletedId"); + + b.Property("MessageUpdated"); + + b.Property("MessageUpdatedId"); + + b.Property("UserBanned"); + + b.Property("UserBannedId"); + + b.Property("UserJoined"); + + b.Property("UserJoinedId"); + + b.Property("UserLeft"); + + b.Property("UserLeftId"); + + b.Property("UserMutedId"); + + b.Property("UserPresenceChannelId"); + + b.Property("UserUnbanned"); + + b.Property("UserUnbannedId"); + + b.Property("UserUpdated"); + + b.Property("UserUpdatedId"); + + b.Property("VoicePresenceChannelId"); + + b.HasKey("Id"); + + b.ToTable("LogSettings"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.ModulePrefix", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("BotConfigId"); + + b.Property("DateAdded"); + + b.Property("ModuleName"); + + b.Property("Prefix"); + + b.HasKey("Id"); + + b.HasIndex("BotConfigId"); + + b.ToTable("ModulePrefixes"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.MusicPlaylist", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("Author"); + + b.Property("AuthorId"); + + b.Property("DateAdded"); + + b.Property("Name"); + + b.HasKey("Id"); + + b.ToTable("MusicPlaylists"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.MutedUserId", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("GuildConfigId"); + + b.Property("UserId"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.ToTable("MutedUserId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.NsfwBlacklitedTag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("GuildConfigId"); + + b.Property("Tag"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.ToTable("NsfwBlacklitedTag"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.Permission", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("NextId"); + + b.Property("PrimaryTarget"); + + b.Property("PrimaryTargetId"); + + b.Property("SecondaryTarget"); + + b.Property("SecondaryTargetName"); + + b.Property("State"); + + b.HasKey("Id"); + + b.HasIndex("NextId") + .IsUnique(); + + b.ToTable("Permission"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.Permissionv2", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("GuildConfigId"); + + b.Property("Index"); + + b.Property("PrimaryTarget"); + + b.Property("PrimaryTargetId"); + + b.Property("SecondaryTarget"); + + b.Property("SecondaryTargetName"); + + b.Property("State"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.ToTable("Permissionv2"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.PlayingStatus", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("BotConfigId"); + + b.Property("DateAdded"); + + b.Property("Status"); + + b.HasKey("Id"); + + b.HasIndex("BotConfigId"); + + b.ToTable("PlayingStatus"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.PlaylistSong", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("MusicPlaylistId"); + + b.Property("Provider"); + + b.Property("ProviderType"); + + b.Property("Query"); + + b.Property("Title"); + + b.Property("Uri"); + + b.HasKey("Id"); + + b.HasIndex("MusicPlaylistId"); + + b.ToTable("PlaylistSong"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.Quote", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AuthorId"); + + b.Property("AuthorName") + .IsRequired(); + + b.Property("DateAdded"); + + b.Property("GuildId"); + + b.Property("Keyword") + .IsRequired(); + + b.Property("Text") + .IsRequired(); + + b.HasKey("Id"); + + b.ToTable("Quotes"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.RaceAnimal", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("BotConfigId"); + + b.Property("DateAdded"); + + b.Property("Icon"); + + b.Property("Name"); + + b.HasKey("Id"); + + b.HasIndex("BotConfigId"); + + b.ToTable("RaceAnimals"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.Reminder", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ChannelId"); + + b.Property("DateAdded"); + + b.Property("IsPrivate"); + + b.Property("Message"); + + b.Property("ServerId"); + + b.Property("UserId"); + + b.Property("When"); + + b.HasKey("Id"); + + b.ToTable("Reminders"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.RewardedUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AmountRewardedThisMonth"); + + b.Property("DateAdded"); + + b.Property("LastReward"); + + b.Property("PatreonUserId"); + + b.Property("UserId"); + + b.HasKey("Id"); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("RewardedUsers"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.SelfAssignedRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("GuildId"); + + b.Property("RoleId"); + + b.HasKey("Id"); + + b.HasIndex("GuildId", "RoleId") + .IsUnique(); + + b.ToTable("SelfAssignableRoles"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.ShopEntry", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AuthorId"); + + b.Property("DateAdded"); + + b.Property("GuildConfigId"); + + b.Property("Index"); + + b.Property("Name"); + + b.Property("Price"); + + b.Property("RoleId"); + + b.Property("RoleName"); + + b.Property("Type"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.ToTable("ShopEntry"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.ShopEntryItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("ShopEntryId"); + + b.Property("Text"); + + b.HasKey("Id"); + + b.HasIndex("ShopEntryId"); + + b.ToTable("ShopEntryItem"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.SlowmodeIgnoredRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("GuildConfigId"); + + b.Property("RoleId"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.ToTable("SlowmodeIgnoredRole"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.SlowmodeIgnoredUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("GuildConfigId"); + + b.Property("UserId"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.ToTable("SlowmodeIgnoredUser"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.StartupCommand", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("BotConfigId"); + + b.Property("ChannelId"); + + b.Property("ChannelName"); + + b.Property("CommandText"); + + b.Property("DateAdded"); + + b.Property("GuildId"); + + b.Property("GuildName"); + + b.Property("Index"); + + b.Property("VoiceChannelId"); + + b.Property("VoiceChannelName"); + + b.HasKey("Id"); + + b.HasIndex("BotConfigId"); + + b.ToTable("StartupCommand"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.StreamRoleBlacklistedUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("StreamRoleSettingsId"); + + b.Property("UserId"); + + b.Property("Username"); + + b.HasKey("Id"); + + b.HasIndex("StreamRoleSettingsId"); + + b.ToTable("StreamRoleBlacklistedUser"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.StreamRoleSettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AddRoleId"); + + b.Property("DateAdded"); + + b.Property("Enabled"); + + b.Property("FromRoleId"); + + b.Property("GuildConfigId"); + + b.Property("Keyword"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId") + .IsUnique(); + + b.ToTable("StreamRoleSettings"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.StreamRoleWhitelistedUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("StreamRoleSettingsId"); + + b.Property("UserId"); + + b.Property("Username"); + + b.HasKey("Id"); + + b.HasIndex("StreamRoleSettingsId"); + + b.ToTable("StreamRoleWhitelistedUser"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.UnmuteTimer", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("GuildConfigId"); + + b.Property("UnmuteAt"); + + b.Property("UserId"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.ToTable("UnmuteTimer"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.UserPokeTypes", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("UserId"); + + b.Property("type"); + + b.HasKey("Id"); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("PokeGame"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.VcRoleInfo", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("GuildConfigId"); + + b.Property("RoleId"); + + b.Property("VoiceChannelId"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.ToTable("VcRoleInfo"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.WaifuInfo", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AffinityId"); + + b.Property("ClaimerId"); + + b.Property("DateAdded"); + + b.Property("Price"); + + b.Property("WaifuId"); + + b.HasKey("Id"); + + b.HasIndex("AffinityId"); + + b.HasIndex("ClaimerId"); + + b.HasIndex("WaifuId") + .IsUnique(); + + b.ToTable("WaifuInfo"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.WaifuItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("Item"); + + b.Property("ItemEmoji"); + + b.Property("Price"); + + b.Property("WaifuInfoId"); + + b.HasKey("Id"); + + b.HasIndex("WaifuInfoId"); + + b.ToTable("WaifuItem"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.WaifuUpdate", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("NewId"); + + b.Property("OldId"); + + b.Property("UpdateType"); + + b.Property("UserId"); + + b.HasKey("Id"); + + b.HasIndex("NewId"); + + b.HasIndex("OldId"); + + b.HasIndex("UserId"); + + b.ToTable("WaifuUpdates"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.Warning", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("Forgiven"); + + b.Property("ForgivenBy"); + + b.Property("GuildId"); + + b.Property("Moderator"); + + b.Property("Reason"); + + b.Property("UserId"); + + b.HasKey("Id"); + + b.ToTable("Warnings"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.WarningPunishment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("Count"); + + b.Property("DateAdded"); + + b.Property("GuildConfigId"); + + b.Property("Punishment"); + + b.Property("Time"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.ToTable("WarningPunishment"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.AntiRaidSetting", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig", "GuildConfig") + .WithOne("AntiRaidSetting") + .HasForeignKey("NadekoBot.Services.Database.Models.AntiRaidSetting", "GuildConfigId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.AntiSpamIgnore", b => + { + b.HasOne("NadekoBot.Services.Database.Models.AntiSpamSetting") + .WithMany("IgnoredChannels") + .HasForeignKey("AntiSpamSettingId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.AntiSpamSetting", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig", "GuildConfig") + .WithOne("AntiSpamSetting") + .HasForeignKey("NadekoBot.Services.Database.Models.AntiSpamSetting", "GuildConfigId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.BlacklistItem", b => + { + b.HasOne("NadekoBot.Services.Database.Models.BotConfig") + .WithMany("Blacklist") + .HasForeignKey("BotConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.BlockedCmdOrMdl", b => + { + b.HasOne("NadekoBot.Services.Database.Models.BotConfig") + .WithMany("BlockedCommands") + .HasForeignKey("BotConfigId"); + + b.HasOne("NadekoBot.Services.Database.Models.BotConfig") + .WithMany("BlockedModules") + .HasForeignKey("BotConfigId1"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.ClashCaller", b => + { + b.HasOne("NadekoBot.Services.Database.Models.ClashWar", "ClashWar") + .WithMany("Bases") + .HasForeignKey("ClashWarId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.CommandAlias", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig") + .WithMany("CommandAliases") + .HasForeignKey("GuildConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.CommandCooldown", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig") + .WithMany("CommandCooldowns") + .HasForeignKey("GuildConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.CommandPrice", b => + { + b.HasOne("NadekoBot.Services.Database.Models.BotConfig") + .WithMany("CommandPrices") + .HasForeignKey("BotConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.EightBallResponse", b => + { + b.HasOne("NadekoBot.Services.Database.Models.BotConfig") + .WithMany("EightBallResponses") + .HasForeignKey("BotConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.FilterChannelId", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig") + .WithMany("FilterInvitesChannelIds") + .HasForeignKey("GuildConfigId"); + + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig") + .WithMany("FilterWordsChannelIds") + .HasForeignKey("GuildConfigId1"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.FilteredWord", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig") + .WithMany("FilteredWords") + .HasForeignKey("GuildConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.FollowedStream", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig") + .WithMany("FollowedStreams") + .HasForeignKey("GuildConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.GCChannelId", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig") + .WithMany("GenerateCurrencyChannelIds") + .HasForeignKey("GuildConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.GuildConfig", b => + { + b.HasOne("NadekoBot.Services.Database.Models.LogSetting", "LogSetting") + .WithMany() + .HasForeignKey("LogSettingId"); + + b.HasOne("NadekoBot.Services.Database.Models.Permission", "RootPermission") + .WithMany() + .HasForeignKey("RootPermissionId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.GuildRepeater", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig") + .WithMany("GuildRepeaters") + .HasForeignKey("GuildConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.IgnoredLogChannel", b => + { + b.HasOne("NadekoBot.Services.Database.Models.LogSetting", "LogSetting") + .WithMany("IgnoredChannels") + .HasForeignKey("LogSettingId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.IgnoredVoicePresenceChannel", b => + { + b.HasOne("NadekoBot.Services.Database.Models.LogSetting", "LogSetting") + .WithMany("IgnoredVoicePresenceChannelIds") + .HasForeignKey("LogSettingId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.ModulePrefix", b => + { + b.HasOne("NadekoBot.Services.Database.Models.BotConfig") + .WithMany("ModulePrefixes") + .HasForeignKey("BotConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.MutedUserId", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig") + .WithMany("MutedUsers") + .HasForeignKey("GuildConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.NsfwBlacklitedTag", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig") + .WithMany("NsfwBlacklistedTags") + .HasForeignKey("GuildConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.Permission", b => + { + b.HasOne("NadekoBot.Services.Database.Models.Permission", "Next") + .WithOne("Previous") + .HasForeignKey("NadekoBot.Services.Database.Models.Permission", "NextId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.Permissionv2", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig") + .WithMany("Permissions") + .HasForeignKey("GuildConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.PlayingStatus", b => + { + b.HasOne("NadekoBot.Services.Database.Models.BotConfig") + .WithMany("RotatingStatusMessages") + .HasForeignKey("BotConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.PlaylistSong", b => + { + b.HasOne("NadekoBot.Services.Database.Models.MusicPlaylist") + .WithMany("Songs") + .HasForeignKey("MusicPlaylistId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.RaceAnimal", b => + { + b.HasOne("NadekoBot.Services.Database.Models.BotConfig") + .WithMany("RaceAnimals") + .HasForeignKey("BotConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.ShopEntry", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig") + .WithMany("ShopEntries") + .HasForeignKey("GuildConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.ShopEntryItem", b => + { + b.HasOne("NadekoBot.Services.Database.Models.ShopEntry") + .WithMany("Items") + .HasForeignKey("ShopEntryId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.SlowmodeIgnoredRole", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig") + .WithMany("SlowmodeIgnoredRoles") + .HasForeignKey("GuildConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.SlowmodeIgnoredUser", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig") + .WithMany("SlowmodeIgnoredUsers") + .HasForeignKey("GuildConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.StartupCommand", b => + { + b.HasOne("NadekoBot.Services.Database.Models.BotConfig") + .WithMany("StartupCommands") + .HasForeignKey("BotConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.StreamRoleBlacklistedUser", b => + { + b.HasOne("NadekoBot.Services.Database.Models.StreamRoleSettings") + .WithMany("Blacklist") + .HasForeignKey("StreamRoleSettingsId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.StreamRoleSettings", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig", "GuildConfig") + .WithOne("StreamRole") + .HasForeignKey("NadekoBot.Services.Database.Models.StreamRoleSettings", "GuildConfigId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.StreamRoleWhitelistedUser", b => + { + b.HasOne("NadekoBot.Services.Database.Models.StreamRoleSettings") + .WithMany("Whitelist") + .HasForeignKey("StreamRoleSettingsId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.UnmuteTimer", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig") + .WithMany("UnmuteTimers") + .HasForeignKey("GuildConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.VcRoleInfo", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig") + .WithMany("VcRoleInfos") + .HasForeignKey("GuildConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.WaifuInfo", b => + { + b.HasOne("NadekoBot.Services.Database.Models.DiscordUser", "Affinity") + .WithMany() + .HasForeignKey("AffinityId"); + + b.HasOne("NadekoBot.Services.Database.Models.DiscordUser", "Claimer") + .WithMany() + .HasForeignKey("ClaimerId"); + + b.HasOne("NadekoBot.Services.Database.Models.DiscordUser", "Waifu") + .WithOne() + .HasForeignKey("NadekoBot.Services.Database.Models.WaifuInfo", "WaifuId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.WaifuItem", b => + { + b.HasOne("NadekoBot.Services.Database.Models.WaifuInfo") + .WithMany("Items") + .HasForeignKey("WaifuInfoId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.WaifuUpdate", b => + { + b.HasOne("NadekoBot.Services.Database.Models.DiscordUser", "New") + .WithMany() + .HasForeignKey("NewId"); + + b.HasOne("NadekoBot.Services.Database.Models.DiscordUser", "Old") + .WithMany() + .HasForeignKey("OldId"); + + b.HasOne("NadekoBot.Services.Database.Models.DiscordUser", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.WarningPunishment", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig") + .WithMany("WarnPunishments") + .HasForeignKey("GuildConfigId"); + }); + } + } +} diff --git a/src/NadekoBot/Migrations/20170815222316_mute-time-antispam.cs b/src/NadekoBot/Migrations/20170815222316_mute-time-antispam.cs new file mode 100644 index 00000000..63765c2a --- /dev/null +++ b/src/NadekoBot/Migrations/20170815222316_mute-time-antispam.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; +using Microsoft.EntityFrameworkCore.Migrations; + +namespace NadekoBot.Migrations +{ + public partial class mutetimeantispam : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "MuteTime", + table: "AntiSpamSetting", + nullable: false, + defaultValue: 0); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "MuteTime", + table: "AntiSpamSetting"); + } + } +} diff --git a/src/NadekoBot/Migrations/NadekoSqliteContextModelSnapshot.cs b/src/NadekoBot/Migrations/NadekoSqliteContextModelSnapshot.cs index f387dcc2..97c06375 100644 --- a/src/NadekoBot/Migrations/NadekoSqliteContextModelSnapshot.cs +++ b/src/NadekoBot/Migrations/NadekoSqliteContextModelSnapshot.cs @@ -70,6 +70,8 @@ namespace NadekoBot.Migrations b.Property("MessageThreshold"); + b.Property("MuteTime"); + b.HasKey("Id"); b.HasIndex("GuildConfigId") diff --git a/src/NadekoBot/Modules/Administration/ProtectionCommands.cs b/src/NadekoBot/Modules/Administration/ProtectionCommands.cs index c9d021ad..6bec27e5 100644 --- a/src/NadekoBot/Modules/Administration/ProtectionCommands.cs +++ b/src/NadekoBot/Modules/Administration/ProtectionCommands.cs @@ -33,9 +33,17 @@ namespace NadekoBot.Modules.Administration if (string.IsNullOrWhiteSpace(ignoredString)) ignoredString = "none"; + + string add = ""; + if (stats.AntiSpamSettings.Action == PunishmentAction.Mute + && stats.AntiSpamSettings.MuteTime > 0) + { + add = " (" + stats.AntiSpamSettings.MuteTime + "s)"; + } + return GetText("spam_stats", Format.Bold(stats.AntiSpamSettings.MessageThreshold.ToString()), - Format.Bold(stats.AntiSpamSettings.Action.ToString()), + Format.Bold(stats.AntiSpamSettings.Action.ToString() + add), ignoredString); } @@ -162,6 +170,7 @@ namespace NadekoBot.Modules.Administration { Action = action, MessageThreshold = messageCount, + MuteTime = time, } }; @@ -169,6 +178,7 @@ namespace NadekoBot.Modules.Administration { stats.AntiSpamSettings.MessageThreshold = messageCount; stats.AntiSpamSettings.Action = action; + stats.AntiSpamSettings.MuteTime = time; return stats; }); diff --git a/src/NadekoBot/Modules/Administration/Services/ProtectionService.cs b/src/NadekoBot/Modules/Administration/Services/ProtectionService.cs index 21796df3..ea65fd3f 100644 --- a/src/NadekoBot/Modules/Administration/Services/ProtectionService.cs +++ b/src/NadekoBot/Modules/Administration/Services/ProtectionService.cs @@ -78,7 +78,7 @@ namespace NadekoBot.Modules.Administration.Services if (spamSettings.UserStats.TryRemove(msg.Author.Id, out stats)) { stats.Dispose(); - await PunishUsers(spamSettings.AntiSpamSettings.Action, ProtectionType.Spamming, (IGuildUser)msg.Author) + await PunishUsers(spamSettings.AntiSpamSettings.Action, ProtectionType.Spamming, spamSettings.AntiSpamSettings.MuteTime, (IGuildUser)msg.Author) .ConfigureAwait(false); } } @@ -111,7 +111,7 @@ namespace NadekoBot.Modules.Administration.Services var users = settings.RaidUsers.ToArray(); settings.RaidUsers.Clear(); - await PunishUsers(settings.AntiRaidSettings.Action, ProtectionType.Raiding, users).ConfigureAwait(false); + await PunishUsers(settings.AntiRaidSettings.Action, ProtectionType.Raiding, 0, users).ConfigureAwait(false); } await Task.Delay(1000 * settings.AntiRaidSettings.Seconds).ConfigureAwait(false); @@ -129,7 +129,7 @@ namespace NadekoBot.Modules.Administration.Services } - private async Task PunishUsers(PunishmentAction action, ProtectionType pt, params IGuildUser[] gus) + private async Task PunishUsers(PunishmentAction action, ProtectionType pt, int muteTime, params IGuildUser[] gus) { _log.Info($"[{pt}] - Punishing [{gus.Length}] users with [{action}] in {gus[0].Guild.Name} guild"); foreach (var gu in gus) @@ -139,7 +139,10 @@ namespace NadekoBot.Modules.Administration.Services case PunishmentAction.Mute: try { - await _mute.MuteUser(gu).ConfigureAwait(false); + if (muteTime <= 0) + await _mute.MuteUser(gu).ConfigureAwait(false); + else + await _mute.TimedMute(gu, TimeSpan.FromSeconds(muteTime)).ConfigureAwait(false); } catch (Exception ex) { _log.Warn(ex, "I can't apply punishement"); } break; diff --git a/src/NadekoBot/Services/Database/Models/AntiProtection.cs b/src/NadekoBot/Services/Database/Models/AntiProtection.cs index 160425ea..fbafc9e3 100644 --- a/src/NadekoBot/Services/Database/Models/AntiProtection.cs +++ b/src/NadekoBot/Services/Database/Models/AntiProtection.cs @@ -18,6 +18,7 @@ namespace NadekoBot.Services.Database.Models public PunishmentAction Action { get; set; } public int MessageThreshold { get; set; } = 3; + public int MuteTime { get; set; } = 0; public HashSet IgnoredChannels { get; set; } = new HashSet(); } From f255ed26dd23b9d27182d11b4e37c1898cccb9c8 Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Wed, 16 Aug 2017 11:51:41 +0200 Subject: [PATCH 268/346] .sad enable/disable string was swapped --- src/NadekoBot/NadekoBot.csproj | 4 ---- src/NadekoBot/_strings/ResponseStrings.en-US.json | 4 ++-- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/src/NadekoBot/NadekoBot.csproj b/src/NadekoBot/NadekoBot.csproj index 224f9f30..5e1db116 100644 --- a/src/NadekoBot/NadekoBot.csproj +++ b/src/NadekoBot/NadekoBot.csproj @@ -91,8 +91,4 @@ - - - - diff --git a/src/NadekoBot/_strings/ResponseStrings.en-US.json b/src/NadekoBot/_strings/ResponseStrings.en-US.json index 42c5998f..2d123486 100644 --- a/src/NadekoBot/_strings/ResponseStrings.en-US.json +++ b/src/NadekoBot/_strings/ResponseStrings.en-US.json @@ -411,8 +411,8 @@ "music_finished_song": "Finished song", "music_fp_disabled": "Fair play disabled.", "music_fp_enabled": "Fair play enabled.", - "music_sad_disabled": "Songs will be deleted from the music queue when they finish playing.", - "music_sad_enabled": "Songs will no longer be deleted from the music queue when they finish playing.", + "music_sad_enabled": "Songs will be deleted from the music queue when they finish playing.", + "music_sad_disabled": "Songs will no longer be deleted from the music queue when they finish playing.", "music_from_position": "From position", "music_id": "Id", "music_invalid_input": "Invalid input.", From 919c81d385f4a0f8bb72a8a4b1878a45ee53a506 Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Wed, 16 Aug 2017 22:05:06 +0200 Subject: [PATCH 269/346] Fixed .wheel example --- src/NadekoBot/Resources/CommandStrings.resx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/NadekoBot/Resources/CommandStrings.resx b/src/NadekoBot/Resources/CommandStrings.resx index 743a1689..e547c0cb 100644 --- a/src/NadekoBot/Resources/CommandStrings.resx +++ b/src/NadekoBot/Resources/CommandStrings.resx @@ -1339,7 +1339,7 @@ Bets a certain amount of currency on the wheel of fortune. Wheel can stop on one of many different multipliers. Won amount is rounded down to the nearest whole number. - `{0}wheel 5` + `{0}wheel 10` leaderboard lb From 658597db9f2668d1afbc4d20c9be3b65b6370c5a Mon Sep 17 00:00:00 2001 From: Alistair Mackenzie Date: Mon, 21 Aug 2017 02:08:57 +0100 Subject: [PATCH 270/346] Fix typo in rategirl command. Solves #1517 --- src/NadekoBot/Modules/Games/Games.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/NadekoBot/Modules/Games/Games.cs b/src/NadekoBot/Modules/Games/Games.cs index b1db0acd..245b75cf 100644 --- a/src/NadekoBot/Modules/Games/Games.cs +++ b/src/NadekoBot/Modules/Games/Games.cs @@ -140,7 +140,7 @@ namespace NadekoBot.Modules.Games hot = NextDouble(0, 5); crazy = NextDouble(4, 10); advice = - "This is your NO-GO ZONE. We do not hang around, and date, and marry women who are atleast, in our mind, a 5. " + + "This is your NO-GO ZONE. We do not hang around, and date, and marry women who are at least, in our mind, a 5. " + "So, this is your no-go zone. You don't go here. You just rule this out. Life is better this way, that's the way it is."; } else if (roll < 750) From e5609a0708f877e06ef2b1be5245dddfc7fa9faf Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Tue, 22 Aug 2017 05:47:57 +0200 Subject: [PATCH 271/346] .prefix bugfix, #1524 --- src/NadekoBot/Services/CommandHandler.cs | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/NadekoBot/Services/CommandHandler.cs b/src/NadekoBot/Services/CommandHandler.cs index 21309283..ad51c579 100644 --- a/src/NadekoBot/Services/CommandHandler.cs +++ b/src/NadekoBot/Services/CommandHandler.cs @@ -43,6 +43,7 @@ namespace NadekoBot.Services public event Func CommandExecuted = delegate { return Task.CompletedTask; }; public event Func CommandErrored = delegate { return Task.CompletedTask; }; + public event Func OnMessageNoTrigger = delegate { return Task.CompletedTask; }; //userid/msg count public ConcurrentDictionary UserMessagesSent { get; } = new ConcurrentDictionary(); @@ -261,13 +262,13 @@ namespace NadekoBot.Services } } var prefix = GetPrefix(guild?.Id); - var isPrefixCommand = messageContent == ".prefix"; + var isPrefixCommand = messageContent.StartsWith(".prefix"); // execute the command and measure the time it took if (messageContent.StartsWith(prefix) || isPrefixCommand) { var result = await ExecuteCommandAsync(new CommandContext(_client, usrMsg), messageContent, isPrefixCommand ? 1 : prefix.Length, _services, MultiMatchHandling.Best); execTime = Environment.TickCount - execTime; - + if (result.Success) { await LogSuccessfulExecution(usrMsg, channel as ITextChannel, exec2, exec3, execTime).ConfigureAwait(false); @@ -276,11 +277,14 @@ namespace NadekoBot.Services } else if (result.Error != null) { - LogErroredExecution(result.Error, usrMsg, channel as ITextChannel, exec2, exec3, execTime); + LogErroredExecution(result.Error, usrMsg, channel as ITextChannel, exec2, exec3, execTime); if (guild != null) await CommandErrored(result.Info, channel as ITextChannel, result.Error); } - + } + else + { + await OnMessageNoTrigger(usrMsg).ConfigureAwait(false); } foreach (var svc in _services) From 088d95340fa9a051ae5735b44ee3ee5804f1e477 Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Tue, 22 Aug 2017 05:48:45 +0200 Subject: [PATCH 272/346] Started work on the xp system --- src/NadekoBot/Common/BotConfigEditType.cs | 2 + .../20170821085106_xp-stuff.Designer.cs | 1825 +++++++++++++++++ .../Migrations/20170821085106_xp-stuff.cs | 153 ++ .../NadekoSqliteContextModelSnapshot.cs | 112 + .../Modules/Games/Services/GamesService.cs | 10 +- .../Modules/Xp/Extensions/Extensions.cs | 34 + .../Modules/Xp/Services/UserCacheItem.cs | 18 + .../Modules/Xp/Services/XpService.cs | 229 +++ src/NadekoBot/Modules/Xp/Xp.cs | 105 + src/NadekoBot/NadekoBot.csproj | 6 + src/NadekoBot/Resources/CommandStrings.resx | 27 + .../Services/Database/IUnitOfWork.cs | 1 + .../Services/Database/Models/BotConfig.cs | 2 + .../Services/Database/Models/GuildConfig.cs | 2 + .../Services/Database/Models/UserXpStats.cs | 10 + .../Services/Database/Models/XpSettings.cs | 50 + .../Services/Database/NadekoContext.cs | 13 + .../Repositories/IGuildConfigRepository.cs | 1 + .../Database/Repositories/IXpRepository.cs | 9 + .../Impl/GuildConfigRepository.cs | 14 + .../Repositories/Impl/XpRepository.cs | 31 + src/NadekoBot/Services/Database/UnitOfWork.cs | 3 + .../Services/Impl/BotConfigProvider.cs | 12 + .../_strings/ResponseStrings.en-US.json | 13 +- 24 files changed, 2676 insertions(+), 6 deletions(-) create mode 100644 src/NadekoBot/Migrations/20170821085106_xp-stuff.Designer.cs create mode 100644 src/NadekoBot/Migrations/20170821085106_xp-stuff.cs create mode 100644 src/NadekoBot/Modules/Xp/Extensions/Extensions.cs create mode 100644 src/NadekoBot/Modules/Xp/Services/UserCacheItem.cs create mode 100644 src/NadekoBot/Modules/Xp/Services/XpService.cs create mode 100644 src/NadekoBot/Modules/Xp/Xp.cs create mode 100644 src/NadekoBot/Services/Database/Models/UserXpStats.cs create mode 100644 src/NadekoBot/Services/Database/Models/XpSettings.cs create mode 100644 src/NadekoBot/Services/Database/Repositories/IXpRepository.cs create mode 100644 src/NadekoBot/Services/Database/Repositories/Impl/XpRepository.cs diff --git a/src/NadekoBot/Common/BotConfigEditType.cs b/src/NadekoBot/Common/BotConfigEditType.cs index 5eadbe9d..92be166a 100644 --- a/src/NadekoBot/Common/BotConfigEditType.cs +++ b/src/NadekoBot/Common/BotConfigEditType.cs @@ -17,6 +17,8 @@ CurrencyDropAmountMax, MinimumBetAmount, TriviaCurrencyReward, + XpPerMessage, + XpMinutesTimeout, //ErrorColor, //after i fix the nadekobot.cs static variables //OkColor diff --git a/src/NadekoBot/Migrations/20170821085106_xp-stuff.Designer.cs b/src/NadekoBot/Migrations/20170821085106_xp-stuff.Designer.cs new file mode 100644 index 00000000..ba66fe44 --- /dev/null +++ b/src/NadekoBot/Migrations/20170821085106_xp-stuff.Designer.cs @@ -0,0 +1,1825 @@ +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using NadekoBot.Services.Database; +using NadekoBot.Services.Database.Models; + +namespace NadekoBot.Migrations +{ + [DbContext(typeof(NadekoContext))] + [Migration("20170821085106_xp-stuff")] + partial class xpstuff + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { + modelBuilder + .HasAnnotation("ProductVersion", "1.1.1"); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.AntiRaidSetting", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("Action"); + + b.Property("DateAdded"); + + b.Property("GuildConfigId"); + + b.Property("Seconds"); + + b.Property("UserThreshold"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId") + .IsUnique(); + + b.ToTable("AntiRaidSetting"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.AntiSpamIgnore", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AntiSpamSettingId"); + + b.Property("ChannelId"); + + b.Property("DateAdded"); + + b.HasKey("Id"); + + b.HasIndex("AntiSpamSettingId"); + + b.ToTable("AntiSpamIgnore"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.AntiSpamSetting", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("Action"); + + b.Property("DateAdded"); + + b.Property("GuildConfigId"); + + b.Property("MessageThreshold"); + + b.Property("MuteTime"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId") + .IsUnique(); + + b.ToTable("AntiSpamSetting"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.BlacklistItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("BotConfigId"); + + b.Property("DateAdded"); + + b.Property("ItemId"); + + b.Property("Type"); + + b.HasKey("Id"); + + b.HasIndex("BotConfigId"); + + b.ToTable("BlacklistItem"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.BlockedCmdOrMdl", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("BotConfigId"); + + b.Property("BotConfigId1"); + + b.Property("DateAdded"); + + b.Property("Name"); + + b.HasKey("Id"); + + b.HasIndex("BotConfigId"); + + b.HasIndex("BotConfigId1"); + + b.ToTable("BlockedCmdOrMdl"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.BotConfig", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("BetflipMultiplier"); + + b.Property("Betroll100Multiplier"); + + b.Property("Betroll67Multiplier"); + + b.Property("Betroll91Multiplier"); + + b.Property("BufferSize"); + + b.Property("CurrencyDropAmount"); + + b.Property("CurrencyDropAmountMax"); + + b.Property("CurrencyGenerationChance"); + + b.Property("CurrencyGenerationCooldown"); + + b.Property("CurrencyName"); + + b.Property("CurrencyPluralName"); + + b.Property("CurrencySign"); + + b.Property("CustomReactionsStartWith"); + + b.Property("DMHelpString"); + + b.Property("DateAdded"); + + b.Property("DefaultPrefix"); + + b.Property("ErrorColor"); + + b.Property("ForwardMessages"); + + b.Property("ForwardToAllOwners"); + + b.Property("HelpString"); + + b.Property("Locale"); + + b.Property("MigrationVersion"); + + b.Property("MinimumBetAmount"); + + b.Property("OkColor"); + + b.Property("PermissionVersion"); + + b.Property("RemindMessageFormat"); + + b.Property("RotatingStatuses"); + + b.Property("TriviaCurrencyReward"); + + b.Property("XpMinutesTimeout"); + + b.Property("XpPerMessage"); + + b.HasKey("Id"); + + b.ToTable("BotConfig"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.ClashCaller", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("BaseDestroyed"); + + b.Property("CallUser"); + + b.Property("ClashWarId"); + + b.Property("DateAdded"); + + b.Property("SequenceNumber"); + + b.Property("Stars"); + + b.Property("TimeAdded"); + + b.HasKey("Id"); + + b.HasIndex("ClashWarId"); + + b.ToTable("ClashCallers"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.ClashWar", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ChannelId"); + + b.Property("DateAdded"); + + b.Property("EnemyClan"); + + b.Property("GuildId"); + + b.Property("Size"); + + b.Property("StartedAt"); + + b.Property("WarState"); + + b.HasKey("Id"); + + b.ToTable("ClashOfClans"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.CommandAlias", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("GuildConfigId"); + + b.Property("Mapping"); + + b.Property("Trigger"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.ToTable("CommandAlias"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.CommandCooldown", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("CommandName"); + + b.Property("DateAdded"); + + b.Property("GuildConfigId"); + + b.Property("Seconds"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.ToTable("CommandCooldown"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.CommandPrice", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("BotConfigId"); + + b.Property("CommandName"); + + b.Property("DateAdded"); + + b.Property("Price"); + + b.HasKey("Id"); + + b.HasIndex("BotConfigId"); + + b.HasIndex("Price") + .IsUnique(); + + b.ToTable("CommandPrice"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.ConvertUnit", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("InternalTrigger"); + + b.Property("Modifier"); + + b.Property("UnitType"); + + b.HasKey("Id"); + + b.ToTable("ConversionUnits"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.Currency", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("Amount"); + + b.Property("DateAdded"); + + b.Property("UserId"); + + b.HasKey("Id"); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("Currency"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.CurrencyTransaction", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("Amount"); + + b.Property("DateAdded"); + + b.Property("Reason"); + + b.Property("UserId"); + + b.HasKey("Id"); + + b.ToTable("CurrencyTransactions"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.CustomReaction", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AutoDeleteTrigger"); + + b.Property("ContainsAnywhere"); + + b.Property("DateAdded"); + + b.Property("DmResponse"); + + b.Property("GuildId"); + + b.Property("IsRegex"); + + b.Property("OwnerOnly"); + + b.Property("Response"); + + b.Property("Trigger"); + + b.HasKey("Id"); + + b.ToTable("CustomReactions"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.DiscordUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AvatarId"); + + b.Property("DateAdded"); + + b.Property("Discriminator"); + + b.Property("UserId"); + + b.Property("Username"); + + b.HasKey("Id"); + + b.HasAlternateKey("UserId"); + + b.ToTable("DiscordUser"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.Donator", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("Amount"); + + b.Property("DateAdded"); + + b.Property("Name"); + + b.Property("UserId"); + + b.HasKey("Id"); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("Donators"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.EightBallResponse", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("BotConfigId"); + + b.Property("DateAdded"); + + b.Property("Text"); + + b.HasKey("Id"); + + b.HasIndex("BotConfigId"); + + b.ToTable("EightBallResponses"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.ExcludedItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("ItemId"); + + b.Property("ItemType"); + + b.Property("XpSettingsId"); + + b.HasKey("Id"); + + b.HasIndex("XpSettingsId"); + + b.ToTable("ExcludedItem"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.FilterChannelId", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ChannelId"); + + b.Property("DateAdded"); + + b.Property("GuildConfigId"); + + b.Property("GuildConfigId1"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.HasIndex("GuildConfigId1"); + + b.ToTable("FilterChannelId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.FilteredWord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("GuildConfigId"); + + b.Property("Word"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.ToTable("FilteredWord"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.FollowedStream", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ChannelId"); + + b.Property("DateAdded"); + + b.Property("GuildConfigId"); + + b.Property("GuildId"); + + b.Property("Type"); + + b.Property("Username"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.ToTable("FollowedStream"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.GCChannelId", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ChannelId"); + + b.Property("DateAdded"); + + b.Property("GuildConfigId"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.ToTable("GCChannelId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.GuildConfig", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AutoAssignRoleId"); + + b.Property("AutoDeleteByeMessages"); + + b.Property("AutoDeleteByeMessagesTimer"); + + b.Property("AutoDeleteGreetMessages"); + + b.Property("AutoDeleteGreetMessagesTimer"); + + b.Property("AutoDeleteSelfAssignedRoleMessages"); + + b.Property("ByeMessageChannelId"); + + b.Property("ChannelByeMessageText"); + + b.Property("ChannelGreetMessageText"); + + b.Property("CleverbotEnabled"); + + b.Property("DateAdded"); + + b.Property("DefaultMusicVolume"); + + b.Property("DeleteMessageOnCommand"); + + b.Property("DmGreetMessageText"); + + b.Property("ExclusiveSelfAssignedRoles"); + + b.Property("FilterInvites"); + + b.Property("FilterWords"); + + b.Property("GameVoiceChannel"); + + b.Property("GreetMessageChannelId"); + + b.Property("GuildId"); + + b.Property("Locale"); + + b.Property("LogSettingId"); + + b.Property("MuteRoleName"); + + b.Property("PermissionRole"); + + b.Property("Prefix"); + + b.Property("RootPermissionId"); + + b.Property("SendChannelByeMessage"); + + b.Property("SendChannelGreetMessage"); + + b.Property("SendDmGreetMessage"); + + b.Property("TimeZoneId"); + + b.Property("VerboseErrors"); + + b.Property("VerbosePermissions"); + + b.Property("VoicePlusTextEnabled"); + + b.Property("WarningsInitialized"); + + b.HasKey("Id"); + + b.HasIndex("GuildId") + .IsUnique(); + + b.HasIndex("LogSettingId"); + + b.HasIndex("RootPermissionId"); + + b.ToTable("GuildConfigs"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.GuildRepeater", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ChannelId"); + + b.Property("DateAdded"); + + b.Property("GuildConfigId"); + + b.Property("GuildId"); + + b.Property("Interval"); + + b.Property("Message"); + + b.Property("StartTimeOfDay"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.ToTable("GuildRepeater"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.IgnoredLogChannel", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ChannelId"); + + b.Property("DateAdded"); + + b.Property("LogSettingId"); + + b.HasKey("Id"); + + b.HasIndex("LogSettingId"); + + b.ToTable("IgnoredLogChannels"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.IgnoredVoicePresenceChannel", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ChannelId"); + + b.Property("DateAdded"); + + b.Property("LogSettingId"); + + b.HasKey("Id"); + + b.HasIndex("LogSettingId"); + + b.ToTable("IgnoredVoicePresenceCHannels"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.LogSetting", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ChannelCreated"); + + b.Property("ChannelCreatedId"); + + b.Property("ChannelDestroyed"); + + b.Property("ChannelDestroyedId"); + + b.Property("ChannelId"); + + b.Property("ChannelUpdated"); + + b.Property("ChannelUpdatedId"); + + b.Property("DateAdded"); + + b.Property("IsLogging"); + + b.Property("LogOtherId"); + + b.Property("LogUserPresence"); + + b.Property("LogUserPresenceId"); + + b.Property("LogVoicePresence"); + + b.Property("LogVoicePresenceId"); + + b.Property("LogVoicePresenceTTSId"); + + b.Property("MessageDeleted"); + + b.Property("MessageDeletedId"); + + b.Property("MessageUpdated"); + + b.Property("MessageUpdatedId"); + + b.Property("UserBanned"); + + b.Property("UserBannedId"); + + b.Property("UserJoined"); + + b.Property("UserJoinedId"); + + b.Property("UserLeft"); + + b.Property("UserLeftId"); + + b.Property("UserMutedId"); + + b.Property("UserPresenceChannelId"); + + b.Property("UserUnbanned"); + + b.Property("UserUnbannedId"); + + b.Property("UserUpdated"); + + b.Property("UserUpdatedId"); + + b.Property("VoicePresenceChannelId"); + + b.HasKey("Id"); + + b.ToTable("LogSettings"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.ModulePrefix", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("BotConfigId"); + + b.Property("DateAdded"); + + b.Property("ModuleName"); + + b.Property("Prefix"); + + b.HasKey("Id"); + + b.HasIndex("BotConfigId"); + + b.ToTable("ModulePrefixes"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.MusicPlaylist", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("Author"); + + b.Property("AuthorId"); + + b.Property("DateAdded"); + + b.Property("Name"); + + b.HasKey("Id"); + + b.ToTable("MusicPlaylists"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.MutedUserId", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("GuildConfigId"); + + b.Property("UserId"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.ToTable("MutedUserId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.NsfwBlacklitedTag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("GuildConfigId"); + + b.Property("Tag"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.ToTable("NsfwBlacklitedTag"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.Permission", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("NextId"); + + b.Property("PrimaryTarget"); + + b.Property("PrimaryTargetId"); + + b.Property("SecondaryTarget"); + + b.Property("SecondaryTargetName"); + + b.Property("State"); + + b.HasKey("Id"); + + b.HasIndex("NextId") + .IsUnique(); + + b.ToTable("Permission"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.Permissionv2", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("GuildConfigId"); + + b.Property("Index"); + + b.Property("PrimaryTarget"); + + b.Property("PrimaryTargetId"); + + b.Property("SecondaryTarget"); + + b.Property("SecondaryTargetName"); + + b.Property("State"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.ToTable("Permissionv2"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.PlayingStatus", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("BotConfigId"); + + b.Property("DateAdded"); + + b.Property("Status"); + + b.HasKey("Id"); + + b.HasIndex("BotConfigId"); + + b.ToTable("PlayingStatus"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.PlaylistSong", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("MusicPlaylistId"); + + b.Property("Provider"); + + b.Property("ProviderType"); + + b.Property("Query"); + + b.Property("Title"); + + b.Property("Uri"); + + b.HasKey("Id"); + + b.HasIndex("MusicPlaylistId"); + + b.ToTable("PlaylistSong"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.Quote", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AuthorId"); + + b.Property("AuthorName") + .IsRequired(); + + b.Property("DateAdded"); + + b.Property("GuildId"); + + b.Property("Keyword") + .IsRequired(); + + b.Property("Text") + .IsRequired(); + + b.HasKey("Id"); + + b.ToTable("Quotes"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.RaceAnimal", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("BotConfigId"); + + b.Property("DateAdded"); + + b.Property("Icon"); + + b.Property("Name"); + + b.HasKey("Id"); + + b.HasIndex("BotConfigId"); + + b.ToTable("RaceAnimals"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.Reminder", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ChannelId"); + + b.Property("DateAdded"); + + b.Property("IsPrivate"); + + b.Property("Message"); + + b.Property("ServerId"); + + b.Property("UserId"); + + b.Property("When"); + + b.HasKey("Id"); + + b.ToTable("Reminders"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.RewardedUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AmountRewardedThisMonth"); + + b.Property("DateAdded"); + + b.Property("LastReward"); + + b.Property("PatreonUserId"); + + b.Property("UserId"); + + b.HasKey("Id"); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("RewardedUsers"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.SelfAssignedRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("GuildId"); + + b.Property("RoleId"); + + b.HasKey("Id"); + + b.HasIndex("GuildId", "RoleId") + .IsUnique(); + + b.ToTable("SelfAssignableRoles"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.ShopEntry", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AuthorId"); + + b.Property("DateAdded"); + + b.Property("GuildConfigId"); + + b.Property("Index"); + + b.Property("Name"); + + b.Property("Price"); + + b.Property("RoleId"); + + b.Property("RoleName"); + + b.Property("Type"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.ToTable("ShopEntry"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.ShopEntryItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("ShopEntryId"); + + b.Property("Text"); + + b.HasKey("Id"); + + b.HasIndex("ShopEntryId"); + + b.ToTable("ShopEntryItem"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.SlowmodeIgnoredRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("GuildConfigId"); + + b.Property("RoleId"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.ToTable("SlowmodeIgnoredRole"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.SlowmodeIgnoredUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("GuildConfigId"); + + b.Property("UserId"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.ToTable("SlowmodeIgnoredUser"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.StartupCommand", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("BotConfigId"); + + b.Property("ChannelId"); + + b.Property("ChannelName"); + + b.Property("CommandText"); + + b.Property("DateAdded"); + + b.Property("GuildId"); + + b.Property("GuildName"); + + b.Property("Index"); + + b.Property("VoiceChannelId"); + + b.Property("VoiceChannelName"); + + b.HasKey("Id"); + + b.HasIndex("BotConfigId"); + + b.ToTable("StartupCommand"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.StreamRoleBlacklistedUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("StreamRoleSettingsId"); + + b.Property("UserId"); + + b.Property("Username"); + + b.HasKey("Id"); + + b.HasIndex("StreamRoleSettingsId"); + + b.ToTable("StreamRoleBlacklistedUser"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.StreamRoleSettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AddRoleId"); + + b.Property("DateAdded"); + + b.Property("Enabled"); + + b.Property("FromRoleId"); + + b.Property("GuildConfigId"); + + b.Property("Keyword"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId") + .IsUnique(); + + b.ToTable("StreamRoleSettings"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.StreamRoleWhitelistedUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("StreamRoleSettingsId"); + + b.Property("UserId"); + + b.Property("Username"); + + b.HasKey("Id"); + + b.HasIndex("StreamRoleSettingsId"); + + b.ToTable("StreamRoleWhitelistedUser"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.UnmuteTimer", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("GuildConfigId"); + + b.Property("UnmuteAt"); + + b.Property("UserId"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.ToTable("UnmuteTimer"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.UserPokeTypes", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("UserId"); + + b.Property("type"); + + b.HasKey("Id"); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("PokeGame"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.UserXpStats", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("GuildId"); + + b.Property("NotifyOnLevelUp"); + + b.Property("UserId"); + + b.Property("Xp"); + + b.HasKey("Id"); + + b.HasIndex("UserId", "GuildId") + .IsUnique(); + + b.ToTable("UserXpStats"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.VcRoleInfo", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("GuildConfigId"); + + b.Property("RoleId"); + + b.Property("VoiceChannelId"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.ToTable("VcRoleInfo"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.WaifuInfo", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AffinityId"); + + b.Property("ClaimerId"); + + b.Property("DateAdded"); + + b.Property("Price"); + + b.Property("WaifuId"); + + b.HasKey("Id"); + + b.HasIndex("AffinityId"); + + b.HasIndex("ClaimerId"); + + b.HasIndex("WaifuId") + .IsUnique(); + + b.ToTable("WaifuInfo"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.WaifuItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("Item"); + + b.Property("ItemEmoji"); + + b.Property("Price"); + + b.Property("WaifuInfoId"); + + b.HasKey("Id"); + + b.HasIndex("WaifuInfoId"); + + b.ToTable("WaifuItem"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.WaifuUpdate", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("NewId"); + + b.Property("OldId"); + + b.Property("UpdateType"); + + b.Property("UserId"); + + b.HasKey("Id"); + + b.HasIndex("NewId"); + + b.HasIndex("OldId"); + + b.HasIndex("UserId"); + + b.ToTable("WaifuUpdates"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.Warning", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("Forgiven"); + + b.Property("ForgivenBy"); + + b.Property("GuildId"); + + b.Property("Moderator"); + + b.Property("Reason"); + + b.Property("UserId"); + + b.HasKey("Id"); + + b.ToTable("Warnings"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.WarningPunishment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("Count"); + + b.Property("DateAdded"); + + b.Property("GuildConfigId"); + + b.Property("Punishment"); + + b.Property("Time"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.ToTable("WarningPunishment"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.XpRoleReward", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("Level"); + + b.Property("RoleId"); + + b.Property("XpSettingsId"); + + b.HasKey("Id"); + + b.HasIndex("XpSettingsId"); + + b.ToTable("XpRoleReward"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.XpSettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("GuildConfigId"); + + b.Property("NotifyMessage"); + + b.Property("ServerExcluded"); + + b.Property("XpRoleRewardExclusive"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId") + .IsUnique(); + + b.ToTable("XpSettings"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.AntiRaidSetting", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig", "GuildConfig") + .WithOne("AntiRaidSetting") + .HasForeignKey("NadekoBot.Services.Database.Models.AntiRaidSetting", "GuildConfigId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.AntiSpamIgnore", b => + { + b.HasOne("NadekoBot.Services.Database.Models.AntiSpamSetting") + .WithMany("IgnoredChannels") + .HasForeignKey("AntiSpamSettingId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.AntiSpamSetting", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig", "GuildConfig") + .WithOne("AntiSpamSetting") + .HasForeignKey("NadekoBot.Services.Database.Models.AntiSpamSetting", "GuildConfigId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.BlacklistItem", b => + { + b.HasOne("NadekoBot.Services.Database.Models.BotConfig") + .WithMany("Blacklist") + .HasForeignKey("BotConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.BlockedCmdOrMdl", b => + { + b.HasOne("NadekoBot.Services.Database.Models.BotConfig") + .WithMany("BlockedCommands") + .HasForeignKey("BotConfigId"); + + b.HasOne("NadekoBot.Services.Database.Models.BotConfig") + .WithMany("BlockedModules") + .HasForeignKey("BotConfigId1"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.ClashCaller", b => + { + b.HasOne("NadekoBot.Services.Database.Models.ClashWar", "ClashWar") + .WithMany("Bases") + .HasForeignKey("ClashWarId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.CommandAlias", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig") + .WithMany("CommandAliases") + .HasForeignKey("GuildConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.CommandCooldown", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig") + .WithMany("CommandCooldowns") + .HasForeignKey("GuildConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.CommandPrice", b => + { + b.HasOne("NadekoBot.Services.Database.Models.BotConfig") + .WithMany("CommandPrices") + .HasForeignKey("BotConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.EightBallResponse", b => + { + b.HasOne("NadekoBot.Services.Database.Models.BotConfig") + .WithMany("EightBallResponses") + .HasForeignKey("BotConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.ExcludedItem", b => + { + b.HasOne("NadekoBot.Services.Database.Models.XpSettings") + .WithMany("ExclusionList") + .HasForeignKey("XpSettingsId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.FilterChannelId", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig") + .WithMany("FilterInvitesChannelIds") + .HasForeignKey("GuildConfigId"); + + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig") + .WithMany("FilterWordsChannelIds") + .HasForeignKey("GuildConfigId1"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.FilteredWord", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig") + .WithMany("FilteredWords") + .HasForeignKey("GuildConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.FollowedStream", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig") + .WithMany("FollowedStreams") + .HasForeignKey("GuildConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.GCChannelId", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig") + .WithMany("GenerateCurrencyChannelIds") + .HasForeignKey("GuildConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.GuildConfig", b => + { + b.HasOne("NadekoBot.Services.Database.Models.LogSetting", "LogSetting") + .WithMany() + .HasForeignKey("LogSettingId"); + + b.HasOne("NadekoBot.Services.Database.Models.Permission", "RootPermission") + .WithMany() + .HasForeignKey("RootPermissionId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.GuildRepeater", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig") + .WithMany("GuildRepeaters") + .HasForeignKey("GuildConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.IgnoredLogChannel", b => + { + b.HasOne("NadekoBot.Services.Database.Models.LogSetting", "LogSetting") + .WithMany("IgnoredChannels") + .HasForeignKey("LogSettingId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.IgnoredVoicePresenceChannel", b => + { + b.HasOne("NadekoBot.Services.Database.Models.LogSetting", "LogSetting") + .WithMany("IgnoredVoicePresenceChannelIds") + .HasForeignKey("LogSettingId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.ModulePrefix", b => + { + b.HasOne("NadekoBot.Services.Database.Models.BotConfig") + .WithMany("ModulePrefixes") + .HasForeignKey("BotConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.MutedUserId", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig") + .WithMany("MutedUsers") + .HasForeignKey("GuildConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.NsfwBlacklitedTag", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig") + .WithMany("NsfwBlacklistedTags") + .HasForeignKey("GuildConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.Permission", b => + { + b.HasOne("NadekoBot.Services.Database.Models.Permission", "Next") + .WithOne("Previous") + .HasForeignKey("NadekoBot.Services.Database.Models.Permission", "NextId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.Permissionv2", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig") + .WithMany("Permissions") + .HasForeignKey("GuildConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.PlayingStatus", b => + { + b.HasOne("NadekoBot.Services.Database.Models.BotConfig") + .WithMany("RotatingStatusMessages") + .HasForeignKey("BotConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.PlaylistSong", b => + { + b.HasOne("NadekoBot.Services.Database.Models.MusicPlaylist") + .WithMany("Songs") + .HasForeignKey("MusicPlaylistId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.RaceAnimal", b => + { + b.HasOne("NadekoBot.Services.Database.Models.BotConfig") + .WithMany("RaceAnimals") + .HasForeignKey("BotConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.ShopEntry", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig") + .WithMany("ShopEntries") + .HasForeignKey("GuildConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.ShopEntryItem", b => + { + b.HasOne("NadekoBot.Services.Database.Models.ShopEntry") + .WithMany("Items") + .HasForeignKey("ShopEntryId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.SlowmodeIgnoredRole", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig") + .WithMany("SlowmodeIgnoredRoles") + .HasForeignKey("GuildConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.SlowmodeIgnoredUser", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig") + .WithMany("SlowmodeIgnoredUsers") + .HasForeignKey("GuildConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.StartupCommand", b => + { + b.HasOne("NadekoBot.Services.Database.Models.BotConfig") + .WithMany("StartupCommands") + .HasForeignKey("BotConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.StreamRoleBlacklistedUser", b => + { + b.HasOne("NadekoBot.Services.Database.Models.StreamRoleSettings") + .WithMany("Blacklist") + .HasForeignKey("StreamRoleSettingsId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.StreamRoleSettings", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig", "GuildConfig") + .WithOne("StreamRole") + .HasForeignKey("NadekoBot.Services.Database.Models.StreamRoleSettings", "GuildConfigId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.StreamRoleWhitelistedUser", b => + { + b.HasOne("NadekoBot.Services.Database.Models.StreamRoleSettings") + .WithMany("Whitelist") + .HasForeignKey("StreamRoleSettingsId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.UnmuteTimer", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig") + .WithMany("UnmuteTimers") + .HasForeignKey("GuildConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.VcRoleInfo", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig") + .WithMany("VcRoleInfos") + .HasForeignKey("GuildConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.WaifuInfo", b => + { + b.HasOne("NadekoBot.Services.Database.Models.DiscordUser", "Affinity") + .WithMany() + .HasForeignKey("AffinityId"); + + b.HasOne("NadekoBot.Services.Database.Models.DiscordUser", "Claimer") + .WithMany() + .HasForeignKey("ClaimerId"); + + b.HasOne("NadekoBot.Services.Database.Models.DiscordUser", "Waifu") + .WithOne() + .HasForeignKey("NadekoBot.Services.Database.Models.WaifuInfo", "WaifuId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.WaifuItem", b => + { + b.HasOne("NadekoBot.Services.Database.Models.WaifuInfo") + .WithMany("Items") + .HasForeignKey("WaifuInfoId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.WaifuUpdate", b => + { + b.HasOne("NadekoBot.Services.Database.Models.DiscordUser", "New") + .WithMany() + .HasForeignKey("NewId"); + + b.HasOne("NadekoBot.Services.Database.Models.DiscordUser", "Old") + .WithMany() + .HasForeignKey("OldId"); + + b.HasOne("NadekoBot.Services.Database.Models.DiscordUser", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.WarningPunishment", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig") + .WithMany("WarnPunishments") + .HasForeignKey("GuildConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.XpRoleReward", b => + { + b.HasOne("NadekoBot.Services.Database.Models.XpSettings") + .WithMany("RoleRewards") + .HasForeignKey("XpSettingsId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.XpSettings", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig", "GuildConfig") + .WithOne("XpSettings") + .HasForeignKey("NadekoBot.Services.Database.Models.XpSettings", "GuildConfigId") + .OnDelete(DeleteBehavior.Cascade); + }); + } + } +} diff --git a/src/NadekoBot/Migrations/20170821085106_xp-stuff.cs b/src/NadekoBot/Migrations/20170821085106_xp-stuff.cs new file mode 100644 index 00000000..166a53f6 --- /dev/null +++ b/src/NadekoBot/Migrations/20170821085106_xp-stuff.cs @@ -0,0 +1,153 @@ +using System; +using System.Collections.Generic; +using Microsoft.EntityFrameworkCore.Migrations; + +namespace NadekoBot.Migrations +{ + public partial class xpstuff : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "XpMinutesTimeout", + table: "BotConfig", + nullable: false, + defaultValue: 0); + + migrationBuilder.AddColumn( + name: "XpPerMessage", + table: "BotConfig", + nullable: false, + defaultValue: 0); + + migrationBuilder.CreateTable( + name: "UserXpStats", + columns: table => new + { + Id = table.Column(nullable: false) + .Annotation("Sqlite:Autoincrement", true), + DateAdded = table.Column(nullable: true), + GuildId = table.Column(nullable: false), + NotifyOnLevelUp = table.Column(nullable: false), + UserId = table.Column(nullable: false), + Xp = table.Column(nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_UserXpStats", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "XpSettings", + columns: table => new + { + Id = table.Column(nullable: false) + .Annotation("Sqlite:Autoincrement", true), + DateAdded = table.Column(nullable: true), + GuildConfigId = table.Column(nullable: false), + NotifyMessage = table.Column(nullable: true), + ServerExcluded = table.Column(nullable: false), + XpRoleRewardExclusive = table.Column(nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_XpSettings", x => x.Id); + table.ForeignKey( + name: "FK_XpSettings_GuildConfigs_GuildConfigId", + column: x => x.GuildConfigId, + principalTable: "GuildConfigs", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "ExcludedItem", + columns: table => new + { + Id = table.Column(nullable: false) + .Annotation("Sqlite:Autoincrement", true), + DateAdded = table.Column(nullable: true), + ItemId = table.Column(nullable: false), + ItemType = table.Column(nullable: false), + XpSettingsId = table.Column(nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_ExcludedItem", x => x.Id); + table.ForeignKey( + name: "FK_ExcludedItem_XpSettings_XpSettingsId", + column: x => x.XpSettingsId, + principalTable: "XpSettings", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + }); + + migrationBuilder.CreateTable( + name: "XpRoleReward", + columns: table => new + { + Id = table.Column(nullable: false) + .Annotation("Sqlite:Autoincrement", true), + DateAdded = table.Column(nullable: true), + Level = table.Column(nullable: false), + RoleId = table.Column(nullable: false), + XpSettingsId = table.Column(nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_XpRoleReward", x => x.Id); + table.ForeignKey( + name: "FK_XpRoleReward_XpSettings_XpSettingsId", + column: x => x.XpSettingsId, + principalTable: "XpSettings", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + }); + + migrationBuilder.CreateIndex( + name: "IX_ExcludedItem_XpSettingsId", + table: "ExcludedItem", + column: "XpSettingsId"); + + migrationBuilder.CreateIndex( + name: "IX_UserXpStats_UserId_GuildId", + table: "UserXpStats", + columns: new[] { "UserId", "GuildId" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_XpRoleReward_XpSettingsId", + table: "XpRoleReward", + column: "XpSettingsId"); + + migrationBuilder.CreateIndex( + name: "IX_XpSettings_GuildConfigId", + table: "XpSettings", + column: "GuildConfigId", + unique: true); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "ExcludedItem"); + + migrationBuilder.DropTable( + name: "UserXpStats"); + + migrationBuilder.DropTable( + name: "XpRoleReward"); + + migrationBuilder.DropTable( + name: "XpSettings"); + + migrationBuilder.DropColumn( + name: "XpMinutesTimeout", + table: "BotConfig"); + + migrationBuilder.DropColumn( + name: "XpPerMessage", + table: "BotConfig"); + } + } +} diff --git a/src/NadekoBot/Migrations/NadekoSqliteContextModelSnapshot.cs b/src/NadekoBot/Migrations/NadekoSqliteContextModelSnapshot.cs index 97c06375..3ebfeafa 100644 --- a/src/NadekoBot/Migrations/NadekoSqliteContextModelSnapshot.cs +++ b/src/NadekoBot/Migrations/NadekoSqliteContextModelSnapshot.cs @@ -183,6 +183,10 @@ namespace NadekoBot.Migrations b.Property("TriviaCurrencyReward"); + b.Property("XpMinutesTimeout"); + + b.Property("XpPerMessage"); + b.HasKey("Id"); b.ToTable("BotConfig"); @@ -445,6 +449,26 @@ namespace NadekoBot.Migrations b.ToTable("EightBallResponses"); }); + modelBuilder.Entity("NadekoBot.Services.Database.Models.ExcludedItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("ItemId"); + + b.Property("ItemType"); + + b.Property("XpSettingsId"); + + b.HasKey("Id"); + + b.HasIndex("XpSettingsId"); + + b.ToTable("ExcludedItem"); + }); + modelBuilder.Entity("NadekoBot.Services.Database.Models.FilterChannelId", b => { b.Property("Id") @@ -1252,6 +1276,29 @@ namespace NadekoBot.Migrations b.ToTable("PokeGame"); }); + modelBuilder.Entity("NadekoBot.Services.Database.Models.UserXpStats", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("GuildId"); + + b.Property("NotifyOnLevelUp"); + + b.Property("UserId"); + + b.Property("Xp"); + + b.HasKey("Id"); + + b.HasIndex("UserId", "GuildId") + .IsUnique(); + + b.ToTable("UserXpStats"); + }); + modelBuilder.Entity("NadekoBot.Services.Database.Models.VcRoleInfo", b => { b.Property("Id") @@ -1393,6 +1440,49 @@ namespace NadekoBot.Migrations b.ToTable("WarningPunishment"); }); + modelBuilder.Entity("NadekoBot.Services.Database.Models.XpRoleReward", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("Level"); + + b.Property("RoleId"); + + b.Property("XpSettingsId"); + + b.HasKey("Id"); + + b.HasIndex("XpSettingsId"); + + b.ToTable("XpRoleReward"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.XpSettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("GuildConfigId"); + + b.Property("NotifyMessage"); + + b.Property("ServerExcluded"); + + b.Property("XpRoleRewardExclusive"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId") + .IsUnique(); + + b.ToTable("XpSettings"); + }); + modelBuilder.Entity("NadekoBot.Services.Database.Models.AntiRaidSetting", b => { b.HasOne("NadekoBot.Services.Database.Models.GuildConfig", "GuildConfig") @@ -1470,6 +1560,13 @@ namespace NadekoBot.Migrations .HasForeignKey("BotConfigId"); }); + modelBuilder.Entity("NadekoBot.Services.Database.Models.ExcludedItem", b => + { + b.HasOne("NadekoBot.Services.Database.Models.XpSettings") + .WithMany("ExclusionList") + .HasForeignKey("XpSettingsId"); + }); + modelBuilder.Entity("NadekoBot.Services.Database.Models.FilterChannelId", b => { b.HasOne("NadekoBot.Services.Database.Models.GuildConfig") @@ -1707,6 +1804,21 @@ namespace NadekoBot.Migrations .WithMany("WarnPunishments") .HasForeignKey("GuildConfigId"); }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.XpRoleReward", b => + { + b.HasOne("NadekoBot.Services.Database.Models.XpSettings") + .WithMany("RoleRewards") + .HasForeignKey("XpSettingsId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.XpSettings", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig", "GuildConfig") + .WithOne("XpSettings") + .HasForeignKey("NadekoBot.Services.Database.Models.XpSettings", "GuildConfigId") + .OnDelete(DeleteBehavior.Cascade); + }); } } } diff --git a/src/NadekoBot/Modules/Games/Services/GamesService.cs b/src/NadekoBot/Modules/Games/Services/GamesService.cs index f09c606b..39164486 100644 --- a/src/NadekoBot/Modules/Games/Services/GamesService.cs +++ b/src/NadekoBot/Modules/Games/Services/GamesService.cs @@ -28,7 +28,7 @@ namespace NadekoBot.Modules.Games.Services public readonly ImmutableArray EightBallResponses; private readonly Timer _t; - private readonly DiscordSocketClient _client; + private readonly CommandHandler _cmd; private readonly NadekoStrings _strings; private readonly IImagesService _images; private readonly Logger _log; @@ -38,11 +38,11 @@ namespace NadekoBot.Modules.Games.Services public List TypingArticles { get; } = new List(); - public GamesService(DiscordSocketClient client, IBotConfigProvider bc, IEnumerable gcs, + public GamesService(CommandHandler cmd, IBotConfigProvider bc, IEnumerable gcs, NadekoStrings strings, IImagesService images, CommandHandler cmdHandler) { _bc = bc; - _client = client; + _cmd = cmd; _strings = strings; _images = images; _cmdHandler = cmdHandler; @@ -59,7 +59,7 @@ namespace NadekoBot.Modules.Games.Services }, null, TimeSpan.FromDays(1), TimeSpan.FromDays(1)); //plantpick - client.MessageReceived += PotentialFlowerGeneration; + _cmd.OnMessageNoTrigger += PotentialFlowerGeneration; GenerationChannels = new ConcurrentHashSet(gcs .SelectMany(c => c.GenerateCurrencyChannelIds.Select(obj => obj.ChannelId))); @@ -102,7 +102,7 @@ namespace NadekoBot.Modules.Games.Services private string GetText(ITextChannel ch, string key, params object[] rep) => _strings.GetText(key, ch.GuildId, "Games".ToLowerInvariant(), rep); - private Task PotentialFlowerGeneration(SocketMessage imsg) + private Task PotentialFlowerGeneration(IUserMessage imsg) { var msg = imsg as SocketUserMessage; if (msg == null || msg.Author.IsBot) diff --git a/src/NadekoBot/Modules/Xp/Extensions/Extensions.cs b/src/NadekoBot/Modules/Xp/Extensions/Extensions.cs new file mode 100644 index 00000000..c5d6605b --- /dev/null +++ b/src/NadekoBot/Modules/Xp/Extensions/Extensions.cs @@ -0,0 +1,34 @@ +using NadekoBot.Modules.Xp.Services; +using NadekoBot.Services.Database.Models; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace NadekoBot.Modules.Xp.Extensions +{ + public static class Extensions + { + public static (int Level, int LevelXp, int LevelRequiredXp) GetLevelData(this UserXpStats stats) + { + var baseXp = XpService.XP_REQUIRED_LVL_1; + + var required = baseXp; + var totalXp = 0; + var lvl = 1; + while (true) + { + required = (int)(baseXp + baseXp / 4.0 * (lvl - 1)); + + if (required + totalXp > stats.Xp) + break; + + totalXp += required; + lvl++; + } + + return (lvl - 1, stats.Xp - totalXp, required); + } + } +} diff --git a/src/NadekoBot/Modules/Xp/Services/UserCacheItem.cs b/src/NadekoBot/Modules/Xp/Services/UserCacheItem.cs new file mode 100644 index 00000000..4cebef92 --- /dev/null +++ b/src/NadekoBot/Modules/Xp/Services/UserCacheItem.cs @@ -0,0 +1,18 @@ +namespace NadekoBot.Modules.Xp.Services +{ + public class UserCacheItem + { + public ulong UserId { get; set; } + public ulong GuildId { get; set; } + + public override int GetHashCode() + { + return UserId.GetHashCode(); + } + + public override bool Equals(object obj) + { + return obj is UserCacheItem uci && uci.UserId == UserId; + } + } +} diff --git a/src/NadekoBot/Modules/Xp/Services/XpService.cs b/src/NadekoBot/Modules/Xp/Services/XpService.cs new file mode 100644 index 00000000..ccd3c6ed --- /dev/null +++ b/src/NadekoBot/Modules/Xp/Services/XpService.cs @@ -0,0 +1,229 @@ +using Discord; +using Discord.WebSocket; +using NadekoBot.Common.Collections; +using NadekoBot.Services; +using NadekoBot.Services.Database.Models; +using NLog; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace NadekoBot.Modules.Xp.Services +{ + public class XpService : INService + { + private readonly DbService _db; + private readonly CommandHandler _cmd; + private readonly IBotConfigProvider _bc; + private readonly Logger _log; + public const int XP_REQUIRED_LVL_1 = 36; + + private readonly ConcurrentDictionary> _excludedRoles + = new ConcurrentDictionary>(); + private readonly ConcurrentDictionary> _excludedChannels + = new ConcurrentDictionary>(); + private readonly ConcurrentHashSet _excludedServers = new ConcurrentHashSet(); + + private readonly ConcurrentHashSet _rewardedUsers = new ConcurrentHashSet(); + + private readonly ConcurrentQueue _addMessageXp = new ConcurrentQueue(); + + private readonly Timer updateXpTimer; + private readonly Timer clearRewardedUsersTimer; + + public XpService(CommandHandler cmd, IBotConfigProvider bc, + IEnumerable allGuildConfigs, DbService db) + { + _db = db; + _cmd = cmd; + _bc = bc; + _log = LogManager.GetCurrentClassLogger(); + + _cmd.OnMessageNoTrigger += _cmd_OnMessageNoTrigger; + + updateXpTimer = new Timer(_ => + { + using (var uow = _db.UnitOfWork) + { + while (_addMessageXp.TryDequeue(out var usr)) + { + var usrObj = uow.Xp.GetOrCreateUser(usr.GuildId, usr.UserId); + usrObj.Xp += _bc.BotConfig.XpPerMessage; + } + uow.Complete(); + } + }, null, TimeSpan.FromSeconds(5), TimeSpan.FromSeconds(5)); + + clearRewardedUsersTimer = new Timer(_ => + { + _rewardedUsers.Clear(); + }, null, TimeSpan.FromSeconds(bc.BotConfig.XpMinutesTimeout), TimeSpan.FromSeconds(bc.BotConfig.XpMinutesTimeout)); + } + + private Task _cmd_OnMessageNoTrigger(IUserMessage arg) + { + if (!(arg.Author is SocketGuildUser user) || user.IsBot) + return Task.CompletedTask; + + var _ = Task.Run(() => + { + if (!SetUserRewarded(user.Id)) + return; + + if (_excludedChannels.TryGetValue(user.Guild.Id, out var chans) && + chans.Contains(arg.Channel.Id)) + return; + + if (_excludedServers.Contains(user.Guild.Id)) + return; + + if (_excludedRoles.TryGetValue(user.Guild.Id, out var roles) && + user.Roles.Any(x => roles.Contains(x.Id))) + return; + + _log.Info("Adding {0} xp to {1} on {2} server", _bc.BotConfig.XpPerMessage, user.ToString(), user.Guild.Name); + _addMessageXp.Enqueue(new UserCacheItem { GuildId = user.Guild.Id, UserId = user.Id }); + }); + return Task.CompletedTask; + } + + public bool IsServerExcluded(ulong id) + { + return _excludedServers.Contains(id); + } + + public IEnumerable GetExcludedRoles(ulong id) + { + if (_excludedRoles.TryGetValue(id, out var val)) + return val.ToArray(); + + return Enumerable.Empty(); + } + + public IEnumerable GetExcludedChannels(ulong id) + { + if (_excludedChannels.TryGetValue(id, out var val)) + return val.ToArray(); + + return Enumerable.Empty(); + } + + private bool SetUserRewarded(ulong userId) + { + return _rewardedUsers.Add(userId); + } + + public UserXpStats GetUserStats(ulong guildId, ulong userId) + { + UserXpStats user; + using (var uow = _db.UnitOfWork) + { + user = uow.Xp.GetOrCreateUser(guildId, userId); + } + + return user; + } + + public string GenerateXpBar(int currentXp, int requiredXp) + { + //todo + return $"{currentXp}/{requiredXp}"; + } + + + //todo exclude in database + public bool ToggleExcludeServer(ulong id) + { + using (var uow = _db.UnitOfWork) + { + var xpSetting = uow.GuildConfigs.XpSettingsFor(id); + if (_excludedServers.Add(id)) + { + xpSetting.ServerExcluded = true; + uow.Complete(); + return true; + } + + _excludedServers.TryRemove(id); + xpSetting.ServerExcluded = false; + uow.Complete(); + return false; + } + } + + public bool ToggleExcludeRole(ulong guildId, ulong rId) + { + var roles = _excludedRoles.GetOrAdd(guildId, _ => new ConcurrentHashSet()); + using (var uow = _db.UnitOfWork) + { + var xpSetting = uow.GuildConfigs.XpSettingsFor(guildId); + var excludeObj = new ExcludedItem + { + ItemId = rId, + ItemType = ExcludedItemType.Role, + }; + + if (roles.Add(rId)) + { + + if (xpSetting.ExclusionList.Add(excludeObj)) + { + uow.Complete(); + } + + return true; + } + else + { + roles.TryRemove(rId); + + if (xpSetting.ExclusionList.Remove(excludeObj)) + { + uow.Complete(); + } + + return false; + } + } + } + + public bool ToggleExcludeChannel(ulong guildId, ulong chId) + { + var channels = _excludedChannels.GetOrAdd(guildId, _ => new ConcurrentHashSet()); + using (var uow = _db.UnitOfWork) + { + var xpSetting = uow.GuildConfigs.XpSettingsFor(guildId); + var excludeObj = new ExcludedItem + { + ItemId = chId, + ItemType = ExcludedItemType.Channel, + }; + + if (channels.Add(chId)) + { + + if (xpSetting.ExclusionList.Add(excludeObj)) + { + uow.Complete(); + } + + return true; + } + else + { + channels.TryRemove(chId); + + if (xpSetting.ExclusionList.Remove(excludeObj)) + { + uow.Complete(); + } + + return false; + } + } + } + } +} diff --git a/src/NadekoBot/Modules/Xp/Xp.cs b/src/NadekoBot/Modules/Xp/Xp.cs new file mode 100644 index 00000000..44b5b006 --- /dev/null +++ b/src/NadekoBot/Modules/Xp/Xp.cs @@ -0,0 +1,105 @@ +using Discord; +using Discord.Commands; +using NadekoBot.Common.Attributes; +using NadekoBot.Extensions; +using NadekoBot.Modules.Xp.Extensions; +using NadekoBot.Modules.Xp.Services; +using System.Linq; +using System.Threading.Tasks; + +namespace NadekoBot.Modules.Xp +{ + public partial class Xp : NadekoTopLevelModule + { + [NadekoCommand, Usage, Description, Aliases] + [RequireContext(ContextType.Guild)] + public async Task Experience(IUser user = null) + { + user = user ?? Context.User; + await Task.Delay(64).ConfigureAwait(false); // wait a bit in case user got XP with this message + + var stats = _service.GetUserStats(Context.Guild.Id, user.Id); + + var levelData = stats.GetLevelData(); + var xpBarStr = _service.GenerateXpBar(levelData.LevelXp, levelData.LevelRequiredXp); + + await Context.Channel.EmbedAsync(new EmbedBuilder() + .WithTitle(user.ToString()) + //.AddField(GetText("server_level"), stats.ServerLevel.ToString(), true) + .AddField(GetText("level"), levelData.Level.ToString(), true) + //.AddField(GetText("club"), stats.ClubName ?? "-", true) + .AddField(GetText("xp"), xpBarStr, false) + .WithOkColor()) + .ConfigureAwait(false); + } + + public enum Server { Server }; + + //[NadekoCommand, Usage, Description, Aliases] + //[RequireContext(ContextType.Guild)] + //[OwnerOnly] + //[Priority(1)] + //public async Task XpExclude(Server _, IGuild guild) + //{ + //} + + [NadekoCommand, Usage, Description, Aliases] + [RequireContext(ContextType.Guild)] + [RequireUserPermission(GuildPermission.Administrator)] + public async Task XpExclude(Server _) + { + var ex = _service.ToggleExcludeServer(Context.Guild.Id); + + await ReplyConfirmLocalized((ex ? "excluded" : "not_excluded"), Format.Bold(Context.Guild.ToString())).ConfigureAwait(false); + } + + public enum Role { Role }; + + [NadekoCommand, Usage, Description, Aliases] + [RequireUserPermission(GuildPermission.ManageRoles)] + [RequireContext(ContextType.Guild)] + public async Task XpExclude(Role _, [Remainder] IRole role) + { + var ex = _service.ToggleExcludeRole(Context.Guild.Id, role.Id); + + await ReplyConfirmLocalized((ex ? "excluded" : "not_excluded"), Format.Bold(role.ToString())).ConfigureAwait(false); + } + + public enum Channel { Channel }; + + [NadekoCommand, Usage, Description, Aliases] + [RequireUserPermission(GuildPermission.ManageChannels)] + [RequireContext(ContextType.Guild)] + public async Task XpExclude(Channel _, [Remainder] ITextChannel channel) + { + var ex = _service.ToggleExcludeChannel(Context.Guild.Id, channel.Id); + + await ReplyConfirmLocalized((ex ? "excluded" : "not_excluded"), Format.Bold(channel.ToString())).ConfigureAwait(false); + } + + [NadekoCommand, Usage, Description, Aliases] + [RequireContext(ContextType.Guild)] + public async Task XpExclusionList() + { + var serverExcluded = _service.IsServerExcluded(Context.Guild.Id); + var roles = _service.GetExcludedRoles(Context.Guild.Id) + .Select(x => Context.Guild.GetRole(x)?.Name) + .Where(x => x != null); + + var chans = (await Task.WhenAll(_service.GetExcludedChannels(Context.Guild.Id) + .Select(x => Context.Guild.GetChannelAsync(x))) + .ConfigureAwait(false)) + .Where(x => x != null) + .Select(x => x.Name); + + var embed = new EmbedBuilder() + .WithTitle(GetText("exclusion_list")) + .WithDescription((serverExcluded ? GetText("server_is_excluded") : GetText("server_is_not_excluded"))) + .AddField(GetText("excluded_roles"), roles.Any() ? string.Join("\n", roles) : "-", false) + .AddField(GetText("excluded_channels"), chans.Any() ? string.Join("\n", chans) : "-", false) + .WithOkColor(); + + await Context.Channel.EmbedAsync(embed).ConfigureAwait(false); + } + } +} diff --git a/src/NadekoBot/NadekoBot.csproj b/src/NadekoBot/NadekoBot.csproj index 5e1db116..f18c711c 100644 --- a/src/NadekoBot/NadekoBot.csproj +++ b/src/NadekoBot/NadekoBot.csproj @@ -91,4 +91,10 @@ + + + + Designer + + diff --git a/src/NadekoBot/Resources/CommandStrings.resx b/src/NadekoBot/Resources/CommandStrings.resx index e547c0cb..8493d383 100644 --- a/src/NadekoBot/Resources/CommandStrings.resx +++ b/src/NadekoBot/Resources/CommandStrings.resx @@ -3573,4 +3573,31 @@ Toggles whether the tag is blacklisted or not in nsfw searches. Provide no parameters to see the list of blacklisted tags. + + experience xp + + + `{0}xp` + + + Shows your xp stats. Specify the user to show that user's stats instead. + + + xpexclusionlist xpexl + + + `{0}xpexl` + + + Shows the roles and channels excluded from the XP system on this server, as well as whether the whole server is excluded. + + + xpexclude xpex + + + `{0}xpex User @b1nzy` `{0}xpex Server` + + + Exclude a user or a role from the xp system, or whole current server. + diff --git a/src/NadekoBot/Services/Database/IUnitOfWork.cs b/src/NadekoBot/Services/Database/IUnitOfWork.cs index 52cb01cf..fdd2eb0d 100644 --- a/src/NadekoBot/Services/Database/IUnitOfWork.cs +++ b/src/NadekoBot/Services/Database/IUnitOfWork.cs @@ -24,6 +24,7 @@ namespace NadekoBot.Services.Database IWaifuRepository Waifus { get; } IDiscordUserRepository DiscordUsers { get; } IWarningsRepository Warnings { get; } + IXpRepository Xp { get; } int Complete(); Task CompleteAsync(); diff --git a/src/NadekoBot/Services/Database/Models/BotConfig.cs b/src/NadekoBot/Services/Database/Models/BotConfig.cs index d765b813..98b84594 100644 --- a/src/NadekoBot/Services/Database/Models/BotConfig.cs +++ b/src/NadekoBot/Services/Database/Models/BotConfig.cs @@ -68,6 +68,8 @@ Nadeko Support Server: https://discord.gg/nadekobot"; public int PermissionVersion { get; set; } public string DefaultPrefix { get; set; } = "."; public bool CustomReactionsStartWith { get; set; } = false; + public int XpPerMessage { get; set; } = 3; + public int XpMinutesTimeout { get; set; } = 5; } public class BlockedCmdOrMdl : DbEntity diff --git a/src/NadekoBot/Services/Database/Models/GuildConfig.cs b/src/NadekoBot/Services/Database/Models/GuildConfig.cs index 7ae60508..82cc8bdf 100644 --- a/src/NadekoBot/Services/Database/Models/GuildConfig.cs +++ b/src/NadekoBot/Services/Database/Models/GuildConfig.cs @@ -86,6 +86,8 @@ namespace NadekoBot.Services.Database.Models public StreamRoleSettings StreamRole { get; set; } + public XpSettings XpSettings { get; set; } + //public List ProtectionIgnoredChannels { get; set; } = new List(); } diff --git a/src/NadekoBot/Services/Database/Models/UserXpStats.cs b/src/NadekoBot/Services/Database/Models/UserXpStats.cs new file mode 100644 index 00000000..9c515ec2 --- /dev/null +++ b/src/NadekoBot/Services/Database/Models/UserXpStats.cs @@ -0,0 +1,10 @@ +namespace NadekoBot.Services.Database.Models +{ + public class UserXpStats : DbEntity + { + public ulong UserId { get; set; } + public ulong GuildId { get; set; } + public int Xp { get; set; } + public bool NotifyOnLevelUp { get; set; } + } +} diff --git a/src/NadekoBot/Services/Database/Models/XpSettings.cs b/src/NadekoBot/Services/Database/Models/XpSettings.cs new file mode 100644 index 00000000..cf89d611 --- /dev/null +++ b/src/NadekoBot/Services/Database/Models/XpSettings.cs @@ -0,0 +1,50 @@ +using System.Collections.Generic; + +namespace NadekoBot.Services.Database.Models +{ + public class XpSettings : DbEntity + { + public int GuildConfigId { get; set; } + public GuildConfig GuildConfig { get; set; } + + public HashSet RoleRewards { get; set; } = new HashSet(); + public bool XpRoleRewardExclusive { get; set; } + public string NotifyMessage { get; set; } = "Congratulations {0}! You have reached level {1}!"; + public HashSet ExclusionList { get; set; } = new HashSet(); + public bool ServerExcluded { get; set; } + } + + public enum ExcludedItemType { Channel, Role } + + public class XpRoleReward : DbEntity + { + public int Level { get; set; } + public ulong RoleId { get; set; } + + public override int GetHashCode() + { + return Level.GetHashCode() ^ RoleId.GetHashCode(); + } + + public override bool Equals(object obj) + { + return obj is XpRoleReward xrr && xrr.Level == Level && xrr.RoleId == RoleId; + } + } + + public class ExcludedItem : DbEntity + { + public ulong ItemId { get; set; } + public ExcludedItemType ItemType { get; set; } + + public override int GetHashCode() + { + return ItemId.GetHashCode() ^ ItemType.GetHashCode(); + } + + public override bool Equals(object obj) + { + return obj is ExcludedItem ei && ei.ItemId == ItemId && ei.ItemType == ItemType; + } + } +} diff --git a/src/NadekoBot/Services/Database/NadekoContext.cs b/src/NadekoBot/Services/Database/NadekoContext.cs index 7c00e1f3..5ae153d0 100644 --- a/src/NadekoBot/Services/Database/NadekoContext.cs +++ b/src/NadekoBot/Services/Database/NadekoContext.cs @@ -43,6 +43,7 @@ namespace NadekoBot.Services.Database public DbSet PokeGame { get; set; } public DbSet WaifuUpdates { get; set; } public DbSet Warnings { get; set; } + public DbSet UserXpStats { get; set; } //logging public DbSet LogSettings { get; set; } @@ -292,6 +293,18 @@ namespace NadekoBot.Services.Database pr.HasIndex(x => x.UserId) .IsUnique(); #endregion + + #region XpStatas + modelBuilder.Entity() + .HasIndex(x => new { x.UserId, x.GuildId }) + .IsUnique(); + #endregion + + #region XpSettings + modelBuilder.Entity() + .HasOne(x => x.GuildConfig) + .WithOne(x => x.XpSettings); + #endregion } } } diff --git a/src/NadekoBot/Services/Database/Repositories/IGuildConfigRepository.cs b/src/NadekoBot/Services/Database/Repositories/IGuildConfigRepository.cs index e56a9026..498b72ed 100644 --- a/src/NadekoBot/Services/Database/Repositories/IGuildConfigRepository.cs +++ b/src/NadekoBot/Services/Database/Repositories/IGuildConfigRepository.cs @@ -16,5 +16,6 @@ namespace NadekoBot.Services.Database.Repositories void SetCleverbotEnabled(ulong id, bool cleverbotEnabled); IEnumerable Permissionsv2ForAll(List include); GuildConfig GcWithPermissionsv2For(ulong guildId); + XpSettings XpSettingsFor(ulong guildId); } } diff --git a/src/NadekoBot/Services/Database/Repositories/IXpRepository.cs b/src/NadekoBot/Services/Database/Repositories/IXpRepository.cs new file mode 100644 index 00000000..1973504a --- /dev/null +++ b/src/NadekoBot/Services/Database/Repositories/IXpRepository.cs @@ -0,0 +1,9 @@ +using NadekoBot.Services.Database.Models; + +namespace NadekoBot.Services.Database.Repositories +{ + public interface IXpRepository : IRepository + { + UserXpStats GetOrCreateUser(ulong guildId, ulong userId); + } +} diff --git a/src/NadekoBot/Services/Database/Repositories/Impl/GuildConfigRepository.cs b/src/NadekoBot/Services/Database/Repositories/Impl/GuildConfigRepository.cs index e6087bec..1c296db7 100644 --- a/src/NadekoBot/Services/Database/Repositories/Impl/GuildConfigRepository.cs +++ b/src/NadekoBot/Services/Database/Repositories/Impl/GuildConfigRepository.cs @@ -189,5 +189,19 @@ namespace NadekoBot.Services.Database.Repositories.Impl conf.CleverbotEnabled = cleverbotEnabled; } + + public XpSettings XpSettingsFor(ulong guildId) + { + var gc = For(guildId, + set => set.Include(x => x.XpSettings) + .ThenInclude(x => x.RoleRewards) + .Include(x => x.XpSettings) + .ThenInclude(x => x.ExclusionList)); + + if (gc.XpSettings == null) + gc.XpSettings = new XpSettings(); + + return gc.XpSettings; + } } } diff --git a/src/NadekoBot/Services/Database/Repositories/Impl/XpRepository.cs b/src/NadekoBot/Services/Database/Repositories/Impl/XpRepository.cs new file mode 100644 index 00000000..6326587d --- /dev/null +++ b/src/NadekoBot/Services/Database/Repositories/Impl/XpRepository.cs @@ -0,0 +1,31 @@ +using NadekoBot.Services.Database.Models; +using System.Linq; +using Microsoft.EntityFrameworkCore; + +namespace NadekoBot.Services.Database.Repositories.Impl +{ + public class XpRepository : Repository, IXpRepository + { + public XpRepository(DbContext context) : base(context) + { + } + + public UserXpStats GetOrCreateUser(ulong guildId, ulong userId) + { + var usr = _set.FirstOrDefault(x => x.UserId == userId); + + if (usr == null) + { + _context.Add(usr = new UserXpStats() + { + Xp = 0, + UserId = userId, + NotifyOnLevelUp = false, + GuildId = guildId, + }); + } + + return usr; + } + } +} diff --git a/src/NadekoBot/Services/Database/UnitOfWork.cs b/src/NadekoBot/Services/Database/UnitOfWork.cs index 02e78a97..3b450f86 100644 --- a/src/NadekoBot/Services/Database/UnitOfWork.cs +++ b/src/NadekoBot/Services/Database/UnitOfWork.cs @@ -57,6 +57,9 @@ namespace NadekoBot.Services.Database private IWarningsRepository _warnings; public IWarningsRepository Warnings => _warnings ?? (_warnings = new WarningsRepository(_context)); + private IXpRepository _xp; + public IXpRepository Xp => _xp ?? (_xp = new XpRepository(_context)); + public UnitOfWork(NadekoContext context) { _context = context; diff --git a/src/NadekoBot/Services/Impl/BotConfigProvider.cs b/src/NadekoBot/Services/Impl/BotConfigProvider.cs index 0ed97550..2d293401 100644 --- a/src/NadekoBot/Services/Impl/BotConfigProvider.cs +++ b/src/NadekoBot/Services/Impl/BotConfigProvider.cs @@ -122,6 +122,18 @@ namespace NadekoBot.Services.Impl else return false; break; + case BotConfigEditType.XpPerMessage: + if (int.TryParse(newValue, out var xp) && xp > 0) + bc.XpPerMessage = xp; + else + return false; + break; + case BotConfigEditType.XpMinutesTimeout: + if (int.TryParse(newValue, out var min) && min > 0) + bc.XpMinutesTimeout = min; + else + return false; + break; default: return false; } diff --git a/src/NadekoBot/_strings/ResponseStrings.en-US.json b/src/NadekoBot/_strings/ResponseStrings.en-US.json index 2d123486..2ccc0b70 100644 --- a/src/NadekoBot/_strings/ResponseStrings.en-US.json +++ b/src/NadekoBot/_strings/ResponseStrings.en-US.json @@ -822,5 +822,16 @@ "administration_prefix_current": "Prefix on this server is {0}", "administration_prefix_new": "Changed prefix on this server from {0} to {1}", "administration_defprefix_current": "Default bot prefix is {0}", - "administration_defprefix_new": "Changed Default bot prefix from {0} to {1}" + "administration_defprefix_new": "Changed Default bot prefix from {0} to {1}", + "xp_server_level": "Server Level", + "xp_level": "Level", + "xp_club": "Club", + "xp_xp": "Experience", + "xp_excluded": "{0} has been excluded from the XP system on this server.", + "xp_not_excluded": "{0} is no longer excluded from the XP system on this server.", + "xp_exclusion_list": "Exclusion List", + "xp_server_is_excluded": "This server is excluded.", + "xp_server_is_not_excluded": "This server is not excluded.", + "xp_excluded_roles": "Excluded Roles", + "xp_excluded_channels": "Excluded Channels" } \ No newline at end of file From 5362821843c34ea23ec6b53ea61d5f4621f286da Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Tue, 22 Aug 2017 05:50:03 +0200 Subject: [PATCH 273/346] Version upped --- src/NadekoBot/Services/Impl/StatsService.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/NadekoBot/Services/Impl/StatsService.cs b/src/NadekoBot/Services/Impl/StatsService.cs index 335db6b8..f9f5add5 100644 --- a/src/NadekoBot/Services/Impl/StatsService.cs +++ b/src/NadekoBot/Services/Impl/StatsService.cs @@ -20,7 +20,7 @@ namespace NadekoBot.Services.Impl private readonly IBotCredentials _creds; private readonly DateTime _started; - public const string BotVersion = "1.7"; + public const string BotVersion = "1.7.1"; public string Author => "Kwoth#2560"; public string Library => "Discord.Net"; From 20fb253eb31604967bc39ab0e5ca2a377dfb4bc5 Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Fri, 25 Aug 2017 22:48:39 +0200 Subject: [PATCH 274/346] Removed .rrc because it's prohibited --- src/NadekoBot/Modules/Utility/Utility.cs | 68 +----------------------- 1 file changed, 1 insertion(+), 67 deletions(-) diff --git a/src/NadekoBot/Modules/Utility/Utility.cs b/src/NadekoBot/Modules/Utility/Utility.cs index 5ee6de70..97debbef 100644 --- a/src/NadekoBot/Modules/Utility/Utility.cs +++ b/src/NadekoBot/Modules/Utility/Utility.cs @@ -24,7 +24,6 @@ namespace NadekoBot.Modules.Utility { public partial class Utility : NadekoTopLevelModule { - private static ConcurrentDictionary _rotatingRoleColors = new ConcurrentDictionary(); private readonly DiscordSocketClient _client; private readonly IStatsService _stats; private readonly IBotCredentials _creds; @@ -36,71 +35,6 @@ namespace NadekoBot.Modules.Utility _stats = stats; _creds = creds; _shardCoord = nadeko.ShardCoord; - } - - [NadekoCommand, Usage, Description, Aliases] - [RequireContext(ContextType.Guild)] - [RequireUserPermission(GuildPermission.ManageRoles)] - [OwnerOnly] - public async Task RotateRoleColor(int timeout, IRole role, params string[] hexes) - { - var channel = (ITextChannel)Context.Channel; - - if ((timeout < 60 && timeout != 0) || timeout > 3600) - return; - - Timer t; - if (timeout == 0 || hexes.Length == 0) - { - if (_rotatingRoleColors.TryRemove(role.Id, out t)) - { - t.Change(Timeout.Infinite, Timeout.Infinite); - await ReplyConfirmLocalized("rrc_stop", Format.Bold(role.Name)).ConfigureAwait(false); - } - return; - } - - var hexColors = hexes.Select(hex => - { - try { return (Rgba32?)Rgba32.FromHex(hex.Replace("#", "")); } catch { return null; } - }) - .Where(c => c != null) - .Select(c => c.Value) - .ToArray(); - - if (!hexColors.Any()) - { - await ReplyErrorLocalized("rrc_no_colors").ConfigureAwait(false); - return; - } - - var images = hexColors.Select(color => - { - var img = new ImageSharp.Image(50, 50); - img.BackgroundColor(color); - return img; - }).Merge().ToStream(); - - var i = 0; - t = new Timer(async (_) => - { - try - { - var color = hexColors[i]; - await role.ModifyAsync(r => r.Color = new Color(color.R, color.G, color.B)).ConfigureAwait(false); - ++i; - if (i >= hexColors.Length) - i = 0; - } - catch { } - }, null, 0, timeout * 1000); - - _rotatingRoleColors.AddOrUpdate(role.Id, t, (key, old) => - { - old.Change(Timeout.Infinite, Timeout.Infinite); - return t; - }); - await channel.SendFileAsync(images, "magicalgirl.jpg", GetText("rrc_start", Format.Bold(role.Name))).ConfigureAwait(false); } [NadekoCommand, Usage, Description, Aliases] @@ -430,4 +364,4 @@ namespace NadekoBot.Modules.Utility await Context.Channel.SendConfirmAsync($"{Format.Bold(Context.User.ToString())} 🏓 {(int)sw.Elapsed.TotalMilliseconds}ms").ConfigureAwait(false); } } -} \ No newline at end of file +} From 2aca71cd8a55084cfd5a45d87dcb7a091988579e Mon Sep 17 00:00:00 2001 From: Deivedux Date: Wed, 30 Aug 2017 20:56:44 +0300 Subject: [PATCH 275/346] Update Permissions System.md --- docs/Permissions System.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/Permissions System.md b/docs/Permissions System.md index 64b76207..32ad3407 100644 --- a/docs/Permissions System.md +++ b/docs/Permissions System.md @@ -49,11 +49,11 @@ To allow users to only see the current song and have a DJ role for queuing follo * Disables music commands for everybody -2. `.sc !!nowplaying enable` +2. `.sc .nowplaying enable` * Enables the "nowplaying" command for everyone -3. `.sc !!listqueue enable` +3. `.sc .listqueue enable` * Enables the "listqueue" command for everyone From 96d792f63ba757389af9a0e1a3f269d975939bda Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Sun, 10 Sep 2017 03:52:34 +0200 Subject: [PATCH 276/346] Xp and clubs finished. Need a lot of testing. Version upped to 1.8-beta --- .../Migrations/20170821085106_xp-stuff.cs | 153 ----- ...> 20170908230730_xp-and-clubs.Designer.cs} | 130 +++- .../Migrations/20170908230730_xp-and-clubs.cs | 296 ++++++++++ src/NadekoBot/Migrations/MigrationQueries.cs | 38 ++ .../NadekoSqliteContextModelSnapshot.cs | 126 +++- .../Administration/MigrationCommands.cs | 48 -- .../CustomReactions/CustomReactions.cs | 2 +- .../Modules/Gambling/WaifuClaimCommands.cs | 10 +- src/NadekoBot/Modules/Games/Games.cs | 2 +- src/NadekoBot/Modules/NSFW/NSFW.cs | 1 + src/NadekoBot/Modules/Searches/LoLCommands.cs | 319 +--------- .../Modules/Searches/TranslatorCommands.cs | 21 + .../Utility/Services/StreamRoleService.cs | 2 +- src/NadekoBot/Modules/Utility/Utility.cs | 2 +- src/NadekoBot/Modules/Xp/Club.cs | 311 ++++++++++ .../Modules/Xp/Common/FullUserStats.cs | 26 + src/NadekoBot/Modules/Xp/Common/LevelStats.cs | 43 ++ .../Modules/Xp/Services/ClubService.cs | 312 ++++++++++ .../Modules/Xp/Services/UserCacheItem.cs | 13 +- .../Modules/Xp/Services/XpService.cs | 554 +++++++++++++++++- src/NadekoBot/Modules/Xp/Xp.cs | 192 +++++- src/NadekoBot/NadekoBot.csproj | 14 +- src/NadekoBot/Resources/CommandStrings.resx | 180 ++++++ src/NadekoBot/Services/CommandHandler.cs | 2 +- .../Services/Database/IUnitOfWork.cs | 1 + .../Services/Database/Models/ClubInfo.cs | 47 ++ .../Services/Database/Models/DiscordUser.cs | 20 +- .../Services/Database/Models/UserXpStats.cs | 10 +- .../Services/Database/NadekoContext.cs | 86 ++- .../Database/Repositories/IClubRepository.cs | 16 + .../Database/Repositories/IXpRepository.cs | 5 + .../Repositories/Impl/BotConfigRepository.cs | 1 - .../Repositories/Impl/ClubRepository.cs | 65 ++ .../Impl/DiscordUserRepository.cs | 11 +- .../Repositories/Impl/XpRepository.cs | 52 +- src/NadekoBot/Services/Database/UnitOfWork.cs | 3 + src/NadekoBot/Services/DbService.cs | 24 +- src/NadekoBot/Services/IImagesService.cs | 2 + .../Services/Impl/GoogleApiService.cs | 3 +- src/NadekoBot/Services/Impl/ImagesService.cs | 6 + src/NadekoBot/Services/Impl/StatsService.cs | 2 +- src/NadekoBot/_Extensions/Extensions.cs | 2 +- .../_Extensions/IMessageChannelExtensions.cs | 10 +- src/NadekoBot/_Extensions/IUserExtensions.cs | 14 +- .../_strings/ResponseStrings.en-US.json | 37 +- src/NadekoBot/data/fonts/Uni Sans.ttf | Bin 0 -> 50856 bytes src/NadekoBot/data/fonts/WhitneyBold.ttf | Bin 0 -> 59652 bytes .../data/fonts/WhitneyBoldItalic.ttf | Bin 0 -> 56336 bytes src/NadekoBot/data/images/xp/xp.png | Bin 0 -> 294486 bytes 49 files changed, 2566 insertions(+), 648 deletions(-) delete mode 100644 src/NadekoBot/Migrations/20170821085106_xp-stuff.cs rename src/NadekoBot/Migrations/{20170821085106_xp-stuff.Designer.cs => 20170908230730_xp-and-clubs.Designer.cs} (92%) create mode 100644 src/NadekoBot/Migrations/20170908230730_xp-and-clubs.cs create mode 100644 src/NadekoBot/Migrations/MigrationQueries.cs create mode 100644 src/NadekoBot/Modules/Xp/Club.cs create mode 100644 src/NadekoBot/Modules/Xp/Common/FullUserStats.cs create mode 100644 src/NadekoBot/Modules/Xp/Common/LevelStats.cs create mode 100644 src/NadekoBot/Modules/Xp/Services/ClubService.cs create mode 100644 src/NadekoBot/Services/Database/Models/ClubInfo.cs create mode 100644 src/NadekoBot/Services/Database/Repositories/IClubRepository.cs create mode 100644 src/NadekoBot/Services/Database/Repositories/Impl/ClubRepository.cs create mode 100644 src/NadekoBot/data/fonts/Uni Sans.ttf create mode 100644 src/NadekoBot/data/fonts/WhitneyBold.ttf create mode 100644 src/NadekoBot/data/fonts/WhitneyBoldItalic.ttf create mode 100644 src/NadekoBot/data/images/xp/xp.png diff --git a/src/NadekoBot/Migrations/20170821085106_xp-stuff.cs b/src/NadekoBot/Migrations/20170821085106_xp-stuff.cs deleted file mode 100644 index 166a53f6..00000000 --- a/src/NadekoBot/Migrations/20170821085106_xp-stuff.cs +++ /dev/null @@ -1,153 +0,0 @@ -using System; -using System.Collections.Generic; -using Microsoft.EntityFrameworkCore.Migrations; - -namespace NadekoBot.Migrations -{ - public partial class xpstuff : Migration - { - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.AddColumn( - name: "XpMinutesTimeout", - table: "BotConfig", - nullable: false, - defaultValue: 0); - - migrationBuilder.AddColumn( - name: "XpPerMessage", - table: "BotConfig", - nullable: false, - defaultValue: 0); - - migrationBuilder.CreateTable( - name: "UserXpStats", - columns: table => new - { - Id = table.Column(nullable: false) - .Annotation("Sqlite:Autoincrement", true), - DateAdded = table.Column(nullable: true), - GuildId = table.Column(nullable: false), - NotifyOnLevelUp = table.Column(nullable: false), - UserId = table.Column(nullable: false), - Xp = table.Column(nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_UserXpStats", x => x.Id); - }); - - migrationBuilder.CreateTable( - name: "XpSettings", - columns: table => new - { - Id = table.Column(nullable: false) - .Annotation("Sqlite:Autoincrement", true), - DateAdded = table.Column(nullable: true), - GuildConfigId = table.Column(nullable: false), - NotifyMessage = table.Column(nullable: true), - ServerExcluded = table.Column(nullable: false), - XpRoleRewardExclusive = table.Column(nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_XpSettings", x => x.Id); - table.ForeignKey( - name: "FK_XpSettings_GuildConfigs_GuildConfigId", - column: x => x.GuildConfigId, - principalTable: "GuildConfigs", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateTable( - name: "ExcludedItem", - columns: table => new - { - Id = table.Column(nullable: false) - .Annotation("Sqlite:Autoincrement", true), - DateAdded = table.Column(nullable: true), - ItemId = table.Column(nullable: false), - ItemType = table.Column(nullable: false), - XpSettingsId = table.Column(nullable: true) - }, - constraints: table => - { - table.PrimaryKey("PK_ExcludedItem", x => x.Id); - table.ForeignKey( - name: "FK_ExcludedItem_XpSettings_XpSettingsId", - column: x => x.XpSettingsId, - principalTable: "XpSettings", - principalColumn: "Id", - onDelete: ReferentialAction.Restrict); - }); - - migrationBuilder.CreateTable( - name: "XpRoleReward", - columns: table => new - { - Id = table.Column(nullable: false) - .Annotation("Sqlite:Autoincrement", true), - DateAdded = table.Column(nullable: true), - Level = table.Column(nullable: false), - RoleId = table.Column(nullable: false), - XpSettingsId = table.Column(nullable: true) - }, - constraints: table => - { - table.PrimaryKey("PK_XpRoleReward", x => x.Id); - table.ForeignKey( - name: "FK_XpRoleReward_XpSettings_XpSettingsId", - column: x => x.XpSettingsId, - principalTable: "XpSettings", - principalColumn: "Id", - onDelete: ReferentialAction.Restrict); - }); - - migrationBuilder.CreateIndex( - name: "IX_ExcludedItem_XpSettingsId", - table: "ExcludedItem", - column: "XpSettingsId"); - - migrationBuilder.CreateIndex( - name: "IX_UserXpStats_UserId_GuildId", - table: "UserXpStats", - columns: new[] { "UserId", "GuildId" }, - unique: true); - - migrationBuilder.CreateIndex( - name: "IX_XpRoleReward_XpSettingsId", - table: "XpRoleReward", - column: "XpSettingsId"); - - migrationBuilder.CreateIndex( - name: "IX_XpSettings_GuildConfigId", - table: "XpSettings", - column: "GuildConfigId", - unique: true); - } - - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropTable( - name: "ExcludedItem"); - - migrationBuilder.DropTable( - name: "UserXpStats"); - - migrationBuilder.DropTable( - name: "XpRoleReward"); - - migrationBuilder.DropTable( - name: "XpSettings"); - - migrationBuilder.DropColumn( - name: "XpMinutesTimeout", - table: "BotConfig"); - - migrationBuilder.DropColumn( - name: "XpPerMessage", - table: "BotConfig"); - } - } -} diff --git a/src/NadekoBot/Migrations/20170821085106_xp-stuff.Designer.cs b/src/NadekoBot/Migrations/20170908230730_xp-and-clubs.Designer.cs similarity index 92% rename from src/NadekoBot/Migrations/20170821085106_xp-stuff.Designer.cs rename to src/NadekoBot/Migrations/20170908230730_xp-and-clubs.Designer.cs index ba66fe44..6b324c08 100644 --- a/src/NadekoBot/Migrations/20170821085106_xp-stuff.Designer.cs +++ b/src/NadekoBot/Migrations/20170908230730_xp-and-clubs.Designer.cs @@ -9,8 +9,8 @@ using NadekoBot.Services.Database.Models; namespace NadekoBot.Migrations { [DbContext(typeof(NadekoContext))] - [Migration("20170821085106_xp-stuff")] - partial class xpstuff + [Migration("20170908230730_xp-and-clubs")] + partial class xpandclubs { protected override void BuildTargetModel(ModelBuilder modelBuilder) { @@ -184,9 +184,13 @@ namespace NadekoBot.Migrations b.Property("TriviaCurrencyReward"); - b.Property("XpMinutesTimeout"); + b.Property("XpMinutesTimeout") + .ValueGeneratedOnAdd() + .HasDefaultValue(5); - b.Property("XpPerMessage"); + b.Property("XpPerMessage") + .ValueGeneratedOnAdd() + .HasDefaultValue(3); b.HasKey("Id"); @@ -243,6 +247,63 @@ namespace NadekoBot.Migrations b.ToTable("ClashOfClans"); }); + modelBuilder.Entity("NadekoBot.Services.Database.Models.ClubApplicants", b => + { + b.Property("ClubId"); + + b.Property("UserId"); + + b.HasKey("ClubId", "UserId"); + + b.HasIndex("UserId"); + + b.ToTable("ClubApplicants"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.ClubBans", b => + { + b.Property("ClubId"); + + b.Property("UserId"); + + b.HasKey("ClubId", "UserId"); + + b.HasIndex("UserId"); + + b.ToTable("ClubBans"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.ClubInfo", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("Discrim"); + + b.Property("ImageUrl"); + + b.Property("MinimumLevelReq"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(20); + + b.Property("OwnerId"); + + b.Property("Xp"); + + b.HasKey("Id"); + + b.HasAlternateKey("Name", "Discrim"); + + b.HasIndex("OwnerId") + .IsUnique(); + + b.ToTable("Clubs"); + }); + modelBuilder.Entity("NadekoBot.Services.Database.Models.CommandAlias", b => { b.Property("Id") @@ -396,10 +457,18 @@ namespace NadekoBot.Migrations b.Property("AvatarId"); + b.Property("ClubId"); + b.Property("DateAdded"); b.Property("Discriminator"); + b.Property("LastLevelUp") + .ValueGeneratedOnAdd() + .HasDefaultValue(new DateTime(2017, 9, 9, 1, 7, 29, 857, DateTimeKind.Local)); + + b.Property("NotifyOnLevelUp"); + b.Property("UserId"); b.Property("Username"); @@ -408,6 +477,8 @@ namespace NadekoBot.Migrations b.HasAlternateKey("UserId"); + b.HasIndex("ClubId"); + b.ToTable("DiscordUser"); }); @@ -1282,11 +1353,17 @@ namespace NadekoBot.Migrations b.Property("Id") .ValueGeneratedOnAdd(); + b.Property("AwardedXp"); + b.Property("DateAdded"); b.Property("GuildId"); - b.Property("NotifyOnLevelUp"); + b.Property("LastLevelUp") + .ValueGeneratedOnAdd() + .HasDefaultValue(new DateTime(2017, 9, 9, 1, 7, 29, 858, DateTimeKind.Local)); + + b.Property("NotifyOnLevelUp"); b.Property("UserId"); @@ -1456,6 +1533,8 @@ namespace NadekoBot.Migrations b.HasKey("Id"); + b.HasAlternateKey("Level"); + b.HasIndex("XpSettingsId"); b.ToTable("XpRoleReward"); @@ -1533,6 +1612,40 @@ namespace NadekoBot.Migrations .OnDelete(DeleteBehavior.Cascade); }); + modelBuilder.Entity("NadekoBot.Services.Database.Models.ClubApplicants", b => + { + b.HasOne("NadekoBot.Services.Database.Models.ClubInfo", "Club") + .WithMany("Applicants") + .HasForeignKey("ClubId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("NadekoBot.Services.Database.Models.DiscordUser", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.ClubBans", b => + { + b.HasOne("NadekoBot.Services.Database.Models.ClubInfo", "Club") + .WithMany("Bans") + .HasForeignKey("ClubId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("NadekoBot.Services.Database.Models.DiscordUser", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.ClubInfo", b => + { + b.HasOne("NadekoBot.Services.Database.Models.DiscordUser", "Owner") + .WithOne() + .HasForeignKey("NadekoBot.Services.Database.Models.ClubInfo", "OwnerId") + .OnDelete(DeleteBehavior.Cascade); + }); + modelBuilder.Entity("NadekoBot.Services.Database.Models.CommandAlias", b => { b.HasOne("NadekoBot.Services.Database.Models.GuildConfig") @@ -1554,6 +1667,13 @@ namespace NadekoBot.Migrations .HasForeignKey("BotConfigId"); }); + modelBuilder.Entity("NadekoBot.Services.Database.Models.DiscordUser", b => + { + b.HasOne("NadekoBot.Services.Database.Models.ClubInfo", "Club") + .WithMany("Users") + .HasForeignKey("ClubId"); + }); + modelBuilder.Entity("NadekoBot.Services.Database.Models.EightBallResponse", b => { b.HasOne("NadekoBot.Services.Database.Models.BotConfig") diff --git a/src/NadekoBot/Migrations/20170908230730_xp-and-clubs.cs b/src/NadekoBot/Migrations/20170908230730_xp-and-clubs.cs new file mode 100644 index 00000000..aad21131 --- /dev/null +++ b/src/NadekoBot/Migrations/20170908230730_xp-and-clubs.cs @@ -0,0 +1,296 @@ +using System; +using System.Collections.Generic; +using Microsoft.EntityFrameworkCore.Migrations; + +namespace NadekoBot.Migrations +{ + public partial class xpandclubs : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "XpMinutesTimeout", + table: "BotConfig", + nullable: false, + defaultValue: 5) + .Annotation("Sqlite:Autoincrement", true); + + migrationBuilder.AddColumn( + name: "XpPerMessage", + table: "BotConfig", + nullable: false, + defaultValue: 3) + .Annotation("Sqlite:Autoincrement", true); + + migrationBuilder.CreateTable( + name: "Clubs", + columns: table => new + { + Id = table.Column(nullable: false) + .Annotation("Sqlite:Autoincrement", true), + DateAdded = table.Column(nullable: true), + Discrim = table.Column(nullable: false), + ImageUrl = table.Column(nullable: true), + MinimumLevelReq = table.Column(nullable: false), + Name = table.Column(maxLength: 20, nullable: false), + OwnerId = table.Column(nullable: false), + Xp = table.Column(nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Clubs", x => x.Id); + table.UniqueConstraint("AK_Clubs_Name_Discrim", x => new { x.Name, x.Discrim }); + table.ForeignKey( + name: "FK_Clubs_DiscordUser_OwnerId", + column: x => x.OwnerId, + principalTable: "DiscordUser", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.Sql(MigrationQueries.UserClub); + + migrationBuilder.AddColumn( + name: "LastLevelUp", + table: "DiscordUser", + nullable: false, + defaultValue: DateTime.UtcNow); + + migrationBuilder.AddColumn( + name: "NotifyOnLevelUp", + table: "DiscordUser", + nullable: false, + defaultValue: 0); + + migrationBuilder.CreateTable( + name: "UserXpStats", + columns: table => new + { + Id = table.Column(nullable: false) + .Annotation("Sqlite:Autoincrement", true), + AwardedXp = table.Column(nullable: false), + DateAdded = table.Column(nullable: true), + GuildId = table.Column(nullable: false), + LastLevelUp = table.Column(nullable: false, defaultValue: new DateTime(2017, 9, 9, 1, 7, 29, 858, DateTimeKind.Local)), + NotifyOnLevelUp = table.Column(nullable: false), + UserId = table.Column(nullable: false), + Xp = table.Column(nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_UserXpStats", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "XpSettings", + columns: table => new + { + Id = table.Column(nullable: false) + .Annotation("Sqlite:Autoincrement", true), + DateAdded = table.Column(nullable: true), + GuildConfigId = table.Column(nullable: false), + NotifyMessage = table.Column(nullable: true), + ServerExcluded = table.Column(nullable: false), + XpRoleRewardExclusive = table.Column(nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_XpSettings", x => x.Id); + table.ForeignKey( + name: "FK_XpSettings_GuildConfigs_GuildConfigId", + column: x => x.GuildConfigId, + principalTable: "GuildConfigs", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "ClubApplicants", + columns: table => new + { + ClubId = table.Column(nullable: false), + UserId = table.Column(nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_ClubApplicants", x => new { x.ClubId, x.UserId }); + table.ForeignKey( + name: "FK_ClubApplicants_Clubs_ClubId", + column: x => x.ClubId, + principalTable: "Clubs", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_ClubApplicants_DiscordUser_UserId", + column: x => x.UserId, + principalTable: "DiscordUser", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "ClubBans", + columns: table => new + { + ClubId = table.Column(nullable: false), + UserId = table.Column(nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_ClubBans", x => new { x.ClubId, x.UserId }); + table.ForeignKey( + name: "FK_ClubBans_Clubs_ClubId", + column: x => x.ClubId, + principalTable: "Clubs", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_ClubBans_DiscordUser_UserId", + column: x => x.UserId, + principalTable: "DiscordUser", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "ExcludedItem", + columns: table => new + { + Id = table.Column(nullable: false) + .Annotation("Sqlite:Autoincrement", true), + DateAdded = table.Column(nullable: true), + ItemId = table.Column(nullable: false), + ItemType = table.Column(nullable: false), + XpSettingsId = table.Column(nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_ExcludedItem", x => x.Id); + table.ForeignKey( + name: "FK_ExcludedItem_XpSettings_XpSettingsId", + column: x => x.XpSettingsId, + principalTable: "XpSettings", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + }); + + migrationBuilder.CreateTable( + name: "XpRoleReward", + columns: table => new + { + Id = table.Column(nullable: false) + .Annotation("Sqlite:Autoincrement", true), + DateAdded = table.Column(nullable: true), + Level = table.Column(nullable: false), + RoleId = table.Column(nullable: false), + XpSettingsId = table.Column(nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_XpRoleReward", x => x.Id); + table.UniqueConstraint("AK_XpRoleReward_Level", x => x.Level); + table.ForeignKey( + name: "FK_XpRoleReward_XpSettings_XpSettingsId", + column: x => x.XpSettingsId, + principalTable: "XpSettings", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + }); + + migrationBuilder.CreateIndex( + name: "IX_DiscordUser_ClubId", + table: "DiscordUser", + column: "ClubId"); + + migrationBuilder.CreateIndex( + name: "IX_ClubApplicants_UserId", + table: "ClubApplicants", + column: "UserId"); + + migrationBuilder.CreateIndex( + name: "IX_ClubBans_UserId", + table: "ClubBans", + column: "UserId"); + + migrationBuilder.CreateIndex( + name: "IX_Clubs_OwnerId", + table: "Clubs", + column: "OwnerId", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_ExcludedItem_XpSettingsId", + table: "ExcludedItem", + column: "XpSettingsId"); + + migrationBuilder.CreateIndex( + name: "IX_UserXpStats_UserId_GuildId", + table: "UserXpStats", + columns: new[] { "UserId", "GuildId" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_XpRoleReward_XpSettingsId", + table: "XpRoleReward", + column: "XpSettingsId"); + + migrationBuilder.CreateIndex( + name: "IX_XpSettings_GuildConfigId", + table: "XpSettings", + column: "GuildConfigId", + unique: true); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_DiscordUser_Clubs_ClubId", + table: "DiscordUser"); + + migrationBuilder.DropTable( + name: "ClubApplicants"); + + migrationBuilder.DropTable( + name: "ClubBans"); + + migrationBuilder.DropTable( + name: "ExcludedItem"); + + migrationBuilder.DropTable( + name: "UserXpStats"); + + migrationBuilder.DropTable( + name: "XpRoleReward"); + + migrationBuilder.DropTable( + name: "Clubs"); + + migrationBuilder.DropTable( + name: "XpSettings"); + + migrationBuilder.DropIndex( + name: "IX_DiscordUser_ClubId", + table: "DiscordUser"); + + migrationBuilder.DropColumn( + name: "ClubId", + table: "DiscordUser"); + + migrationBuilder.DropColumn( + name: "LastLevelUp", + table: "DiscordUser"); + + migrationBuilder.DropColumn( + name: "NotifyOnLevelUp", + table: "DiscordUser"); + + migrationBuilder.DropColumn( + name: "XpMinutesTimeout", + table: "BotConfig"); + + migrationBuilder.DropColumn( + name: "XpPerMessage", + table: "BotConfig"); + } + } +} diff --git a/src/NadekoBot/Migrations/MigrationQueries.cs b/src/NadekoBot/Migrations/MigrationQueries.cs new file mode 100644 index 00000000..201f0c34 --- /dev/null +++ b/src/NadekoBot/Migrations/MigrationQueries.cs @@ -0,0 +1,38 @@ +namespace NadekoBot.Migrations +{ + internal class MigrationQueries + { + public static string UserClub { get; } = @" +CREATE TABLE DiscordUser_tmp( + Id INTEGER PRIMARY KEY, + AvatarId TEXT, + Discriminator TEXT, + UserId INTEGER UNIQUE NOT NULL, + DateAdded TEXT, + Username TEXT +); + +INSERT INTO DiscordUser_tmp + SELECT Id, AvatarId, Discriminator, UserId, DateAdded, Username + FROM DiscordUser; + +DROP TABLE DiscordUser; + +CREATE TABLE DiscordUser( + Id INTEGER PRIMARY KEY, + AvatarId TEXT, + Discriminator TEXT, + UserId INTEGER UNIQUE NOT NULL, + DateAdded TEXT, + Username TEXT, + ClubId INTEGER, + CONSTRAINT FK_DiscordUser_Clubs_ClubId FOREIGN KEY(ClubId) REFERENCES Clubs(Id) ON DELETE RESTRICT +); + +INSERT INTO DiscordUser + SELECT Id, AvatarId, Discriminator, UserId, DateAdded, Username, NULL + FROM DiscordUser_tmp; + +DROP TABLE DiscordUser_tmp;"; + } +} \ No newline at end of file diff --git a/src/NadekoBot/Migrations/NadekoSqliteContextModelSnapshot.cs b/src/NadekoBot/Migrations/NadekoSqliteContextModelSnapshot.cs index 3ebfeafa..da931d6b 100644 --- a/src/NadekoBot/Migrations/NadekoSqliteContextModelSnapshot.cs +++ b/src/NadekoBot/Migrations/NadekoSqliteContextModelSnapshot.cs @@ -183,9 +183,13 @@ namespace NadekoBot.Migrations b.Property("TriviaCurrencyReward"); - b.Property("XpMinutesTimeout"); + b.Property("XpMinutesTimeout") + .ValueGeneratedOnAdd() + .HasDefaultValue(5); - b.Property("XpPerMessage"); + b.Property("XpPerMessage") + .ValueGeneratedOnAdd() + .HasDefaultValue(3); b.HasKey("Id"); @@ -242,6 +246,63 @@ namespace NadekoBot.Migrations b.ToTable("ClashOfClans"); }); + modelBuilder.Entity("NadekoBot.Services.Database.Models.ClubApplicants", b => + { + b.Property("ClubId"); + + b.Property("UserId"); + + b.HasKey("ClubId", "UserId"); + + b.HasIndex("UserId"); + + b.ToTable("ClubApplicants"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.ClubBans", b => + { + b.Property("ClubId"); + + b.Property("UserId"); + + b.HasKey("ClubId", "UserId"); + + b.HasIndex("UserId"); + + b.ToTable("ClubBans"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.ClubInfo", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("Discrim"); + + b.Property("ImageUrl"); + + b.Property("MinimumLevelReq"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(20); + + b.Property("OwnerId"); + + b.Property("Xp"); + + b.HasKey("Id"); + + b.HasAlternateKey("Name", "Discrim"); + + b.HasIndex("OwnerId") + .IsUnique(); + + b.ToTable("Clubs"); + }); + modelBuilder.Entity("NadekoBot.Services.Database.Models.CommandAlias", b => { b.Property("Id") @@ -395,10 +456,18 @@ namespace NadekoBot.Migrations b.Property("AvatarId"); + b.Property("ClubId"); + b.Property("DateAdded"); b.Property("Discriminator"); + b.Property("LastLevelUp") + .ValueGeneratedOnAdd() + .HasDefaultValue(new DateTime(2017, 9, 9, 1, 7, 29, 857, DateTimeKind.Local)); + + b.Property("NotifyOnLevelUp"); + b.Property("UserId"); b.Property("Username"); @@ -407,6 +476,8 @@ namespace NadekoBot.Migrations b.HasAlternateKey("UserId"); + b.HasIndex("ClubId"); + b.ToTable("DiscordUser"); }); @@ -1281,11 +1352,17 @@ namespace NadekoBot.Migrations b.Property("Id") .ValueGeneratedOnAdd(); + b.Property("AwardedXp"); + b.Property("DateAdded"); b.Property("GuildId"); - b.Property("NotifyOnLevelUp"); + b.Property("LastLevelUp") + .ValueGeneratedOnAdd() + .HasDefaultValue(new DateTime(2017, 9, 9, 1, 7, 29, 858, DateTimeKind.Local)); + + b.Property("NotifyOnLevelUp"); b.Property("UserId"); @@ -1455,6 +1532,8 @@ namespace NadekoBot.Migrations b.HasKey("Id"); + b.HasAlternateKey("Level"); + b.HasIndex("XpSettingsId"); b.ToTable("XpRoleReward"); @@ -1532,6 +1611,40 @@ namespace NadekoBot.Migrations .OnDelete(DeleteBehavior.Cascade); }); + modelBuilder.Entity("NadekoBot.Services.Database.Models.ClubApplicants", b => + { + b.HasOne("NadekoBot.Services.Database.Models.ClubInfo", "Club") + .WithMany("Applicants") + .HasForeignKey("ClubId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("NadekoBot.Services.Database.Models.DiscordUser", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.ClubBans", b => + { + b.HasOne("NadekoBot.Services.Database.Models.ClubInfo", "Club") + .WithMany("Bans") + .HasForeignKey("ClubId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("NadekoBot.Services.Database.Models.DiscordUser", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.ClubInfo", b => + { + b.HasOne("NadekoBot.Services.Database.Models.DiscordUser", "Owner") + .WithOne() + .HasForeignKey("NadekoBot.Services.Database.Models.ClubInfo", "OwnerId") + .OnDelete(DeleteBehavior.Cascade); + }); + modelBuilder.Entity("NadekoBot.Services.Database.Models.CommandAlias", b => { b.HasOne("NadekoBot.Services.Database.Models.GuildConfig") @@ -1553,6 +1666,13 @@ namespace NadekoBot.Migrations .HasForeignKey("BotConfigId"); }); + modelBuilder.Entity("NadekoBot.Services.Database.Models.DiscordUser", b => + { + b.HasOne("NadekoBot.Services.Database.Models.ClubInfo", "Club") + .WithMany("Users") + .HasForeignKey("ClubId"); + }); + modelBuilder.Entity("NadekoBot.Services.Database.Models.EightBallResponse", b => { b.HasOne("NadekoBot.Services.Database.Models.BotConfig") diff --git a/src/NadekoBot/Modules/Administration/MigrationCommands.cs b/src/NadekoBot/Modules/Administration/MigrationCommands.cs index 1228690a..5c9b4a70 100644 --- a/src/NadekoBot/Modules/Administration/MigrationCommands.cs +++ b/src/NadekoBot/Modules/Administration/MigrationCommands.cs @@ -352,54 +352,6 @@ namespace NadekoBot.Modules.Administration oldConfig.RotatingStatuses.ForEach(i => messages.Add(new PlayingStatus { Status = i })); botConfig.RotatingStatusMessages = messages; - //Prefix - botConfig.ModulePrefixes.Clear(); - botConfig.ModulePrefixes.AddRange(new HashSet - { - new ModulePrefix() - { - ModuleName = "Administration", - Prefix = oldConfig.CommandPrefixes.Administration - }, - new ModulePrefix() - { - ModuleName = "Searches", - Prefix = oldConfig.CommandPrefixes.Searches - }, - new ModulePrefix() {ModuleName = "NSFW", Prefix = oldConfig.CommandPrefixes.NSFW}, - new ModulePrefix() - { - ModuleName = "Conversations", - Prefix = oldConfig.CommandPrefixes.Conversations - }, - new ModulePrefix() - { - ModuleName = "ClashOfClans", - Prefix = oldConfig.CommandPrefixes.ClashOfClans - }, - new ModulePrefix() {ModuleName = "Help", Prefix = oldConfig.CommandPrefixes.Help}, - new ModulePrefix() {ModuleName = "Music", Prefix = oldConfig.CommandPrefixes.Music}, - new ModulePrefix() {ModuleName = "Trello", Prefix = oldConfig.CommandPrefixes.Trello}, - new ModulePrefix() {ModuleName = "Games", Prefix = oldConfig.CommandPrefixes.Games}, - new ModulePrefix() - { - ModuleName = "Gambling", - Prefix = oldConfig.CommandPrefixes.Gambling - }, - new ModulePrefix() - { - ModuleName = "Permissions", - Prefix = oldConfig.CommandPrefixes.Permissions - }, - new ModulePrefix() - { - ModuleName = "Programming", - Prefix = oldConfig.CommandPrefixes.Programming - }, - new ModulePrefix() {ModuleName = "Pokemon", Prefix = oldConfig.CommandPrefixes.Pokemon}, - new ModulePrefix() {ModuleName = "Utility", Prefix = oldConfig.CommandPrefixes.Utility} - }); - //Blacklist var blacklist = new HashSet(oldConfig.ServerBlacklist.Select(server => new BlacklistItem() { ItemId = server, Type = BlacklistType.Server })); blacklist.AddRange(oldConfig.ChannelBlacklist.Select(channel => new BlacklistItem() { ItemId = channel, Type = BlacklistType.Channel })); diff --git a/src/NadekoBot/Modules/CustomReactions/CustomReactions.cs b/src/NadekoBot/Modules/CustomReactions/CustomReactions.cs index b466b158..9867dbcb 100644 --- a/src/NadekoBot/Modules/CustomReactions/CustomReactions.cs +++ b/src/NadekoBot/Modules/CustomReactions/CustomReactions.cs @@ -153,7 +153,7 @@ namespace NadekoBot.Modules.CustomReactions if (Context.Guild == null) // its a private one, just send back await Context.Channel.SendFileAsync(txtStream, "customreactions.txt", GetText("list_all")).ConfigureAwait(false); else - await ((IGuildUser)Context.User).SendFileAsync(txtStream, "customreactions.txt", GetText("list_all")).ConfigureAwait(false); + await ((IGuildUser)Context.User).SendFileAsync(txtStream, "customreactions.txt", GetText("list_all"), false).ConfigureAwait(false); } [NadekoCommand, Usage, Description, Aliases] diff --git a/src/NadekoBot/Modules/Gambling/WaifuClaimCommands.cs b/src/NadekoBot/Modules/Gambling/WaifuClaimCommands.cs index 3019155e..5d46f976 100644 --- a/src/NadekoBot/Modules/Gambling/WaifuClaimCommands.cs +++ b/src/NadekoBot/Modules/Gambling/WaifuClaimCommands.cs @@ -48,8 +48,8 @@ namespace NadekoBot.Modules.Gambling [Group] public class WaifuClaimCommands : NadekoSubmodule { - private static ConcurrentDictionary divorceCooldowns { get; } = new ConcurrentDictionary(); - private static ConcurrentDictionary affinityCooldowns { get; } = new ConcurrentDictionary(); + private static ConcurrentDictionary _divorceCooldowns { get; } = new ConcurrentDictionary(); + private static ConcurrentDictionary _affinityCooldowns { get; } = new ConcurrentDictionary(); enum WaifuClaimResult { @@ -219,7 +219,7 @@ namespace NadekoBot.Modules.Gambling var now = DateTime.UtcNow; if (w?.Claimer == null || w.Claimer.UserId != Context.User.Id) result = DivorceResult.NotYourWife; - else if (divorceCooldowns.AddOrUpdate(Context.User.Id, + else if (_divorceCooldowns.AddOrUpdate(Context.User.Id, now, (key, old) => ((difference = now.Subtract(old)) > _divorceLimit) ? now : old) != now) { @@ -303,7 +303,7 @@ namespace NadekoBot.Modules.Gambling if (w?.Affinity?.UserId == u?.Id) { } - else if (affinityCooldowns.AddOrUpdate(Context.User.Id, + else if (_affinityCooldowns.AddOrUpdate(Context.User.Id, now, (key, old) => ((difference = now.Subtract(old)) > _affinityLimit) ? now : old) != now) { @@ -459,7 +459,7 @@ namespace NadekoBot.Modules.Gambling .AddField(efb => efb.WithName(GetText("likes")).WithValue(w.Affinity?.ToString() ?? nobody).WithIsInline(true)) .AddField(efb => efb.WithName(GetText("changes_of_heart")).WithValue($"{affInfo.Count} - \"the {affInfo.Title}\"").WithIsInline(true)) .AddField(efb => efb.WithName(GetText("divorces")).WithValue(divorces.ToString()).WithIsInline(true)) - .AddField(efb => efb.WithName(GetText("gifts")).WithValue(!w.Items.Any() ? "-" : string.Join("\n", w.Items.OrderBy(x => x.Price).GroupBy(x => x.ItemEmoji).Select(x => $"{x.Key} x{x.Count()}"))).WithIsInline(true)) + .AddField(efb => efb.WithName(GetText("gifts")).WithValue(!w.Items.Any() ? "-" : string.Join("\n", w.Items.OrderBy(x => x.Price).GroupBy(x => x.ItemEmoji).Select(x => $"{x.Key} x{x.Count()}"))).WithIsInline(false)) .AddField(efb => efb.WithName($"Waifus ({claims.Count})").WithValue(claims.Count == 0 ? nobody : string.Join("\n", claims.OrderBy(x => rng.Next()).Take(30).Select(x => x.Waifu))).WithIsInline(false)); await Context.Channel.EmbedAsync(embed).ConfigureAwait(false); diff --git a/src/NadekoBot/Modules/Games/Games.cs b/src/NadekoBot/Modules/Games/Games.cs index b1db0acd..811a1a1f 100644 --- a/src/NadekoBot/Modules/Games/Games.cs +++ b/src/NadekoBot/Modules/Games/Games.cs @@ -11,7 +11,7 @@ using NadekoBot.Modules.Games.Services; namespace NadekoBot.Modules.Games { - /*todo more games + /* more games - Blackjack - Shiritori - Simple RPG adventure diff --git a/src/NadekoBot/Modules/NSFW/NSFW.cs b/src/NadekoBot/Modules/NSFW/NSFW.cs index 81daf9c6..522713dd 100644 --- a/src/NadekoBot/Modules/NSFW/NSFW.cs +++ b/src/NadekoBot/Modules/NSFW/NSFW.cs @@ -15,6 +15,7 @@ using NadekoBot.Modules.Searches.Common; using NadekoBot.Modules.Searches.Services; using NadekoBot.Modules.NSFW.Exceptions; +//todo static httpclient namespace NadekoBot.Modules.NSFW { public class NSFW : NadekoTopLevelModule diff --git a/src/NadekoBot/Modules/Searches/LoLCommands.cs b/src/NadekoBot/Modules/Searches/LoLCommands.cs index 78b6928c..39c86636 100644 --- a/src/NadekoBot/Modules/Searches/LoLCommands.cs +++ b/src/NadekoBot/Modules/Searches/LoLCommands.cs @@ -10,7 +10,6 @@ using System.Threading.Tasks; using NadekoBot.Common; using NadekoBot.Common.Attributes; -//todo 50 drawing namespace NadekoBot.Modules.Searches { public partial class Searches @@ -66,320 +65,4 @@ namespace NadekoBot.Modules.Searches } } } -} - -// private class CachedChampion -// { -// public System.IO.Stream ImageStream { get; set; } -// public DateTime AddedAt { get; set; } -// public string Name { get; set; } -// } - -// -// private static Dictionary CachedChampionImages = new Dictionary(); - -// private System.Timers.Timer clearTimer { get; } = new System.Timers.Timer(); -// public LoLCommands(DiscordModule module) : base(module) -// { -// clearTimer.Interval = new TimeSpan(0, 10, 0).TotalMilliseconds; -// clearTimer.Start(); -// clearTimer.Elapsed += (s, e) => -// { -// try -// { -// CachedChampionImages = CachedChampionImages -// .Where(kvp => DateTime.UtcNow - kvp.Value.AddedAt > new TimeSpan(1, 0, 0)) -// .ToDictionary(kvp => kvp.Key, kvp => kvp.Value); -// } -// catch { } -// }; -// } - -// public Func DoFunc() -// { -// throw new NotImplementedException(); -// } - -// private class MatchupModel -// { -// public int Games { get; set; } -// public float WinRate { get; set; } -// [Newtonsoft.Json.JsonProperty("key")] -// public string Name { get; set; } -// public float StatScore { get; set; } -// } - -// public override void Init(CommandGroupBuilder cgb) -// { -// cgb.CreateCommand(Module.Name + "lolchamp") -// .Description($"Shows League Of Legends champion statistics. If there are spaces/apostrophes or in the name - omit them. Optional second parameter is a role. |`{Prefix}lolchamp Riven` or `{Prefix}lolchamp Annie sup`") -// .Parameter("champ", ParameterType.Required) -// .Parameter("position", ParameterType.Unparsed) -// .Do(async e => -// { -// try -// { -// //get role -// var role = ResolvePos(position); -// var resolvedRole = role; -// var name = champ.Replace(" ", "").ToLower(); -// CachedChampion champ = null; - -// if (CachedChampionImages.TryGetValue(name + "_" + resolvedRole, out champ)) -// if (champ != null) -// { -// champ.ImageStream.Position = 0; -// await e.Channel.SendFile("champ.png", champ.ImageStream).ConfigureAwait(false); -// return; -// } -// var allData = JArray.Parse(await Classes.http.GetStringAsync($"http://api.champion.gg/champion/{name}?api_key={_creds.LOLAPIKey}").ConfigureAwait(false)); -// JToken data = null; -// if (role != null) -// { -// for (var i = 0; i < allData.Count; i++) -// { -// if (allData[i]["role"].ToString().Equals(role)) -// { -// data = allData[i]; -// break; -// } -// } -// if (data == null) -// { -// await channel.SendMessageAsync("💢 Data for that role does not exist.").ConfigureAwait(false); -// return; -// } -// } -// else -// { -// data = allData[0]; -// role = allData[0]["role"].ToString(); -// resolvedRole = ResolvePos(role); -// } -// if (CachedChampionImages.TryGetValue(name + "_" + resolvedRole, out champ)) -// if (champ != null) -// { -// champ.ImageStream.Position = 0; -// await e.Channel.SendFile("champ.png", champ.ImageStream).ConfigureAwait(false); -// return; -// } -// //name = data["title"].ToString(); -// // get all possible roles, and "select" the shown one -// var roles = new string[allData.Count]; -// for (var i = 0; i < allData.Count; i++) -// { -// roles[i] = allData[i]["role"].ToString(); -// if (roles[i] == role) -// roles[i] = ">" + roles[i] + "<"; -// } -// var general = JArray.Parse(await http.GetStringAsync($"http://api.champion.gg/stats/" + -// $"champs/{name}?api_key={_creds.LOLAPIKey}") -// .ConfigureAwait(false)) -// .FirstOrDefault(jt => jt["role"].ToString() == role)?["general"]; -// if (general == null) -// { -// return; -// } -// //get build data for this role -// var buildData = data["items"]["mostGames"]["items"]; -// var items = new string[6]; -// for (var i = 0; i < 6; i++) -// { -// items[i] = buildData[i]["id"].ToString(); -// } - -// //get matchup data to show counters and countered champions -// var matchupDataIE = data["matchups"].ToObject>(); - -// var matchupData = matchupDataIE.OrderBy(m => m.StatScore).ToArray(); - -// var countered = new[] { matchupData[0].Name, matchupData[1].Name, matchupData[2].Name }; -// var counters = new[] { matchupData[matchupData.Length - 1].Name, matchupData[matchupData.Length - 2].Name, matchupData[matchupData.Length - 3].Name }; - -// //get runes data -// var runesJArray = data["runes"]["mostGames"]["runes"] as JArray; -// var runes = string.Join("\n", runesJArray.OrderBy(jt => int.Parse(jt["number"].ToString())).Select(jt => jt["number"].ToString() + "x" + jt["name"])); - -// // get masteries data - -// var masteries = (data["masteries"]["mostGames"]["masteries"] as JArray); - -// //get skill order data - -// var orderArr = (data["skills"]["mostGames"]["order"] as JArray); - -// var img = Image.FromFile("data/lol/bg.png"); -// using (var g = Graphics.FromImage(img)) -// { -// g.CompositingQuality = System.Drawing.Drawing2D.CompositingQuality.HighQuality; -// //g.CompositingMode = System.Drawing.Drawing2D.CompositingMode.SourceCopy; -// const int margin = 5; -// const int imageSize = 75; -// var normalFont = new Font("Monaco", 8, FontStyle.Regular); -// var smallFont = new Font("Monaco", 7, FontStyle.Regular); -// //draw champ image -// var champName = data["key"].ToString().Replace(" ", ""); - -// g.DrawImage(GetImage(champName), new Rectangle(margin, margin, imageSize, imageSize)); -// //draw champ name -// if (champName == "MonkeyKing") -// champName = "Wukong"; -// g.DrawString($"{champName}", new Font("Times New Roman", 24, FontStyle.Regular), Brushes.WhiteSmoke, margin + imageSize + margin, margin); -// //draw champ surname - -// //draw skill order -// if (orderArr.Count != 0) -// { -// float orderFormula = 120 / orderArr.Count; -// const float orderVerticalSpacing = 10; -// for (var i = 0; i < orderArr.Count; i++) -// { -// var orderX = margin + margin + imageSize + orderFormula * i + i; -// float orderY = margin + 35; -// var spellName = orderArr[i].ToString().ToLowerInvariant(); - -// switch (spellName) -// { -// case "w": -// orderY += orderVerticalSpacing; -// break; -// case "e": -// orderY += orderVerticalSpacing * 2; -// break; -// case "r": -// orderY += orderVerticalSpacing * 3; -// break; -// default: -// break; -// } - -// g.DrawString(spellName.ToUpperInvariant(), new Font("Monaco", 7), Brushes.LimeGreen, orderX, orderY); -// } -// } -// //draw roles -// g.DrawString("Roles: " + string.Join(", ", roles), normalFont, Brushes.WhiteSmoke, margin, margin + imageSize + margin); - -// //draw average stats -// g.DrawString( -//$@" Average Stats - -//Kills: {general["kills"]} CS: {general["minionsKilled"]} -//Deaths: {general["deaths"]} Win: {general["winPercent"]}% -//Assists: {general["assists"]} Ban: {general["banRate"]}% -//", normalFont, Brushes.WhiteSmoke, img.Width - 150, margin); -// //draw masteries -// g.DrawString($"Masteries: {string.Join(" / ", masteries?.Select(jt => jt["total"]))}", normalFont, Brushes.WhiteSmoke, margin, margin + imageSize + margin + 20); -// //draw runes -// g.DrawString($"{runes}", smallFont, Brushes.WhiteSmoke, margin, margin + imageSize + margin + 40); -// //draw counters -// g.DrawString($"Best against", smallFont, Brushes.WhiteSmoke, margin, img.Height - imageSize + margin); -// var smallImgSize = 50; - -// for (var i = 0; i < counters.Length; i++) -// { -// g.DrawImage(GetImage(counters[i]), -// new Rectangle(i * (smallImgSize + margin) + margin, img.Height - smallImgSize - margin, -// smallImgSize, -// smallImgSize)); -// } -// //draw countered by -// g.DrawString($"Worst against", smallFont, Brushes.WhiteSmoke, img.Width - 3 * (smallImgSize + margin), img.Height - imageSize + margin); - -// for (var i = 0; i < countered.Length; i++) -// { -// var j = countered.Length - i; -// g.DrawImage(GetImage(countered[i]), -// new Rectangle(img.Width - (j * (smallImgSize + margin) + margin), img.Height - smallImgSize - margin, -// smallImgSize, -// smallImgSize)); -// } -// //draw item build -// g.DrawString("Popular build", normalFont, Brushes.WhiteSmoke, img.Width - (3 * (smallImgSize + margin) + margin), 77); - -// for (var i = 0; i < 6; i++) -// { -// var inverseI = 5 - i; -// var j = inverseI % 3 + 1; -// var k = inverseI / 3; -// g.DrawImage(GetImage(items[i], GetImageType.Item), -// new Rectangle(img.Width - (j * (smallImgSize + margin) + margin), 92 + k * (smallImgSize + margin), -// smallImgSize, -// smallImgSize)); -// } -// } -// var cachedChamp = new CachedChampion { AddedAt = DateTime.UtcNow, ImageStream = img.ToStream(System.Drawing.Imaging.ImageFormat.Png), Name = name.ToLower() + "_" + resolvedRole }; -// CachedChampionImages.Add(cachedChamp.Name, cachedChamp); -// await e.Channel.SendFile(data["title"] + "_stats.png", cachedChamp.ImageStream).ConfigureAwait(false); -// } -// catch (Exception ex) -// { -// await channel.SendMessageAsync("💢 Failed retreiving data for that champion.").ConfigureAwait(false); -// } -// }); -// } - -// private enum GetImageType -// { -// Champion, -// Item -// } -// private static Image GetImage(string id, GetImageType imageType = GetImageType.Champion) -// { -// try -// { -// switch (imageType) -// { -// case GetImageType.Champion: -// return Image.FromFile($"data/lol/champions/{id}.png"); -// case GetImageType.Item: -// default: -// return Image.FromFile($"data/lol/items/{id}.png"); -// } -// } -// catch (Exception) -// { -// return Image.FromFile("data/lol/_ERROR.png"); -// } -// } - -// private static string ResolvePos(string pos) -// { -// if (string.IsNullOrWhiteSpace(pos)) -// return null; -// switch (pos.ToLowerInvariant()) -// { -// case "m": -// case "mid": -// case "midorfeed": -// case "midd": -// case "middle": -// return "Middle"; -// case "top": -// case "topp": -// case "t": -// case "toporfeed": -// return "Top"; -// case "j": -// case "jun": -// case "jungl": -// case "jungle": -// return "Jungle"; -// case "a": -// case "ad": -// case "adc": -// case "carry": -// case "ad carry": -// case "adcarry": -// case "c": -// return "ADC"; -// case "s": -// case "sup": -// case "supp": -// case "support": -// return "Support"; -// default: -// return pos; -// } -// } -// } -//} +} \ No newline at end of file diff --git a/src/NadekoBot/Modules/Searches/TranslatorCommands.cs b/src/NadekoBot/Modules/Searches/TranslatorCommands.cs index 5084e2f3..9f4960fe 100644 --- a/src/NadekoBot/Modules/Searches/TranslatorCommands.cs +++ b/src/NadekoBot/Modules/Searches/TranslatorCommands.cs @@ -38,6 +38,27 @@ namespace NadekoBot.Modules.Searches } } + //[NadekoCommand, Usage, Description, Aliases] + //[OwnerOnly] + //public async Task Obfuscate([Remainder] string txt) + //{ + // var lastItem = "en"; + // foreach (var item in _google.Languages.Except(new[] { "en" }).Where(x => x.Length < 4)) + // { + // var txt2 = await _searches.Translate(lastItem + ">" + item, txt); + // await Context.Channel.EmbedAsync(new EmbedBuilder() + // .WithOkColor() + // .WithTitle(lastItem + ">" + item) + // .AddField("Input", txt) + // .AddField("Output", txt2)); + // txt = txt2; + // await Task.Delay(500); + // lastItem = item; + // } + // txt = await _searches.Translate(lastItem + ">en", txt); + // await Context.Channel.SendConfirmAsync("Final output:\n\n" + txt); + //} + public enum AutoDeleteAutoTranslate { Del, diff --git a/src/NadekoBot/Modules/Utility/Services/StreamRoleService.cs b/src/NadekoBot/Modules/Utility/Services/StreamRoleService.cs index 81018a77..98f34eaf 100644 --- a/src/NadekoBot/Modules/Utility/Services/StreamRoleService.cs +++ b/src/NadekoBot/Modules/Utility/Services/StreamRoleService.cs @@ -207,7 +207,7 @@ namespace NadekoBot.Modules.Utility.Services if (guildSettings.TryRemove(guild.Id, out var setting) && cleanup) await RescanUsers(guild).ConfigureAwait(false); } - //todo multiple rescans at the same time? + private async Task RescanUser(IGuildUser user, StreamRoleSettings setting, IRole addRole = null) { if (user.Game.HasValue && diff --git a/src/NadekoBot/Modules/Utility/Utility.cs b/src/NadekoBot/Modules/Utility/Utility.cs index 5ee6de70..d3f0e112 100644 --- a/src/NadekoBot/Modules/Utility/Utility.cs +++ b/src/NadekoBot/Modules/Utility/Utility.cs @@ -417,7 +417,7 @@ namespace NadekoBot.Modules.Utility }) }); await Context.User.SendFileAsync( - await JsonConvert.SerializeObject(grouping, Formatting.Indented).ToStream().ConfigureAwait(false), title, title).ConfigureAwait(false); + await JsonConvert.SerializeObject(grouping, Formatting.Indented).ToStream().ConfigureAwait(false), title, title, false).ConfigureAwait(false); } [NadekoCommand, Usage, Description, Aliases] public async Task Ping() diff --git a/src/NadekoBot/Modules/Xp/Club.cs b/src/NadekoBot/Modules/Xp/Club.cs new file mode 100644 index 00000000..a911ab4f --- /dev/null +++ b/src/NadekoBot/Modules/Xp/Club.cs @@ -0,0 +1,311 @@ +using Discord; +using Discord.Commands; +using Discord.WebSocket; +using NadekoBot.Common.Attributes; +using NadekoBot.Extensions; +using NadekoBot.Modules.Xp.Common; +using NadekoBot.Modules.Xp.Services; +using NadekoBot.Services.Database.Models; +using System; +using System.Linq; +using System.Threading.Tasks; + +namespace NadekoBot.Modules.Xp +{ + public partial class Xp + { + [Group] + public class Club : NadekoSubmodule + { + private readonly XpService _xps; + private readonly DiscordSocketClient _client; + + public Club(XpService xps, DiscordSocketClient client) + { + _xps = xps; + _client = client; + } + + [NadekoCommand, Usage, Description, Aliases] + public async Task ClubCreate([Remainder]string clubName) + { + if (string.IsNullOrWhiteSpace(clubName) || clubName.Length > 20) + return; + + if (!_service.CreateClub(Context.User, clubName, out ClubInfo club)) + { + await ReplyErrorLocalized("club_create_error").ConfigureAwait(false); + return; + } + + await ReplyConfirmLocalized("club_created", Format.Bold(club.ToString())).ConfigureAwait(false); + } + + [NadekoCommand, Usage, Description, Aliases] + public Task ClubIcon([Remainder]string url = null) + { + if ((!Uri.IsWellFormedUriString(url, UriKind.Absolute) && url != null) + || !_service.SetClubIcon(Context.User.Id, url)) + { + return ReplyErrorLocalized("club_icon_error"); + } + + return ReplyConfirmLocalized("club_icon_set"); + } + + [NadekoCommand, Usage, Description, Aliases] + [Priority(1)] + public async Task ClubInformation(IUser user = null) + { + user = user ?? Context.User; + var club = _service.GetClubByMember(user); + if (club == null) + { + await ReplyErrorLocalized("club_not_exists").ConfigureAwait(false); + return; + } + + await ClubInformation(club.ToString()).ConfigureAwait(false); + } + + [NadekoCommand, Usage, Description, Aliases] + [Priority(0)] + public async Task ClubInformation([Remainder]string clubName = null) + { + if (string.IsNullOrWhiteSpace(clubName)) + { + await ClubInformation(Context.User).ConfigureAwait(false); + return; + } + + ClubInfo club; + if (!_service.GetClubByName(clubName, out club)) + { + await ReplyErrorLocalized("club_not_exists").ConfigureAwait(false); + return; + } + + var lvl = new LevelStats(club.Xp); + + await Context.Channel.SendPaginatedConfirmAsync(_client, 0, (page) => + { + var embed = new EmbedBuilder() + .WithOkColor() + .WithTitle($"{club.ToString()}") + .WithDescription(GetText("level_x", lvl.Level) + $" ({club.Xp} xp)") + .AddField("Owner", club.Owner.ToString(), true) + .AddField("Level Req.", club.MinimumLevelReq.ToString(), true) + .AddField("Members", string.Join("\n", club.Users + .Skip(page * 10) + .Take(10)), false); + + if (Uri.IsWellFormedUriString(club.ImageUrl, UriKind.Absolute)) + return embed.WithThumbnailUrl(club.ImageUrl); + + return embed; + }, club.Users.Count / 10); + } + + [NadekoCommand, Usage, Description, Aliases] + public Task ClubBans(int page = 1) + { + if (--page < 0) + return Task.CompletedTask; + + var club = _service.GetBansAndApplications(Context.User.Id); + if (club == null) + return ReplyErrorLocalized("club_not_exists"); + + var bans = club + .Bans + .Select(x => x.User) + .ToArray(); + + return Context.Channel.SendPaginatedConfirmAsync(_client, page, + curPage => + { + var toShow = string.Join("\n", bans + .Skip(page * 10) + .Take(10) + .Select(x => x.ToString())); + + return new EmbedBuilder() + .WithTitle(GetText("club_bans_for", club.ToString())) + .WithDescription(toShow); + + }, bans.Length / 10); + } + + + [NadekoCommand, Usage, Description, Aliases] + public Task ClubApps(int page = 1) + { + if (--page < 0) + return Task.CompletedTask; + + var club = _service.GetBansAndApplications(Context.User.Id); + if (club == null) + return ReplyErrorLocalized("club_not_exists"); + + var bans = club + .Applicants + .Select(x => x.User) + .ToArray(); + + return Context.Channel.SendPaginatedConfirmAsync(_client, page, + curPage => + { + var toShow = string.Join("\n", bans + .Skip(page * 10) + .Take(10) + .Select(x => x.ToString())); + + return new EmbedBuilder() + .WithTitle(GetText("club_apps_for", club.ToString())) + .WithDescription(toShow); + + }, bans.Length / 10); + } + + [NadekoCommand, Usage, Description, Aliases] + public async Task ClubApply([Remainder]string clubName) + { + if (string.IsNullOrWhiteSpace(clubName)) + return; + + if (!_service.GetClubByName(clubName, out ClubInfo club)) + { + await ReplyErrorLocalized("club_not_exists").ConfigureAwait(false); + return; + } + + if (_service.ApplyToClub(Context.User, club)) + { + await ReplyConfirmLocalized("club_applied", Format.Bold(club.ToString())).ConfigureAwait(false); + } + else + { + await ReplyErrorLocalized("club_apply_error").ConfigureAwait(false); + } + } + + [NadekoCommand, Usage, Description, Aliases] + [Priority(1)] + public Task ClubAccept(IUser user) + => ClubAccept(user.ToString()); + + [NadekoCommand, Usage, Description, Aliases] + [Priority(0)] + public async Task ClubAccept([Remainder]string userName) + { + if (_service.AcceptApplication(Context.User.Id, userName, out var discordUser)) + { + await ReplyConfirmLocalized("club_accepted", Format.Bold(discordUser.ToString())).ConfigureAwait(false); + } + else + await ReplyErrorLocalized("club_accept_error").ConfigureAwait(false); + } + + [NadekoCommand, Usage, Description, Aliases] + public async Task Clubleave() + { + if (_service.LeaveClub(Context.User)) + await ReplyConfirmLocalized("club_left").ConfigureAwait(false); + else + await ReplyErrorLocalized("club_not_in_club").ConfigureAwait(false); + } + + [NadekoCommand, Usage, Description, Aliases] + [Priority(1)] + public Task ClubKick([Remainder]IUser user) + => ClubKick(user.ToString()); + + [NadekoCommand, Usage, Description, Aliases] + [Priority(0)] + public Task ClubKick([Remainder]string userName) + { + if (_service.Kick(Context.User.Id, userName, out var club)) + return ReplyConfirmLocalized("club_user_kick", Format.Bold(userName), Format.Bold(club.ToString())); + else + return ReplyErrorLocalized("club_user_kick_fail"); + } + + [NadekoCommand, Usage, Description, Aliases] + [Priority(1)] + public Task ClubBan([Remainder]IUser user) + => ClubBan(user.ToString()); + + [NadekoCommand, Usage, Description, Aliases] + [Priority(0)] + public Task ClubBan([Remainder]string userName) + { + if (_service.Ban(Context.User.Id, userName, out var club)) + return ReplyConfirmLocalized("club_user_banned", Format.Bold(userName), Format.Bold(club.ToString())); + else + return ReplyErrorLocalized("club_user_ban_fail"); + } + + [NadekoCommand, Usage, Description, Aliases] + [Priority(1)] + public Task ClubUnBan([Remainder]IUser user) + => ClubUnBan(user.ToString()); + + [NadekoCommand, Usage, Description, Aliases] + [Priority(0)] + public Task ClubUnBan([Remainder]string userName) + { + if (_service.UnBan(Context.User.Id, userName, out var club)) + return ReplyConfirmLocalized("club_user_unbanned", Format.Bold(userName), Format.Bold(club.ToString())); + else + return ReplyErrorLocalized("club_user_unban_fail"); + } + + [NadekoCommand, Usage, Description, Aliases] + public async Task ClubLevelReq(int level) + { + if (_service.ChangeClubLevelReq(Context.User.Id, level)) + { + await ReplyConfirmLocalized("club_level_req_changed", Format.Bold(level.ToString())).ConfigureAwait(false); + } + else + { + await ReplyErrorLocalized("club_level_req_change_error").ConfigureAwait(false); + } + } + + [NadekoCommand, Usage, Description, Aliases] + public async Task ClubDisband() + { + if (_service.Disband(Context.User.Id, out ClubInfo club)) + { + await ReplyConfirmLocalized("club_disbanded", Format.Bold(club.ToString())).ConfigureAwait(false); + } + else + { + await ReplyErrorLocalized("club_disaband_error").ConfigureAwait(false); + } + } + + [NadekoCommand, Usage, Description, Aliases] + public Task ClubLeaderboard(int page = 1) + { + if (--page < 0) + return Task.CompletedTask; + + var clubs = _service.GetClubLeaderboardPage(page); + + var embed = new EmbedBuilder() + .WithTitle(GetText("club_leaderboard", page + 1)) + .WithOkColor(); + + var i = page * 9; + foreach (var club in clubs) + { + embed.AddField($"#{++i} " + club.ToString(), club.Xp.ToString() + " xp", false); + } + + return Context.Channel.EmbedAsync(embed); + } + } + } +} \ No newline at end of file diff --git a/src/NadekoBot/Modules/Xp/Common/FullUserStats.cs b/src/NadekoBot/Modules/Xp/Common/FullUserStats.cs new file mode 100644 index 00000000..11f38648 --- /dev/null +++ b/src/NadekoBot/Modules/Xp/Common/FullUserStats.cs @@ -0,0 +1,26 @@ +using NadekoBot.Services.Database.Models; + +namespace NadekoBot.Modules.Xp.Common +{ + public class FullUserStats + { + public DiscordUser User { get; } + public UserXpStats FullGuildStats { get; } + public LevelStats Global { get; } + public LevelStats Guild { get; } + public int GlobalRanking { get; } + public int GuildRanking { get; } + + public FullUserStats(DiscordUser usr, + UserXpStats fullGuildStats, LevelStats global, + LevelStats guild, int globalRanking, int guildRanking) + { + this.User = usr; + this.Global = global; + this.Guild = guild; + this.GlobalRanking = globalRanking; + this.GuildRanking = guildRanking; + this.FullGuildStats = fullGuildStats; + } + } +} diff --git a/src/NadekoBot/Modules/Xp/Common/LevelStats.cs b/src/NadekoBot/Modules/Xp/Common/LevelStats.cs new file mode 100644 index 00000000..11dfc8c7 --- /dev/null +++ b/src/NadekoBot/Modules/Xp/Common/LevelStats.cs @@ -0,0 +1,43 @@ +using NadekoBot.Modules.Xp.Services; +using System; + +namespace NadekoBot.Modules.Xp.Common +{ + public class LevelStats + { + public int Level { get; } + public int LevelXp { get; } + public int RequiredXp { get; } + public int TotalXp { get; } + + public LevelStats(int xp) + { + if (xp < 0) + xp = 0; + + TotalXp = xp; + + const int baseXp = XpService.XP_REQUIRED_LVL_1; + + var required = baseXp; + var totalXp = 0; + var lvl = 1; + while (true) + { + required = (int)(baseXp + baseXp / 4.0 * (lvl - 1)); + + if (required + totalXp > xp) + break; + + totalXp += required; + lvl++; + } + + Level = lvl - 1; + LevelXp = xp - totalXp; + RequiredXp = required; + } + + public static LevelStats FromXp(int xp) => new LevelStats(xp); + } +} diff --git a/src/NadekoBot/Modules/Xp/Services/ClubService.cs b/src/NadekoBot/Modules/Xp/Services/ClubService.cs new file mode 100644 index 00000000..f8d08f66 --- /dev/null +++ b/src/NadekoBot/Modules/Xp/Services/ClubService.cs @@ -0,0 +1,312 @@ +using NadekoBot.Services; +using System; +using NadekoBot.Services.Database.Models; +using Discord; +using NadekoBot.Modules.Xp.Common; +using System.Linq; +using Microsoft.EntityFrameworkCore; +using System.Collections.Generic; + +namespace NadekoBot.Modules.Xp.Services +{ + public class ClubService : INService + { + private readonly DbService _db; + + public ClubService(DbService db) + { + _db = db; + } + + public bool CreateClub(IUser user, string clubName, out ClubInfo club) + { + //must be lvl 5 and must not be in a club already + + club = null; + using (var uow = _db.UnitOfWork) + { + var du = uow.DiscordUsers.GetOrCreate(user); + uow._context.SaveChanges(); + var xp = new LevelStats(uow.Xp.GetTotalUserXp(user.Id)); + + if (xp.Level >= 5 && du.Club == null) + { + du.Club = new ClubInfo() + { + Name = clubName, + Discrim = uow.Clubs.GetNextDiscrim(clubName), + Owner = du, + }; + uow.Clubs.Add(du.Club); + uow._context.SaveChanges(); + } + else + return false; + + uow._context.Set() + .RemoveRange(uow._context.Set().Where(x => x.UserId == du.Id)); + club = du.Club; + uow.Complete(); + } + + return true; + } + + public ClubInfo GetClubByMember(IUser user) + { + using (var uow = _db.UnitOfWork) + { + return uow.Clubs.GetByMember(user.Id); + } + } + + public bool SetClubIcon(ulong ownerUserId, string url) + { + using (var uow = _db.UnitOfWork) + { + var club = uow.Clubs.GetByOwner(ownerUserId, set => set); + + if (club == null) + return false; + + club.ImageUrl = url; + uow.Complete(); + } + + return true; + } + + public bool GetClubByName(string clubName, out ClubInfo club) + { + club = null; + var arr = clubName.Split('#'); + if (arr.Length < 2 || !int.TryParse(arr[arr.Length - 1], out var discrim)) + return false; + + //incase club has # in it + var name = string.Concat(arr.Except(new[] { arr[arr.Length - 1] })); + + if (string.IsNullOrWhiteSpace(name)) + return false; + + using (var uow = _db.UnitOfWork) + { + club = uow.Clubs.GetByName(name.Trim().ToLowerInvariant(), discrim); + if (club == null) + return false; + else + return true; + } + } + + public bool ApplyToClub(IUser user, ClubInfo club) + { + using (var uow = _db.UnitOfWork) + { + var du = uow.DiscordUsers.GetOrCreate(user); + uow._context.SaveChanges(); + + if (du.Club != null + || new LevelStats(uow.Xp.GetTotalUserXp(user.Id)).Level < club.MinimumLevelReq + || club.Bans.Any(x => x.UserId == du.Id) + || club.Applicants.Any(x => x.UserId == du.Id)) + { + //user banned or a member of a club, or already applied, + // or doesn't min minumum level requirement, can't apply + return false; + } + + var app = new ClubApplicants + { + ClubId = club.Id, + UserId = du.Id, + }; + + uow._context.Set().Add(app); + + uow.Complete(); + } + return true; + } + + public bool AcceptApplication(ulong clubOwnerUserId, string userName, out DiscordUser discordUser) + { + discordUser = null; + using (var uow = _db.UnitOfWork) + { + var club = uow.Clubs.GetByOwner(clubOwnerUserId, + set => set.Include(x => x.Applicants) + .ThenInclude(x => x.Club) + .Include(x => x.Applicants) + .ThenInclude(x => x.User)); + if (club == null) + return false; + + var applicant = club.Applicants.FirstOrDefault(x => x.User.ToString().ToLowerInvariant() == userName.ToLowerInvariant()); + if (applicant == null) + return false; + + applicant.User.Club = club; + club.Applicants.Remove(applicant); + + //remove that user's all other applications + uow._context.Set() + .RemoveRange(uow._context.Set().Where(x => x.UserId == applicant.User.Id)); + + discordUser = applicant.User; + uow.Complete(); + } + return true; + } + + public ClubInfo GetBansAndApplications(ulong ownerUserId) + { + using (var uow = _db.UnitOfWork) + { + return uow.Clubs.GetByOwner(ownerUserId, + x => x.Include(y => y.Bans) + .ThenInclude(y => y.User) + .Include(y => y.Applicants) + .ThenInclude(y => y.User)); + } + } + + public bool LeaveClub(IUser user) + { + using (var uow = _db.UnitOfWork) + { + var du = uow.DiscordUsers.GetOrCreate(user); + if (du.Club == null || du.Club.OwnerId == du.Id) + return false; + + du.Club = null; + uow.Complete(); + } + return true; + } + + public bool ChangeClubLevelReq(ulong userId, int level) + { + if (level < 5) + return false; + + using (var uow = _db.UnitOfWork) + { + var club = uow.Clubs.GetByOwner(userId); + if (club == null) + return false; + + club.MinimumLevelReq = level; + uow.Complete(); + } + + return true; + } + + public bool Disband(ulong userId, out ClubInfo club) + { + using (var uow = _db.UnitOfWork) + { + club = uow.Clubs.GetByOwner(userId); + if (club == null) + return false; + + uow.Clubs.Remove(club); + uow.Complete(); + } + return true; + } + + public bool Ban(ulong ownerUserId, string userName, out ClubInfo club) + { + using (var uow = _db.UnitOfWork) + { + club = uow.Clubs.GetByOwner(ownerUserId, + set => set.Include(x => x.Applicants) + .ThenInclude(x => x.User)); + if (club == null) + return false; + + var usr = club.Users.FirstOrDefault(x => x.ToString().ToLowerInvariant() == userName.ToLowerInvariant()) + ?? club.Applicants.FirstOrDefault(x => x.User.ToString().ToLowerInvariant() == userName.ToLowerInvariant())?.User; + if (usr == null) + return false; + + if (club.OwnerId == usr.Id) // can't ban the owner kek, whew + return false; + + club.Bans.Add(new ClubBans + { + Club = club, + User = usr, + }); + club.Users.Remove(usr); + + var app = club.Applicants.FirstOrDefault(x => x.UserId == usr.Id); + if (app != null) + club.Applicants.Remove(app); + + uow.Complete(); + } + + return true; + } + + public bool UnBan(ulong ownerUserId, string userName, out ClubInfo club) + { + using (var uow = _db.UnitOfWork) + { + club = uow.Clubs.GetByOwner(ownerUserId, + set => set.Include(x => x.Bans) + .ThenInclude(x => x.User)); + if (club == null) + return false; + + var ban = club.Bans.FirstOrDefault(x => x.User.ToString().ToLowerInvariant() == userName.ToLowerInvariant()); + if (ban == null) + return false; + + club.Bans.Remove(ban); + uow.Complete(); + } + + return true; + } + + public bool Kick(ulong ownerUserId, string userName, out ClubInfo club) + { + using (var uow = _db.UnitOfWork) + { + club = uow.Clubs.GetByOwner(ownerUserId); + if (club == null) + return false; + + var usr = club.Users.FirstOrDefault(x => x.ToString().ToLowerInvariant() == userName.ToLowerInvariant()); + if (usr == null) + return false; + + if (club.OwnerId == usr.Id) + return false; + + club.Users.Remove(usr); + var app = club.Applicants.FirstOrDefault(x => x.UserId == usr.Id); + if (app != null) + club.Applicants.Remove(app); + uow.Complete(); + } + + return true; + } + + public ClubInfo[] GetClubLeaderboardPage(int page) + { + if (page < 0) + throw new ArgumentOutOfRangeException(nameof(page)); + + using (var uow = _db.UnitOfWork) + { + return uow.Clubs.GetClubLeaderboardPage(page); + } + } + } +} diff --git a/src/NadekoBot/Modules/Xp/Services/UserCacheItem.cs b/src/NadekoBot/Modules/Xp/Services/UserCacheItem.cs index 4cebef92..d1a07c11 100644 --- a/src/NadekoBot/Modules/Xp/Services/UserCacheItem.cs +++ b/src/NadekoBot/Modules/Xp/Services/UserCacheItem.cs @@ -1,18 +1,21 @@ -namespace NadekoBot.Modules.Xp.Services +using Discord; + +namespace NadekoBot.Modules.Xp.Services { public class UserCacheItem { - public ulong UserId { get; set; } - public ulong GuildId { get; set; } + public IGuildUser User { get; set; } + public IGuild Guild { get; set; } + public IMessageChannel Channel { get; set; } public override int GetHashCode() { - return UserId.GetHashCode(); + return User.GetHashCode(); } public override bool Equals(object obj) { - return obj is UserCacheItem uci && uci.UserId == UserId; + return obj is UserCacheItem uci && uci.User == User; } } } diff --git a/src/NadekoBot/Modules/Xp/Services/XpService.cs b/src/NadekoBot/Modules/Xp/Services/XpService.cs index ccd3c6ed..52944064 100644 --- a/src/NadekoBot/Modules/Xp/Services/XpService.cs +++ b/src/NadekoBot/Modules/Xp/Services/XpService.cs @@ -1,8 +1,11 @@ using Discord; using Discord.WebSocket; using NadekoBot.Common.Collections; +using NadekoBot.Extensions; +using NadekoBot.Modules.Xp.Common; using NadekoBot.Services; using NadekoBot.Services.Database.Models; +using NadekoBot.Services.Impl; using NLog; using System; using System.Collections.Concurrent; @@ -10,57 +13,289 @@ using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; +using ImageSharp; +using Image = ImageSharp.Image; +using SixLabors.Fonts; +using System.IO; +using SixLabors.Primitives; +using System.Net.Http; +using SixLabors.Shapes; +using System.Numerics; +using ImageSharp.Drawing.Pens; +using ImageSharp.Drawing.Brushes; namespace NadekoBot.Modules.Xp.Services { public class XpService : INService { + private enum NotifOf { Server, Global } // is it a server level-up or global level-up notification + private readonly DbService _db; private readonly CommandHandler _cmd; private readonly IBotConfigProvider _bc; + private readonly IImagesService _images; private readonly Logger _log; + private readonly NadekoStrings _strings; + private readonly FontCollection _fonts = new FontCollection(); public const int XP_REQUIRED_LVL_1 = 36; - private readonly ConcurrentDictionary> _excludedRoles + private readonly ConcurrentDictionary> _excludedRoles = new ConcurrentDictionary>(); - private readonly ConcurrentDictionary> _excludedChannels + + private readonly ConcurrentDictionary> _excludedChannels = new ConcurrentDictionary>(); - private readonly ConcurrentHashSet _excludedServers = new ConcurrentHashSet(); - private readonly ConcurrentHashSet _rewardedUsers = new ConcurrentHashSet(); + private readonly ConcurrentHashSet _excludedServers + = new ConcurrentHashSet(); - private readonly ConcurrentQueue _addMessageXp = new ConcurrentQueue(); + private readonly ConcurrentHashSet _rewardedUsers + = new ConcurrentHashSet(); + + private readonly ConcurrentQueue _addMessageXp + = new ConcurrentQueue(); + + private readonly ConcurrentDictionary _imageStreams + = new ConcurrentDictionary(); private readonly Timer updateXpTimer; - private readonly Timer clearRewardedUsersTimer; + private readonly HttpClient http = new HttpClient(); + private FontFamily _usernameFontFamily; + private FontFamily _clubFontFamily; + private Font _levelFont; + private Font _xpFont; + private Font _awardedFont; + private Font _rankFont; + private Font _timeFont; - public XpService(CommandHandler cmd, IBotConfigProvider bc, - IEnumerable allGuildConfigs, DbService db) + public XpService(CommandHandler cmd, IBotConfigProvider bc, + IEnumerable allGuildConfigs, IImagesService images, + DbService db, NadekoStrings strings) { _db = db; _cmd = cmd; _bc = bc; + _images = images; _log = LogManager.GetCurrentClassLogger(); + _strings = strings; + + //todo 60 move to font provider or somethign + _fonts = new FontCollection(); + if (Directory.Exists("data/fonts")) + foreach (var file in Directory.GetFiles("data/fonts")) + { + _fonts.Install(file); + } + + InitializeFonts(); _cmd.OnMessageNoTrigger += _cmd_OnMessageNoTrigger; - updateXpTimer = new Timer(_ => + updateXpTimer = new Timer(async _ => { - using (var uow = _db.UnitOfWork) + try { + var toNotify = new List<(IMessageChannel MessageChannel, IUser User, int Level, XpNotificationType NotifyType, NotifOf NotifOf)>(); + var roleRewards = new Dictionary>(); + + var toAddTo = new List(); while (_addMessageXp.TryDequeue(out var usr)) + toAddTo.Add(usr); + + var group = toAddTo.GroupBy(x => (GuildId: x.Guild.Id, User: x.User)); + if (toAddTo.Count == 0) + return; + + _log.Info("Adding XP to {0} users.", toAddTo.Count); + + using (var uow = _db.UnitOfWork) { - var usrObj = uow.Xp.GetOrCreateUser(usr.GuildId, usr.UserId); - usrObj.Xp += _bc.BotConfig.XpPerMessage; + foreach (var item in group) + { + var xp = item.Select(x => bc.BotConfig.XpPerMessage).Sum(); + + var usr = uow.Xp.GetOrCreateUser(item.Key.GuildId, item.Key.User.Id); + var du = uow.DiscordUsers.GetOrCreate(item.Key.User); + + var globalXp = uow.Xp.GetTotalUserXp(item.Key.User.Id); + var oldGlobalLevelData = new LevelStats(globalXp); + var newGlobalLevelData = new LevelStats(globalXp + xp); + + var oldGuildLevelData = new LevelStats(usr.Xp + usr.AwardedXp); + usr.Xp += xp; + if (du.Club != null) + du.Club.Xp += xp; + var newGuildLevelData = new LevelStats(usr.Xp + usr.AwardedXp); + + if (oldGlobalLevelData.Level < newGlobalLevelData.Level) + { + du.LastLevelUp = DateTime.UtcNow; + var first = item.First(); + if (du.NotifyOnLevelUp != XpNotificationType.None) + toNotify.Add((first.Channel, first.User, newGlobalLevelData.Level, du.NotifyOnLevelUp, NotifOf.Global)); + } + + if (oldGuildLevelData.Level < newGuildLevelData.Level) + { + usr.LastLevelUp = DateTime.UtcNow; + //send level up notification + var first = item.First(); + if (usr.NotifyOnLevelUp != XpNotificationType.None) + toNotify.Add((first.Channel, first.User, newGuildLevelData.Level, usr.NotifyOnLevelUp, NotifOf.Server)); + + //give role + if (!roleRewards.TryGetValue(usr.GuildId, out var rewards)) + { + rewards = uow.GuildConfigs.XpSettingsFor(usr.GuildId).RoleRewards.ToList(); + roleRewards.Add(usr.GuildId, rewards); + } + + var rew = rewards.FirstOrDefault(x => x.Level == newGuildLevelData.Level); + if (rew != null) + { + var role = first.User.Guild.GetRole(rew.RoleId); + if (role != null) + { + var __ = first.User.AddRoleAsync(role); + } + } + } + } + + uow.Complete(); } - uow.Complete(); + + await Task.WhenAll(toNotify.Select(async x => + { + if (x.NotifOf == NotifOf.Server) + { + if (x.NotifyType == XpNotificationType.Dm) + { + var chan = await x.User.GetOrCreateDMChannelAsync().ConfigureAwait(false); + if (chan != null) + await chan.SendConfirmAsync(_strings.GetText("level_up_dm", + (x.MessageChannel as ITextChannel)?.GuildId, + "xp", + x.User.Mention, Format.Bold(x.Level.ToString()), + Format.Bold((x.MessageChannel as ITextChannel)?.Guild.ToString() ?? "-"))) + .ConfigureAwait(false); + } + else // channel + { + await x.MessageChannel.SendConfirmAsync(_strings.GetText("level_up_channel", + (x.MessageChannel as ITextChannel)?.GuildId, + "xp", + x.User.Mention, Format.Bold(x.Level.ToString()))) + .ConfigureAwait(false); + } + } + else + { + IMessageChannel chan; + if (x.NotifyType == XpNotificationType.Dm) + { + chan = await x.User.GetOrCreateDMChannelAsync().ConfigureAwait(false); + } + else // channel + { + chan = x.MessageChannel; + } + await chan.SendConfirmAsync(_strings.GetText("level_up_global", + (x.MessageChannel as ITextChannel)?.GuildId, + "xp", + x.User.Mention, Format.Bold(x.Level.ToString()))) + .ConfigureAwait(false); + } + })); + } + catch (Exception ex) + { + _log.Warn(ex); } }, null, TimeSpan.FromSeconds(5), TimeSpan.FromSeconds(5)); - clearRewardedUsersTimer = new Timer(_ => + var clearRewardTimer = Task.Run(async () => { - _rewardedUsers.Clear(); - }, null, TimeSpan.FromSeconds(bc.BotConfig.XpMinutesTimeout), TimeSpan.FromSeconds(bc.BotConfig.XpMinutesTimeout)); + while (true) + { + _rewardedUsers.Clear(); + + await Task.Delay(TimeSpan.FromMinutes(_bc.BotConfig.XpMinutesTimeout)); + } + }); + } + + public IEnumerable GetRoleRewards(ulong id) + { + using (var uow = _db.UnitOfWork) + { + return uow.GuildConfigs.XpSettingsFor(id) + .RoleRewards + .ToArray(); + } + } + + public void SetRoleReward(ulong guildId, int level, ulong? roleId) + { + using (var uow = _db.UnitOfWork) + { + var settings = uow.GuildConfigs.XpSettingsFor(guildId); + + if (roleId == null) + { + settings.RoleRewards.RemoveWhere(x => x.Level == level); + } + else + { + var rew = settings.RoleRewards.FirstOrDefault(x => x.Level == level); + + if (rew != null) + rew.RoleId = roleId.Value; + else + settings.RoleRewards.Add(new XpRoleReward() + { + Level = level, + RoleId = roleId.Value, + }); + } + + uow.Complete(); + } + } + + public UserXpStats[] GetUserXps(ulong guildId, int page) + { + using (var uow = _db.UnitOfWork) + { + return uow.Xp.GetUsersFor(guildId, page); + } + } + + public (ulong UserId, int TotalXp)[] GetUserXps(int page) + { + using (var uow = _db.UnitOfWork) + { + return uow.Xp.GetUsersFor(page); + } + } + + public async Task ChangeNotificationType(ulong userId, ulong guildId, XpNotificationType type) + { + using (var uow = _db.UnitOfWork) + { + var user = uow.Xp.GetOrCreateUser(guildId, userId); + user.NotifyOnLevelUp = type; + await uow.CompleteAsync().ConfigureAwait(false); + } + } + + public async Task ChangeNotificationType(IUser user, XpNotificationType type) + { + using (var uow = _db.UnitOfWork) + { + var du = uow.DiscordUsers.GetOrCreate(user); + du.NotifyOnLevelUp = type; + await uow.CompleteAsync().ConfigureAwait(false); + } } private Task _cmd_OnMessageNoTrigger(IUserMessage arg) @@ -84,12 +319,25 @@ namespace NadekoBot.Modules.Xp.Services user.Roles.Any(x => roles.Contains(x.Id))) return; - _log.Info("Adding {0} xp to {1} on {2} server", _bc.BotConfig.XpPerMessage, user.ToString(), user.Guild.Name); - _addMessageXp.Enqueue(new UserCacheItem { GuildId = user.Guild.Id, UserId = user.Id }); + if (!arg.Content.Contains(' ') && arg.Content.Length < 5) + return; + _addMessageXp.Enqueue(new UserCacheItem { Guild = user.Guild, Channel = arg.Channel, User = user }); }); return Task.CompletedTask; } + public void AddXp(ulong userId, ulong guildId, int amount) + { + using (var uow = _db.UnitOfWork) + { + var usr = uow.Xp.GetOrCreateUser(guildId, userId); + + usr.AwardedXp += amount; + + uow.Complete(); + } + } + public bool IsServerExcluded(ulong id) { return _excludedServers.Contains(id); @@ -116,25 +364,62 @@ namespace NadekoBot.Modules.Xp.Services return _rewardedUsers.Add(userId); } - public UserXpStats GetUserStats(ulong guildId, ulong userId) + public LevelStats GetGlobalUserStats(ulong userId) { - UserXpStats user; + int totalXp; using (var uow = _db.UnitOfWork) { - user = uow.Xp.GetOrCreateUser(guildId, userId); + totalXp = uow.Xp.GetTotalUserXp(userId); } - return user; + return new LevelStats(totalXp); } - public string GenerateXpBar(int currentXp, int requiredXp) + public FullUserStats GetUserStats(IGuildUser user) { - //todo - return $"{currentXp}/{requiredXp}"; + DiscordUser du; + UserXpStats stats; + int totalXp; + int globalRank; + int guildRank; + using (var uow = _db.UnitOfWork) + { + du = uow.DiscordUsers.GetOrCreate(user); + stats = uow.Xp.GetOrCreateUser(user.GuildId, user.Id); + totalXp = uow.Xp.GetTotalUserXp(user.Id); + globalRank = uow.Xp.GetUserGlobalRanking(user.Id); + guildRank = uow.Xp.GetUserGuildRanking(user.Id, user.GuildId); + } + + return new FullUserStats(du, + stats, + new LevelStats(totalXp), + new LevelStats(stats.Xp + stats.AwardedXp), + globalRank, + guildRank); } + public static (int Level, int LevelXp, int LevelRequiredXp) GetLevelData(UserXpStats stats) + { + var baseXp = XpService.XP_REQUIRED_LVL_1; + + var required = baseXp; + var totalXp = 0; + var lvl = 1; + while (true) + { + required = (int)(baseXp + baseXp / 4.0 * (lvl - 1)); + + if (required + totalXp > stats.Xp) + break; + + totalXp += required; + lvl++; + } + + return (lvl - 1, stats.Xp - totalXp, required); + } - //todo exclude in database public bool ToggleExcludeServer(ulong id) { using (var uow = _db.UnitOfWork) @@ -225,5 +510,222 @@ namespace NadekoBot.Modules.Xp.Services } } } + + public Task> GenerateImageAsync(IGuildUser user) + { + return GenerateImageAsync(GetUserStats(user)); + } + + private void InitializeFonts() + { + _usernameFontFamily = _fonts.Find("Whitney-Bold"); + _clubFontFamily = _fonts.Find("Whitney-Bold"); + _levelFont = _fonts.Find("Whitney-Bold").CreateFont(45); + _xpFont = _fonts.Find("Whitney-Bold").CreateFont(50); + _awardedFont = _fonts.Find("Whitney-Bold").CreateFont(25); + _rankFont = _fonts.Find("Uni Sans Thin CAPS").CreateFont(30); + _timeFont = _fonts.Find("Whitney-Bold").CreateFont(20); + } + + public async Task> GenerateImageAsync(FullUserStats stats) + { + var img = Image.Load(_images.XpCard.ToArray()); + + var username = stats.User.ToString(); + var usernameFont = _usernameFontFamily + .CreateFont(username.Length <= 6 + ? 50 + : 50 - username.Length); + + img.DrawText("@" + username, usernameFont, Rgba32.White, + new PointF(130, 5)); + + // level + + img.DrawText(stats.Global.Level.ToString(), _levelFont, Rgba32.White, + new PointF(47, 137)); + + img.DrawText(stats.Guild.Level.ToString(), _levelFont, Rgba32.White, + new PointF(47, 285)); + + //club name + + var clubName = stats.User.Club?.ToString() ?? "-"; + + var clubFont = _clubFontFamily + .CreateFont(clubName.Length <= 8 + ? 35 + : 35 - (clubName.Length / 2)); + + img.DrawText(clubName, clubFont, Rgba32.White, + new PointF(650 - clubName.Length * 10, 40)); + + var pen = new Pen(Rgba32.Black, 1); + var brush = Brushes.Solid(Rgba32.White); + var xpBgBrush = Brushes.Solid(new Rgba32(0, 0, 0, 0.4f)); + + var global = stats.Global; + var guild = stats.Guild; + + //xp bar + + img.FillPolygon(xpBgBrush, new[] { + new PointF(321, 104), + new PointF(321 + (450 * (global.LevelXp / (float)global.RequiredXp)), 104), + new PointF(286 + (450 * (global.LevelXp / (float)global.RequiredXp)), 235), + new PointF(286, 235), + }); + img.DrawText($"{global.LevelXp}/{global.RequiredXp}", _xpFont, brush, pen, + new PointF(430, 130)); + + img.FillPolygon(xpBgBrush, new[] { + new PointF(282, 248), + new PointF(282 + (450 * (guild.LevelXp / (float)guild.RequiredXp)), 248), + new PointF(247 + (450 * (guild.LevelXp / (float)guild.RequiredXp)), 379), + new PointF(247, 379), + }); + img.DrawText($"{guild.LevelXp}/{guild.RequiredXp}", _xpFont, brush, pen, + new PointF(400, 270)); + + if (stats.FullGuildStats.AwardedXp != 0) + { + var sign = stats.FullGuildStats.AwardedXp > 0 + ? "+ " + : ""; + img.DrawText($"({sign}{stats.FullGuildStats.AwardedXp})", _awardedFont, brush, pen, + new PointF(445 - (Math.Max(0, (stats.FullGuildStats.AwardedXp.ToString().Length - 2)) * 5), 335)); + } + + //ranking + + img.DrawText(stats.GlobalRanking.ToString(), _rankFont, Rgba32.White, + new PointF(148, 170)); + + img.DrawText(stats.GuildRanking.ToString(), _rankFont, Rgba32.White, + new PointF(148, 317)); + + //time on this level + + string GetTimeSpent(DateTime time) + { + var offset = DateTime.UtcNow - time; + return $"{offset.Days}d{offset.Hours}h{offset.Minutes}m"; + } + + img.DrawText(GetTimeSpent(stats.User.LastLevelUp), _timeFont, Rgba32.White, + new PointF(50, 197)); + + img.DrawText(GetTimeSpent(stats.FullGuildStats.LastLevelUp), _timeFont, Rgba32.White, + new PointF(50, 344)); + + //avatar + + if (stats.User.AvatarId != null) + { + try + { + var avatarUrl = stats.User.RealAvatarUrl(); + + byte[] s; + if (!_imageStreams.TryGetValue(avatarUrl, out s)) + { + using (var temp = await http.GetStreamAsync(avatarUrl)) + { + var tempDraw = Image.Load(temp); + tempDraw = tempDraw.Resize(69, 70); + ApplyRoundedCorners(tempDraw, 35); + s = tempDraw.ToStream().ToArray(); + } + + _imageStreams.AddOrUpdate(avatarUrl, s, (k, v) => s); + } + var toDraw = Image.Load(s); + + + img.DrawImage(toDraw, + 1, + new Size(69, 70), + new Point(32, 10)); + } + catch (Exception ex) + { + _log.Warn(ex); + } + } + + //club image + + if (!string.IsNullOrWhiteSpace(stats.User.Club?.ImageUrl)) + { + var imgUrl = stats.User.Club.ImageUrl; + try + { + byte[] s; + if (!_imageStreams.TryGetValue(imgUrl, out s)) + { + using (var temp = await http.GetStreamAsync(imgUrl)) + { + var tempDraw = Image.Load(temp); + tempDraw = tempDraw.Resize(45, 45); + ApplyRoundedCorners(tempDraw, 22.5f); + s = tempDraw.ToStream().ToArray(); + } + + _imageStreams.AddOrUpdate(imgUrl, s, (k, v) => s); + } + var toDraw = Image.Load(s); + + img.DrawImage(toDraw, + 1, + new Size(45, 45), + new Point(722, 25)); + } + catch (Exception ex) + { + _log.Warn(ex); + } + } + + var arr = img.ToStream().ToArray(); + + //_log.Info("{0:F2} KB", arr.Length * 1.0f / 1.KB()); + + return img; + } + + + // https://github.com/SixLabors/ImageSharp/tree/master/samples/AvatarWithRoundedCorner + public static void ApplyRoundedCorners(Image img, float cornerRadius) + { + var corners = BuildCorners(img.Width, img.Height, cornerRadius); + // now we have our corners time to draw them + img.Fill(Rgba32.Transparent, corners, new GraphicsOptions(true) + { + BlenderMode = ImageSharp.PixelFormats.PixelBlenderMode.Src // enforces that any part of this shape that has color is punched out of the background + }); + } + + public static IPathCollection BuildCorners(int imageWidth, int imageHeight, float cornerRadius) + { + // first create a square + var rect = new RectangularePolygon(-0.5f, -0.5f, cornerRadius, cornerRadius); + + // then cut out of the square a circle so we are left with a corner + var cornerToptLeft = rect.Clip(new EllipsePolygon(cornerRadius - 0.5f, cornerRadius - 0.5f, cornerRadius)); + + // corner is now a corner shape positions top left + //lets make 3 more positioned correctly, we can do that by translating the orgional around the center of the image + var center = new Vector2(imageWidth / 2, imageHeight / 2); + + float rightPos = imageWidth - cornerToptLeft.Bounds.Width + 1; + float bottomPos = imageHeight - cornerToptLeft.Bounds.Height + 1; + + // move it across the width of the image - the width of the shape + var cornerTopRight = cornerToptLeft.RotateDegree(90).Translate(rightPos, 0); + var cornerBottomLeft = cornerToptLeft.RotateDegree(-90).Translate(0, bottomPos); + var cornerBottomRight = cornerToptLeft.RotateDegree(180).Translate(rightPos, bottomPos); + + return new PathCollection(cornerToptLeft, cornerBottomLeft, cornerTopRight, cornerBottomRight); + } } } diff --git a/src/NadekoBot/Modules/Xp/Xp.cs b/src/NadekoBot/Modules/Xp/Xp.cs index 44b5b006..4362e06c 100644 --- a/src/NadekoBot/Modules/Xp/Xp.cs +++ b/src/NadekoBot/Modules/Xp/Xp.cs @@ -1,9 +1,12 @@ using Discord; using Discord.Commands; +using Discord.WebSocket; using NadekoBot.Common.Attributes; using NadekoBot.Extensions; -using NadekoBot.Modules.Xp.Extensions; +using NadekoBot.Modules.Xp.Common; using NadekoBot.Modules.Xp.Services; +using NadekoBot.Services.Database.Models; +using System.Diagnostics; using System.Linq; using System.Threading.Tasks; @@ -11,37 +14,94 @@ namespace NadekoBot.Modules.Xp { public partial class Xp : NadekoTopLevelModule { + private readonly DiscordSocketClient _client; + + public Xp(DiscordSocketClient client) + { + _client = client; + } + [NadekoCommand, Usage, Description, Aliases] [RequireContext(ContextType.Guild)] - public async Task Experience(IUser user = null) + public async Task Experience([Remainder]IUser user = null) { user = user ?? Context.User; - await Task.Delay(64).ConfigureAwait(false); // wait a bit in case user got XP with this message - var stats = _service.GetUserStats(Context.Guild.Id, user.Id); - - var levelData = stats.GetLevelData(); - var xpBarStr = _service.GenerateXpBar(levelData.LevelXp, levelData.LevelRequiredXp); - - await Context.Channel.EmbedAsync(new EmbedBuilder() - .WithTitle(user.ToString()) - //.AddField(GetText("server_level"), stats.ServerLevel.ToString(), true) - .AddField(GetText("level"), levelData.Level.ToString(), true) - //.AddField(GetText("club"), stats.ClubName ?? "-", true) - .AddField(GetText("xp"), xpBarStr, false) - .WithOkColor()) + await Context.Channel.TriggerTypingAsync(); + var img = await _service.GenerateImageAsync((IGuildUser)user); + + await Context.Channel.SendFileAsync(img.ToStream(), $"{user.Id}_xp.png") .ConfigureAwait(false); } + [NadekoCommand, Usage, Description, Aliases] + [RequireContext(ContextType.Guild)] + public Task XpRoleRewards(int page = 1) + { + page--; + + if (page < 0) + return Task.CompletedTask; + + var roles = _service.GetRoleRewards(Context.Guild.Id) + .OrderBy(x => x.Level) + .Skip(page * 9) + .Take(9); + + var embed = new EmbedBuilder() + .WithTitle(GetText("role_rewards")) + .WithOkColor(); + + if (!roles.Any()) + return Context.Channel.EmbedAsync(embed.WithDescription(GetText("no_role_rewards"))); + + foreach (var rolerew in roles) + { + var role = Context.Guild.GetRole(rolerew.RoleId); + + if (role == null) + continue; + + embed.AddField(GetText("level_x", Format.Bold(rolerew.Level.ToString())), role.ToString()); + } + return Context.Channel.EmbedAsync(embed); + } + + [NadekoCommand, Usage, Description, Aliases] + [RequireUserPermission(GuildPermission.ManageRoles)] + [RequireContext(ContextType.Guild)] + public async Task XpRoleReward(int level, [Remainder] IRole role = null) + { + if (level < 1) + return; + + _service.SetRoleReward(Context.Guild.Id, level, role?.Id); + + if(role == null) + await ReplyConfirmLocalized("role_reward_cleared", level).ConfigureAwait(false); + else + await ReplyConfirmLocalized("role_reward_added", level, Format.Bold(role.ToString())).ConfigureAwait(false); + } + + public enum NotifyPlace + { + Server = 0, + Guild = 0, + Global = 1, + } + + [NadekoCommand, Usage, Description, Aliases] + [RequireContext(ContextType.Guild)] + public async Task XpNotify(NotifyPlace place = NotifyPlace.Guild, XpNotificationType type = XpNotificationType.Channel) + { + if (place == NotifyPlace.Guild) + await _service.ChangeNotificationType(Context.User.Id, Context.Guild.Id, type); + else + await _service.ChangeNotificationType(Context.User, type); + await Context.Channel.SendConfirmAsync("👌").ConfigureAwait(false); + } + public enum Server { Server }; - - //[NadekoCommand, Usage, Description, Aliases] - //[RequireContext(ContextType.Guild)] - //[OwnerOnly] - //[Priority(1)] - //public async Task XpExclude(Server _, IGuild guild) - //{ - //} [NadekoCommand, Usage, Description, Aliases] [RequireContext(ContextType.Guild)] @@ -101,5 +161,91 @@ namespace NadekoBot.Modules.Xp await Context.Channel.EmbedAsync(embed).ConfigureAwait(false); } + + [NadekoCommand, Usage, Description, Aliases] + [RequireContext(ContextType.Guild)] + public Task XpLeaderboard(int page = 1) + { + if (--page < 0) + return Task.CompletedTask; + + return Context.Channel.SendPaginatedConfirmAsync(_client, page, async (curPage) => + { + var users = _service.GetUserXps(Context.Guild.Id, curPage); + + var embed = new EmbedBuilder() + .WithTitle(GetText("server_leaderboard")) + .WithOkColor(); + + if (!users.Any()) + return embed.WithDescription("-"); + else + { + for (int i = 0; i < users.Length; i++) + { + var levelStats = LevelStats.FromXp(users[i].Xp + users[i].AwardedXp); + var user = await Context.Guild.GetUserAsync(users[i].UserId).ConfigureAwait(false); + + var userXpData = users[i]; + + var awardStr = ""; + if (userXpData.AwardedXp > 0) + awardStr = $"(+{userXpData.AwardedXp})"; + else if (userXpData.AwardedXp < 0) + awardStr = $"({userXpData.AwardedXp.ToString()})"; + + embed.AddField( + $"#{(i + 1 + curPage * 9)} {(user?.ToString() ?? users[i].UserId.ToString())}", + $"{GetText("level_x", levelStats.Level)} - {levelStats.TotalXp}xp {awardStr}"); + } + return embed; + } + }, addPaginatedFooter: false); + } + + [NadekoCommand, Usage, Description, Aliases] + [RequireContext(ContextType.Guild)] + public async Task XpGlobalLeaderboard(int page = 1) + { + if (--page < 0) + return; + + await Context.Channel.SendPaginatedConfirmAsync(_client, page, async (curPage) => + { + var users = _service.GetUserXps(curPage); + + var embed = new EmbedBuilder() + .WithTitle(GetText("global_leaderboard")) + .WithOkColor(); + + if (!users.Any()) + return embed.WithDescription("-"); + else + { + for (int i = 0; i < users.Length; i++) + { + var user = await Context.Guild.GetUserAsync(users[i].UserId).ConfigureAwait(false); + embed.AddField( + $"#{(i + 1 + curPage * 9)} {(user?.ToString() ?? users[i].UserId.ToString())}", + $"{GetText("level_x", LevelStats.FromXp(users[i].TotalXp).Level)} - {users[i].TotalXp}xp"); + } + + return embed; + } + }, addPaginatedFooter: false); + } + + [NadekoCommand, Usage, Description, Aliases] + [RequireContext(ContextType.Guild)] + [RequireUserPermission(GuildPermission.Administrator)] + public async Task XpAdd(int amount, [Remainder] IGuildUser user) + { + if (amount == 0) + return; + + _service.AddXp(user.Id, Context.Guild.Id, amount); + + await ReplyConfirmLocalized("modified", Format.Bold(user.ToString()), Format.Bold(amount.ToString())).ConfigureAwait(false); + } } } diff --git a/src/NadekoBot/NadekoBot.csproj b/src/NadekoBot/NadekoBot.csproj index f18c711c..22abd80f 100644 --- a/src/NadekoBot/NadekoBot.csproj +++ b/src/NadekoBot/NadekoBot.csproj @@ -58,14 +58,14 @@ - + - - + + @@ -87,6 +87,14 @@ $(NoWarn);CS1573;CS1591 + + latest + + + + latest + + diff --git a/src/NadekoBot/Resources/CommandStrings.resx b/src/NadekoBot/Resources/CommandStrings.resx index 8493d383..81133fdc 100644 --- a/src/NadekoBot/Resources/CommandStrings.resx +++ b/src/NadekoBot/Resources/CommandStrings.resx @@ -3600,4 +3600,184 @@ Exclude a user or a role from the xp system, or whole current server. + + xpnotify xpn + + + `{0}xpn global dm` `{0}xpn server channel` + + + Sets how the bot should notify you when you get a `server` or `global` level. You can set `dm` (for the bot to send a direct message), `channel` (to get notified in the channel you sent the last message in) or `none` to disable. + + + xprolerewards xprrs + + + `{0}xprrs` + + + Shows currently set role rewards. + + + xprolereward xprr + + + `{0}xprr 3 Social` + + + Sets a role reward on a specified level. + + + xpleaderboard xplb + + + `{0}xplb` + + + Shows current server's xp leaderboard. + + + xpgleaderboard xpglb + + + `{0}xpglb` + + + Shows current server's xp leaderboard. + + + xpadd + + + `{0}xpadd 100 @b1nzy` + + + Adds xp to a user on the server. This does not affect their global ranking. You can use negative values. + + + clubcreate + + + `{0}clubcreate b1nzy's friends` + + + Creates a club. You must be atleast level 5 and not be in the club already. + + + clubinfo + + + `{0}clubinfo b1nzy's friends#123` + + + Shows information about the club. + + + clubapply + + + `{0}clubapply b1nzy's friends#123` + + + Apply to join a club. You must meet that club's minimum level requirement, and not be on its ban list. + + + clubaccept + + + `{0}clubaccept b1nzy#1337` + + + Apply to join a club. You must meet that club's minimum level requirement, and not be on its ban list. + + + clubleave + + + `{0}clubleave` + + + Leaves the club you're currently in. + + + clubdisband + + + `{0}clubdisband` + + + Disbands the club you're the owner of. This action is irreversible. + + + clubkick + + + `{0}clubkick b1nzy#1337` + + + Kicks the user from the club. You must be the club owner. They will be able to apply again. + + + clubban + + + `{0}clubban b1nzy#1337` + + + Bans the user from the club. You must be the club owner. They will not be able to apply again. + + + clubunban + + + `{0}clubunban b1nzy#1337` + + + Unbans the previously banned user from the club. You must be the club owner. + + + clublevelreq + + + `{0}clublevelreq 7` + + + Sets the club required level to apply to join the club. You must be club owner. You can't set this number below 5. + + + clubicon + + + `{0}clubicon https://i.imgur.com/htfDMfU.png` + + + Sets the club icon. + + + clubapps + + + `{0}clubapps 2` + + + Shows the list of users who have applied to your club. Paginated. You must be club owner to use this command. + + + clubbans + + + `{0}clubbans 2` + + + Shows the list of users who have banned from your club. Paginated. You must be club owner to use this command. + + + clublb + + + `{0}clublb 2` + + + Shows club rankings on the specified page. + diff --git a/src/NadekoBot/Services/CommandHandler.cs b/src/NadekoBot/Services/CommandHandler.cs index ad51c579..c06d14ab 100644 --- a/src/NadekoBot/Services/CommandHandler.cs +++ b/src/NadekoBot/Services/CommandHandler.cs @@ -39,7 +39,7 @@ namespace NadekoBot.Services public string DefaultPrefix { get; private set; } private ConcurrentDictionary _prefixes { get; } = new ConcurrentDictionary(); - private ImmutableArray> ownerChannels { get; set; } = new ImmutableArray>(); + private ImmutableArray> OwnerChannels { get; set; } = new ImmutableArray>(); public event Func CommandExecuted = delegate { return Task.CompletedTask; }; public event Func CommandErrored = delegate { return Task.CompletedTask; }; diff --git a/src/NadekoBot/Services/Database/IUnitOfWork.cs b/src/NadekoBot/Services/Database/IUnitOfWork.cs index fdd2eb0d..6b97a085 100644 --- a/src/NadekoBot/Services/Database/IUnitOfWork.cs +++ b/src/NadekoBot/Services/Database/IUnitOfWork.cs @@ -25,6 +25,7 @@ namespace NadekoBot.Services.Database IDiscordUserRepository DiscordUsers { get; } IWarningsRepository Warnings { get; } IXpRepository Xp { get; } + IClubRepository Clubs { get; } int Complete(); Task CompleteAsync(); diff --git a/src/NadekoBot/Services/Database/Models/ClubInfo.cs b/src/NadekoBot/Services/Database/Models/ClubInfo.cs new file mode 100644 index 00000000..68f68bc3 --- /dev/null +++ b/src/NadekoBot/Services/Database/Models/ClubInfo.cs @@ -0,0 +1,47 @@ +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; + +namespace NadekoBot.Services.Database.Models +{ + public class ClubInfo : DbEntity + { + [MaxLength(20)] + public string Name { get; set; } + public int Discrim { get; set; } + + public string ImageUrl { get; set; } = ""; + public int MinimumLevelReq { get; set; } = 5; + public int Xp { get; set; } = 0; + + public int OwnerId { get; set; } + public DiscordUser Owner { get; set; } + + public List Users { get; set; } = new List(); + + public List Applicants { get; set; } = new List(); + public List Bans { get; set; } = new List(); + + public override string ToString() + { + return Name + "#" + Discrim; + } + } + + public class ClubApplicants + { + public int ClubId { get; set; } + public ClubInfo Club { get; set; } + + public int UserId { get; set; } + public DiscordUser User { get; set; } + } + + public class ClubBans + { + public int ClubId { get; set; } + public ClubInfo Club { get; set; } + + public int UserId { get; set; } + public DiscordUser User { get; set; } + } +} diff --git a/src/NadekoBot/Services/Database/Models/DiscordUser.cs b/src/NadekoBot/Services/Database/Models/DiscordUser.cs index 86b84d5b..2ca8fc68 100644 --- a/src/NadekoBot/Services/Database/Models/DiscordUser.cs +++ b/src/NadekoBot/Services/Database/Models/DiscordUser.cs @@ -1,4 +1,6 @@ -namespace NadekoBot.Services.Database.Models +using System; + +namespace NadekoBot.Services.Database.Models { public class DiscordUser : DbEntity { @@ -6,6 +8,22 @@ public string Username { get; set; } public string Discriminator { get; set; } public string AvatarId { get; set; } + + public ClubInfo Club { get; set; } + public DateTime LastLevelUp { get; set; } = DateTime.UtcNow; + public XpNotificationType NotifyOnLevelUp { get; set; } + + public override bool Equals(object obj) + { + return obj is DiscordUser du + ? du.UserId == UserId + : false; + } + + public override int GetHashCode() + { + return UserId.GetHashCode(); + } public override string ToString() => Username + "#" + Discriminator; diff --git a/src/NadekoBot/Services/Database/Models/UserXpStats.cs b/src/NadekoBot/Services/Database/Models/UserXpStats.cs index 9c515ec2..8695298e 100644 --- a/src/NadekoBot/Services/Database/Models/UserXpStats.cs +++ b/src/NadekoBot/Services/Database/Models/UserXpStats.cs @@ -1,10 +1,16 @@ -namespace NadekoBot.Services.Database.Models +using System; + +namespace NadekoBot.Services.Database.Models { public class UserXpStats : DbEntity { public ulong UserId { get; set; } public ulong GuildId { get; set; } public int Xp { get; set; } - public bool NotifyOnLevelUp { get; set; } + public int AwardedXp { get; set; } + public XpNotificationType NotifyOnLevelUp { get; set; } + public DateTime LastLevelUp { get; set; } = DateTime.UtcNow; } + + public enum XpNotificationType { None, Dm, Channel } } diff --git a/src/NadekoBot/Services/Database/NadekoContext.cs b/src/NadekoBot/Services/Database/NadekoContext.cs index 5ae153d0..5c04c816 100644 --- a/src/NadekoBot/Services/Database/NadekoContext.cs +++ b/src/NadekoBot/Services/Database/NadekoContext.cs @@ -4,6 +4,7 @@ using System.Linq; using NadekoBot.Services.Database.Models; using NadekoBot.Extensions; using Microsoft.EntityFrameworkCore.Infrastructure; +using System; namespace NadekoBot.Services.Database { @@ -44,6 +45,7 @@ namespace NadekoBot.Services.Database public DbSet WaifuUpdates { get; set; } public DbSet Warnings { get; set; } public DbSet UserXpStats { get; set; } + public DbSet Clubs { get; set; } //logging public DbSet LogSettings { get; set; } @@ -71,24 +73,6 @@ namespace NadekoBot.Services.Database { var bc = new BotConfig(); - bc.ModulePrefixes.AddRange(new HashSet() - { - new ModulePrefix() { ModuleName = "Administration", Prefix = "." }, - new ModulePrefix() { ModuleName = "Searches", Prefix = "~" }, - new ModulePrefix() { ModuleName = "Translator", Prefix = "~" }, - new ModulePrefix() { ModuleName = "NSFW", Prefix = "~" }, - new ModulePrefix() { ModuleName = "ClashOfClans", Prefix = "," }, - new ModulePrefix() { ModuleName = "Help", Prefix = "-" }, - new ModulePrefix() { ModuleName = "Music", Prefix = "!!" }, - new ModulePrefix() { ModuleName = "Trello", Prefix = "trello" }, - new ModulePrefix() { ModuleName = "Games", Prefix = ">" }, - new ModulePrefix() { ModuleName = "Gambling", Prefix = "$" }, - new ModulePrefix() { ModuleName = "Permissions", Prefix = ";" }, - new ModulePrefix() { ModuleName = "Pokemon", Prefix = ">" }, - new ModulePrefix() { ModuleName = "Utility", Prefix = "." }, - new ModulePrefix() { ModuleName = "CustomReactions", Prefix = "." }, - new ModulePrefix() { ModuleName = "PokeGame", Prefix = ">" } - }); bc.RaceAnimals.AddRange(new HashSet { new RaceAnimal { Icon = "🐼", Name = "Panda" }, @@ -177,7 +161,14 @@ namespace NadekoBot.Services.Database #endregion #region BotConfig - //var botConfigEntity = modelBuilder.Entity(); + var botConfigEntity = modelBuilder.Entity(); + + botConfigEntity.Property(x => x.XpMinutesTimeout) + .HasDefaultValue(5); + + botConfigEntity.Property(x => x.XpPerMessage) + .HasDefaultValue(3); + //botConfigEntity // .HasMany(c => c.ModulePrefixes) // .WithOne(mp => mp.BotConfig) @@ -278,9 +269,19 @@ namespace NadekoBot.Services.Database // .WithOne(); // //.HasForeignKey(w => w.ClaimerId) // //.IsRequired(false); + #endregion + #region DiscordUser + var du = modelBuilder.Entity(); du.HasAlternateKey(w => w.UserId); + du.HasOne(x => x.Club) + .WithMany(x => x.Users) + .IsRequired(false); + + modelBuilder.Entity() + .Property(x => x.LastLevelUp) + .HasDefaultValue(DateTime.Now); #endregion @@ -294,10 +295,14 @@ namespace NadekoBot.Services.Database .IsUnique(); #endregion - #region XpStatas + #region XpStats modelBuilder.Entity() .HasIndex(x => new { x.UserId, x.GuildId }) .IsUnique(); + + modelBuilder.Entity() + .Property(x => x.LastLevelUp) + .HasDefaultValue(DateTime.Now); #endregion #region XpSettings @@ -305,6 +310,47 @@ namespace NadekoBot.Services.Database .HasOne(x => x.GuildConfig) .WithOne(x => x.XpSettings); #endregion + + #region XpRoleReward + modelBuilder.Entity() + .HasAlternateKey(x => x.Level); + #endregion + + #region Club + var ci = modelBuilder.Entity(); + ci.HasOne(x => x.Owner) + .WithOne() + .HasForeignKey(x => x.OwnerId); + + + ci.HasAlternateKey(x => new { x.Name, x.Discrim }); + #endregion + + #region ClubManytoMany + + modelBuilder.Entity() + .HasKey(t => new { t.ClubId, t.UserId }); + + modelBuilder.Entity() + .HasOne(pt => pt.User) + .WithMany(); + + modelBuilder.Entity() + .HasOne(pt => pt.Club) + .WithMany(x => x.Applicants); + + modelBuilder.Entity() + .HasKey(t => new { t.ClubId, t.UserId }); + + modelBuilder.Entity() + .HasOne(pt => pt.User) + .WithMany(); + + modelBuilder.Entity() + .HasOne(pt => pt.Club) + .WithMany(x => x.Bans); + + #endregion } } } diff --git a/src/NadekoBot/Services/Database/Repositories/IClubRepository.cs b/src/NadekoBot/Services/Database/Repositories/IClubRepository.cs new file mode 100644 index 00000000..086d638b --- /dev/null +++ b/src/NadekoBot/Services/Database/Repositories/IClubRepository.cs @@ -0,0 +1,16 @@ +using Microsoft.EntityFrameworkCore; +using NadekoBot.Services.Database.Models; +using System; +using System.Linq; + +namespace NadekoBot.Services.Database.Repositories +{ + public interface IClubRepository : IRepository + { + int GetNextDiscrim(string clubName); + ClubInfo GetByName(string v, int discrim, Func, IQueryable> func = null); + ClubInfo GetByOwner(ulong userId, Func, IQueryable> func = null); + ClubInfo GetByMember(ulong userId, Func, IQueryable> func = null); + ClubInfo[] GetClubLeaderboardPage(int page); + } +} diff --git a/src/NadekoBot/Services/Database/Repositories/IXpRepository.cs b/src/NadekoBot/Services/Database/Repositories/IXpRepository.cs index 1973504a..667dd315 100644 --- a/src/NadekoBot/Services/Database/Repositories/IXpRepository.cs +++ b/src/NadekoBot/Services/Database/Repositories/IXpRepository.cs @@ -5,5 +5,10 @@ namespace NadekoBot.Services.Database.Repositories public interface IXpRepository : IRepository { UserXpStats GetOrCreateUser(ulong guildId, ulong userId); + int GetTotalUserXp(ulong userId); + UserXpStats[] GetUsersFor(ulong guildId, int page); + (ulong UserId, int TotalXp)[] GetUsersFor(int page); + int GetUserGlobalRanking(ulong userId); + int GetUserGuildRanking(ulong userId, ulong guildId); } } diff --git a/src/NadekoBot/Services/Database/Repositories/Impl/BotConfigRepository.cs b/src/NadekoBot/Services/Database/Repositories/Impl/BotConfigRepository.cs index dd329796..c21b1ff2 100644 --- a/src/NadekoBot/Services/Database/Repositories/Impl/BotConfigRepository.cs +++ b/src/NadekoBot/Services/Database/Repositories/Impl/BotConfigRepository.cs @@ -20,7 +20,6 @@ namespace NadekoBot.Services.Database.Repositories.Impl .Include(bc => bc.RaceAnimals) .Include(bc => bc.Blacklist) .Include(bc => bc.EightBallResponses) - .Include(bc => bc.ModulePrefixes) .Include(bc => bc.StartupCommands) .Include(bc => bc.BlockedCommands) .Include(bc => bc.BlockedModules) diff --git a/src/NadekoBot/Services/Database/Repositories/Impl/ClubRepository.cs b/src/NadekoBot/Services/Database/Repositories/Impl/ClubRepository.cs new file mode 100644 index 00000000..21421bfa --- /dev/null +++ b/src/NadekoBot/Services/Database/Repositories/Impl/ClubRepository.cs @@ -0,0 +1,65 @@ +using NadekoBot.Services.Database.Models; +using System.Linq; +using Microsoft.EntityFrameworkCore; +using System; + +namespace NadekoBot.Services.Database.Repositories.Impl +{ + public class ClubRepository : Repository, IClubRepository + { + public ClubRepository(DbContext context) : base(context) + { + } + + public ClubInfo GetByOwner(ulong userId, Func, IQueryable> func = null) + { + if (func == null) + return _set + .Include(x => x.Bans) + .Include(x => x.Applicants) + .Include(x => x.Users) + .Include(x => x.Owner) + .FirstOrDefault(x => x.Owner.UserId == userId); + + return func(_set).FirstOrDefault(x => x.Owner.UserId == userId); + } + + public ClubInfo GetByName(string name, int discrim, Func, IQueryable> func = null) + { + if (func == null) + return _set + .Include(x => x.Bans) + .Include(x => x.Applicants) + .Include(x => x.Users) + .FirstOrDefault(x => x.Name.ToLowerInvariant() == name && x.Discrim == discrim); + + return func(_set).FirstOrDefault(x => x.Name == name && x.Discrim == discrim); + } + + public int GetNextDiscrim(string clubName) + { + return _set + .Where(x => x.Name.ToLowerInvariant() == clubName.ToLowerInvariant()) + .Max(x => x.Discrim) + 1; + } + + public ClubInfo GetByMember(ulong userId, Func, IQueryable> func = null) + { + if (func == null) + return _set.Include(x => x.Users) + .Include(x => x.Bans) + .Include(x => x.Applicants) + .FirstOrDefault(x => x.Users.Any(y => y.UserId == userId)); + + return func(_set).FirstOrDefault(x => x.Users.Any(y => y.UserId == userId)); + } + + public ClubInfo[] GetClubLeaderboardPage(int page) + { + return _set.OrderBy(x => x.Xp) + .Skip(page * 9) + .Take(9) + .ToArray(); + } + } +} diff --git a/src/NadekoBot/Services/Database/Repositories/Impl/DiscordUserRepository.cs b/src/NadekoBot/Services/Database/Repositories/Impl/DiscordUserRepository.cs index e61c2d0e..6fa22ebc 100644 --- a/src/NadekoBot/Services/Database/Repositories/Impl/DiscordUserRepository.cs +++ b/src/NadekoBot/Services/Database/Repositories/Impl/DiscordUserRepository.cs @@ -15,7 +15,15 @@ namespace NadekoBot.Services.Database.Repositories.Impl { DiscordUser toReturn; - toReturn = _set.FirstOrDefault(u => u.UserId == original.Id); + toReturn = _set.Include(x => x.Club) + .FirstOrDefault(u => u.UserId == original.Id); + + if (toReturn != null) + { + toReturn.AvatarId = original.AvatarId; + toReturn.Username = original.Username; + toReturn.Discriminator = original.Discriminator; + } if (toReturn == null) _set.Add(toReturn = new DiscordUser() @@ -24,6 +32,7 @@ namespace NadekoBot.Services.Database.Repositories.Impl Discriminator = original.Discriminator, UserId = original.Id, Username = original.Username, + Club = null, }); return toReturn; diff --git a/src/NadekoBot/Services/Database/Repositories/Impl/XpRepository.cs b/src/NadekoBot/Services/Database/Repositories/Impl/XpRepository.cs index 6326587d..ddd5a8e2 100644 --- a/src/NadekoBot/Services/Database/Repositories/Impl/XpRepository.cs +++ b/src/NadekoBot/Services/Database/Repositories/Impl/XpRepository.cs @@ -2,6 +2,7 @@ using System.Linq; using Microsoft.EntityFrameworkCore; +//todo add pagination to .lb namespace NadekoBot.Services.Database.Repositories.Impl { public class XpRepository : Repository, IXpRepository @@ -12,7 +13,7 @@ namespace NadekoBot.Services.Database.Repositories.Impl public UserXpStats GetOrCreateUser(ulong guildId, ulong userId) { - var usr = _set.FirstOrDefault(x => x.UserId == userId); + var usr = _set.FirstOrDefault(x => x.UserId == userId && x.GuildId == guildId); if (usr == null) { @@ -20,12 +21,57 @@ namespace NadekoBot.Services.Database.Repositories.Impl { Xp = 0, UserId = userId, - NotifyOnLevelUp = false, + NotifyOnLevelUp = XpNotificationType.None, GuildId = guildId, }); } return usr; } + + public int GetTotalUserXp(ulong userId) + { + return _set.Where(x => x.UserId == userId).Sum(x => x.Xp); + } + + public UserXpStats[] GetUsersFor(ulong guildId, int page) + { + return _set.Where(x => x.GuildId == guildId) + .OrderByDescending(x => x.Xp + x.AwardedXp) + .Skip(page * 9) + .Take(9) + .ToArray(); + } + + public int GetUserGlobalRanking(ulong userId) + { + return _set + .GroupBy(x => x.UserId) + .Count(x => x.Sum(y => y.Xp) > _set + .Where(y => y.UserId == userId) + .Sum(y => y.Xp)) + 1; + } + + public int GetUserGuildRanking(ulong userId, ulong guildId) + { + return _set + .Where(x => x.GuildId == guildId) + .Count(x => x.Xp > (_set + .Where(y => y.UserId == userId && y.GuildId == guildId) + .Sum(y => y.Xp))) + 1; + } + + public (ulong UserId, int TotalXp)[] GetUsersFor(int page) + { + return (from orduser in _set + group orduser by orduser.UserId into g + orderby g.Sum(x => x.Xp) descending + select new { UserId = g.Key, TotalXp = g.Sum(x => x.Xp) }) + .Skip(page * 9) + .Take(9) + .AsEnumerable() + .Select(x => (x.UserId, x.TotalXp)) + .ToArray(); + } } -} +} \ No newline at end of file diff --git a/src/NadekoBot/Services/Database/UnitOfWork.cs b/src/NadekoBot/Services/Database/UnitOfWork.cs index 3b450f86..27a43b13 100644 --- a/src/NadekoBot/Services/Database/UnitOfWork.cs +++ b/src/NadekoBot/Services/Database/UnitOfWork.cs @@ -60,6 +60,9 @@ namespace NadekoBot.Services.Database private IXpRepository _xp; public IXpRepository Xp => _xp ?? (_xp = new XpRepository(_context)); + private IClubRepository _clubs; + public IClubRepository Clubs => _clubs ?? (_clubs = new ClubRepository(_context)); + public UnitOfWork(NadekoContext context) { _context = context; diff --git a/src/NadekoBot/Services/DbService.cs b/src/NadekoBot/Services/DbService.cs index 6e9532ca..5bc9f6bf 100644 --- a/src/NadekoBot/Services/DbService.cs +++ b/src/NadekoBot/Services/DbService.cs @@ -1,11 +1,13 @@ using Microsoft.EntityFrameworkCore; using NadekoBot.Services.Database; +using System.Linq; namespace NadekoBot.Services { public class DbService { private readonly DbContextOptions options; + private readonly DbContextOptions migrateOptions; private readonly string _connectionString; @@ -15,25 +17,23 @@ namespace NadekoBot.Services var optionsBuilder = new DbContextOptionsBuilder(); optionsBuilder.UseSqlite(creds.Db.ConnectionString); options = optionsBuilder.Options; - //switch (_creds.Db.Type.ToUpperInvariant()) - //{ - // case "SQLITE": - // dbType = typeof(NadekoSqliteContext); - // break; - // //case "SQLSERVER": - // // dbType = typeof(NadekoSqlServerContext); - // // break; - // default: - // break; - //} + optionsBuilder = new DbContextOptionsBuilder(); + optionsBuilder.UseSqlite(creds.Db.ConnectionString, x => x.SuppressForeignKeyEnforcement()); + migrateOptions = optionsBuilder.Options; } public NadekoContext GetDbContext() { var context = new NadekoContext(options); + if (context.Database.GetPendingMigrations().Any()) + { + var mContext = new NadekoContext(migrateOptions); + mContext.Database.Migrate(); + mContext.SaveChanges(); + mContext.Dispose(); + } context.Database.SetCommandTimeout(60); - context.Database.Migrate(); context.EnsureSeedData(); //set important sqlite stuffs diff --git a/src/NadekoBot/Services/IImagesService.cs b/src/NadekoBot/Services/IImagesService.cs index 08d8f504..0b2b94f2 100644 --- a/src/NadekoBot/Services/IImagesService.cs +++ b/src/NadekoBot/Services/IImagesService.cs @@ -17,6 +17,8 @@ namespace NadekoBot.Services ImmutableArray WifeMatrix { get; } ImmutableArray RategirlDot { get; } + ImmutableArray XpCard { get; } + void Reload(); } } diff --git a/src/NadekoBot/Services/Impl/GoogleApiService.cs b/src/NadekoBot/Services/Impl/GoogleApiService.cs index ec09f169..45727fba 100644 --- a/src/NadekoBot/Services/Impl/GoogleApiService.cs +++ b/src/NadekoBot/Services/Impl/GoogleApiService.cs @@ -377,8 +377,7 @@ namespace NadekoBot.Services.Impl private string ConvertToLanguageCode(string language) { - string mode; - _languageDictionary.TryGetValue(language, out mode); + _languageDictionary.TryGetValue(language, out var mode); return mode; } } diff --git a/src/NadekoBot/Services/Impl/ImagesService.cs b/src/NadekoBot/Services/Impl/ImagesService.cs index 7153328f..3bb0bd2e 100644 --- a/src/NadekoBot/Services/Impl/ImagesService.cs +++ b/src/NadekoBot/Services/Impl/ImagesService.cs @@ -25,6 +25,8 @@ namespace NadekoBot.Services.Impl private const string _wifeMatrixPath = _basePath + "rategirl/wifematrix.png"; private const string _rategirlDot = _basePath + "rategirl/dot.png"; + private const string _xpCardPath = _basePath + "xp/xp.png"; + public ImmutableArray Heads { get; private set; } public ImmutableArray Tails { get; private set; } @@ -40,6 +42,8 @@ namespace NadekoBot.Services.Impl public ImmutableArray WifeMatrix { get; private set; } public ImmutableArray RategirlDot { get; private set; } + public ImmutableArray XpCard { get; private set; } + public ImagesService() { _log = LogManager.GetCurrentClassLogger(); @@ -76,6 +80,8 @@ namespace NadekoBot.Services.Impl WifeMatrix = File.ReadAllBytes(_wifeMatrixPath).ToImmutableArray(); RategirlDot = File.ReadAllBytes(_rategirlDot).ToImmutableArray(); + + XpCard = File.ReadAllBytes(_xpCardPath).ToImmutableArray(); } catch (Exception ex) { diff --git a/src/NadekoBot/Services/Impl/StatsService.cs b/src/NadekoBot/Services/Impl/StatsService.cs index 335db6b8..7e5f560d 100644 --- a/src/NadekoBot/Services/Impl/StatsService.cs +++ b/src/NadekoBot/Services/Impl/StatsService.cs @@ -20,7 +20,7 @@ namespace NadekoBot.Services.Impl private readonly IBotCredentials _creds; private readonly DateTime _started; - public const string BotVersion = "1.7"; + public const string BotVersion = "1.8"; public string Author => "Kwoth#2560"; public string Library => "Discord.Net"; diff --git a/src/NadekoBot/_Extensions/Extensions.cs b/src/NadekoBot/_Extensions/Extensions.cs index c901afaf..84901d21 100644 --- a/src/NadekoBot/_Extensions/Extensions.cs +++ b/src/NadekoBot/_Extensions/Extensions.cs @@ -132,7 +132,7 @@ namespace NadekoBot.Extensions public static string ToJson(this T any, Formatting formatting = Formatting.Indented) => JsonConvert.SerializeObject(any, formatting); - public static Stream ToStream(this ImageSharp.Image img) + public static MemoryStream ToStream(this ImageSharp.Image img) { var imageStream = new MemoryStream(); img.SaveAsPng(imageStream); diff --git a/src/NadekoBot/_Extensions/IMessageChannelExtensions.cs b/src/NadekoBot/_Extensions/IMessageChannelExtensions.cs index 806ec904..992d7453 100644 --- a/src/NadekoBot/_Extensions/IMessageChannelExtensions.cs +++ b/src/NadekoBot/_Extensions/IMessageChannelExtensions.cs @@ -10,7 +10,7 @@ namespace NadekoBot.Extensions public static class IMessageChannelExtensions { public static Task EmbedAsync(this IMessageChannel ch, EmbedBuilder embed, string msg = "") - => ch.SendMessageAsync(msg, embed: embed); + => ch.SendMessageAsync(msg, embed: embed.Build()); public static Task SendErrorAsync(this IMessageChannel ch, string title, string error, string url = null, string footer = null) { @@ -20,11 +20,11 @@ namespace NadekoBot.Extensions eb.WithUrl(url); if (!string.IsNullOrWhiteSpace(footer)) eb.WithFooter(efb => efb.WithText(footer)); - return ch.SendMessageAsync("", embed: eb); + return ch.SendMessageAsync("", embed: eb.Build()); } public static Task SendErrorAsync(this IMessageChannel ch, string error) - => ch.SendMessageAsync("", embed: new EmbedBuilder().WithErrorColor().WithDescription(error)); + => ch.SendMessageAsync("", embed: new EmbedBuilder().WithErrorColor().WithDescription(error).Build()); public static Task SendConfirmAsync(this IMessageChannel ch, string title, string text, string url = null, string footer = null) { @@ -34,11 +34,11 @@ namespace NadekoBot.Extensions eb.WithUrl(url); if (!string.IsNullOrWhiteSpace(footer)) eb.WithFooter(efb => efb.WithText(footer)); - return ch.SendMessageAsync("", embed: eb); + return ch.SendMessageAsync("", embed: eb.Build()); } public static Task SendConfirmAsync(this IMessageChannel ch, string text) - => ch.SendMessageAsync("", embed: new EmbedBuilder().WithOkColor().WithDescription(text)); + => ch.SendMessageAsync("", embed: new EmbedBuilder().WithOkColor().WithDescription(text).Build()); public static Task SendTableAsync(this IMessageChannel ch, string seed, IEnumerable items, Func howToPrint, int columns = 3) { diff --git a/src/NadekoBot/_Extensions/IUserExtensions.cs b/src/NadekoBot/_Extensions/IUserExtensions.cs index d1183e19..89650f21 100644 --- a/src/NadekoBot/_Extensions/IUserExtensions.cs +++ b/src/NadekoBot/_Extensions/IUserExtensions.cs @@ -1,4 +1,5 @@ using Discord; +using NadekoBot.Services.Database.Models; using System; using System.IO; using System.Threading.Tasks; @@ -8,14 +9,14 @@ namespace NadekoBot.Extensions public static class IUserExtensions { public static async Task SendConfirmAsync(this IUser user, string text) - => await (await user.GetOrCreateDMChannelAsync()).SendMessageAsync("", embed: new EmbedBuilder().WithOkColor().WithDescription(text)); + => await (await user.GetOrCreateDMChannelAsync()).SendMessageAsync("", embed: new EmbedBuilder().WithOkColor().WithDescription(text).Build()); public static async Task SendConfirmAsync(this IUser user, string title, string text, string url = null) { var eb = new EmbedBuilder().WithOkColor().WithDescription(text); if (url != null && Uri.IsWellFormedUriString(url, UriKind.Absolute)) eb.WithUrl(url); - return await (await user.GetOrCreateDMChannelAsync()).SendMessageAsync("", embed: eb); + return await (await user.GetOrCreateDMChannelAsync()).SendMessageAsync("", embed: eb.Build()); } public static async Task SendErrorAsync(this IUser user, string title, string error, string url = null) @@ -23,11 +24,11 @@ namespace NadekoBot.Extensions var eb = new EmbedBuilder().WithErrorColor().WithDescription(error); if (url != null && Uri.IsWellFormedUriString(url, UriKind.Absolute)) eb.WithUrl(url); - return await (await user.GetOrCreateDMChannelAsync()).SendMessageAsync("", embed: eb); + return await (await user.GetOrCreateDMChannelAsync()).SendMessageAsync("", embed: eb.Build()); } public static async Task SendErrorAsync(this IUser user, string error) - => await (await user.GetOrCreateDMChannelAsync()).SendMessageAsync("", embed: new EmbedBuilder().WithErrorColor().WithDescription(error)); + => await (await user.GetOrCreateDMChannelAsync()).SendMessageAsync("", embed: new EmbedBuilder().WithErrorColor().WithDescription(error).Build()); public static async Task SendFileAsync(this IUser user, string filePath, string caption = null, string text = null, bool isTTS = false) => await (await user.GetOrCreateDMChannelAsync().ConfigureAwait(false)).SendFileAsync(File.Open(filePath, FileMode.Open), caption ?? "x", text, isTTS).ConfigureAwait(false); @@ -39,5 +40,10 @@ namespace NadekoBot.Extensions usr.AvatarId.StartsWith("a_") ? $"{DiscordConfig.CDNUrl}avatars/{usr.Id}/{usr.AvatarId}.gif" : usr.GetAvatarUrl(ImageFormat.Auto); + + public static string RealAvatarUrl(this DiscordUser usr) => + usr.AvatarId.StartsWith("a_") + ? $"{DiscordConfig.CDNUrl}avatars/{usr.UserId}/{usr.AvatarId}.gif" + : $"{DiscordConfig.CDNUrl}avatars/{usr.UserId}/{usr.AvatarId}.png"; } } diff --git a/src/NadekoBot/_strings/ResponseStrings.en-US.json b/src/NadekoBot/_strings/ResponseStrings.en-US.json index 2ccc0b70..1ee47eed 100644 --- a/src/NadekoBot/_strings/ResponseStrings.en-US.json +++ b/src/NadekoBot/_strings/ResponseStrings.en-US.json @@ -833,5 +833,40 @@ "xp_server_is_excluded": "This server is excluded.", "xp_server_is_not_excluded": "This server is not excluded.", "xp_excluded_roles": "Excluded Roles", - "xp_excluded_channels": "Excluded Channels" + "xp_excluded_channels": "Excluded Channels", + "xp_level_up_channel": "Congratulations {0}, You've reached level {1}!", + "xp_level_up_dm": "Congratulations {0}, You've reached level {1} on {2} server!", + "xp_level_up_global": "Congratulations {0}, You've reached global level {1}!", + "xp_role_reward_cleared": "Level {0} will no longer reward a role.", + "xp_role_reward_added": "Users who reach level {0} will receive {1} role.", + "xp_role_rewards": "Role Rewards", + "xp_level_x": "Level {0}", + "xp_no_role_rewards": "No role reward on this page.", + "xp_server_leaderboard": "Server XP Leaderboard", + "xp_global_leaderboard": "Global XP Leaderboard", + "xp_modified": "Modified server XP of the user {0} by {1}", + "xp_club_create_error": "Failed creating the club. Make sure you're above level 5 and not a member of a club already.", + "xp_club_created": "Club {0} successfully created!", + "xp_club_not_exists": "That club doesn't exist.", + "xp_club_applied": "You've applied for membership in {0} club.", + "xp_club_apply_error": "Error applying. You are either already a member of the club, or you don't meet the minimum level requirement, or you've been banned from this one.", + "xp_club_accepted": "Accepted user {0} to the club.", + "xp_club_accept_error": "User not found", + "xp_club_left": "You've left the club.", + "xp_club_not_in_club": "You are not in a club, or you're trying to leave the club you're the owner of.", + "xp_club_user_kick": "User {0} kicked from {1} club.", + "xp_club_user_kick_fail": "Error kicking. You're either not the club owner, or that user is not in your club.", + "xp_club_user_banned": "Banned user {0} from {1} club.", + "xp_club_user_ban_fail": "Failed to ban. You're either not the club owner, or that user is not in your club or applied to it.", + "xp_club_user_unbanned": "Unbanned user {0} in {1} club.", + "xp_club_user_unban_fail": "Failed to unban. You're either not the club owner, or that user is not in your club or applied to it.", + "xp_club_level_req_changed": "Changed club's level requirement to {0}", + "xp_club_level_req_change_error": "Failed changing level requirement.", + "xp_club_disbanded": "Club {0} has been disbanded", + "xp_club_disband_error": "Error. You are either not in a club, or you are not the owner of your club.", + "xp_club_icon_error": "Not a valid image url or you're not the club owner.", + "xp_club_icon_set": "New club icon set.", + "xp_club_bans_for": "Bans for {0} club", + "xp_club_apps_for": "Applicants for {0} club", + "xp_club_leaderboard": "Club leaderboard - page {0}" } \ No newline at end of file diff --git a/src/NadekoBot/data/fonts/Uni Sans.ttf b/src/NadekoBot/data/fonts/Uni Sans.ttf new file mode 100644 index 0000000000000000000000000000000000000000..a7b39d02e5e7a21622648839874e4277332031d1 GIT binary patch literal 50856 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

Z;>+L8& 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 z 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 literal 0 HcmV?d00001 diff --git a/src/NadekoBot/data/images/xp/xp.png b/src/NadekoBot/data/images/xp/xp.png new file mode 100644 index 0000000000000000000000000000000000000000..28025180e2ed45fffe08372a846a9f5bcf1b5b1b GIT binary patch literal 294486 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�gWDMx(atllKvB7CY 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 literal 0 HcmV?d00001 From cd2a86c6244f61d19176ae57037330b3453a2862 Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Sun, 10 Sep 2017 04:00:56 +0200 Subject: [PATCH 277/346] Updated commandlist --- docs/Commands List.md | 45 +++++++++++++++++++++++++++++++++++++------ 1 file changed, 39 insertions(+), 6 deletions(-) diff --git a/docs/Commands List.md b/docs/Commands List.md index 0d784a0e..37e5085a 100644 --- a/docs/Commands List.md +++ b/docs/Commands List.md @@ -12,6 +12,7 @@ You can support the project on patreon: or paypa - [Pokemon](#pokemon) - [Searches](#searches) - [Utility](#utility) +- [Xp](#xp) ### Administration @@ -168,7 +169,8 @@ Commands and aliases | Description | Usage `.affinity` | Sets your affinity towards someone you want to be claimed by. Setting affinity will reduce their `.claim` on you by 20%. You can leave second argument empty to clear your affinity. 30 minutes cooldown. | `.affinity @MyHusband` or `.affinity` `.waifus` `.waifulb` | Shows top 9 waifus. You can specify another page to show other waifus. | `.waifus` or `.waifulb 3` `.waifuinfo` `.waifustats` | Shows waifu stats for a target person. Defaults to you if no user is provided. | `.waifuinfo @MyCrush` or `.waifuinfo` -`.wheeloffortune` `.wheel` | Bets a certain amount of currency on the wheel of fortune. Wheel can stop on one of many different multipliers. Won amount is rounded down to the nearest whole number. | `.wheel 5` +`.waifugift` `.gift` `.gifts` | Gift an item to someone. This will increase their waifu value by 50% of the gifted item's value if they don't have affinity set towards you, or 100% if they do. Provide no arguments to see a list of items that you can gift. | `.gifts` or `.gift Rose @Himesama` +`.wheeloffortune` `.wheel` | Bets a certain amount of currency on the wheel of fortune. Wheel can stop on one of many different multipliers. Won amount is rounded down to the nearest whole number. | `.wheel 10` ###### [Back to ToC](#table-of-contents) @@ -183,6 +185,7 @@ Commands and aliases | Description | Usage `.leet` | Converts a text to leetspeak with 6 (1-6) severity levels | `.leet 3 Hello` `.acrophobia` `.acro` | Starts an Acrophobia game. Second argument is optional round length in seconds. (default is 60) | `.acro` or `.acro 30` `.cleverbot` | Toggles cleverbot session. When enabled, the bot will reply to messages starting with bot mention in the server. Custom reactions starting with %mention% won't work if cleverbot is enabled. **Requires ManageMessages server permission.** | `.cleverbot` +`.connect4` `.con4` | Creates or joins an existing connect4 game. 2 players are required for the game. Objective of the game is to get 4 of your pieces next to each other in a vertical, horizontal or diagonal line. | `.connect4` `.hangmanlist` | Shows a list of hangman term types. | `.hangmanlist` `.hangman` | Starts a game of hangman in the channel. Use `.hangmanlist` to see a list of available term types. Defaults to 'all'. | `.hangman` or `.hangman movies` `.hangmanstop` | Stops the active hangman game on this channel if it exists. | `.hangmanstop` @@ -198,7 +201,7 @@ Commands and aliases | Description | Usage `.typeadd` | Adds a new article to the typing contest. **Bot owner only** | `.typeadd wordswords` `.typelist` | Lists added typing articles with their IDs. 15 per page. | `.typelist` or `.typelist 3` `.typedel` | Deletes a typing article given the ID. **Bot owner only** | `.typedel 3` -`.tictactoe` `.ttt` | Starts a game of tic tac toe. Another user must run the command in the same channel in order to accept the challenge. Use numbers 1-9 to play. 15 seconds per move. | >ttt +`.tictactoe` `.ttt` | Starts a game of tic tac toe. Another user must run the command in the same channel in order to accept the challenge. Use numbers 1-9 to play. 15 seconds per move. | .ttt `.trivia` `.t` | Starts a game of trivia. You can add `nohint` to prevent hints. First player to get to 10 points wins by default. You can specify a different number. 30 seconds per question. | `.t` or `.t 5 nohint` `.tl` | Shows a current trivia leaderboard. | `.tl` `.tq` | Quits current trivia after current question. | `.tq` @@ -222,6 +225,7 @@ Commands and aliases | Description | Usage ----------------|--------------|------- `.play` `.start` | If no arguments are specified, acts as `.next 1` command. If you specify a song number, it will jump to that song. If you specify a search query, acts as a `.q` command | `.play` or `.play 5` or `.play Dream Of Venice` `.queue` `.q` `.yq` | Queue a song using keywords or a link. Bot will join your voice channel. **You must be in a voice channel**. | `.q Dream Of Venice` +`.queuenext` `.qn` | Works the same as `.queue` command, except it enqueues the new song after the current one. **You must be in a voice channel**. | `.qn Dream Of Venice` `.queuesearch` `.qs` `.yqs` | Search for top 5 youtube song result using keywords, and type the index of the song to play that song. Bot will join your voice channel. **You must be in a voice channel**. | `.qs Dream Of Venice` `.listqueue` `.lq` | Lists 10 currently queued songs per page. Default page is 1. | `.lq` or `.lq 2` `.next` `.n` | Goes to the next song in the queue. You have to be in the same voice channel as the bot. You can skip multiple songs, but in that case songs will not be requeued if .rcs or .rpl is enabled. | `.n` or `.n 5` @@ -236,6 +240,7 @@ Commands and aliases | Description | Usage `.save` | Saves a playlist under a certain name. Playlist name must be no longer than 20 characters and must not contain dashes. | `.save classical1` `.load` | Loads a saved playlist using its ID. Use `.pls` to list all saved playlists and `.save` to save new ones. | `.load 5` `.fairplay` `.fp` | Toggles fairplay. While enabled, the bot will prioritize songs from users who didn't have their song recently played instead of the song's position in the queue. | `.fp` +`.songautodelete` `.sad` | Toggles whether the song should be automatically removed from the music queue when it finishes playing. | `.sad` `.soundcloudqueue` `.sq` | Queue a soundcloud song using keywords. Bot will join your voice channel. **You must be in a voice channel**. | `.sq Dream Of Venice` `.soundcloudpl` `.scpl` | Queue a Soundcloud playlist using a link. | `.scpl soundcloudseturl` `.nowplaying` `.np` | Shows the song that the bot is currently playing. | `.np` @@ -371,11 +376,11 @@ Commands and aliases | Description | Usage `.place` | Shows a placeholder image of a given tag. Use `.placelist` to see all available tags. You can specify the width and height of the image as the last two optional arguments. | `.place Cage` or `.place steven 500 400` `.pokemon` `.poke` | Searches for a pokemon. | `.poke Sylveon` `.pokemonability` `.pokeab` | Searches for a pokemon ability. | `.pokeab overgrow` -`.hitbox` `.hb` | Notifies this channel when a certain user starts streaming. **Requires ManageMessages server permission.** | `.hitbox SomeStreamer` +`.smashcast` `.hb` | Notifies this channel when a certain user starts streaming. **Requires ManageMessages server permission.** | `.smashcast SomeStreamer` `.twitch` `.tw` | Notifies this channel when a certain user starts streaming. **Requires ManageMessages server permission.** | `.twitch SomeStreamer` -`.beam` `.bm` | Notifies this channel when a certain user starts streaming. **Requires ManageMessages server permission.** | `.beam SomeStreamer` +`.mixer` `.bm` | Notifies this channel when a certain user starts streaming. **Requires ManageMessages server permission.** | `.mixer SomeStreamer` `.liststreams` `.ls` | Lists all streams you are following on this server. | `.ls` -`.removestream` `.rms` | Removes notifications of a certain streamer from a certain platform on this channel. **Requires ManageMessages server permission.** | `.rms Twitch SomeGuy` or `.rms Beam SomeOtherGuy` +`.removestream` `.rms` | Removes notifications of a certain streamer from a certain platform on this channel. **Requires ManageMessages server permission.** | `.rms Twitch SomeGuy` or `.rms mixer SomeOtherGuy` `.checkstream` `.cs` | Checks if a user is online on a certain streaming platform. | `.cs twitch MyFavStreamer` `.translate` `.trans` | Translates from>to text. From the given language to the destination language. | `.trans en>fr Hello` `.autotrans` `.at` | Starts automatic translation of all messages by users who set their `.atl` in this channel. You can set "del" argument to automatically delete all translated user messages. **Requires Administrator server permission.** **Bot owner only** | `.at` or `.at del` @@ -388,7 +393,6 @@ Commands and aliases | Description | Usage ### Utility Commands and aliases | Description | Usage ----------------|--------------|------- -`.rotaterolecolor` `.rrc` | Rotates a roles color on an interval with a list of supplied colors. First argument is interval in seconds (Minimum 60). Second argument is a role, followed by a space-separated list of colors in hex. Provide a rolename with a 0 interval to disable. **Requires ManageRoles server permission.** **Bot owner only** | `.rrc 60 MyLsdRole #ff0000 #00ff00 #0000ff` or `.rrc 0 MyLsdRole` `.togethertube` `.totube` | Creates a new room on and shows the link in the chat. | `.totube` `.whosplaying` `.whpl` | Shows a list of users who are playing the specified game. | `.whpl Overwatch` `.inrole` | Lists every person from the specified role on this server. You can use role ID, role name. | `.inrole Some Role` @@ -436,3 +440,32 @@ Commands and aliases | Description | Usage `.convertlist` | List of the convertible dimensions and currencies. | `.convertlist` `.convert` | Convert quantities. Use `.convertlist` to see supported dimensions and currencies. | `.convert m km 1000` `.verboseerror` `.ve` | Toggles whether the bot should print command errors when a command is incorrectly used. **Requires ManageMessages server permission.** | `.ve` + +###### [Back to ToC](#table-of-contents) + +### Xp +Commands and aliases | Description | Usage +----------------|--------------|------- +`.experience` `.xp` | Shows your xp stats. Specify the user to show that user's stats instead. | `.xp` +`.xprolerewards` `.xprrs` | Shows currently set role rewards. | `.xprrs` +`.xprolereward` `.xprr` | Sets a role reward on a specified level. **Requires ManageRoles server permission.** | `.xprr 3 Social` +`.xpnotify` `.xpn` | Sets how the bot should notify you when you get a `server` or `global` level. You can set `dm` (for the bot to send a direct message), `channel` (to get notified in the channel you sent the last message in) or `none` to disable. | `.xpn global dm` `.xpn server channel` +`.xpexclude` `.xpex` | Exclude a user or a role from the xp system, or whole current server. **Requires Administrator server permission.** | `.xpex User @b1nzy` `.xpex Server` +`.xpexclusionlist` `.xpexl` | Shows the roles and channels excluded from the XP system on this server, as well as whether the whole server is excluded. | `.xpexl` +`.xpleaderboard` `.xplb` | Shows current server's xp leaderboard. | `.xplb` +`.xpgleaderboard` `.xpglb` | Shows current server's xp leaderboard. | `.xpglb` +`.xpadd` | Adds xp to a user on the server. This does not affect their global ranking. You can use negative values. **Requires Administrator server permission.** | `.xpadd 100 @b1nzy` +`.clubcreate` | Creates a club. You must be atleast level 5 and not be in the club already. | `.clubcreate b1nzy's friends` +`.clubicon` | Sets the club icon. | `.clubicon https://i.imgur.com/htfDMfU.png` +`.clubinfo` | Shows information about the club. | `.clubinfo b1nzy's friends#123` +`.clubbans` | Shows the list of users who have banned from your club. Paginated. You must be club owner to use this command. | `.clubbans 2` +`.clubapps` | Shows the list of users who have applied to your club. Paginated. You must be club owner to use this command. | `.clubapps 2` +`.clubapply` | Apply to join a club. You must meet that club's minimum level requirement, and not be on its ban list. | `.clubapply b1nzy's friends#123` +`.clubaccept` | Apply to join a club. You must meet that club's minimum level requirement, and not be on its ban list. | `.clubaccept b1nzy#1337` +`.clubleave` | Leaves the club you're currently in. | `.clubleave` +`.clubkick` | Kicks the user from the club. You must be the club owner. They will be able to apply again. | `.clubkick b1nzy#1337` +`.clubban` | Bans the user from the club. You must be the club owner. They will not be able to apply again. | `.clubban b1nzy#1337` +`.clubunban` | Unbans the previously banned user from the club. You must be the club owner. | `.clubunban b1nzy#1337` +`.clublevelreq` | Sets the club required level to apply to join the club. You must be club owner. You can't set this number below 5. | `.clublevelreq 7` +`.clubdisband` | Disbands the club you're the owner of. This action is irreversible. | `.clubdisband` +`.clublb` | Shows club rankings on the specified page. | `.clublb 2` From 9531fb7717b1a4602b4bface3a4be26c39c331eb Mon Sep 17 00:00:00 2001 From: numbermaniac Date: Sun, 10 Sep 2017 14:16:11 +1000 Subject: [PATCH 278/346] fix spelling of "client" in JSON Explanations --- docs/JSON Explanations.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/JSON Explanations.md b/docs/JSON Explanations.md index db9aa239..5a5e00e3 100644 --- a/docs/JSON Explanations.md +++ b/docs/JSON Explanations.md @@ -48,7 +48,7 @@ If you do not see `credentials.json` you will need to rename `credentials_exampl - Replace the **`12345678`** in this link: `https://discordapp.com/oauth2/authorize?client_id=`**`12345678`**`&scope=bot&permissions=66186303` with your `Client ID`. - The link should now look like this: -`https://discordapp.com/oauth2/authorize?client_id=`**`YOUR_CLENT_ID_HERE`**`&scope=bot&permissions=66186303` +`https://discordapp.com/oauth2/authorize?client_id=`**`YOUR_CLIENT_ID_HERE`**`&scope=bot&permissions=66186303` - Go to the newly created link and pick the server we created, and click `Authorize`. - The bot should have been added to your server. @@ -214,4 +214,4 @@ and that will save all the changes. [Google Console]: https://console.developers.google.com [DiscordApp]: https://discordapp.com/developers/applications/me -[Invite Guide]: http://discord.kongslien.net/guide.html \ No newline at end of file +[Invite Guide]: http://discord.kongslien.net/guide.html From 14490024eab2d8741dff68ee60bcf3755896eca9 Mon Sep 17 00:00:00 2001 From: shivaco Date: Sun, 10 Sep 2017 16:20:55 +0600 Subject: [PATCH 279/346] FIx of some mistypes --- docs/guides/Docker Guide.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/guides/Docker Guide.md b/docs/guides/Docker Guide.md index 7023f39f..e249cbff 100644 --- a/docs/guides/Docker Guide.md +++ b/docs/guides/Docker Guide.md @@ -10,7 +10,7 @@ For this guide we will be using the folder /nadeko as our config root folder. ```bash docker create --name=nadeko -v /nadeko/conf/:/root/nadeko -v /nadeko/data:/opt/NadekoBot/src/NadekoBot/bin/Release/netcoreapp1.1/data uirel/nadeko:1.4 ``` --If you are coming from a previous version of nadeko (the old docker) make sure your crednetials.json has been copied into this directory and is the only thing in this folder. +-If you are coming from a previous version of nadeko (the old docker) make sure your credentials.json has been copied into this directory and is the only thing in this folder. -If you are making a fresh install, create your credentials.json from the following guide and place it in the /nadeko folder [Nadeko JSON Guide](http://nadekobot.readthedocs.io/en/latest/JSON%20Explanations/) @@ -21,7 +21,7 @@ Next start the docker up with The docker will start and the log file will start scrolling past. Depending on hardware the bot start can take up to 5 minutes on a small DigitalOcean droplet. Once the log ends with "NadekoBot | Starting NadekoBot v1.0-rc2" the bot is ready and can be invited to your server. Ctrl+C at this point to stop viewing the logs. -After a few moments you should be able to invite Nadeko to your server. If you cannot check the log file for errors +After a few moments you should be able to invite Nadeko to your server. If you cannot check the log file for errors. ## Monitoring From af334a0b5c7715430a0c2709f482ed105e4b62fc Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Sun, 10 Sep 2017 17:04:32 +0200 Subject: [PATCH 280/346] fixed club creation bug --- .../Services/Database/Repositories/Impl/ClubRepository.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/NadekoBot/Services/Database/Repositories/Impl/ClubRepository.cs b/src/NadekoBot/Services/Database/Repositories/Impl/ClubRepository.cs index 21421bfa..adf34135 100644 --- a/src/NadekoBot/Services/Database/Repositories/Impl/ClubRepository.cs +++ b/src/NadekoBot/Services/Database/Repositories/Impl/ClubRepository.cs @@ -40,7 +40,9 @@ namespace NadekoBot.Services.Database.Repositories.Impl { return _set .Where(x => x.Name.ToLowerInvariant() == clubName.ToLowerInvariant()) - .Max(x => x.Discrim) + 1; + .Select(x => x.Discrim) + .DefaultIfEmpty() + .Max() + 1; } public ClubInfo GetByMember(ulong userId, Func, IQueryable> func = null) From 268f9b0448df419d07cc9a1c319a8dbfa30dd05f Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Sun, 10 Sep 2017 17:58:32 +0200 Subject: [PATCH 281/346] fixed .xpglb help string. Version upped to 1.8.1 --- src/NadekoBot/Resources/CommandStrings.resx | 2 +- src/NadekoBot/Services/Impl/StatsService.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/NadekoBot/Resources/CommandStrings.resx b/src/NadekoBot/Resources/CommandStrings.resx index 81133fdc..74bc5d06 100644 --- a/src/NadekoBot/Resources/CommandStrings.resx +++ b/src/NadekoBot/Resources/CommandStrings.resx @@ -3643,7 +3643,7 @@ `{0}xpglb` - Shows current server's xp leaderboard. + Shows the global xp leaderboard. xpadd diff --git a/src/NadekoBot/Services/Impl/StatsService.cs b/src/NadekoBot/Services/Impl/StatsService.cs index 7e5f560d..798e389a 100644 --- a/src/NadekoBot/Services/Impl/StatsService.cs +++ b/src/NadekoBot/Services/Impl/StatsService.cs @@ -20,7 +20,7 @@ namespace NadekoBot.Services.Impl private readonly IBotCredentials _creds; private readonly DateTime _started; - public const string BotVersion = "1.8"; + public const string BotVersion = "1.8.1"; public string Author => "Kwoth#2560"; public string Library => "Discord.Net"; From b9f22df756e992510489c1d661088c0c370d80d8 Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Sun, 10 Sep 2017 19:14:29 +0200 Subject: [PATCH 282/346] Fixed .xpex command help, .xpex channel will now default to current channel --- src/NadekoBot/Modules/Xp/Xp.cs | 5 ++++- src/NadekoBot/Resources/CommandStrings.resx | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/NadekoBot/Modules/Xp/Xp.cs b/src/NadekoBot/Modules/Xp/Xp.cs index 4362e06c..ad687792 100644 --- a/src/NadekoBot/Modules/Xp/Xp.cs +++ b/src/NadekoBot/Modules/Xp/Xp.cs @@ -130,8 +130,11 @@ namespace NadekoBot.Modules.Xp [NadekoCommand, Usage, Description, Aliases] [RequireUserPermission(GuildPermission.ManageChannels)] [RequireContext(ContextType.Guild)] - public async Task XpExclude(Channel _, [Remainder] ITextChannel channel) + public async Task XpExclude(Channel _, [Remainder] ITextChannel channel = null) { + if (channel == null) + channel = (ITextChannel)Context.Channel; + var ex = _service.ToggleExcludeChannel(Context.Guild.Id, channel.Id); await ReplyConfirmLocalized((ex ? "excluded" : "not_excluded"), Format.Bold(channel.ToString())).ConfigureAwait(false); diff --git a/src/NadekoBot/Resources/CommandStrings.resx b/src/NadekoBot/Resources/CommandStrings.resx index 74bc5d06..abe5f6cf 100644 --- a/src/NadekoBot/Resources/CommandStrings.resx +++ b/src/NadekoBot/Resources/CommandStrings.resx @@ -3595,7 +3595,7 @@ xpexclude xpex - `{0}xpex User @b1nzy` `{0}xpex Server` + `{0}xpex Role Excluded-Role` `{0}xpex Server` Exclude a user or a role from the xp system, or whole current server. From 6f12ad14782e157d180ba2b49cb6f933f5103e2d Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Sun, 10 Sep 2017 22:32:29 +0200 Subject: [PATCH 283/346] Docs updates, closes #1570 --- src/NadekoBot/Resources/CommandStrings.resx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/NadekoBot/Resources/CommandStrings.resx b/src/NadekoBot/Resources/CommandStrings.resx index abe5f6cf..60eb05c8 100644 --- a/src/NadekoBot/Resources/CommandStrings.resx +++ b/src/NadekoBot/Resources/CommandStrings.resx @@ -3625,7 +3625,7 @@ `{0}xprr 3 Social` - Sets a role reward on a specified level. + Sets a role reward on a specified level. Provide no role name in order to remove the role reward. xpleaderboard xplb From 2ab4274c22f949173340675d803d98e167ff1083 Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Sun, 10 Sep 2017 22:44:23 +0200 Subject: [PATCH 284/346] Don't lose ignored channels if updating .antiraid #1402 --- .../Administration/ProtectionCommands.cs | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/src/NadekoBot/Modules/Administration/ProtectionCommands.cs b/src/NadekoBot/Modules/Administration/ProtectionCommands.cs index 6bec27e5..d9a2cd54 100644 --- a/src/NadekoBot/Modules/Administration/ProtectionCommands.cs +++ b/src/NadekoBot/Modules/Administration/ProtectionCommands.cs @@ -29,21 +29,22 @@ namespace NadekoBot.Modules.Administration private string GetAntiSpamString(AntiSpamStats stats) { - var ignoredString = string.Join(", ", stats.AntiSpamSettings.IgnoredChannels.Select(c => $"<#{c.ChannelId}>")); + var settings = stats.AntiSpamSettings; + var ignoredString = string.Join(", ", settings.IgnoredChannels.Select(c => $"<#{c.ChannelId}>")); if (string.IsNullOrWhiteSpace(ignoredString)) ignoredString = "none"; string add = ""; - if (stats.AntiSpamSettings.Action == PunishmentAction.Mute - && stats.AntiSpamSettings.MuteTime > 0) + if (settings.Action == PunishmentAction.Mute + && settings.MuteTime > 0) { - add = " (" + stats.AntiSpamSettings.MuteTime + "s)"; + add = " (" + settings.MuteTime + "s)"; } return GetText("spam_stats", - Format.Bold(stats.AntiSpamSettings.MessageThreshold.ToString()), - Format.Bold(stats.AntiSpamSettings.Action.ToString() + add), + Format.Bold(settings.MessageThreshold.ToString()), + Format.Bold(settings.Action.ToString() + add), ignoredString); } @@ -174,11 +175,9 @@ namespace NadekoBot.Modules.Administration } }; - _service.AntiSpamGuilds.AddOrUpdate(Context.Guild.Id, stats, (key, old) => + stats = _service.AntiSpamGuilds.AddOrUpdate(Context.Guild.Id, stats, (key, old) => { - stats.AntiSpamSettings.MessageThreshold = messageCount; - stats.AntiSpamSettings.Action = action; - stats.AntiSpamSettings.MuteTime = time; + stats.AntiSpamSettings.IgnoredChannels = old.AntiSpamSettings.IgnoredChannels; return stats; }); From 771e0df06412e5c360f4020dfc9cd0a5f305c3c5 Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Sun, 10 Sep 2017 23:01:26 +0200 Subject: [PATCH 285/346] closes #1500, #1433 --- docs/Commands List.md | 6 +++--- .../Modules/Administration/SelfCommands.cs | 13 ------------- src/NadekoBot/Modules/Utility/InfoCommands.cs | 13 ++++++------- src/NadekoBot/Resources/CommandStrings.resx | 9 --------- 4 files changed, 9 insertions(+), 32 deletions(-) diff --git a/docs/Commands List.md b/docs/Commands List.md index 37e5085a..a72309e8 100644 --- a/docs/Commands List.md +++ b/docs/Commands List.md @@ -448,12 +448,12 @@ Commands and aliases | Description | Usage ----------------|--------------|------- `.experience` `.xp` | Shows your xp stats. Specify the user to show that user's stats instead. | `.xp` `.xprolerewards` `.xprrs` | Shows currently set role rewards. | `.xprrs` -`.xprolereward` `.xprr` | Sets a role reward on a specified level. **Requires ManageRoles server permission.** | `.xprr 3 Social` +`.xprolereward` `.xprr` | Sets a role reward on a specified level. Provide no role name in order to remove the role reward. **Requires ManageRoles server permission.** | `.xprr 3 Social` `.xpnotify` `.xpn` | Sets how the bot should notify you when you get a `server` or `global` level. You can set `dm` (for the bot to send a direct message), `channel` (to get notified in the channel you sent the last message in) or `none` to disable. | `.xpn global dm` `.xpn server channel` -`.xpexclude` `.xpex` | Exclude a user or a role from the xp system, or whole current server. **Requires Administrator server permission.** | `.xpex User @b1nzy` `.xpex Server` +`.xpexclude` `.xpex` | Exclude a user or a role from the xp system, or whole current server. **Requires Administrator server permission.** | `.xpex Role Excluded-Role` `.xpex Server` `.xpexclusionlist` `.xpexl` | Shows the roles and channels excluded from the XP system on this server, as well as whether the whole server is excluded. | `.xpexl` `.xpleaderboard` `.xplb` | Shows current server's xp leaderboard. | `.xplb` -`.xpgleaderboard` `.xpglb` | Shows current server's xp leaderboard. | `.xpglb` +`.xpgleaderboard` `.xpglb` | Shows the global xp leaderboard. | `.xpglb` `.xpadd` | Adds xp to a user on the server. This does not affect their global ranking. You can use negative values. **Requires Administrator server permission.** | `.xpadd 100 @b1nzy` `.clubcreate` | Creates a club. You must be atleast level 5 and not be in the club already. | `.clubcreate b1nzy's friends` `.clubicon` | Sets the club icon. | `.clubicon https://i.imgur.com/htfDMfU.png` diff --git a/src/NadekoBot/Modules/Administration/SelfCommands.cs b/src/NadekoBot/Modules/Administration/SelfCommands.cs index 9edc1684..622e1b6b 100644 --- a/src/NadekoBot/Modules/Administration/SelfCommands.cs +++ b/src/NadekoBot/Modules/Administration/SelfCommands.cs @@ -409,19 +409,6 @@ namespace NadekoBot.Modules.Administration await ReplyConfirmLocalized("message_sent").ConfigureAwait(false); } - [NadekoCommand, Usage, Description, Aliases] - [OwnerOnly] - public async Task Announce([Remainder] string message) - { - var channels = _client.Guilds.Select(g => g.DefaultChannel).ToArray(); - if (channels == null) - return; - await Task.WhenAll(channels.Where(c => c != null).Select(c => c.SendConfirmAsync(GetText("message_from_bo", Context.User.ToString()), message))) - .ConfigureAwait(false); - - await ReplyConfirmLocalized("message_sent").ConfigureAwait(false); - } - [NadekoCommand, Usage, Description, Aliases] [OwnerOnly] public async Task ReloadImages() diff --git a/src/NadekoBot/Modules/Utility/InfoCommands.cs b/src/NadekoBot/Modules/Utility/InfoCommands.cs index 0eb448f8..95a3f6b1 100644 --- a/src/NadekoBot/Modules/Utility/InfoCommands.cs +++ b/src/NadekoBot/Modules/Utility/InfoCommands.cs @@ -31,19 +31,18 @@ namespace NadekoBot.Modules.Utility { var channel = (ITextChannel)Context.Channel; guildName = guildName?.ToUpperInvariant(); - IGuild guild; + SocketGuild guild; if (string.IsNullOrWhiteSpace(guildName)) - guild = channel.Guild; + guild = (SocketGuild)channel.Guild; else guild = _client.Guilds.FirstOrDefault(g => g.Name.ToUpperInvariant() == guildName.ToUpperInvariant()); if (guild == null) return; - var ownername = await guild.GetUserAsync(guild.OwnerId); - var textchn = (await guild.GetTextChannelsAsync()).Count(); - var voicechn = (await guild.GetVoiceChannelsAsync()).Count(); + 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 users = await guild.GetUsersAsync().ConfigureAwait(false); var features = string.Join("\n", guild.Features); if (string.IsNullOrWhiteSpace(features)) features = "-"; @@ -52,7 +51,7 @@ namespace NadekoBot.Modules.Utility .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(users.Count.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)) diff --git a/src/NadekoBot/Resources/CommandStrings.resx b/src/NadekoBot/Resources/CommandStrings.resx index 60eb05c8..dceae623 100644 --- a/src/NadekoBot/Resources/CommandStrings.resx +++ b/src/NadekoBot/Resources/CommandStrings.resx @@ -774,15 +774,6 @@ `{0}donadd Donate Amount` - - announce - - - Sends a message to all servers' default channel that bot is connected to. - - - `{0}announce Useless spam` - savechat From 76249c5b29704c2f80c0b78a4e8124fa88e3af01 Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Sun, 10 Sep 2017 23:08:46 +0200 Subject: [PATCH 286/346] Made numbers 3 and 7 smaller in slot image because they were overflowing --- src/NadekoBot/data/images/slots/numbers/1.png | Bin 392 -> 419 bytes src/NadekoBot/data/images/slots/numbers/3.png | Bin 539 -> 529 bytes src/NadekoBot/data/images/slots/numbers/7.png | Bin 417 -> 431 bytes 3 files changed, 0 insertions(+), 0 deletions(-) diff --git a/src/NadekoBot/data/images/slots/numbers/1.png b/src/NadekoBot/data/images/slots/numbers/1.png index 4766b8541bee5c2fd6231871876cc6b42cf907c9..eeec3949f6be336e79596e4bd904265ec8860d4e 100644 GIT binary patch delta 375 zcmV--0f_#H1ET{WiBL{Q4GJ0x0000DNk~Le0000B0000L2nGNE0FZDL&5=Kx4pSkf0}!Bxts;fy9vApRIg?a@V{9aYrlo$zPs4xx7z{+Nf_)?{gCF zew?K{p!|GgG1d&c6b~}juy+CyVp&wO1 VaI=J2E_DC^002ovPDHLkV1jCk8E1rhMd$v%+ks zFm3?Rpair*grr|PDaL$omUDp$;RYZ-d~lRBe(j>X?ya+2 zJ5(4q0EdzzFqB-Bjsg=Okx2s@53h-d2Y5`o(KM#Peo!PX@qskFca|~)Chui$o#h*$ u!uY`}TR9}jw=T+I@9m`>ph7eQ3=9Cha&d8zdG1aC0000!1^@s6kZ=^uks%uj9{>O@9|3sofnSkEAb&PQX|MGF00E9k zL_t(IPqkA$OG7afy`mz{4ubtm?t9%uk%DwFFG)d*i-QP?i$6ej{R93AaS+?d;-qeZ zi~a-Kr8r1mlJ_bo4r9!VlV>ouSbb}8X;f9)H+pzpW z6<`n|bOf$xMvv^qnSb%4veCjL59Gd#%fbAeNHxCv#BB5}QaPiZYgxp=MHP z-y>H_x-DpX)NL~|Zx}B6@)J3!_#S17=hizCx4_uY?MA2sc7K(`ryd19Ye63rcMjuA z?3-9_QkUtn@8wr*QJ;3L1z9nSEGSNv+5$J?l4L|}_PKMRrPGY1WJGEBf;NAZ#{V_t z%`C8CWZ~y$m|2=tlD6bvU(Ph%#KShwa&xGy^uUy?oJVa*EFKCu=E+*iD? zc{@kOLzblwN-k&D2)qlV`)vPSk9K}-2k|iwwEp722mrtD;TT1c&I#iPOsB)>*+DYdfoQ7kUarBf?ge_hP4Ozx?bEj?i6eUBZ7 z(k{@2;&p-IO?u2~{>l|FS^KWF6f3bo2zvrK9^!cu;CWo>8d9pdIN$55})4P RsJQ?D002ovPDHLkV1mfB+ZX@< diff --git a/src/NadekoBot/data/images/slots/numbers/7.png b/src/NadekoBot/data/images/slots/numbers/7.png index 0ffc89b99c4419bb9665afbfeccbfc24d7a36b2e..c668c23f3ca7330245ff2720a7d6e79d7c858378 100644 GIT binary patch delta 366 zcmV-!0g?Wp1Fr*+7YY>!1^@s6kZ=^uks%uj9{>O@9|3sofnSkEAb&PQX|MGF00AsX zL_t(IPpy-?N&`U@hNsX%P!Pl-WV6Z6C7`X1s7r*MIkTM)klxD9#%Hkb0W5tA-#`#; z617lCg4l$)>=G19oyf#O7|ennoaW;&mw(P61LV*GzOtO&X`sDXS)hHx>8;`T49%G2 z_{2)qH-e3fq=TOlbbqLUJ!(nMKRamQW(zH%qSIW}c|9^IY5cBNAPvo%8y3i^+wH=y zALcEMYYOm#0jj+UQ)Ub+L5A)Q6R*Ndo3&(5CT|yBh52q4Rhof3TiGtX3iFZ1iI!ww zfc<^u_wA_eV!Kp=_Ph%75}_rPk$si54!rs&)Xv(9g`{gaIZN^?jNPrpE{$s!-^i;l z(N4 z{?=XfDIB}0K6~S)`Vc5L52}=b;j^of*lTwM^*1i6n&|koo0`T;S9t@VE%iVHra`sf z247udrQW!!TmUM54u2KG4L&%lD!q17xes*CC#Vo^08;$MUG>g8548_aA>4r0idF8u zb65Ka6~Ya^+si8eL*nXNcePhgA>81jsgy7EUJ)u&J)+~B2_D9>9LrO5a0%0&Qwn4ThJ9; Date: Sun, 10 Sep 2017 23:15:58 +0200 Subject: [PATCH 287/346] Possible trivia weirdness fix, #1522 --- .../Games/Common/Trivia/TriviaQuestion.cs | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/NadekoBot/Modules/Games/Common/Trivia/TriviaQuestion.cs b/src/NadekoBot/Modules/Games/Common/Trivia/TriviaQuestion.cs index 06a40dec..66268d4e 100644 --- a/src/NadekoBot/Modules/Games/Common/Trivia/TriviaQuestion.cs +++ b/src/NadekoBot/Modules/Games/Common/Trivia/TriviaQuestion.cs @@ -23,11 +23,13 @@ namespace NadekoBot.Modules.Games.Common.Trivia public string ImageUrl { get; set; } public string AnswerImageUrl { get; set; } public string Answer { get; set; } + public string CleanAnswer { get; } public TriviaQuestion(string q, string a, string c, string img = null, string answerImage = null) { this.Question = q; this.Answer = a; + this.CleanAnswer = Clean(a); this.Category = c; this.ImageUrl = img; this.AnswerImageUrl = answerImage ?? img; @@ -37,20 +39,20 @@ namespace NadekoBot.Modules.Games.Common.Trivia public bool IsAnswerCorrect(string guess) { - guess = CleanGuess(guess); if (Answer.Equals(guess)) { return true; } - Answer = CleanGuess(Answer); - guess = CleanGuess(guess); - if (Answer.Equals(guess)) + var cleanGuess = Clean(guess); + if (CleanAnswer.Equals(cleanGuess)) { return true; } - int levDistance = Answer.LevenshteinDistance(guess); - return JudgeGuess(Answer.Length, guess.Length, levDistance); + int levDistanceClean = CleanAnswer.LevenshteinDistance(cleanGuess); + int levDistanceNormal = Answer.LevenshteinDistance(guess); + return JudgeGuess(CleanAnswer.Length, cleanGuess.Length, levDistanceClean) + || JudgeGuess(Answer.Length, guess.Length, levDistanceNormal); } private bool JudgeGuess(int guessLength, int answerLength, int levDistance) @@ -68,7 +70,7 @@ namespace NadekoBot.Modules.Games.Common.Trivia return false; } - private string CleanGuess(string str) + private string Clean(string str) { str = " " + str.ToLower() + " "; str = Regex.Replace(str, "\\s+", " "); From 4adf85a9eb322c4fc47c675b0f0b3c225663e0e8 Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Sun, 10 Sep 2017 23:44:24 +0200 Subject: [PATCH 288/346] Possible fix for #1523, im not testing that though :D --- src/NadekoBot/Modules/NSFW/NSFW.cs | 12 +--- .../Searches/Common/SearchImageCacher.cs | 64 ++++++++++--------- .../Searches/Services/SearchesService.cs | 29 +++++++-- 3 files changed, 58 insertions(+), 47 deletions(-) diff --git a/src/NadekoBot/Modules/NSFW/NSFW.cs b/src/NadekoBot/Modules/NSFW/NSFW.cs index 522713dd..1858606c 100644 --- a/src/NadekoBot/Modules/NSFW/NSFW.cs +++ b/src/NadekoBot/Modules/NSFW/NSFW.cs @@ -4,7 +4,6 @@ using Newtonsoft.Json.Linq; using System; using System.Linq; using System.Threading.Tasks; -using System.Net.Http; using NadekoBot.Extensions; using System.Threading; using System.Collections.Concurrent; @@ -15,7 +14,6 @@ using NadekoBot.Modules.Searches.Common; using NadekoBot.Modules.Searches.Services; using NadekoBot.Modules.NSFW.Exceptions; -//todo static httpclient namespace NadekoBot.Modules.NSFW { public class NSFW : NadekoTopLevelModule @@ -160,10 +158,7 @@ namespace NadekoBot.Modules.NSFW try { JToken obj; - using (var http = new HttpClient()) - { - obj = JArray.Parse(await http.GetStringAsync($"http://api.oboobs.ru/boobs/{new NadekoRandom().Next(0, 10330)}").ConfigureAwait(false))[0]; - } + obj = JArray.Parse(await _service.Http.GetStringAsync($"http://api.oboobs.ru/boobs/{new NadekoRandom().Next(0, 10330)}").ConfigureAwait(false))[0]; await Context.Channel.SendMessageAsync($"http://media.oboobs.ru/{obj["preview"]}").ConfigureAwait(false); } catch (Exception ex) @@ -178,10 +173,7 @@ namespace NadekoBot.Modules.NSFW try { JToken obj; - using (var http = new HttpClient()) - { - obj = JArray.Parse(await http.GetStringAsync($"http://api.obutts.ru/butts/{new NadekoRandom().Next(0, 4335)}").ConfigureAwait(false))[0]; - } + obj = JArray.Parse(await _service.Http.GetStringAsync($"http://api.obutts.ru/butts/{new NadekoRandom().Next(0, 4335)}").ConfigureAwait(false))[0]; await Context.Channel.SendMessageAsync($"http://media.obutts.ru/{obj["preview"]}").ConfigureAwait(false); } catch (Exception ex) diff --git a/src/NadekoBot/Modules/Searches/Common/SearchImageCacher.cs b/src/NadekoBot/Modules/Searches/Common/SearchImageCacher.cs index 18bc2064..6ccd9522 100644 --- a/src/NadekoBot/Modules/Searches/Common/SearchImageCacher.cs +++ b/src/NadekoBot/Modules/Searches/Common/SearchImageCacher.cs @@ -20,18 +20,25 @@ namespace NadekoBot.Modules.Searches.Common private readonly SortedSet _cache; private readonly Logger _log; + private readonly HttpClient _http; public SearchImageCacher() { + _http = new HttpClient(); + _http.AddFakeHeaders(); + _log = LogManager.GetCurrentClassLogger(); _rng = new NadekoRandom(); _cache = new SortedSet(); } - public async Task GetImage(string tag, bool forceExplicit, DapiSearchType type) + public async Task GetImage(string tag, bool forceExplicit, DapiSearchType type, + HashSet blacklistedTags = null) { tag = tag?.ToLowerInvariant(); + blacklistedTags = blacklistedTags ?? new HashSet(); + if (type == DapiSearchType.E621) tag = tag?.Replace("yuri", "female/female"); @@ -63,6 +70,9 @@ namespace NadekoBot.Modules.Searches.Common else { var images = await DownloadImages(tag, forceExplicit, type).ConfigureAwait(false); + images = images + .Where(x => x.Tags.All(t => !blacklistedTags.Contains(t))) + .ToArray(); if (images.Length == 0) return null; var toReturn = images[_rng.Next(images.Length)]; @@ -116,48 +126,40 @@ namespace NadekoBot.Modules.Searches.Common website = $"https://yande.re/post.json?limit=100&tags={tag}"; break; } - - using (var http = new HttpClient()) - { - http.AddFakeHeaders(); - if (type == DapiSearchType.Konachan || type == DapiSearchType.Yandere || - type == DapiSearchType.E621 || type == DapiSearchType.Danbooru) - { - var data = await http.GetStringAsync(website).ConfigureAwait(false); - return JsonConvert.DeserializeObject(data) - .Where(x => x.File_Url != null) - .Select(x => new ImageCacherObject(x, type)) - .ToArray(); - } - - return (await LoadXmlAsync(website, type)).ToArray(); + if (type == DapiSearchType.Konachan || type == DapiSearchType.Yandere || + type == DapiSearchType.E621 || type == DapiSearchType.Danbooru) + { + var data = await _http.GetStringAsync(website).ConfigureAwait(false); + return JsonConvert.DeserializeObject(data) + .Where(x => x.File_Url != null) + .Select(x => new ImageCacherObject(x, type)) + .ToArray(); } + + return (await LoadXmlAsync(website, type)).ToArray(); } private async Task LoadXmlAsync(string website, DapiSearchType type) { var list = new List(); - using (var http = new HttpClient()) + using (var reader = XmlReader.Create(await _http.GetStreamAsync(website), new XmlReaderSettings() { - using (var reader = XmlReader.Create(await http.GetStreamAsync(website), new XmlReaderSettings() + Async = true, + })) + { + while (await reader.ReadAsync()) { - Async = true, - })) - { - while (await reader.ReadAsync()) + if (reader.NodeType == XmlNodeType.Element && + reader.Name == "post") { - if (reader.NodeType == XmlNodeType.Element && - reader.Name == "post") + list.Add(new ImageCacherObject(new DapiImageObject() { - list.Add(new ImageCacherObject(new DapiImageObject() - { - File_Url = reader["file_url"], - Tags = reader["tags"], - Rating = reader["rating"] ?? "e" + File_Url = reader["file_url"], + Tags = reader["tags"], + Rating = reader["rating"] ?? "e" - }, type)); - } + }, type)); } } } diff --git a/src/NadekoBot/Modules/Searches/Services/SearchesService.cs b/src/NadekoBot/Modules/Searches/Services/SearchesService.cs index 368c5618..e4a3e40b 100644 --- a/src/NadekoBot/Modules/Searches/Services/SearchesService.cs +++ b/src/NadekoBot/Modules/Searches/Services/SearchesService.cs @@ -14,11 +14,14 @@ using NadekoBot.Services.Database.Models; using System.Linq; using Microsoft.EntityFrameworkCore; using NadekoBot.Modules.NSFW.Exceptions; +using System.Net.Http; namespace NadekoBot.Modules.Searches.Services { public class SearchesService : INService { + public HttpClient Http { get; } + private readonly DiscordSocketClient _client; private readonly IGoogleApiService _google; private readonly DbService _db; @@ -41,6 +44,8 @@ namespace NadekoBot.Modules.Searches.Services public SearchesService(DiscordSocketClient client, IGoogleApiService google, DbService db, IEnumerable gcs) { + Http = new HttpClient(); + Http.AddFakeHeaders(); _client = client; _google = google; _db = db; @@ -128,14 +133,26 @@ namespace NadekoBot.Modules.Searches.Services public Task DapiSearch(string tag, DapiSearchType type, ulong? guild, bool isExplicit = false) { - if (guild.HasValue && GetBlacklistedTags(guild.Value) - .Any(x => tag.ToLowerInvariant().Contains(x))) + if (guild.HasValue) { - throw new TagBlacklistedException(); + var blacklistedTags = GetBlacklistedTags(guild.Value); + + if (blacklistedTags + .Any(x => tag.ToLowerInvariant().Contains(x))) + { + throw new TagBlacklistedException(); + } + + var cacher = _imageCacher.GetOrAdd(guild.Value, (key) => new SearchImageCacher()); + + return cacher.GetImage(tag, isExplicit, type, blacklistedTags); + } + else + { + var cacher = _imageCacher.GetOrAdd(guild ?? 0, (key) => new SearchImageCacher()); + + return cacher.GetImage(tag, isExplicit, type); } - var cacher = _imageCacher.GetOrAdd(guild ?? 0, (key) => new SearchImageCacher()); - - return cacher.GetImage(tag, isExplicit, type); } public HashSet GetBlacklistedTags(ulong guildId) From 1af4679d9e2815c583990a7dc7971d94f4e54f7e Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Sun, 10 Sep 2017 23:48:34 +0200 Subject: [PATCH 289/346] When you guess a letter in hangman, updated message will now correctly show previous guesses. closes #1541 --- src/NadekoBot/Modules/Games/HangmanCommands.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/NadekoBot/Modules/Games/HangmanCommands.cs b/src/NadekoBot/Modules/Games/HangmanCommands.cs index f7098f8e..4070bee1 100644 --- a/src/NadekoBot/Modules/Games/HangmanCommands.cs +++ b/src/NadekoBot/Modules/Games/HangmanCommands.cs @@ -113,7 +113,8 @@ namespace NadekoBot.Modules.Games private Task Hm_OnGuessSucceeded(Hangman game, string user, char guess) { - return Context.Channel.SendConfirmAsync($"Hangman Game ({game.TermType})", $"{user} guessed a letter `{guess}`!\n" + game.ScrambledWord + "\n" + game.GetHangman()); + return Context.Channel.SendConfirmAsync($"Hangman Game ({game.TermType})", $"{user} guessed a letter `{guess}`!\n" + game.ScrambledWord + "\n" + game.GetHangman(), + footer: string.Join(" ", game.PreviousGuesses)); } private Task Hm_OnGuessFailed(Hangman game, string user, char guess) From 3de9a40ffd00728b1544dc58de04c9264bdd168e Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Mon, 11 Sep 2017 00:01:31 +0200 Subject: [PATCH 290/346] Fixed catfact thanks to twindragon, closes #1547 --- src/NadekoBot/Modules/Searches/Searches.cs | 98 ++++++++++------------ 1 file changed, 42 insertions(+), 56 deletions(-) diff --git a/src/NadekoBot/Modules/Searches/Searches.cs b/src/NadekoBot/Modules/Searches/Searches.cs index eadcd2c7..0738414a 100644 --- a/src/NadekoBot/Modules/Searches/Searches.cs +++ b/src/NadekoBot/Modules/Searches/Searches.cs @@ -41,8 +41,7 @@ namespace NadekoBot.Modules.Searches return; string response; - using (var http = new HttpClient()) - response = await http.GetStringAsync($"http://api.openweathermap.org/data/2.5/weather?q={query}&appid=42cd627dd60debf25a5739e50a217d74&units=metric").ConfigureAwait(false); + response = await _service.Http.GetStringAsync($"http://api.openweathermap.org/data/2.5/weather?q={query}&appid=42cd627dd60debf25a5739e50a217d74&units=metric").ConfigureAwait(false); var data = JsonConvert.DeserializeObject(response); @@ -67,19 +66,17 @@ namespace NadekoBot.Modules.Searches if (string.IsNullOrWhiteSpace(arg) || string.IsNullOrWhiteSpace(_creds.GoogleApiKey)) return; - using (var http = new HttpClient()) - { - var res = await http.GetStringAsync($"https://maps.googleapis.com/maps/api/geocode/json?address={arg}&key={_creds.GoogleApiKey}").ConfigureAwait(false); - var obj = JsonConvert.DeserializeObject(res); + var res = await _service.Http.GetStringAsync($"https://maps.googleapis.com/maps/api/geocode/json?address={arg}&key={_creds.GoogleApiKey}").ConfigureAwait(false); + var obj = JsonConvert.DeserializeObject(res); - var currentSeconds = DateTime.UtcNow.UnixTimestamp(); - var timeRes = await http.GetStringAsync($"https://maps.googleapis.com/maps/api/timezone/json?location={obj.results[0].Geometry.Location.Lat},{obj.results[0].Geometry.Location.Lng}×tamp={currentSeconds}&key={_creds.GoogleApiKey}").ConfigureAwait(false); - var timeObj = JsonConvert.DeserializeObject(timeRes); + var currentSeconds = DateTime.UtcNow.UnixTimestamp(); + var timeRes = await _service.Http.GetStringAsync($"https://maps.googleapis.com/maps/api/timezone/json?location={obj.results[0].Geometry.Location.Lat},{obj.results[0].Geometry.Location.Lng}×tamp={currentSeconds}&key={_creds.GoogleApiKey}").ConfigureAwait(false); + var timeObj = JsonConvert.DeserializeObject(timeRes); - var time = DateTime.UtcNow.AddSeconds(timeObj.DstOffset + timeObj.RawOffset); + var time = DateTime.UtcNow.AddSeconds(timeObj.DstOffset + timeObj.RawOffset); + + await ReplyConfirmLocalized("time", Format.Bold(obj.results[0].FormattedAddress), Format.Code(time.ToString("HH:mm")), timeObj.TimeZoneName).ConfigureAwait(false); - await ReplyConfirmLocalized("time", Format.Bold(obj.results[0].FormattedAddress), Format.Code(time.ToString("HH:mm")), timeObj.TimeZoneName).ConfigureAwait(false); - } } [NadekoCommand, Usage, Description, Aliases] @@ -114,21 +111,15 @@ namespace NadekoBot.Modules.Searches [NadekoCommand, Usage, Description, Aliases] public async Task RandomCat() { - using (var http = new HttpClient()) - { - var res = JObject.Parse(await http.GetStringAsync("http://www.random.cat/meow").ConfigureAwait(false)); - await Context.Channel.SendMessageAsync(Uri.EscapeUriString(res["file"].ToString())).ConfigureAwait(false); - } + var res = JObject.Parse(await _service.Http.GetStringAsync("http://www.random.cat/meow").ConfigureAwait(false)); + await Context.Channel.SendMessageAsync(Uri.EscapeUriString(res["file"].ToString())).ConfigureAwait(false); } [NadekoCommand, Usage, Description, Aliases] public async Task RandomDog() { - using (var http = new HttpClient()) - { - await Context.Channel.SendMessageAsync("http://random.dog/" + await http.GetStringAsync("http://random.dog/woof") - .ConfigureAwait(false)).ConfigureAwait(false); - } + await Context.Channel.SendMessageAsync("http://random.dog/" + await _service.Http.GetStringAsync("http://random.dog/woof") + .ConfigureAwait(false)).ConfigureAwait(false); } [NadekoCommand, Usage, Description, Aliases] @@ -503,34 +494,32 @@ namespace NadekoBot.Modules.Searches if (string.IsNullOrWhiteSpace(word)) return; - using (var http = new HttpClient()) + var res = await _service.Http.GetStringAsync("http://api.pearson.com/v2/dictionaries/entries?headword=" + WebUtility.UrlEncode(word.Trim())).ConfigureAwait(false); + + var data = JsonConvert.DeserializeObject(res); + + var sense = data.Results.FirstOrDefault(x => x.Senses?[0].Definition != null)?.Senses[0]; + + if (sense?.Definition == null) { - var res = await http.GetStringAsync("http://api.pearson.com/v2/dictionaries/entries?headword=" + WebUtility.UrlEncode(word.Trim())).ConfigureAwait(false); - - var data = JsonConvert.DeserializeObject(res); - - var sense = data.Results.FirstOrDefault(x => x.Senses?[0].Definition != null)?.Senses[0]; - - if (sense?.Definition == null) - { - await ReplyErrorLocalized("define_unknown").ConfigureAwait(false); - return; - } - - var definition = sense.Definition.ToString(); - if (!(sense.Definition is string)) - definition = ((JArray)JToken.Parse(sense.Definition.ToString())).First.ToString(); - - var embed = new EmbedBuilder().WithOkColor() - .WithTitle(GetText("define") + " " + word) - .WithDescription(definition) - .WithFooter(efb => efb.WithText(sense.Gramatical_info?.type)); - - if (sense.Examples != null) - embed.AddField(efb => efb.WithName(GetText("example")).WithValue(sense.Examples.First().text)); - - await Context.Channel.EmbedAsync(embed).ConfigureAwait(false); + await ReplyErrorLocalized("define_unknown").ConfigureAwait(false); + return; } + + var definition = sense.Definition.ToString(); + if (!(sense.Definition is string)) + definition = ((JArray)JToken.Parse(sense.Definition.ToString())).First.ToString(); + + var embed = new EmbedBuilder().WithOkColor() + .WithTitle(GetText("define") + " " + word) + .WithDescription(definition) + .WithFooter(efb => efb.WithText(sense.Gramatical_info?.type)); + + if (sense.Examples != null) + embed.AddField(efb => efb.WithName(GetText("example")).WithValue(sense.Examples.First().text)); + + await Context.Channel.EmbedAsync(embed).ConfigureAwait(false); + } [NadekoCommand, Usage, Description, Aliases] @@ -577,15 +566,12 @@ namespace NadekoBot.Modules.Searches [NadekoCommand, Usage, Description, Aliases] public async Task Catfact() { - using (var http = new HttpClient()) - { - var response = await http.GetStringAsync("http://catfacts-api.appspot.com/api/facts").ConfigureAwait(false); - if (response == null) - return; + var response = await _service.Http.GetStringAsync("https://catfact.ninja/fact").ConfigureAwait(false); + if (response == null) + return; - var fact = JObject.Parse(response)["facts"][0].ToString(); - await Context.Channel.SendConfirmAsync("🐈" + GetText("catfact"), fact).ConfigureAwait(false); - } + var fact = JObject.Parse(response)["fact"].ToString(); + await Context.Channel.SendConfirmAsync("🐈" + GetText("catfact"), fact).ConfigureAwait(false); } [NadekoCommand, Usage, Description, Aliases] From 927e98514aad2927655caebfbd5057781eed9100 Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Mon, 11 Sep 2017 00:21:14 +0200 Subject: [PATCH 291/346] Fixed exclusion list after restarts, closes #1571 --- .../Modules/Games/Common/Trivia/TriviaGame.cs | 18 ++++++------- .../Modules/Xp/Services/XpService.cs | 25 +++++++++++++++++++ .../Impl/GuildConfigRepository.cs | 2 ++ 3 files changed, 36 insertions(+), 9 deletions(-) diff --git a/src/NadekoBot/Modules/Games/Common/Trivia/TriviaGame.cs b/src/NadekoBot/Modules/Games/Common/Trivia/TriviaGame.cs index 77714d58..abafdb67 100644 --- a/src/NadekoBot/Modules/Games/Common/Trivia/TriviaGame.cs +++ b/src/NadekoBot/Modules/Games/Common/Trivia/TriviaGame.cs @@ -27,11 +27,11 @@ namespace NadekoBot.Modules.Games.Common.Trivia public IGuild Guild { get; } public ITextChannel Channel { get; } - private int questionDurationMiliseconds { get; } = 30000; - private int hintTimeoutMiliseconds { get; } = 6000; + private readonly int _questionDurationMiliseconds = 30000; + private readonly int _hintTimeoutMiliseconds = 6000; public bool ShowHints { get; } public bool IsPokemon { get; } - private CancellationTokenSource triviaCancelSource { get; set; } + private CancellationTokenSource _triviaCancelSource; public TriviaQuestion CurrentQuestion { get; private set; } public HashSet OldQuestions { get; } = new HashSet(); @@ -71,7 +71,7 @@ namespace NadekoBot.Modules.Games.Common.Trivia while (!ShouldStopGame) { // reset the cancellation source - triviaCancelSource = new CancellationTokenSource(); + _triviaCancelSource = new CancellationTokenSource(); // load question CurrentQuestion = TriviaQuestionPool.Instance.GetRandomQuestion(OldQuestions, IsPokemon); @@ -118,7 +118,7 @@ namespace NadekoBot.Modules.Games.Common.Trivia try { //hint - await Task.Delay(hintTimeoutMiliseconds, triviaCancelSource.Token).ConfigureAwait(false); + await Task.Delay(_hintTimeoutMiliseconds, _triviaCancelSource.Token).ConfigureAwait(false); if (ShowHints) try { @@ -132,7 +132,7 @@ namespace NadekoBot.Modules.Games.Common.Trivia catch (Exception ex) { _log.Warn(ex); } //timeout - await Task.Delay(questionDurationMiliseconds - hintTimeoutMiliseconds, triviaCancelSource.Token).ConfigureAwait(false); + await Task.Delay(_questionDurationMiliseconds - _hintTimeoutMiliseconds, _triviaCancelSource.Token).ConfigureAwait(false); } catch (TaskCanceledException) { } //means someone guessed the answer @@ -142,7 +142,7 @@ namespace NadekoBot.Modules.Games.Common.Trivia GameActive = false; _client.MessageReceived -= PotentialGuess; } - if (!triviaCancelSource.IsCancellationRequested) + if (!_triviaCancelSource.IsCancellationRequested) { try { @@ -202,7 +202,7 @@ namespace NadekoBot.Modules.Games.Common.Trivia await _guessLock.WaitAsync().ConfigureAwait(false); try { - if (GameActive && CurrentQuestion.IsAnswerCorrect(umsg.Content) && !triviaCancelSource.IsCancellationRequested) + if (GameActive && CurrentQuestion.IsAnswerCorrect(umsg.Content) && !_triviaCancelSource.IsCancellationRequested) { Users.AddOrUpdate(guildUser, 1, (gu, old) => ++old); guess = true; @@ -210,7 +210,7 @@ namespace NadekoBot.Modules.Games.Common.Trivia } finally { _guessLock.Release(); } if (!guess) return; - triviaCancelSource.Cancel(); + _triviaCancelSource.Cancel(); if (Users[guildUser] == WinRequirement) diff --git a/src/NadekoBot/Modules/Xp/Services/XpService.cs b/src/NadekoBot/Modules/Xp/Services/XpService.cs index 52944064..11a7aaa9 100644 --- a/src/NadekoBot/Modules/Xp/Services/XpService.cs +++ b/src/NadekoBot/Modules/Xp/Services/XpService.cs @@ -78,6 +78,31 @@ namespace NadekoBot.Modules.Xp.Services _log = LogManager.GetCurrentClassLogger(); _strings = strings; + //load settings + _excludedChannels = allGuildConfigs + .ToDictionary( + x => x.GuildId, + x => new ConcurrentHashSet(x.XpSettings + .ExclusionList + .Where(ex => ex.ItemType == ExcludedItemType.Channel) + .Select(ex => ex.ItemId) + .Distinct())) + .ToConcurrent(); + + _excludedRoles = allGuildConfigs + .ToDictionary( + x => x.GuildId, + x => new ConcurrentHashSet(x.XpSettings + .ExclusionList + .Where(ex => ex.ItemType == ExcludedItemType.Role) + .Select(ex => ex.ItemId) + .Distinct())) + .ToConcurrent(); + + _excludedServers = new ConcurrentHashSet( + allGuildConfigs.Where(x => x.XpSettings.ServerExcluded) + .Select(x => x.GuildId)); + //todo 60 move to font provider or somethign _fonts = new FontCollection(); if (Directory.Exists("data/fonts")) diff --git a/src/NadekoBot/Services/Database/Repositories/Impl/GuildConfigRepository.cs b/src/NadekoBot/Services/Database/Repositories/Impl/GuildConfigRepository.cs index 1c296db7..720fd794 100644 --- a/src/NadekoBot/Services/Database/Repositories/Impl/GuildConfigRepository.cs +++ b/src/NadekoBot/Services/Database/Repositories/Impl/GuildConfigRepository.cs @@ -47,6 +47,8 @@ namespace NadekoBot.Services.Database.Repositories.Impl .Include(gc => gc.FollowedStreams) .Include(gc => gc.StreamRole) .Include(gc => gc.NsfwBlacklistedTags) + .Include(gc => gc.XpSettings) + .ThenInclude(x => x.ExclusionList) .ToList(); ///

From 6c3025ecf14726c334ab3d74d9663b8e99173b32 Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Mon, 11 Sep 2017 01:42:39 +0200 Subject: [PATCH 292/346] Potential xp null error fix --- src/NadekoBot/Modules/Xp/Services/XpService.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/NadekoBot/Modules/Xp/Services/XpService.cs b/src/NadekoBot/Modules/Xp/Services/XpService.cs index 11a7aaa9..1533e4b0 100644 --- a/src/NadekoBot/Modules/Xp/Services/XpService.cs +++ b/src/NadekoBot/Modules/Xp/Services/XpService.cs @@ -79,6 +79,7 @@ namespace NadekoBot.Modules.Xp.Services _strings = strings; //load settings + allGuildConfigs = allGuildConfigs.Where(x => x.XpSettings != null); _excludedChannels = allGuildConfigs .ToDictionary( x => x.GuildId, From 62c016c7cf9573b53d874e4c8cc643b124c29d66 Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Mon, 11 Sep 2017 01:49:51 +0200 Subject: [PATCH 293/346] 1.8.2 --- src/NadekoBot/Services/Impl/StatsService.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/NadekoBot/Services/Impl/StatsService.cs b/src/NadekoBot/Services/Impl/StatsService.cs index 798e389a..ba6db3cd 100644 --- a/src/NadekoBot/Services/Impl/StatsService.cs +++ b/src/NadekoBot/Services/Impl/StatsService.cs @@ -20,7 +20,7 @@ namespace NadekoBot.Services.Impl private readonly IBotCredentials _creds; private readonly DateTime _started; - public const string BotVersion = "1.8.1"; + public const string BotVersion = "1.8.2"; public string Author => "Kwoth#2560"; public string Library => "Discord.Net"; From 1a858250493e9640734ba34de16f1328a20bad13 Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Mon, 11 Sep 2017 19:12:49 +0200 Subject: [PATCH 294/346] Fixed trivia, closes #1578 --- src/NadekoBot/Modules/Games/Common/Trivia/TriviaQuestion.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/NadekoBot/Modules/Games/Common/Trivia/TriviaQuestion.cs b/src/NadekoBot/Modules/Games/Common/Trivia/TriviaQuestion.cs index 66268d4e..3533aa9e 100644 --- a/src/NadekoBot/Modules/Games/Common/Trivia/TriviaQuestion.cs +++ b/src/NadekoBot/Modules/Games/Common/Trivia/TriviaQuestion.cs @@ -23,13 +23,13 @@ namespace NadekoBot.Modules.Games.Common.Trivia public string ImageUrl { get; set; } public string AnswerImageUrl { get; set; } public string Answer { get; set; } - public string CleanAnswer { get; } + private string _cleanAnswer; + public string CleanAnswer => _cleanAnswer ?? (_cleanAnswer = Clean(Answer)); public TriviaQuestion(string q, string a, string c, string img = null, string answerImage = null) { this.Question = q; this.Answer = a; - this.CleanAnswer = Clean(a); this.Category = c; this.ImageUrl = img; this.AnswerImageUrl = answerImage ?? img; From cdf15d6c0104e2c2c6ce46a5adedab0bfaddcb71 Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Mon, 11 Sep 2017 19:20:18 +0200 Subject: [PATCH 295/346] Fixed club leaderboard --- .../Services/Database/Repositories/Impl/ClubRepository.cs | 3 ++- src/NadekoBot/Services/Impl/StatsService.cs | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/NadekoBot/Services/Database/Repositories/Impl/ClubRepository.cs b/src/NadekoBot/Services/Database/Repositories/Impl/ClubRepository.cs index adf34135..eb09e8b0 100644 --- a/src/NadekoBot/Services/Database/Repositories/Impl/ClubRepository.cs +++ b/src/NadekoBot/Services/Database/Repositories/Impl/ClubRepository.cs @@ -58,7 +58,8 @@ namespace NadekoBot.Services.Database.Repositories.Impl public ClubInfo[] GetClubLeaderboardPage(int page) { - return _set.OrderBy(x => x.Xp) + return _set + .OrderByDescending(x => x.Xp) .Skip(page * 9) .Take(9) .ToArray(); diff --git a/src/NadekoBot/Services/Impl/StatsService.cs b/src/NadekoBot/Services/Impl/StatsService.cs index ba6db3cd..3a07b0a3 100644 --- a/src/NadekoBot/Services/Impl/StatsService.cs +++ b/src/NadekoBot/Services/Impl/StatsService.cs @@ -20,7 +20,7 @@ namespace NadekoBot.Services.Impl private readonly IBotCredentials _creds; private readonly DateTime _started; - public const string BotVersion = "1.8.2"; + public const string BotVersion = "1.8.3"; public string Author => "Kwoth#2560"; public string Library => "Discord.Net"; From 81a7c6f398f334c11aded7639cf1b797618b817a Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Mon, 11 Sep 2017 19:30:02 +0200 Subject: [PATCH 296/346] Added .nsfwcc command to prevent memory leaks if nsfw is spammed a lot. --- src/NadekoBot/Modules/NSFW/NSFW.cs | 9 +++++++++ .../Modules/Searches/Common/SearchImageCacher.cs | 5 +++++ .../Modules/Searches/Services/SearchesService.cs | 8 ++++++++ src/NadekoBot/Resources/CommandStrings.resx | 9 +++++++++ 4 files changed, 31 insertions(+) diff --git a/src/NadekoBot/Modules/NSFW/NSFW.cs b/src/NadekoBot/Modules/NSFW/NSFW.cs index 1858606c..5e3591db 100644 --- a/src/NadekoBot/Modules/NSFW/NSFW.cs +++ b/src/NadekoBot/Modules/NSFW/NSFW.cs @@ -206,6 +206,15 @@ namespace NadekoBot.Modules.NSFW } } + [NadekoCommand, Usage, Description, Aliases] + [RequireContext(ContextType.Guild)] + [OwnerOnly] + public Task NsfwClearCache() + { + _service.ClearCache(); + return Context.Channel.SendConfirmAsync("👌"); + } + public async Task InternalDapiCommand(string tag, DapiSearchType type, bool forceExplicit) { ImageCacherObject imgObj; diff --git a/src/NadekoBot/Modules/Searches/Common/SearchImageCacher.cs b/src/NadekoBot/Modules/Searches/Common/SearchImageCacher.cs index 6ccd9522..323a5eae 100644 --- a/src/NadekoBot/Modules/Searches/Common/SearchImageCacher.cs +++ b/src/NadekoBot/Modules/Searches/Common/SearchImageCacher.cs @@ -165,6 +165,11 @@ namespace NadekoBot.Modules.Searches.Common } return list.ToArray(); } + + public void Clear() + { + _cache.Clear(); + } } public class ImageCacherObject : IComparable diff --git a/src/NadekoBot/Modules/Searches/Services/SearchesService.cs b/src/NadekoBot/Modules/Searches/Services/SearchesService.cs index e4a3e40b..ed87452a 100644 --- a/src/NadekoBot/Modules/Searches/Services/SearchesService.cs +++ b/src/NadekoBot/Modules/Searches/Services/SearchesService.cs @@ -187,6 +187,14 @@ namespace NadekoBot.Modules.Searches.Services } return added; } + + public void ClearCache() + { + foreach (var c in _imageCacher) + { + c.Value?.Clear(); + } + } } public struct UserChannelPair diff --git a/src/NadekoBot/Resources/CommandStrings.resx b/src/NadekoBot/Resources/CommandStrings.resx index dceae623..ded835c9 100644 --- a/src/NadekoBot/Resources/CommandStrings.resx +++ b/src/NadekoBot/Resources/CommandStrings.resx @@ -3771,4 +3771,13 @@ Shows club rankings on the specified page. + + nsfwcc + + + `{0}nsfwcc` + + + Clears nsfw cache. + From d658fe7414e18de459bf04e104785e109776a3ce Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Mon, 11 Sep 2017 20:25:06 +0200 Subject: [PATCH 297/346] Updated discord.net --- src/NadekoBot/NadekoBot.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/NadekoBot/NadekoBot.csproj b/src/NadekoBot/NadekoBot.csproj index 22abd80f..ff8ed03e 100644 --- a/src/NadekoBot/NadekoBot.csproj +++ b/src/NadekoBot/NadekoBot.csproj @@ -58,7 +58,7 @@ - + From 4c591a69b196574b91e64317a9fd6e4546e4f06c Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Mon, 11 Sep 2017 22:43:32 +0200 Subject: [PATCH 298/346] Fixes to .xprr, leveling system --- .../20170911200031_lastXpGain.Designer.cs | 1947 +++++++++++++++++ .../Migrations/20170911200031_lastXpGain.cs | 27 + .../NadekoSqliteContextModelSnapshot.cs | 6 +- .../Modules/Xp/Services/XpService.cs | 30 +- .../Services/Database/Models/DiscordUser.cs | 1 + .../data/images/xp/UNUSED_old_xp_high_res.png | Bin 0 -> 294486 bytes src/NadekoBot/data/images/xp/xp.png | Bin 294486 -> 77461 bytes 7 files changed, 2000 insertions(+), 11 deletions(-) create mode 100644 src/NadekoBot/Migrations/20170911200031_lastXpGain.Designer.cs create mode 100644 src/NadekoBot/Migrations/20170911200031_lastXpGain.cs create mode 100644 src/NadekoBot/data/images/xp/UNUSED_old_xp_high_res.png diff --git a/src/NadekoBot/Migrations/20170911200031_lastXpGain.Designer.cs b/src/NadekoBot/Migrations/20170911200031_lastXpGain.Designer.cs new file mode 100644 index 00000000..e5c805df --- /dev/null +++ b/src/NadekoBot/Migrations/20170911200031_lastXpGain.Designer.cs @@ -0,0 +1,1947 @@ +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using NadekoBot.Services.Database; +using NadekoBot.Services.Database.Models; + +namespace NadekoBot.Migrations +{ + [DbContext(typeof(NadekoContext))] + [Migration("20170911200031_lastXpGain")] + partial class lastXpGain + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { + modelBuilder + .HasAnnotation("ProductVersion", "1.1.1"); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.AntiRaidSetting", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("Action"); + + b.Property("DateAdded"); + + b.Property("GuildConfigId"); + + b.Property("Seconds"); + + b.Property("UserThreshold"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId") + .IsUnique(); + + b.ToTable("AntiRaidSetting"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.AntiSpamIgnore", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AntiSpamSettingId"); + + b.Property("ChannelId"); + + b.Property("DateAdded"); + + b.HasKey("Id"); + + b.HasIndex("AntiSpamSettingId"); + + b.ToTable("AntiSpamIgnore"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.AntiSpamSetting", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("Action"); + + b.Property("DateAdded"); + + b.Property("GuildConfigId"); + + b.Property("MessageThreshold"); + + b.Property("MuteTime"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId") + .IsUnique(); + + b.ToTable("AntiSpamSetting"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.BlacklistItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("BotConfigId"); + + b.Property("DateAdded"); + + b.Property("ItemId"); + + b.Property("Type"); + + b.HasKey("Id"); + + b.HasIndex("BotConfigId"); + + b.ToTable("BlacklistItem"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.BlockedCmdOrMdl", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("BotConfigId"); + + b.Property("BotConfigId1"); + + b.Property("DateAdded"); + + b.Property("Name"); + + b.HasKey("Id"); + + b.HasIndex("BotConfigId"); + + b.HasIndex("BotConfigId1"); + + b.ToTable("BlockedCmdOrMdl"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.BotConfig", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("BetflipMultiplier"); + + b.Property("Betroll100Multiplier"); + + b.Property("Betroll67Multiplier"); + + b.Property("Betroll91Multiplier"); + + b.Property("BufferSize"); + + b.Property("CurrencyDropAmount"); + + b.Property("CurrencyDropAmountMax"); + + b.Property("CurrencyGenerationChance"); + + b.Property("CurrencyGenerationCooldown"); + + b.Property("CurrencyName"); + + b.Property("CurrencyPluralName"); + + b.Property("CurrencySign"); + + b.Property("CustomReactionsStartWith"); + + b.Property("DMHelpString"); + + b.Property("DateAdded"); + + b.Property("DefaultPrefix"); + + b.Property("ErrorColor"); + + b.Property("ForwardMessages"); + + b.Property("ForwardToAllOwners"); + + b.Property("HelpString"); + + b.Property("Locale"); + + b.Property("MigrationVersion"); + + b.Property("MinimumBetAmount"); + + b.Property("OkColor"); + + b.Property("PermissionVersion"); + + b.Property("RemindMessageFormat"); + + b.Property("RotatingStatuses"); + + b.Property("TriviaCurrencyReward"); + + b.Property("XpMinutesTimeout") + .ValueGeneratedOnAdd() + .HasDefaultValue(5); + + b.Property("XpPerMessage") + .ValueGeneratedOnAdd() + .HasDefaultValue(3); + + b.HasKey("Id"); + + b.ToTable("BotConfig"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.ClashCaller", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("BaseDestroyed"); + + b.Property("CallUser"); + + b.Property("ClashWarId"); + + b.Property("DateAdded"); + + b.Property("SequenceNumber"); + + b.Property("Stars"); + + b.Property("TimeAdded"); + + b.HasKey("Id"); + + b.HasIndex("ClashWarId"); + + b.ToTable("ClashCallers"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.ClashWar", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ChannelId"); + + b.Property("DateAdded"); + + b.Property("EnemyClan"); + + b.Property("GuildId"); + + b.Property("Size"); + + b.Property("StartedAt"); + + b.Property("WarState"); + + b.HasKey("Id"); + + b.ToTable("ClashOfClans"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.ClubApplicants", b => + { + b.Property("ClubId"); + + b.Property("UserId"); + + b.HasKey("ClubId", "UserId"); + + b.HasIndex("UserId"); + + b.ToTable("ClubApplicants"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.ClubBans", b => + { + b.Property("ClubId"); + + b.Property("UserId"); + + b.HasKey("ClubId", "UserId"); + + b.HasIndex("UserId"); + + b.ToTable("ClubBans"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.ClubInfo", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("Discrim"); + + b.Property("ImageUrl"); + + b.Property("MinimumLevelReq"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(20); + + b.Property("OwnerId"); + + b.Property("Xp"); + + b.HasKey("Id"); + + b.HasAlternateKey("Name", "Discrim"); + + b.HasIndex("OwnerId") + .IsUnique(); + + b.ToTable("Clubs"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.CommandAlias", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("GuildConfigId"); + + b.Property("Mapping"); + + b.Property("Trigger"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.ToTable("CommandAlias"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.CommandCooldown", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("CommandName"); + + b.Property("DateAdded"); + + b.Property("GuildConfigId"); + + b.Property("Seconds"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.ToTable("CommandCooldown"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.CommandPrice", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("BotConfigId"); + + b.Property("CommandName"); + + b.Property("DateAdded"); + + b.Property("Price"); + + b.HasKey("Id"); + + b.HasIndex("BotConfigId"); + + b.HasIndex("Price") + .IsUnique(); + + b.ToTable("CommandPrice"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.ConvertUnit", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("InternalTrigger"); + + b.Property("Modifier"); + + b.Property("UnitType"); + + b.HasKey("Id"); + + b.ToTable("ConversionUnits"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.Currency", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("Amount"); + + b.Property("DateAdded"); + + b.Property("UserId"); + + b.HasKey("Id"); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("Currency"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.CurrencyTransaction", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("Amount"); + + b.Property("DateAdded"); + + b.Property("Reason"); + + b.Property("UserId"); + + b.HasKey("Id"); + + b.ToTable("CurrencyTransactions"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.CustomReaction", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AutoDeleteTrigger"); + + b.Property("ContainsAnywhere"); + + b.Property("DateAdded"); + + b.Property("DmResponse"); + + b.Property("GuildId"); + + b.Property("IsRegex"); + + b.Property("OwnerOnly"); + + b.Property("Response"); + + b.Property("Trigger"); + + b.HasKey("Id"); + + b.ToTable("CustomReactions"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.DiscordUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AvatarId"); + + b.Property("ClubId"); + + b.Property("DateAdded"); + + b.Property("Discriminator"); + + b.Property("LastLevelUp") + .ValueGeneratedOnAdd() + .HasDefaultValue(new DateTime(2017, 9, 11, 22, 0, 31, 236, DateTimeKind.Local)); + + b.Property("LastXpGain"); + + b.Property("NotifyOnLevelUp"); + + b.Property("UserId"); + + b.Property("Username"); + + b.HasKey("Id"); + + b.HasAlternateKey("UserId"); + + b.HasIndex("ClubId"); + + b.ToTable("DiscordUser"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.Donator", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("Amount"); + + b.Property("DateAdded"); + + b.Property("Name"); + + b.Property("UserId"); + + b.HasKey("Id"); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("Donators"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.EightBallResponse", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("BotConfigId"); + + b.Property("DateAdded"); + + b.Property("Text"); + + b.HasKey("Id"); + + b.HasIndex("BotConfigId"); + + b.ToTable("EightBallResponses"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.ExcludedItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("ItemId"); + + b.Property("ItemType"); + + b.Property("XpSettingsId"); + + b.HasKey("Id"); + + b.HasIndex("XpSettingsId"); + + b.ToTable("ExcludedItem"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.FilterChannelId", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ChannelId"); + + b.Property("DateAdded"); + + b.Property("GuildConfigId"); + + b.Property("GuildConfigId1"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.HasIndex("GuildConfigId1"); + + b.ToTable("FilterChannelId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.FilteredWord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("GuildConfigId"); + + b.Property("Word"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.ToTable("FilteredWord"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.FollowedStream", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ChannelId"); + + b.Property("DateAdded"); + + b.Property("GuildConfigId"); + + b.Property("GuildId"); + + b.Property("Type"); + + b.Property("Username"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.ToTable("FollowedStream"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.GCChannelId", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ChannelId"); + + b.Property("DateAdded"); + + b.Property("GuildConfigId"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.ToTable("GCChannelId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.GuildConfig", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AutoAssignRoleId"); + + b.Property("AutoDeleteByeMessages"); + + b.Property("AutoDeleteByeMessagesTimer"); + + b.Property("AutoDeleteGreetMessages"); + + b.Property("AutoDeleteGreetMessagesTimer"); + + b.Property("AutoDeleteSelfAssignedRoleMessages"); + + b.Property("ByeMessageChannelId"); + + b.Property("ChannelByeMessageText"); + + b.Property("ChannelGreetMessageText"); + + b.Property("CleverbotEnabled"); + + b.Property("DateAdded"); + + b.Property("DefaultMusicVolume"); + + b.Property("DeleteMessageOnCommand"); + + b.Property("DmGreetMessageText"); + + b.Property("ExclusiveSelfAssignedRoles"); + + b.Property("FilterInvites"); + + b.Property("FilterWords"); + + b.Property("GameVoiceChannel"); + + b.Property("GreetMessageChannelId"); + + b.Property("GuildId"); + + b.Property("Locale"); + + b.Property("LogSettingId"); + + b.Property("MuteRoleName"); + + b.Property("PermissionRole"); + + b.Property("Prefix"); + + b.Property("RootPermissionId"); + + b.Property("SendChannelByeMessage"); + + b.Property("SendChannelGreetMessage"); + + b.Property("SendDmGreetMessage"); + + b.Property("TimeZoneId"); + + b.Property("VerboseErrors"); + + b.Property("VerbosePermissions"); + + b.Property("VoicePlusTextEnabled"); + + b.Property("WarningsInitialized"); + + b.HasKey("Id"); + + b.HasIndex("GuildId") + .IsUnique(); + + b.HasIndex("LogSettingId"); + + b.HasIndex("RootPermissionId"); + + b.ToTable("GuildConfigs"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.GuildRepeater", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ChannelId"); + + b.Property("DateAdded"); + + b.Property("GuildConfigId"); + + b.Property("GuildId"); + + b.Property("Interval"); + + b.Property("Message"); + + b.Property("StartTimeOfDay"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.ToTable("GuildRepeater"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.IgnoredLogChannel", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ChannelId"); + + b.Property("DateAdded"); + + b.Property("LogSettingId"); + + b.HasKey("Id"); + + b.HasIndex("LogSettingId"); + + b.ToTable("IgnoredLogChannels"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.IgnoredVoicePresenceChannel", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ChannelId"); + + b.Property("DateAdded"); + + b.Property("LogSettingId"); + + b.HasKey("Id"); + + b.HasIndex("LogSettingId"); + + b.ToTable("IgnoredVoicePresenceCHannels"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.LogSetting", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ChannelCreated"); + + b.Property("ChannelCreatedId"); + + b.Property("ChannelDestroyed"); + + b.Property("ChannelDestroyedId"); + + b.Property("ChannelId"); + + b.Property("ChannelUpdated"); + + b.Property("ChannelUpdatedId"); + + b.Property("DateAdded"); + + b.Property("IsLogging"); + + b.Property("LogOtherId"); + + b.Property("LogUserPresence"); + + b.Property("LogUserPresenceId"); + + b.Property("LogVoicePresence"); + + b.Property("LogVoicePresenceId"); + + b.Property("LogVoicePresenceTTSId"); + + b.Property("MessageDeleted"); + + b.Property("MessageDeletedId"); + + b.Property("MessageUpdated"); + + b.Property("MessageUpdatedId"); + + b.Property("UserBanned"); + + b.Property("UserBannedId"); + + b.Property("UserJoined"); + + b.Property("UserJoinedId"); + + b.Property("UserLeft"); + + b.Property("UserLeftId"); + + b.Property("UserMutedId"); + + b.Property("UserPresenceChannelId"); + + b.Property("UserUnbanned"); + + b.Property("UserUnbannedId"); + + b.Property("UserUpdated"); + + b.Property("UserUpdatedId"); + + b.Property("VoicePresenceChannelId"); + + b.HasKey("Id"); + + b.ToTable("LogSettings"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.ModulePrefix", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("BotConfigId"); + + b.Property("DateAdded"); + + b.Property("ModuleName"); + + b.Property("Prefix"); + + b.HasKey("Id"); + + b.HasIndex("BotConfigId"); + + b.ToTable("ModulePrefixes"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.MusicPlaylist", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("Author"); + + b.Property("AuthorId"); + + b.Property("DateAdded"); + + b.Property("Name"); + + b.HasKey("Id"); + + b.ToTable("MusicPlaylists"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.MutedUserId", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("GuildConfigId"); + + b.Property("UserId"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.ToTable("MutedUserId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.NsfwBlacklitedTag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("GuildConfigId"); + + b.Property("Tag"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.ToTable("NsfwBlacklitedTag"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.Permission", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("NextId"); + + b.Property("PrimaryTarget"); + + b.Property("PrimaryTargetId"); + + b.Property("SecondaryTarget"); + + b.Property("SecondaryTargetName"); + + b.Property("State"); + + b.HasKey("Id"); + + b.HasIndex("NextId") + .IsUnique(); + + b.ToTable("Permission"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.Permissionv2", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("GuildConfigId"); + + b.Property("Index"); + + b.Property("PrimaryTarget"); + + b.Property("PrimaryTargetId"); + + b.Property("SecondaryTarget"); + + b.Property("SecondaryTargetName"); + + b.Property("State"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.ToTable("Permissionv2"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.PlayingStatus", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("BotConfigId"); + + b.Property("DateAdded"); + + b.Property("Status"); + + b.HasKey("Id"); + + b.HasIndex("BotConfigId"); + + b.ToTable("PlayingStatus"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.PlaylistSong", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("MusicPlaylistId"); + + b.Property("Provider"); + + b.Property("ProviderType"); + + b.Property("Query"); + + b.Property("Title"); + + b.Property("Uri"); + + b.HasKey("Id"); + + b.HasIndex("MusicPlaylistId"); + + b.ToTable("PlaylistSong"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.Quote", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AuthorId"); + + b.Property("AuthorName") + .IsRequired(); + + b.Property("DateAdded"); + + b.Property("GuildId"); + + b.Property("Keyword") + .IsRequired(); + + b.Property("Text") + .IsRequired(); + + b.HasKey("Id"); + + b.ToTable("Quotes"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.RaceAnimal", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("BotConfigId"); + + b.Property("DateAdded"); + + b.Property("Icon"); + + b.Property("Name"); + + b.HasKey("Id"); + + b.HasIndex("BotConfigId"); + + b.ToTable("RaceAnimals"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.Reminder", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ChannelId"); + + b.Property("DateAdded"); + + b.Property("IsPrivate"); + + b.Property("Message"); + + b.Property("ServerId"); + + b.Property("UserId"); + + b.Property("When"); + + b.HasKey("Id"); + + b.ToTable("Reminders"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.RewardedUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AmountRewardedThisMonth"); + + b.Property("DateAdded"); + + b.Property("LastReward"); + + b.Property("PatreonUserId"); + + b.Property("UserId"); + + b.HasKey("Id"); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("RewardedUsers"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.SelfAssignedRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("GuildId"); + + b.Property("RoleId"); + + b.HasKey("Id"); + + b.HasIndex("GuildId", "RoleId") + .IsUnique(); + + b.ToTable("SelfAssignableRoles"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.ShopEntry", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AuthorId"); + + b.Property("DateAdded"); + + b.Property("GuildConfigId"); + + b.Property("Index"); + + b.Property("Name"); + + b.Property("Price"); + + b.Property("RoleId"); + + b.Property("RoleName"); + + b.Property("Type"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.ToTable("ShopEntry"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.ShopEntryItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("ShopEntryId"); + + b.Property("Text"); + + b.HasKey("Id"); + + b.HasIndex("ShopEntryId"); + + b.ToTable("ShopEntryItem"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.SlowmodeIgnoredRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("GuildConfigId"); + + b.Property("RoleId"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.ToTable("SlowmodeIgnoredRole"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.SlowmodeIgnoredUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("GuildConfigId"); + + b.Property("UserId"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.ToTable("SlowmodeIgnoredUser"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.StartupCommand", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("BotConfigId"); + + b.Property("ChannelId"); + + b.Property("ChannelName"); + + b.Property("CommandText"); + + b.Property("DateAdded"); + + b.Property("GuildId"); + + b.Property("GuildName"); + + b.Property("Index"); + + b.Property("VoiceChannelId"); + + b.Property("VoiceChannelName"); + + b.HasKey("Id"); + + b.HasIndex("BotConfigId"); + + b.ToTable("StartupCommand"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.StreamRoleBlacklistedUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("StreamRoleSettingsId"); + + b.Property("UserId"); + + b.Property("Username"); + + b.HasKey("Id"); + + b.HasIndex("StreamRoleSettingsId"); + + b.ToTable("StreamRoleBlacklistedUser"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.StreamRoleSettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AddRoleId"); + + b.Property("DateAdded"); + + b.Property("Enabled"); + + b.Property("FromRoleId"); + + b.Property("GuildConfigId"); + + b.Property("Keyword"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId") + .IsUnique(); + + b.ToTable("StreamRoleSettings"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.StreamRoleWhitelistedUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("StreamRoleSettingsId"); + + b.Property("UserId"); + + b.Property("Username"); + + b.HasKey("Id"); + + b.HasIndex("StreamRoleSettingsId"); + + b.ToTable("StreamRoleWhitelistedUser"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.UnmuteTimer", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("GuildConfigId"); + + b.Property("UnmuteAt"); + + b.Property("UserId"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.ToTable("UnmuteTimer"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.UserPokeTypes", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("UserId"); + + b.Property("type"); + + b.HasKey("Id"); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("PokeGame"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.UserXpStats", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AwardedXp"); + + b.Property("DateAdded"); + + b.Property("GuildId"); + + b.Property("LastLevelUp") + .ValueGeneratedOnAdd() + .HasDefaultValue(new DateTime(2017, 9, 11, 22, 0, 31, 238, DateTimeKind.Local)); + + b.Property("NotifyOnLevelUp"); + + b.Property("UserId"); + + b.Property("Xp"); + + b.HasKey("Id"); + + b.HasIndex("UserId", "GuildId") + .IsUnique(); + + b.ToTable("UserXpStats"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.VcRoleInfo", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("GuildConfigId"); + + b.Property("RoleId"); + + b.Property("VoiceChannelId"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.ToTable("VcRoleInfo"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.WaifuInfo", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AffinityId"); + + b.Property("ClaimerId"); + + b.Property("DateAdded"); + + b.Property("Price"); + + b.Property("WaifuId"); + + b.HasKey("Id"); + + b.HasIndex("AffinityId"); + + b.HasIndex("ClaimerId"); + + b.HasIndex("WaifuId") + .IsUnique(); + + b.ToTable("WaifuInfo"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.WaifuItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("Item"); + + b.Property("ItemEmoji"); + + b.Property("Price"); + + b.Property("WaifuInfoId"); + + b.HasKey("Id"); + + b.HasIndex("WaifuInfoId"); + + b.ToTable("WaifuItem"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.WaifuUpdate", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("NewId"); + + b.Property("OldId"); + + b.Property("UpdateType"); + + b.Property("UserId"); + + b.HasKey("Id"); + + b.HasIndex("NewId"); + + b.HasIndex("OldId"); + + b.HasIndex("UserId"); + + b.ToTable("WaifuUpdates"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.Warning", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("Forgiven"); + + b.Property("ForgivenBy"); + + b.Property("GuildId"); + + b.Property("Moderator"); + + b.Property("Reason"); + + b.Property("UserId"); + + b.HasKey("Id"); + + b.ToTable("Warnings"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.WarningPunishment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("Count"); + + b.Property("DateAdded"); + + b.Property("GuildConfigId"); + + b.Property("Punishment"); + + b.Property("Time"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.ToTable("WarningPunishment"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.XpRoleReward", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("Level"); + + b.Property("RoleId"); + + b.Property("XpSettingsId"); + + b.HasKey("Id"); + + b.HasAlternateKey("Level"); + + b.HasIndex("XpSettingsId"); + + b.ToTable("XpRoleReward"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.XpSettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("GuildConfigId"); + + b.Property("NotifyMessage"); + + b.Property("ServerExcluded"); + + b.Property("XpRoleRewardExclusive"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId") + .IsUnique(); + + b.ToTable("XpSettings"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.AntiRaidSetting", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig", "GuildConfig") + .WithOne("AntiRaidSetting") + .HasForeignKey("NadekoBot.Services.Database.Models.AntiRaidSetting", "GuildConfigId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.AntiSpamIgnore", b => + { + b.HasOne("NadekoBot.Services.Database.Models.AntiSpamSetting") + .WithMany("IgnoredChannels") + .HasForeignKey("AntiSpamSettingId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.AntiSpamSetting", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig", "GuildConfig") + .WithOne("AntiSpamSetting") + .HasForeignKey("NadekoBot.Services.Database.Models.AntiSpamSetting", "GuildConfigId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.BlacklistItem", b => + { + b.HasOne("NadekoBot.Services.Database.Models.BotConfig") + .WithMany("Blacklist") + .HasForeignKey("BotConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.BlockedCmdOrMdl", b => + { + b.HasOne("NadekoBot.Services.Database.Models.BotConfig") + .WithMany("BlockedCommands") + .HasForeignKey("BotConfigId"); + + b.HasOne("NadekoBot.Services.Database.Models.BotConfig") + .WithMany("BlockedModules") + .HasForeignKey("BotConfigId1"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.ClashCaller", b => + { + b.HasOne("NadekoBot.Services.Database.Models.ClashWar", "ClashWar") + .WithMany("Bases") + .HasForeignKey("ClashWarId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.ClubApplicants", b => + { + b.HasOne("NadekoBot.Services.Database.Models.ClubInfo", "Club") + .WithMany("Applicants") + .HasForeignKey("ClubId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("NadekoBot.Services.Database.Models.DiscordUser", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.ClubBans", b => + { + b.HasOne("NadekoBot.Services.Database.Models.ClubInfo", "Club") + .WithMany("Bans") + .HasForeignKey("ClubId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("NadekoBot.Services.Database.Models.DiscordUser", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.ClubInfo", b => + { + b.HasOne("NadekoBot.Services.Database.Models.DiscordUser", "Owner") + .WithOne() + .HasForeignKey("NadekoBot.Services.Database.Models.ClubInfo", "OwnerId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.CommandAlias", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig") + .WithMany("CommandAliases") + .HasForeignKey("GuildConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.CommandCooldown", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig") + .WithMany("CommandCooldowns") + .HasForeignKey("GuildConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.CommandPrice", b => + { + b.HasOne("NadekoBot.Services.Database.Models.BotConfig") + .WithMany("CommandPrices") + .HasForeignKey("BotConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.DiscordUser", b => + { + b.HasOne("NadekoBot.Services.Database.Models.ClubInfo", "Club") + .WithMany("Users") + .HasForeignKey("ClubId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.EightBallResponse", b => + { + b.HasOne("NadekoBot.Services.Database.Models.BotConfig") + .WithMany("EightBallResponses") + .HasForeignKey("BotConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.ExcludedItem", b => + { + b.HasOne("NadekoBot.Services.Database.Models.XpSettings") + .WithMany("ExclusionList") + .HasForeignKey("XpSettingsId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.FilterChannelId", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig") + .WithMany("FilterInvitesChannelIds") + .HasForeignKey("GuildConfigId"); + + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig") + .WithMany("FilterWordsChannelIds") + .HasForeignKey("GuildConfigId1"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.FilteredWord", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig") + .WithMany("FilteredWords") + .HasForeignKey("GuildConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.FollowedStream", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig") + .WithMany("FollowedStreams") + .HasForeignKey("GuildConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.GCChannelId", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig") + .WithMany("GenerateCurrencyChannelIds") + .HasForeignKey("GuildConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.GuildConfig", b => + { + b.HasOne("NadekoBot.Services.Database.Models.LogSetting", "LogSetting") + .WithMany() + .HasForeignKey("LogSettingId"); + + b.HasOne("NadekoBot.Services.Database.Models.Permission", "RootPermission") + .WithMany() + .HasForeignKey("RootPermissionId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.GuildRepeater", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig") + .WithMany("GuildRepeaters") + .HasForeignKey("GuildConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.IgnoredLogChannel", b => + { + b.HasOne("NadekoBot.Services.Database.Models.LogSetting", "LogSetting") + .WithMany("IgnoredChannels") + .HasForeignKey("LogSettingId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.IgnoredVoicePresenceChannel", b => + { + b.HasOne("NadekoBot.Services.Database.Models.LogSetting", "LogSetting") + .WithMany("IgnoredVoicePresenceChannelIds") + .HasForeignKey("LogSettingId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.ModulePrefix", b => + { + b.HasOne("NadekoBot.Services.Database.Models.BotConfig") + .WithMany("ModulePrefixes") + .HasForeignKey("BotConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.MutedUserId", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig") + .WithMany("MutedUsers") + .HasForeignKey("GuildConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.NsfwBlacklitedTag", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig") + .WithMany("NsfwBlacklistedTags") + .HasForeignKey("GuildConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.Permission", b => + { + b.HasOne("NadekoBot.Services.Database.Models.Permission", "Next") + .WithOne("Previous") + .HasForeignKey("NadekoBot.Services.Database.Models.Permission", "NextId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.Permissionv2", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig") + .WithMany("Permissions") + .HasForeignKey("GuildConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.PlayingStatus", b => + { + b.HasOne("NadekoBot.Services.Database.Models.BotConfig") + .WithMany("RotatingStatusMessages") + .HasForeignKey("BotConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.PlaylistSong", b => + { + b.HasOne("NadekoBot.Services.Database.Models.MusicPlaylist") + .WithMany("Songs") + .HasForeignKey("MusicPlaylistId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.RaceAnimal", b => + { + b.HasOne("NadekoBot.Services.Database.Models.BotConfig") + .WithMany("RaceAnimals") + .HasForeignKey("BotConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.ShopEntry", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig") + .WithMany("ShopEntries") + .HasForeignKey("GuildConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.ShopEntryItem", b => + { + b.HasOne("NadekoBot.Services.Database.Models.ShopEntry") + .WithMany("Items") + .HasForeignKey("ShopEntryId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.SlowmodeIgnoredRole", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig") + .WithMany("SlowmodeIgnoredRoles") + .HasForeignKey("GuildConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.SlowmodeIgnoredUser", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig") + .WithMany("SlowmodeIgnoredUsers") + .HasForeignKey("GuildConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.StartupCommand", b => + { + b.HasOne("NadekoBot.Services.Database.Models.BotConfig") + .WithMany("StartupCommands") + .HasForeignKey("BotConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.StreamRoleBlacklistedUser", b => + { + b.HasOne("NadekoBot.Services.Database.Models.StreamRoleSettings") + .WithMany("Blacklist") + .HasForeignKey("StreamRoleSettingsId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.StreamRoleSettings", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig", "GuildConfig") + .WithOne("StreamRole") + .HasForeignKey("NadekoBot.Services.Database.Models.StreamRoleSettings", "GuildConfigId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.StreamRoleWhitelistedUser", b => + { + b.HasOne("NadekoBot.Services.Database.Models.StreamRoleSettings") + .WithMany("Whitelist") + .HasForeignKey("StreamRoleSettingsId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.UnmuteTimer", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig") + .WithMany("UnmuteTimers") + .HasForeignKey("GuildConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.VcRoleInfo", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig") + .WithMany("VcRoleInfos") + .HasForeignKey("GuildConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.WaifuInfo", b => + { + b.HasOne("NadekoBot.Services.Database.Models.DiscordUser", "Affinity") + .WithMany() + .HasForeignKey("AffinityId"); + + b.HasOne("NadekoBot.Services.Database.Models.DiscordUser", "Claimer") + .WithMany() + .HasForeignKey("ClaimerId"); + + b.HasOne("NadekoBot.Services.Database.Models.DiscordUser", "Waifu") + .WithOne() + .HasForeignKey("NadekoBot.Services.Database.Models.WaifuInfo", "WaifuId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.WaifuItem", b => + { + b.HasOne("NadekoBot.Services.Database.Models.WaifuInfo") + .WithMany("Items") + .HasForeignKey("WaifuInfoId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.WaifuUpdate", b => + { + b.HasOne("NadekoBot.Services.Database.Models.DiscordUser", "New") + .WithMany() + .HasForeignKey("NewId"); + + b.HasOne("NadekoBot.Services.Database.Models.DiscordUser", "Old") + .WithMany() + .HasForeignKey("OldId"); + + b.HasOne("NadekoBot.Services.Database.Models.DiscordUser", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.WarningPunishment", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig") + .WithMany("WarnPunishments") + .HasForeignKey("GuildConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.XpRoleReward", b => + { + b.HasOne("NadekoBot.Services.Database.Models.XpSettings") + .WithMany("RoleRewards") + .HasForeignKey("XpSettingsId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.XpSettings", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig", "GuildConfig") + .WithOne("XpSettings") + .HasForeignKey("NadekoBot.Services.Database.Models.XpSettings", "GuildConfigId") + .OnDelete(DeleteBehavior.Cascade); + }); + } + } +} diff --git a/src/NadekoBot/Migrations/20170911200031_lastXpGain.cs b/src/NadekoBot/Migrations/20170911200031_lastXpGain.cs new file mode 100644 index 00000000..50bd6b9e --- /dev/null +++ b/src/NadekoBot/Migrations/20170911200031_lastXpGain.cs @@ -0,0 +1,27 @@ +using System; +using System.Collections.Generic; +using Microsoft.EntityFrameworkCore.Migrations; + +namespace NadekoBot.Migrations +{ + public partial class lastXpGain : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "LastXpGain", + table: "DiscordUser", + nullable: false, + defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified)); + + migrationBuilder.Sql("DELETE FROM XpRoleReward WHERE XpSettingsId is null"); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "LastXpGain", + table: "DiscordUser"); + } + } +} diff --git a/src/NadekoBot/Migrations/NadekoSqliteContextModelSnapshot.cs b/src/NadekoBot/Migrations/NadekoSqliteContextModelSnapshot.cs index da931d6b..25117604 100644 --- a/src/NadekoBot/Migrations/NadekoSqliteContextModelSnapshot.cs +++ b/src/NadekoBot/Migrations/NadekoSqliteContextModelSnapshot.cs @@ -464,7 +464,9 @@ namespace NadekoBot.Migrations b.Property("LastLevelUp") .ValueGeneratedOnAdd() - .HasDefaultValue(new DateTime(2017, 9, 9, 1, 7, 29, 857, DateTimeKind.Local)); + .HasDefaultValue(new DateTime(2017, 9, 11, 22, 0, 31, 236, DateTimeKind.Local)); + + b.Property("LastXpGain"); b.Property("NotifyOnLevelUp"); @@ -1360,7 +1362,7 @@ namespace NadekoBot.Migrations b.Property("LastLevelUp") .ValueGeneratedOnAdd() - .HasDefaultValue(new DateTime(2017, 9, 9, 1, 7, 29, 858, DateTimeKind.Local)); + .HasDefaultValue(new DateTime(2017, 9, 11, 22, 0, 31, 238, DateTimeKind.Local)); b.Property("NotifyOnLevelUp"); diff --git a/src/NadekoBot/Modules/Xp/Services/XpService.cs b/src/NadekoBot/Modules/Xp/Services/XpService.cs index 1533e4b0..88835081 100644 --- a/src/NadekoBot/Modules/Xp/Services/XpService.cs +++ b/src/NadekoBot/Modules/Xp/Services/XpService.cs @@ -131,8 +131,6 @@ namespace NadekoBot.Modules.Xp.Services if (toAddTo.Count == 0) return; - _log.Info("Adding XP to {0} users.", toAddTo.Count); - using (var uow = _db.UnitOfWork) { foreach (var item in group) @@ -142,6 +140,11 @@ namespace NadekoBot.Modules.Xp.Services var usr = uow.Xp.GetOrCreateUser(item.Key.GuildId, item.Key.User.Id); var du = uow.DiscordUsers.GetOrCreate(item.Key.User); + if (du.LastXpGain + TimeSpan.FromMinutes(_bc.BotConfig.XpMinutesTimeout) > DateTime.UtcNow) + continue; + + du.LastXpGain = DateTime.UtcNow; + var globalXp = uow.Xp.GetTotalUserXp(item.Key.User.Id); var oldGlobalLevelData = new LevelStats(globalXp); var newGlobalLevelData = new LevelStats(globalXp + xp); @@ -239,6 +242,9 @@ namespace NadekoBot.Modules.Xp.Services } }, null, TimeSpan.FromSeconds(5), TimeSpan.FromSeconds(5)); + + //just a first line, in order to prevent queries. But since other shards can try to do this too, + //i'll check in the db too. var clearRewardTimer = Task.Run(async () => { while (true) @@ -268,10 +274,16 @@ namespace NadekoBot.Modules.Xp.Services if (roleId == null) { - settings.RoleRewards.RemoveWhere(x => x.Level == level); + var toRemove = settings.RoleRewards.FirstOrDefault(x => x.Level == level); + if (toRemove != null) + { + uow._context.Remove(toRemove); + settings.RoleRewards.Remove(toRemove); + } } else { + var rew = settings.RoleRewards.FirstOrDefault(x => x.Level == level); if (rew != null) @@ -553,7 +565,7 @@ namespace NadekoBot.Modules.Xp.Services _timeFont = _fonts.Find("Whitney-Bold").CreateFont(20); } - public async Task> GenerateImageAsync(FullUserStats stats) + public Task> GenerateImageAsync(FullUserStats stats) => Task.Run(async () => { var img = Image.Load(_images.XpCard.ToArray()); @@ -567,7 +579,7 @@ namespace NadekoBot.Modules.Xp.Services new PointF(130, 5)); // level - + img.DrawText(stats.Global.Level.ToString(), _levelFont, Rgba32.White, new PointF(47, 137)); @@ -575,14 +587,14 @@ namespace NadekoBot.Modules.Xp.Services new PointF(47, 285)); //club name - + var clubName = stats.User.Club?.ToString() ?? "-"; var clubFont = _clubFontFamily .CreateFont(clubName.Length <= 8 ? 35 : 35 - (clubName.Length / 2)); - + img.DrawText(clubName, clubFont, Rgba32.White, new PointF(650 - clubName.Length * 10, 40)); @@ -700,7 +712,7 @@ namespace NadekoBot.Modules.Xp.Services _imageStreams.AddOrUpdate(imgUrl, s, (k, v) => s); } var toDraw = Image.Load(s); - + img.DrawImage(toDraw, 1, new Size(45, 45), @@ -717,7 +729,7 @@ namespace NadekoBot.Modules.Xp.Services //_log.Info("{0:F2} KB", arr.Length * 1.0f / 1.KB()); return img; - } + }); // https://github.com/SixLabors/ImageSharp/tree/master/samples/AvatarWithRoundedCorner diff --git a/src/NadekoBot/Services/Database/Models/DiscordUser.cs b/src/NadekoBot/Services/Database/Models/DiscordUser.cs index 2ca8fc68..6c02388d 100644 --- a/src/NadekoBot/Services/Database/Models/DiscordUser.cs +++ b/src/NadekoBot/Services/Database/Models/DiscordUser.cs @@ -11,6 +11,7 @@ namespace NadekoBot.Services.Database.Models public ClubInfo Club { get; set; } public DateTime LastLevelUp { get; set; } = DateTime.UtcNow; + public DateTime LastXpGain { get; set; } = DateTime.MinValue; public XpNotificationType NotifyOnLevelUp { get; set; } public override bool Equals(object obj) diff --git a/src/NadekoBot/data/images/xp/UNUSED_old_xp_high_res.png b/src/NadekoBot/data/images/xp/UNUSED_old_xp_high_res.png new file mode 100644 index 0000000000000000000000000000000000000000..28025180e2ed45fffe08372a846a9f5bcf1b5b1b GIT binary patch literal 294486 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�gWDMx(atllKvB7CY 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 literal 0 HcmV?d00001 diff --git a/src/NadekoBot/data/images/xp/xp.png b/src/NadekoBot/data/images/xp/xp.png index 28025180e2ed45fffe08372a846a9f5bcf1b5b1b..d5af22b4218ce42f4b50223220e0d5b5e8626468 100644 GIT binary patch literal 77461 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�gWDMx(atllKvB7CY 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 From ea0ca1471fdba5947e0e67f32c56d1e61a342dbf Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Mon, 11 Sep 2017 23:39:28 +0200 Subject: [PATCH 299/346] Xp system fixes --- src/NadekoBot/Modules/Xp/Services/XpService.cs | 7 ++++--- .../Services/Database/Repositories/Impl/XpRepository.cs | 4 +++- src/NadekoBot/_Extensions/Extensions.cs | 2 +- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/src/NadekoBot/Modules/Xp/Services/XpService.cs b/src/NadekoBot/Modules/Xp/Services/XpService.cs index 88835081..78dcc5f9 100644 --- a/src/NadekoBot/Modules/Xp/Services/XpService.cs +++ b/src/NadekoBot/Modules/Xp/Services/XpService.cs @@ -343,9 +343,6 @@ namespace NadekoBot.Modules.Xp.Services var _ = Task.Run(() => { - if (!SetUserRewarded(user.Id)) - return; - if (_excludedChannels.TryGetValue(user.Guild.Id, out var chans) && chans.Contains(arg.Channel.Id)) return; @@ -359,6 +356,10 @@ namespace NadekoBot.Modules.Xp.Services if (!arg.Content.Contains(' ') && arg.Content.Length < 5) return; + + if (!SetUserRewarded(user.Id)) + return; + _addMessageXp.Enqueue(new UserCacheItem { Guild = user.Guild, Channel = arg.Channel, User = user }); }); return Task.CompletedTask; diff --git a/src/NadekoBot/Services/Database/Repositories/Impl/XpRepository.cs b/src/NadekoBot/Services/Database/Repositories/Impl/XpRepository.cs index ddd5a8e2..50025620 100644 --- a/src/NadekoBot/Services/Database/Repositories/Impl/XpRepository.cs +++ b/src/NadekoBot/Services/Database/Repositories/Impl/XpRepository.cs @@ -58,7 +58,9 @@ namespace NadekoBot.Services.Database.Repositories.Impl .Where(x => x.GuildId == guildId) .Count(x => x.Xp > (_set .Where(y => y.UserId == userId && y.GuildId == guildId) - .Sum(y => y.Xp))) + 1; + .Select(y => y.Xp) + .DefaultIfEmpty() + .Sum())) + 1; } public (ulong UserId, int TotalXp)[] GetUsersFor(int page) diff --git a/src/NadekoBot/_Extensions/Extensions.cs b/src/NadekoBot/_Extensions/Extensions.cs index 84901d21..d1bd796b 100644 --- a/src/NadekoBot/_Extensions/Extensions.cs +++ b/src/NadekoBot/_Extensions/Extensions.cs @@ -135,7 +135,7 @@ namespace NadekoBot.Extensions public static MemoryStream ToStream(this ImageSharp.Image img) { var imageStream = new MemoryStream(); - img.SaveAsPng(imageStream); + img.SaveAsPng(imageStream, new ImageSharp.Formats.PngEncoder() { CompressionLevel = 9}); imageStream.Position = 0; return imageStream; } From f08fd3bdb1d9392065f9740c5c09f023f9e83524 Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Mon, 11 Sep 2017 23:39:44 +0200 Subject: [PATCH 300/346] 1.8.4 --- src/NadekoBot/Services/Impl/StatsService.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/NadekoBot/Services/Impl/StatsService.cs b/src/NadekoBot/Services/Impl/StatsService.cs index 3a07b0a3..8e94ceec 100644 --- a/src/NadekoBot/Services/Impl/StatsService.cs +++ b/src/NadekoBot/Services/Impl/StatsService.cs @@ -20,7 +20,7 @@ namespace NadekoBot.Services.Impl private readonly IBotCredentials _creds; private readonly DateTime _started; - public const string BotVersion = "1.8.3"; + public const string BotVersion = "1.8.4"; public string Author => "Kwoth#2560"; public string Library => "Discord.Net"; From d51c28b73cd228fc96c33baf8ba27e161af07d39 Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Tue, 12 Sep 2017 05:00:14 +0200 Subject: [PATCH 301/346] .xpglb should be faster now. --- .../Database/Repositories/Impl/XpRepository.cs | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/src/NadekoBot/Services/Database/Repositories/Impl/XpRepository.cs b/src/NadekoBot/Services/Database/Repositories/Impl/XpRepository.cs index 50025620..43d35202 100644 --- a/src/NadekoBot/Services/Database/Repositories/Impl/XpRepository.cs +++ b/src/NadekoBot/Services/Database/Repositories/Impl/XpRepository.cs @@ -65,15 +65,13 @@ namespace NadekoBot.Services.Database.Repositories.Impl public (ulong UserId, int TotalXp)[] GetUsersFor(int page) { - return (from orduser in _set - group orduser by orduser.UserId into g - orderby g.Sum(x => x.Xp) descending - select new { UserId = g.Key, TotalXp = g.Sum(x => x.Xp) }) - .Skip(page * 9) - .Take(9) - .AsEnumerable() - .Select(x => (x.UserId, x.TotalXp)) - .ToArray(); + return _set.GroupBy(x => x.UserId) + .OrderByDescending(x => x.Sum(y => y.Xp)) + .Skip(page * 9) + .Take(9) + .AsEnumerable() + .Select(x => (x.Key, x.Sum(y => y.Xp))) + .ToArray(); } } } \ No newline at end of file From 90b698f18ec6fb5d3ee4f675208407bb9d75657a Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Tue, 12 Sep 2017 08:14:58 +0200 Subject: [PATCH 302/346] Fixed .clubapply help string --- docs/Commands List.md | 4 ++-- src/NadekoBot/Resources/CommandStrings.resx | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/Commands List.md b/docs/Commands List.md index a72309e8..6d6a6d9a 100644 --- a/docs/Commands List.md +++ b/docs/Commands List.md @@ -90,7 +90,6 @@ Commands and aliases | Description | Usage `.setgame` | Sets the bots game. **Bot owner only** | `.setgame with snakes` `.setstream` | Sets the bots stream. First argument is the twitch link, second argument is stream name. **Bot owner only** | `.setstream TWITCHLINK Hello` `.send` | Sends a message to someone on a different server through the bot. Separate server and channel/user ids with `|` and prefix the channel id with `c:` and the user id with `u:`. **Bot owner only** | `.send serverid|c:channelid message` or `.send serverid|u:userid message` -`.announce` | Sends a message to all servers' default channel that bot is connected to. **Bot owner only** | `.announce Useless spam` `.reloadimages` | Reloads images bot is using. Safe to use even when bot is being used heavily. **Bot owner only** | `.reloadimages` `.greetdel` `.grdel` | Sets the time it takes (in seconds) for greet messages to be auto-deleted. Set it to 0 to disable automatic deletion. **Requires ManageServer server permission.** | `.greetdel 0` or `.greetdel 30` `.greet` | Toggles anouncements on the current channel when someone joins the server. **Requires ManageServer server permission.** | `.greet` @@ -275,6 +274,7 @@ Commands and aliases | Description | Usage `.boobs` | Real adult content. | `.boobs` `.butts` `.ass` `.butt` | Real adult content. | `.butts` or `.ass` `.nsfwtagbl` `.nsfwtbl` | Toggles whether the tag is blacklisted or not in nsfw searches. Provide no parameters to see the list of blacklisted tags. | `.nsfwtbl poop` +`.nsfwcc` | Clears nsfw cache. **Bot owner only** | `.nsfwcc` ###### [Back to ToC](#table-of-contents) @@ -461,7 +461,7 @@ Commands and aliases | Description | Usage `.clubbans` | Shows the list of users who have banned from your club. Paginated. You must be club owner to use this command. | `.clubbans 2` `.clubapps` | Shows the list of users who have applied to your club. Paginated. You must be club owner to use this command. | `.clubapps 2` `.clubapply` | Apply to join a club. You must meet that club's minimum level requirement, and not be on its ban list. | `.clubapply b1nzy's friends#123` -`.clubaccept` | Apply to join a club. You must meet that club's minimum level requirement, and not be on its ban list. | `.clubaccept b1nzy#1337` +`.clubaccept` | Accept a user who applied to your club. | `.clubaccept b1nzy#1337` `.clubleave` | Leaves the club you're currently in. | `.clubleave` `.clubkick` | Kicks the user from the club. You must be the club owner. They will be able to apply again. | `.clubkick b1nzy#1337` `.clubban` | Bans the user from the club. You must be the club owner. They will not be able to apply again. | `.clubban b1nzy#1337` diff --git a/src/NadekoBot/Resources/CommandStrings.resx b/src/NadekoBot/Resources/CommandStrings.resx index ded835c9..079f5fc4 100644 --- a/src/NadekoBot/Resources/CommandStrings.resx +++ b/src/NadekoBot/Resources/CommandStrings.resx @@ -3679,7 +3679,7 @@ `{0}clubaccept b1nzy#1337` - Apply to join a club. You must meet that club's minimum level requirement, and not be on its ban list. + Accept a user who applied to your club. clubleave From 46f9de01d667416b057affdc4bef34cea4f5f6e1 Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Tue, 12 Sep 2017 22:27:51 +0200 Subject: [PATCH 303/346] Using redis to cache avatar images, reduced xp image size. --- NadekoBot.sln | 5 +++- .../Modules/Xp/Services/XpService.cs | 29 +++++++++---------- src/NadekoBot/Modules/Xp/Xp.cs | 8 +++-- src/NadekoBot/NadekoBot.cs | 1 + src/NadekoBot/NadekoBot.csproj | 6 ++++ src/NadekoBot/Services/IDataCache.cs | 14 +++++++++ src/NadekoBot/Services/Impl/RedisCache.cs | 28 ++++++++++++++++++ 7 files changed, 73 insertions(+), 18 deletions(-) create mode 100644 src/NadekoBot/Services/IDataCache.cs create mode 100644 src/NadekoBot/Services/Impl/RedisCache.cs diff --git a/NadekoBot.sln b/NadekoBot.sln index 8c235940..42b343e5 100644 --- a/NadekoBot.sln +++ b/NadekoBot.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 15 -VisualStudioVersion = 15.0.26430.12 +VisualStudioVersion = 15.0.26730.3 MinimumVisualStudioVersion = 10.0.40219.1 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{04929013-5BAB-42B0-B9B2-8F2BB8F16AF2}" EndProject @@ -33,4 +33,7 @@ Global GlobalSection(NestedProjects) = preSolution {45EC1473-C678-4857-A544-07DFE0D0B478} = {04929013-5BAB-42B0-B9B2-8F2BB8F16AF2} EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {5F3F555C-855F-4BE8-B526-D062D3E8ACA4} + EndGlobalSection EndGlobal diff --git a/src/NadekoBot/Modules/Xp/Services/XpService.cs b/src/NadekoBot/Modules/Xp/Services/XpService.cs index 78dcc5f9..f82741bf 100644 --- a/src/NadekoBot/Modules/Xp/Services/XpService.cs +++ b/src/NadekoBot/Modules/Xp/Services/XpService.cs @@ -36,6 +36,7 @@ namespace NadekoBot.Modules.Xp.Services private readonly IImagesService _images; private readonly Logger _log; private readonly NadekoStrings _strings; + private readonly IDataCache _cache; private readonly FontCollection _fonts = new FontCollection(); public const int XP_REQUIRED_LVL_1 = 36; @@ -54,9 +55,6 @@ namespace NadekoBot.Modules.Xp.Services private readonly ConcurrentQueue _addMessageXp = new ConcurrentQueue(); - private readonly ConcurrentDictionary _imageStreams - = new ConcurrentDictionary(); - private readonly Timer updateXpTimer; private readonly HttpClient http = new HttpClient(); private FontFamily _usernameFontFamily; @@ -69,7 +67,7 @@ namespace NadekoBot.Modules.Xp.Services public XpService(CommandHandler cmd, IBotConfigProvider bc, IEnumerable allGuildConfigs, IImagesService images, - DbService db, NadekoStrings strings) + DbService db, NadekoStrings strings, IDataCache cache) { _db = db; _cmd = cmd; @@ -77,6 +75,7 @@ namespace NadekoBot.Modules.Xp.Services _images = images; _log = LogManager.GetCurrentClassLogger(); _strings = strings; + _cache = cache; //load settings allGuildConfigs = allGuildConfigs.Where(x => x.XpSettings != null); @@ -665,20 +664,20 @@ namespace NadekoBot.Modules.Xp.Services { var avatarUrl = stats.User.RealAvatarUrl(); - byte[] s; - if (!_imageStreams.TryGetValue(avatarUrl, out s)) + var (succ, data) = await _cache.TryGetImageDataAsync(avatarUrl); + if (!succ) { using (var temp = await http.GetStreamAsync(avatarUrl)) { var tempDraw = Image.Load(temp); tempDraw = tempDraw.Resize(69, 70); ApplyRoundedCorners(tempDraw, 35); - s = tempDraw.ToStream().ToArray(); + data = tempDraw.ToStream().ToArray(); } - _imageStreams.AddOrUpdate(avatarUrl, s, (k, v) => s); + await _cache.SetImageDataAsync(avatarUrl, data); } - var toDraw = Image.Load(s); + var toDraw = Image.Load(data); img.DrawImage(toDraw, @@ -699,20 +698,20 @@ namespace NadekoBot.Modules.Xp.Services var imgUrl = stats.User.Club.ImageUrl; try { - byte[] s; - if (!_imageStreams.TryGetValue(imgUrl, out s)) + var (succ, data) = await _cache.TryGetImageDataAsync(imgUrl); + if (!succ) { using (var temp = await http.GetStreamAsync(imgUrl)) { var tempDraw = Image.Load(temp); tempDraw = tempDraw.Resize(45, 45); ApplyRoundedCorners(tempDraw, 22.5f); - s = tempDraw.ToStream().ToArray(); + data = tempDraw.ToStream().ToArray(); } - _imageStreams.AddOrUpdate(imgUrl, s, (k, v) => s); + await _cache.SetImageDataAsync(imgUrl, data); } - var toDraw = Image.Load(s); + var toDraw = Image.Load(data); img.DrawImage(toDraw, 1, @@ -724,7 +723,7 @@ namespace NadekoBot.Modules.Xp.Services _log.Warn(ex); } } - + img.Resize(432, 211); var arr = img.ToStream().ToArray(); //_log.Info("{0:F2} KB", arr.Length * 1.0f / 1.KB()); diff --git a/src/NadekoBot/Modules/Xp/Xp.cs b/src/NadekoBot/Modules/Xp/Xp.cs index ad687792..ee88aa00 100644 --- a/src/NadekoBot/Modules/Xp/Xp.cs +++ b/src/NadekoBot/Modules/Xp/Xp.cs @@ -26,12 +26,16 @@ namespace NadekoBot.Modules.Xp public async Task Experience([Remainder]IUser user = null) { user = user ?? Context.User; - + var sw = Stopwatch.StartNew(); await Context.Channel.TriggerTypingAsync(); var img = await _service.GenerateImageAsync((IGuildUser)user); - + sw.Stop(); + _log.Info("Generating finished in {0:F2}s", sw.Elapsed.TotalSeconds); + sw.Restart(); await Context.Channel.SendFileAsync(img.ToStream(), $"{user.Id}_xp.png") .ConfigureAwait(false); + sw.Stop(); + _log.Info("Sending finished in {0:F2}s", sw.Elapsed.TotalSeconds); } [NadekoCommand, Usage, Description, Aliases] diff --git a/src/NadekoBot/NadekoBot.cs b/src/NadekoBot/NadekoBot.cs index a92380ca..a85da294 100644 --- a/src/NadekoBot/NadekoBot.cs +++ b/src/NadekoBot/NadekoBot.cs @@ -139,6 +139,7 @@ namespace NadekoBot .AddManual>(AllGuildConfigs) //todo wrap this .AddManual(this) .AddManual(uow) + .AddManual(new RedisCache()) .LoadFrom(Assembly.GetEntryAssembly()) .Build(); diff --git a/src/NadekoBot/NadekoBot.csproj b/src/NadekoBot/NadekoBot.csproj index ff8ed03e..1a5bf786 100644 --- a/src/NadekoBot/NadekoBot.csproj +++ b/src/NadekoBot/NadekoBot.csproj @@ -21,6 +21,7 @@ 1.0.0.0 nadeko_icon.ico win7-x64 + Debug;Release;global_nadeko @@ -78,6 +79,7 @@ + @@ -95,6 +97,10 @@ latest + + latest + + diff --git a/src/NadekoBot/Services/IDataCache.cs b/src/NadekoBot/Services/IDataCache.cs new file mode 100644 index 00000000..67223dfe --- /dev/null +++ b/src/NadekoBot/Services/IDataCache.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace NadekoBot.Services +{ + public interface IDataCache + { + Task<(bool Success, byte[] Data)> TryGetImageDataAsync(string key); + Task SetImageDataAsync(string key, byte[] data); + } +} diff --git a/src/NadekoBot/Services/Impl/RedisCache.cs b/src/NadekoBot/Services/Impl/RedisCache.cs new file mode 100644 index 00000000..b4d9a89d --- /dev/null +++ b/src/NadekoBot/Services/Impl/RedisCache.cs @@ -0,0 +1,28 @@ +using StackExchange.Redis; +using System.Threading.Tasks; + +namespace NadekoBot.Services.Impl +{ + public class RedisCache : IDataCache + { + private readonly ConnectionMultiplexer _redis; + private readonly IDatabase _db; + + public RedisCache() + { + _redis = ConnectionMultiplexer.Connect("localhost"); + _db = _redis.GetDatabase(); + } + + public async Task<(bool Success, byte[] Data)> TryGetImageDataAsync(string key) + { + byte[] x = await _db.StringGetAsync("image_" + key); + return (x != null, x); + } + + public Task SetImageDataAsync(string key, byte[] data) + { + return _db.StringSetAsync("image_" + key, data); + } + } +} From 438f68cde7e1b14ce3c6951e04d5d995878d17ae Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Tue, 12 Sep 2017 23:49:37 +0200 Subject: [PATCH 304/346] global nadeko won't cache nsfw images --- .../Searches/Common/SearchImageCacher.cs | 2 + .../Modules/Xp/Services/XpService.cs | 278 +++++++++--------- src/NadekoBot/Modules/Xp/Xp.cs | 2 +- 3 files changed, 140 insertions(+), 142 deletions(-) diff --git a/src/NadekoBot/Modules/Searches/Common/SearchImageCacher.cs b/src/NadekoBot/Modules/Searches/Common/SearchImageCacher.cs index 323a5eae..8bd5d919 100644 --- a/src/NadekoBot/Modules/Searches/Common/SearchImageCacher.cs +++ b/src/NadekoBot/Modules/Searches/Common/SearchImageCacher.cs @@ -76,11 +76,13 @@ namespace NadekoBot.Modules.Searches.Common if (images.Length == 0) return null; var toReturn = images[_rng.Next(images.Length)]; +#if !GLOBAL_NADEKO foreach (var dledImg in images) { if(dledImg != toReturn) _cache.Add(dledImg); } +#endif return toReturn; } } diff --git a/src/NadekoBot/Modules/Xp/Services/XpService.cs b/src/NadekoBot/Modules/Xp/Services/XpService.cs index f82741bf..9bc1969b 100644 --- a/src/NadekoBot/Modules/Xp/Services/XpService.cs +++ b/src/NadekoBot/Modules/Xp/Services/XpService.cs @@ -549,7 +549,7 @@ namespace NadekoBot.Modules.Xp.Services } } - public Task> GenerateImageAsync(IGuildUser user) + public Task GenerateImageAsync(IGuildUser user) { return GenerateImageAsync(GetUserStats(user)); } @@ -565,170 +565,166 @@ namespace NadekoBot.Modules.Xp.Services _timeFont = _fonts.Find("Whitney-Bold").CreateFont(20); } - public Task> GenerateImageAsync(FullUserStats stats) => Task.Run(async () => + public Task GenerateImageAsync(FullUserStats stats) => Task.Run(async () => { - var img = Image.Load(_images.XpCard.ToArray()); - - var username = stats.User.ToString(); - var usernameFont = _usernameFontFamily - .CreateFont(username.Length <= 6 - ? 50 - : 50 - username.Length); - - img.DrawText("@" + username, usernameFont, Rgba32.White, - new PointF(130, 5)); - - // level - - img.DrawText(stats.Global.Level.ToString(), _levelFont, Rgba32.White, - new PointF(47, 137)); - - img.DrawText(stats.Guild.Level.ToString(), _levelFont, Rgba32.White, - new PointF(47, 285)); - - //club name - - var clubName = stats.User.Club?.ToString() ?? "-"; - - var clubFont = _clubFontFamily - .CreateFont(clubName.Length <= 8 - ? 35 - : 35 - (clubName.Length / 2)); - - img.DrawText(clubName, clubFont, Rgba32.White, - new PointF(650 - clubName.Length * 10, 40)); - - var pen = new Pen(Rgba32.Black, 1); - var brush = Brushes.Solid(Rgba32.White); - var xpBgBrush = Brushes.Solid(new Rgba32(0, 0, 0, 0.4f)); - - var global = stats.Global; - var guild = stats.Guild; - - //xp bar - - img.FillPolygon(xpBgBrush, new[] { - new PointF(321, 104), - new PointF(321 + (450 * (global.LevelXp / (float)global.RequiredXp)), 104), - new PointF(286 + (450 * (global.LevelXp / (float)global.RequiredXp)), 235), - new PointF(286, 235), - }); - img.DrawText($"{global.LevelXp}/{global.RequiredXp}", _xpFont, brush, pen, - new PointF(430, 130)); - - img.FillPolygon(xpBgBrush, new[] { - new PointF(282, 248), - new PointF(282 + (450 * (guild.LevelXp / (float)guild.RequiredXp)), 248), - new PointF(247 + (450 * (guild.LevelXp / (float)guild.RequiredXp)), 379), - new PointF(247, 379), - }); - img.DrawText($"{guild.LevelXp}/{guild.RequiredXp}", _xpFont, brush, pen, - new PointF(400, 270)); - - if (stats.FullGuildStats.AwardedXp != 0) + using (var img = Image.Load(_images.XpCard.ToArray())) { - var sign = stats.FullGuildStats.AwardedXp > 0 - ? "+ " - : ""; - img.DrawText($"({sign}{stats.FullGuildStats.AwardedXp})", _awardedFont, brush, pen, - new PointF(445 - (Math.Max(0, (stats.FullGuildStats.AwardedXp.ToString().Length - 2)) * 5), 335)); - } - //ranking + var username = stats.User.ToString(); + var usernameFont = _usernameFontFamily + .CreateFont(username.Length <= 6 + ? 50 + : 50 - username.Length); - img.DrawText(stats.GlobalRanking.ToString(), _rankFont, Rgba32.White, - new PointF(148, 170)); + img.DrawText("@" + username, usernameFont, Rgba32.White, + new PointF(130, 5)); - img.DrawText(stats.GuildRanking.ToString(), _rankFont, Rgba32.White, - new PointF(148, 317)); + // level - //time on this level + img.DrawText(stats.Global.Level.ToString(), _levelFont, Rgba32.White, + new PointF(47, 137)); - string GetTimeSpent(DateTime time) - { - var offset = DateTime.UtcNow - time; - return $"{offset.Days}d{offset.Hours}h{offset.Minutes}m"; - } + img.DrawText(stats.Guild.Level.ToString(), _levelFont, Rgba32.White, + new PointF(47, 285)); - img.DrawText(GetTimeSpent(stats.User.LastLevelUp), _timeFont, Rgba32.White, - new PointF(50, 197)); + //club name - img.DrawText(GetTimeSpent(stats.FullGuildStats.LastLevelUp), _timeFont, Rgba32.White, - new PointF(50, 344)); + var clubName = stats.User.Club?.ToString() ?? "-"; - //avatar + var clubFont = _clubFontFamily + .CreateFont(clubName.Length <= 8 + ? 35 + : 35 - (clubName.Length / 2)); - if (stats.User.AvatarId != null) - { - try + img.DrawText(clubName, clubFont, Rgba32.White, + new PointF(650 - clubName.Length * 10, 40)); + + var pen = new Pen(Rgba32.Black, 1); + var brush = Brushes.Solid(Rgba32.White); + var xpBgBrush = Brushes.Solid(new Rgba32(0, 0, 0, 0.4f)); + + var global = stats.Global; + var guild = stats.Guild; + + //xp bar + + img.FillPolygon(xpBgBrush, new[] { + new PointF(321, 104), + new PointF(321 + (450 * (global.LevelXp / (float)global.RequiredXp)), 104), + new PointF(286 + (450 * (global.LevelXp / (float)global.RequiredXp)), 235), + new PointF(286, 235), + }); + img.DrawText($"{global.LevelXp}/{global.RequiredXp}", _xpFont, brush, pen, + new PointF(430, 130)); + + img.FillPolygon(xpBgBrush, new[] { + new PointF(282, 248), + new PointF(282 + (450 * (guild.LevelXp / (float)guild.RequiredXp)), 248), + new PointF(247 + (450 * (guild.LevelXp / (float)guild.RequiredXp)), 379), + new PointF(247, 379), + }); + img.DrawText($"{guild.LevelXp}/{guild.RequiredXp}", _xpFont, brush, pen, + new PointF(400, 270)); + + if (stats.FullGuildStats.AwardedXp != 0) { - var avatarUrl = stats.User.RealAvatarUrl(); + var sign = stats.FullGuildStats.AwardedXp > 0 + ? "+ " + : ""; + img.DrawText($"({sign}{stats.FullGuildStats.AwardedXp})", _awardedFont, brush, pen, + new PointF(445 - (Math.Max(0, (stats.FullGuildStats.AwardedXp.ToString().Length - 2)) * 5), 335)); + } - var (succ, data) = await _cache.TryGetImageDataAsync(avatarUrl); - if (!succ) + //ranking + + img.DrawText(stats.GlobalRanking.ToString(), _rankFont, Rgba32.White, + new PointF(148, 170)); + + img.DrawText(stats.GuildRanking.ToString(), _rankFont, Rgba32.White, + new PointF(148, 317)); + + //time on this level + + string GetTimeSpent(DateTime time) + { + var offset = DateTime.UtcNow - time; + return $"{offset.Days}d{offset.Hours}h{offset.Minutes}m"; + } + + img.DrawText(GetTimeSpent(stats.User.LastLevelUp), _timeFont, Rgba32.White, + new PointF(50, 197)); + + img.DrawText(GetTimeSpent(stats.FullGuildStats.LastLevelUp), _timeFont, Rgba32.White, + new PointF(50, 344)); + + //avatar + + if (stats.User.AvatarId != null) + { + try { - using (var temp = await http.GetStreamAsync(avatarUrl)) + var avatarUrl = stats.User.RealAvatarUrl(); + + var (succ, data) = await _cache.TryGetImageDataAsync(avatarUrl); + if (!succ) { - var tempDraw = Image.Load(temp); - tempDraw = tempDraw.Resize(69, 70); - ApplyRoundedCorners(tempDraw, 35); - data = tempDraw.ToStream().ToArray(); + using (var temp = await http.GetStreamAsync(avatarUrl)) + using (var tempDraw = Image.Load(temp).Resize(69, 70)) + { + ApplyRoundedCorners(tempDraw, 35); + data = tempDraw.ToStream().ToArray(); + } + + await _cache.SetImageDataAsync(avatarUrl, data); } + var toDraw = Image.Load(data); - await _cache.SetImageDataAsync(avatarUrl, data); + + img.DrawImage(toDraw, + 1, + new Size(69, 70), + new Point(32, 10)); } - var toDraw = Image.Load(data); - - - img.DrawImage(toDraw, - 1, - new Size(69, 70), - new Point(32, 10)); - } - catch (Exception ex) - { - _log.Warn(ex); - } - } - - //club image - - if (!string.IsNullOrWhiteSpace(stats.User.Club?.ImageUrl)) - { - var imgUrl = stats.User.Club.ImageUrl; - try - { - var (succ, data) = await _cache.TryGetImageDataAsync(imgUrl); - if (!succ) + catch (Exception ex) { - using (var temp = await http.GetStreamAsync(imgUrl)) - { - var tempDraw = Image.Load(temp); - tempDraw = tempDraw.Resize(45, 45); - ApplyRoundedCorners(tempDraw, 22.5f); - data = tempDraw.ToStream().ToArray(); - } - - await _cache.SetImageDataAsync(imgUrl, data); + _log.Warn(ex); } - var toDraw = Image.Load(data); - - img.DrawImage(toDraw, - 1, - new Size(45, 45), - new Point(722, 25)); } - catch (Exception ex) + + //club image + + if (!string.IsNullOrWhiteSpace(stats.User.Club?.ImageUrl)) { - _log.Warn(ex); + var imgUrl = stats.User.Club.ImageUrl; + try + { + var (succ, data) = await _cache.TryGetImageDataAsync(imgUrl); + if (!succ) + { + using (var temp = await http.GetStreamAsync(imgUrl)) + using (var tempDraw = Image.Load(temp).Resize(45, 45)) + { + ApplyRoundedCorners(tempDraw, 22.5f); + data = tempDraw.ToStream().ToArray(); + } + + await _cache.SetImageDataAsync(imgUrl, data); + } + var toDraw = Image.Load(data); + + img.DrawImage(toDraw, + 1, + new Size(45, 45), + new Point(722, 25)); + } + catch (Exception ex) + { + _log.Warn(ex); + } } + + return img.Resize(432, 211).ToStream(); } - img.Resize(432, 211); - var arr = img.ToStream().ToArray(); - - //_log.Info("{0:F2} KB", arr.Length * 1.0f / 1.KB()); - - return img; }); diff --git a/src/NadekoBot/Modules/Xp/Xp.cs b/src/NadekoBot/Modules/Xp/Xp.cs index ee88aa00..31a02b35 100644 --- a/src/NadekoBot/Modules/Xp/Xp.cs +++ b/src/NadekoBot/Modules/Xp/Xp.cs @@ -32,7 +32,7 @@ namespace NadekoBot.Modules.Xp sw.Stop(); _log.Info("Generating finished in {0:F2}s", sw.Elapsed.TotalSeconds); sw.Restart(); - await Context.Channel.SendFileAsync(img.ToStream(), $"{user.Id}_xp.png") + await Context.Channel.SendFileAsync(img, $"{user.Id}_xp.png") .ConfigureAwait(false); sw.Stop(); _log.Info("Sending finished in {0:F2}s", sw.Elapsed.TotalSeconds); From a2c4695557313663e4675b8f6e14ff780bb049a7 Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Wed, 13 Sep 2017 03:12:40 +0200 Subject: [PATCH 305/346] Global custom reactions now use redis pub/sub --- .../CustomReactions/CustomReactions.cs | 6 +-- .../Services/CustomReactionsService.cs | 39 ++++++++++++++++++- .../Database/Models/CustomReaction.cs | 6 ++- src/NadekoBot/Services/IDataCache.cs | 4 +- src/NadekoBot/Services/Impl/RedisCache.cs | 7 ++-- 5 files changed, 51 insertions(+), 11 deletions(-) diff --git a/src/NadekoBot/Modules/CustomReactions/CustomReactions.cs b/src/NadekoBot/Modules/CustomReactions/CustomReactions.cs index 9867dbcb..33af0699 100644 --- a/src/NadekoBot/Modules/CustomReactions/CustomReactions.cs +++ b/src/NadekoBot/Modules/CustomReactions/CustomReactions.cs @@ -58,8 +58,7 @@ namespace NadekoBot.Modules.CustomReactions if (channel == null) { - Array.Resize(ref _service.GlobalReactions, _service.GlobalReactions.Length + 1); - _service.GlobalReactions[_service.GlobalReactions.Length - 1] = cr; + await _service.AddGcr(cr).ConfigureAwait(false); } else { @@ -237,8 +236,7 @@ namespace NadekoBot.Modules.CustomReactions if ((toDelete.GuildId == null || toDelete.GuildId == 0) && Context.Guild == null) { uow.CustomReactions.Remove(toDelete); - //todo 91 i can dramatically improve performance of this, if Ids are ordered. - _service.GlobalReactions = _service.GlobalReactions.Where(cr => cr?.Id != toDelete.Id).ToArray(); + await _service.DelGcr(toDelete.Id); success = true; } else if ((toDelete.GuildId != null && toDelete.GuildId != 0) && Context.Guild.Id == toDelete.GuildId) diff --git a/src/NadekoBot/Modules/CustomReactions/Services/CustomReactionsService.cs b/src/NadekoBot/Modules/CustomReactions/Services/CustomReactionsService.cs index 2944ab6c..bcb36784 100644 --- a/src/NadekoBot/Modules/CustomReactions/Services/CustomReactionsService.cs +++ b/src/NadekoBot/Modules/CustomReactions/Services/CustomReactionsService.cs @@ -15,6 +15,7 @@ using NadekoBot.Modules.CustomReactions.Extensions; using NadekoBot.Modules.Permissions.Common; using NadekoBot.Modules.Permissions.Services; using NadekoBot.Services.Impl; +using Newtonsoft.Json; namespace NadekoBot.Modules.CustomReactions.Services { @@ -32,9 +33,11 @@ namespace NadekoBot.Modules.CustomReactions.Services private readonly CommandHandler _cmd; private readonly IBotConfigProvider _bc; private readonly NadekoStrings _strings; + private readonly IDataCache _cache; public CustomReactionsService(PermissionService perms, DbService db, NadekoStrings strings, - DiscordSocketClient client, CommandHandler cmd, IBotConfigProvider bc, IUnitOfWork uow) + DiscordSocketClient client, CommandHandler cmd, IBotConfigProvider bc, IUnitOfWork uow, + IDataCache cache) { _log = LogManager.GetCurrentClassLogger(); _db = db; @@ -43,10 +46,42 @@ namespace NadekoBot.Modules.CustomReactions.Services _cmd = cmd; _bc = bc; _strings = strings; - + _cache = cache; + + var sub = _cache.Redis.GetSubscriber(); + sub.Subscribe("gcr.added", (ch, msg) => + { + Array.Resize(ref GlobalReactions, GlobalReactions.Length + 1); + GlobalReactions[GlobalReactions.Length - 1] = JsonConvert.DeserializeObject(msg); + }, StackExchange.Redis.CommandFlags.FireAndForget); + sub.Subscribe("gcr.deleted", (ch, msg) => + { + var id = int.Parse(msg); + GlobalReactions = GlobalReactions.Where(cr => cr?.Id != id).ToArray(); + }, StackExchange.Redis.CommandFlags.FireAndForget); + var items = uow.CustomReactions.GetAll(); + GuildReactions = new ConcurrentDictionary(items.Where(g => g.GuildId != null && g.GuildId != 0).GroupBy(k => k.GuildId.Value).ToDictionary(g => g.Key, g => g.ToArray())); GlobalReactions = items.Where(g => g.GuildId == null || g.GuildId == 0).ToArray(); + foreach (var item in items) + { + _log.Info(item.Id); + _log.Info(item.Trigger); + _log.Info(item.GuildId); + } + } + + public Task AddGcr(CustomReaction cr) + { + var sub = _cache.Redis.GetSubscriber(); + return sub.PublishAsync("gcr.added", JsonConvert.SerializeObject(cr)); + } + + public Task DelGcr(int id) + { + var sub = _cache.Redis.GetSubscriber(); + return sub.PublishAsync("gcr.deleted", id); } public void ClearStats() => ReactionStats.Clear(); diff --git a/src/NadekoBot/Services/Database/Models/CustomReaction.cs b/src/NadekoBot/Services/Database/Models/CustomReaction.cs index 2f268849..cd57bc35 100644 --- a/src/NadekoBot/Services/Database/Models/CustomReaction.cs +++ b/src/NadekoBot/Services/Database/Models/CustomReaction.cs @@ -1,4 +1,5 @@ -using System.ComponentModel.DataAnnotations.Schema; +using Newtonsoft.Json; +using System.ComponentModel.DataAnnotations.Schema; using System.Text.RegularExpressions; namespace NadekoBot.Services.Database.Models @@ -6,7 +7,9 @@ namespace NadekoBot.Services.Database.Models public class CustomReaction : DbEntity { public ulong? GuildId { get; set; } + [NotMapped] + [JsonIgnore] public Regex Regex { get; set; } public string Response { get; set; } public string Trigger { get; set; } @@ -16,6 +19,7 @@ namespace NadekoBot.Services.Database.Models public bool AutoDeleteTrigger { get; set; } public bool DmResponse { get; set; } + [JsonIgnore] public bool IsGlobal => !GuildId.HasValue; public bool ContainsAnywhere { get; set; } diff --git a/src/NadekoBot/Services/IDataCache.cs b/src/NadekoBot/Services/IDataCache.cs index 67223dfe..b708a66b 100644 --- a/src/NadekoBot/Services/IDataCache.cs +++ b/src/NadekoBot/Services/IDataCache.cs @@ -1,4 +1,5 @@ -using System; +using StackExchange.Redis; +using System; using System.Collections.Generic; using System.Linq; using System.Text; @@ -8,6 +9,7 @@ namespace NadekoBot.Services { public interface IDataCache { + ConnectionMultiplexer Redis { get; } Task<(bool Success, byte[] Data)> TryGetImageDataAsync(string key); Task SetImageDataAsync(string key, byte[] data); } diff --git a/src/NadekoBot/Services/Impl/RedisCache.cs b/src/NadekoBot/Services/Impl/RedisCache.cs index b4d9a89d..5e54979d 100644 --- a/src/NadekoBot/Services/Impl/RedisCache.cs +++ b/src/NadekoBot/Services/Impl/RedisCache.cs @@ -5,13 +5,14 @@ namespace NadekoBot.Services.Impl { public class RedisCache : IDataCache { - private readonly ConnectionMultiplexer _redis; + public ConnectionMultiplexer Redis { get; } private readonly IDatabase _db; public RedisCache() { - _redis = ConnectionMultiplexer.Connect("localhost"); - _db = _redis.GetDatabase(); + Redis = ConnectionMultiplexer.Connect("localhost"); + Redis.PreserveAsyncOrder = false; + _db = Redis.GetDatabase(); } public async Task<(bool Success, byte[] Data)> TryGetImageDataAsync(string key) From 067297478edb7464df57369cbf8d41d6528b4281 Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Wed, 13 Sep 2017 03:18:33 +0200 Subject: [PATCH 306/346] Removed leftover logs --- .../CustomReactions/Services/CustomReactionsService.cs | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/NadekoBot/Modules/CustomReactions/Services/CustomReactionsService.cs b/src/NadekoBot/Modules/CustomReactions/Services/CustomReactionsService.cs index bcb36784..4cada529 100644 --- a/src/NadekoBot/Modules/CustomReactions/Services/CustomReactionsService.cs +++ b/src/NadekoBot/Modules/CustomReactions/Services/CustomReactionsService.cs @@ -64,12 +64,6 @@ namespace NadekoBot.Modules.CustomReactions.Services GuildReactions = new ConcurrentDictionary(items.Where(g => g.GuildId != null && g.GuildId != 0).GroupBy(k => k.GuildId.Value).ToDictionary(g => g.Key, g => g.ToArray())); GlobalReactions = items.Where(g => g.GuildId == null || g.GuildId == 0).ToArray(); - foreach (var item in items) - { - _log.Info(item.Id); - _log.Info(item.Trigger); - _log.Info(item.GuildId); - } } public Task AddGcr(CustomReaction cr) From 48adfc19af30042524bdb9d803888267497a279b Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Wed, 13 Sep 2017 05:10:26 +0200 Subject: [PATCH 307/346] Fixed slow `.xp` and `.xpglb` --- .../20170913022654_total-xp.Designer.cs | 1949 +++++++++++++++++ .../Migrations/20170913022654_total-xp.cs | 27 + src/NadekoBot/Migrations/MigrationQueries.cs | 4 + .../NadekoSqliteContextModelSnapshot.cs | 6 +- .../Modules/Xp/Services/XpService.cs | 7 +- src/NadekoBot/Modules/Xp/Xp.cs | 74 +- src/NadekoBot/Resources/CommandStrings.resx | 2 +- .../Services/Database/Models/DiscordUser.cs | 2 + .../Repositories/IDiscordUserRepository.cs | 2 + .../Database/Repositories/IXpRepository.cs | 4 +- .../Impl/DiscordUserRepository.cs | 19 + .../Repositories/Impl/XpRepository.cs | 20 - 12 files changed, 2063 insertions(+), 53 deletions(-) create mode 100644 src/NadekoBot/Migrations/20170913022654_total-xp.Designer.cs create mode 100644 src/NadekoBot/Migrations/20170913022654_total-xp.cs diff --git a/src/NadekoBot/Migrations/20170913022654_total-xp.Designer.cs b/src/NadekoBot/Migrations/20170913022654_total-xp.Designer.cs new file mode 100644 index 00000000..6764dd56 --- /dev/null +++ b/src/NadekoBot/Migrations/20170913022654_total-xp.Designer.cs @@ -0,0 +1,1949 @@ +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using NadekoBot.Services.Database; +using NadekoBot.Services.Database.Models; + +namespace NadekoBot.Migrations +{ + [DbContext(typeof(NadekoContext))] + [Migration("20170913022654_total-xp")] + partial class totalxp + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { + modelBuilder + .HasAnnotation("ProductVersion", "1.1.1"); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.AntiRaidSetting", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("Action"); + + b.Property("DateAdded"); + + b.Property("GuildConfigId"); + + b.Property("Seconds"); + + b.Property("UserThreshold"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId") + .IsUnique(); + + b.ToTable("AntiRaidSetting"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.AntiSpamIgnore", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AntiSpamSettingId"); + + b.Property("ChannelId"); + + b.Property("DateAdded"); + + b.HasKey("Id"); + + b.HasIndex("AntiSpamSettingId"); + + b.ToTable("AntiSpamIgnore"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.AntiSpamSetting", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("Action"); + + b.Property("DateAdded"); + + b.Property("GuildConfigId"); + + b.Property("MessageThreshold"); + + b.Property("MuteTime"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId") + .IsUnique(); + + b.ToTable("AntiSpamSetting"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.BlacklistItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("BotConfigId"); + + b.Property("DateAdded"); + + b.Property("ItemId"); + + b.Property("Type"); + + b.HasKey("Id"); + + b.HasIndex("BotConfigId"); + + b.ToTable("BlacklistItem"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.BlockedCmdOrMdl", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("BotConfigId"); + + b.Property("BotConfigId1"); + + b.Property("DateAdded"); + + b.Property("Name"); + + b.HasKey("Id"); + + b.HasIndex("BotConfigId"); + + b.HasIndex("BotConfigId1"); + + b.ToTable("BlockedCmdOrMdl"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.BotConfig", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("BetflipMultiplier"); + + b.Property("Betroll100Multiplier"); + + b.Property("Betroll67Multiplier"); + + b.Property("Betroll91Multiplier"); + + b.Property("BufferSize"); + + b.Property("CurrencyDropAmount"); + + b.Property("CurrencyDropAmountMax"); + + b.Property("CurrencyGenerationChance"); + + b.Property("CurrencyGenerationCooldown"); + + b.Property("CurrencyName"); + + b.Property("CurrencyPluralName"); + + b.Property("CurrencySign"); + + b.Property("CustomReactionsStartWith"); + + b.Property("DMHelpString"); + + b.Property("DateAdded"); + + b.Property("DefaultPrefix"); + + b.Property("ErrorColor"); + + b.Property("ForwardMessages"); + + b.Property("ForwardToAllOwners"); + + b.Property("HelpString"); + + b.Property("Locale"); + + b.Property("MigrationVersion"); + + b.Property("MinimumBetAmount"); + + b.Property("OkColor"); + + b.Property("PermissionVersion"); + + b.Property("RemindMessageFormat"); + + b.Property("RotatingStatuses"); + + b.Property("TriviaCurrencyReward"); + + b.Property("XpMinutesTimeout") + .ValueGeneratedOnAdd() + .HasDefaultValue(5); + + b.Property("XpPerMessage") + .ValueGeneratedOnAdd() + .HasDefaultValue(3); + + b.HasKey("Id"); + + b.ToTable("BotConfig"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.ClashCaller", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("BaseDestroyed"); + + b.Property("CallUser"); + + b.Property("ClashWarId"); + + b.Property("DateAdded"); + + b.Property("SequenceNumber"); + + b.Property("Stars"); + + b.Property("TimeAdded"); + + b.HasKey("Id"); + + b.HasIndex("ClashWarId"); + + b.ToTable("ClashCallers"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.ClashWar", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ChannelId"); + + b.Property("DateAdded"); + + b.Property("EnemyClan"); + + b.Property("GuildId"); + + b.Property("Size"); + + b.Property("StartedAt"); + + b.Property("WarState"); + + b.HasKey("Id"); + + b.ToTable("ClashOfClans"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.ClubApplicants", b => + { + b.Property("ClubId"); + + b.Property("UserId"); + + b.HasKey("ClubId", "UserId"); + + b.HasIndex("UserId"); + + b.ToTable("ClubApplicants"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.ClubBans", b => + { + b.Property("ClubId"); + + b.Property("UserId"); + + b.HasKey("ClubId", "UserId"); + + b.HasIndex("UserId"); + + b.ToTable("ClubBans"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.ClubInfo", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("Discrim"); + + b.Property("ImageUrl"); + + b.Property("MinimumLevelReq"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(20); + + b.Property("OwnerId"); + + b.Property("Xp"); + + b.HasKey("Id"); + + b.HasAlternateKey("Name", "Discrim"); + + b.HasIndex("OwnerId") + .IsUnique(); + + b.ToTable("Clubs"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.CommandAlias", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("GuildConfigId"); + + b.Property("Mapping"); + + b.Property("Trigger"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.ToTable("CommandAlias"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.CommandCooldown", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("CommandName"); + + b.Property("DateAdded"); + + b.Property("GuildConfigId"); + + b.Property("Seconds"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.ToTable("CommandCooldown"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.CommandPrice", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("BotConfigId"); + + b.Property("CommandName"); + + b.Property("DateAdded"); + + b.Property("Price"); + + b.HasKey("Id"); + + b.HasIndex("BotConfigId"); + + b.HasIndex("Price") + .IsUnique(); + + b.ToTable("CommandPrice"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.ConvertUnit", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("InternalTrigger"); + + b.Property("Modifier"); + + b.Property("UnitType"); + + b.HasKey("Id"); + + b.ToTable("ConversionUnits"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.Currency", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("Amount"); + + b.Property("DateAdded"); + + b.Property("UserId"); + + b.HasKey("Id"); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("Currency"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.CurrencyTransaction", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("Amount"); + + b.Property("DateAdded"); + + b.Property("Reason"); + + b.Property("UserId"); + + b.HasKey("Id"); + + b.ToTable("CurrencyTransactions"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.CustomReaction", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AutoDeleteTrigger"); + + b.Property("ContainsAnywhere"); + + b.Property("DateAdded"); + + b.Property("DmResponse"); + + b.Property("GuildId"); + + b.Property("IsRegex"); + + b.Property("OwnerOnly"); + + b.Property("Response"); + + b.Property("Trigger"); + + b.HasKey("Id"); + + b.ToTable("CustomReactions"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.DiscordUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AvatarId"); + + b.Property("ClubId"); + + b.Property("DateAdded"); + + b.Property("Discriminator"); + + b.Property("LastLevelUp") + .ValueGeneratedOnAdd() + .HasDefaultValue(new DateTime(2017, 9, 13, 4, 26, 53, 906, DateTimeKind.Local)); + + b.Property("LastXpGain"); + + b.Property("NotifyOnLevelUp"); + + b.Property("TotalXp"); + + b.Property("UserId"); + + b.Property("Username"); + + b.HasKey("Id"); + + b.HasAlternateKey("UserId"); + + b.HasIndex("ClubId"); + + b.ToTable("DiscordUser"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.Donator", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("Amount"); + + b.Property("DateAdded"); + + b.Property("Name"); + + b.Property("UserId"); + + b.HasKey("Id"); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("Donators"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.EightBallResponse", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("BotConfigId"); + + b.Property("DateAdded"); + + b.Property("Text"); + + b.HasKey("Id"); + + b.HasIndex("BotConfigId"); + + b.ToTable("EightBallResponses"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.ExcludedItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("ItemId"); + + b.Property("ItemType"); + + b.Property("XpSettingsId"); + + b.HasKey("Id"); + + b.HasIndex("XpSettingsId"); + + b.ToTable("ExcludedItem"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.FilterChannelId", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ChannelId"); + + b.Property("DateAdded"); + + b.Property("GuildConfigId"); + + b.Property("GuildConfigId1"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.HasIndex("GuildConfigId1"); + + b.ToTable("FilterChannelId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.FilteredWord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("GuildConfigId"); + + b.Property("Word"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.ToTable("FilteredWord"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.FollowedStream", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ChannelId"); + + b.Property("DateAdded"); + + b.Property("GuildConfigId"); + + b.Property("GuildId"); + + b.Property("Type"); + + b.Property("Username"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.ToTable("FollowedStream"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.GCChannelId", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ChannelId"); + + b.Property("DateAdded"); + + b.Property("GuildConfigId"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.ToTable("GCChannelId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.GuildConfig", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AutoAssignRoleId"); + + b.Property("AutoDeleteByeMessages"); + + b.Property("AutoDeleteByeMessagesTimer"); + + b.Property("AutoDeleteGreetMessages"); + + b.Property("AutoDeleteGreetMessagesTimer"); + + b.Property("AutoDeleteSelfAssignedRoleMessages"); + + b.Property("ByeMessageChannelId"); + + b.Property("ChannelByeMessageText"); + + b.Property("ChannelGreetMessageText"); + + b.Property("CleverbotEnabled"); + + b.Property("DateAdded"); + + b.Property("DefaultMusicVolume"); + + b.Property("DeleteMessageOnCommand"); + + b.Property("DmGreetMessageText"); + + b.Property("ExclusiveSelfAssignedRoles"); + + b.Property("FilterInvites"); + + b.Property("FilterWords"); + + b.Property("GameVoiceChannel"); + + b.Property("GreetMessageChannelId"); + + b.Property("GuildId"); + + b.Property("Locale"); + + b.Property("LogSettingId"); + + b.Property("MuteRoleName"); + + b.Property("PermissionRole"); + + b.Property("Prefix"); + + b.Property("RootPermissionId"); + + b.Property("SendChannelByeMessage"); + + b.Property("SendChannelGreetMessage"); + + b.Property("SendDmGreetMessage"); + + b.Property("TimeZoneId"); + + b.Property("VerboseErrors"); + + b.Property("VerbosePermissions"); + + b.Property("VoicePlusTextEnabled"); + + b.Property("WarningsInitialized"); + + b.HasKey("Id"); + + b.HasIndex("GuildId") + .IsUnique(); + + b.HasIndex("LogSettingId"); + + b.HasIndex("RootPermissionId"); + + b.ToTable("GuildConfigs"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.GuildRepeater", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ChannelId"); + + b.Property("DateAdded"); + + b.Property("GuildConfigId"); + + b.Property("GuildId"); + + b.Property("Interval"); + + b.Property("Message"); + + b.Property("StartTimeOfDay"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.ToTable("GuildRepeater"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.IgnoredLogChannel", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ChannelId"); + + b.Property("DateAdded"); + + b.Property("LogSettingId"); + + b.HasKey("Id"); + + b.HasIndex("LogSettingId"); + + b.ToTable("IgnoredLogChannels"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.IgnoredVoicePresenceChannel", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ChannelId"); + + b.Property("DateAdded"); + + b.Property("LogSettingId"); + + b.HasKey("Id"); + + b.HasIndex("LogSettingId"); + + b.ToTable("IgnoredVoicePresenceCHannels"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.LogSetting", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ChannelCreated"); + + b.Property("ChannelCreatedId"); + + b.Property("ChannelDestroyed"); + + b.Property("ChannelDestroyedId"); + + b.Property("ChannelId"); + + b.Property("ChannelUpdated"); + + b.Property("ChannelUpdatedId"); + + b.Property("DateAdded"); + + b.Property("IsLogging"); + + b.Property("LogOtherId"); + + b.Property("LogUserPresence"); + + b.Property("LogUserPresenceId"); + + b.Property("LogVoicePresence"); + + b.Property("LogVoicePresenceId"); + + b.Property("LogVoicePresenceTTSId"); + + b.Property("MessageDeleted"); + + b.Property("MessageDeletedId"); + + b.Property("MessageUpdated"); + + b.Property("MessageUpdatedId"); + + b.Property("UserBanned"); + + b.Property("UserBannedId"); + + b.Property("UserJoined"); + + b.Property("UserJoinedId"); + + b.Property("UserLeft"); + + b.Property("UserLeftId"); + + b.Property("UserMutedId"); + + b.Property("UserPresenceChannelId"); + + b.Property("UserUnbanned"); + + b.Property("UserUnbannedId"); + + b.Property("UserUpdated"); + + b.Property("UserUpdatedId"); + + b.Property("VoicePresenceChannelId"); + + b.HasKey("Id"); + + b.ToTable("LogSettings"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.ModulePrefix", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("BotConfigId"); + + b.Property("DateAdded"); + + b.Property("ModuleName"); + + b.Property("Prefix"); + + b.HasKey("Id"); + + b.HasIndex("BotConfigId"); + + b.ToTable("ModulePrefixes"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.MusicPlaylist", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("Author"); + + b.Property("AuthorId"); + + b.Property("DateAdded"); + + b.Property("Name"); + + b.HasKey("Id"); + + b.ToTable("MusicPlaylists"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.MutedUserId", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("GuildConfigId"); + + b.Property("UserId"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.ToTable("MutedUserId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.NsfwBlacklitedTag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("GuildConfigId"); + + b.Property("Tag"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.ToTable("NsfwBlacklitedTag"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.Permission", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("NextId"); + + b.Property("PrimaryTarget"); + + b.Property("PrimaryTargetId"); + + b.Property("SecondaryTarget"); + + b.Property("SecondaryTargetName"); + + b.Property("State"); + + b.HasKey("Id"); + + b.HasIndex("NextId") + .IsUnique(); + + b.ToTable("Permission"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.Permissionv2", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("GuildConfigId"); + + b.Property("Index"); + + b.Property("PrimaryTarget"); + + b.Property("PrimaryTargetId"); + + b.Property("SecondaryTarget"); + + b.Property("SecondaryTargetName"); + + b.Property("State"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.ToTable("Permissionv2"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.PlayingStatus", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("BotConfigId"); + + b.Property("DateAdded"); + + b.Property("Status"); + + b.HasKey("Id"); + + b.HasIndex("BotConfigId"); + + b.ToTable("PlayingStatus"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.PlaylistSong", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("MusicPlaylistId"); + + b.Property("Provider"); + + b.Property("ProviderType"); + + b.Property("Query"); + + b.Property("Title"); + + b.Property("Uri"); + + b.HasKey("Id"); + + b.HasIndex("MusicPlaylistId"); + + b.ToTable("PlaylistSong"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.Quote", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AuthorId"); + + b.Property("AuthorName") + .IsRequired(); + + b.Property("DateAdded"); + + b.Property("GuildId"); + + b.Property("Keyword") + .IsRequired(); + + b.Property("Text") + .IsRequired(); + + b.HasKey("Id"); + + b.ToTable("Quotes"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.RaceAnimal", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("BotConfigId"); + + b.Property("DateAdded"); + + b.Property("Icon"); + + b.Property("Name"); + + b.HasKey("Id"); + + b.HasIndex("BotConfigId"); + + b.ToTable("RaceAnimals"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.Reminder", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ChannelId"); + + b.Property("DateAdded"); + + b.Property("IsPrivate"); + + b.Property("Message"); + + b.Property("ServerId"); + + b.Property("UserId"); + + b.Property("When"); + + b.HasKey("Id"); + + b.ToTable("Reminders"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.RewardedUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AmountRewardedThisMonth"); + + b.Property("DateAdded"); + + b.Property("LastReward"); + + b.Property("PatreonUserId"); + + b.Property("UserId"); + + b.HasKey("Id"); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("RewardedUsers"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.SelfAssignedRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("GuildId"); + + b.Property("RoleId"); + + b.HasKey("Id"); + + b.HasIndex("GuildId", "RoleId") + .IsUnique(); + + b.ToTable("SelfAssignableRoles"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.ShopEntry", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AuthorId"); + + b.Property("DateAdded"); + + b.Property("GuildConfigId"); + + b.Property("Index"); + + b.Property("Name"); + + b.Property("Price"); + + b.Property("RoleId"); + + b.Property("RoleName"); + + b.Property("Type"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.ToTable("ShopEntry"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.ShopEntryItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("ShopEntryId"); + + b.Property("Text"); + + b.HasKey("Id"); + + b.HasIndex("ShopEntryId"); + + b.ToTable("ShopEntryItem"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.SlowmodeIgnoredRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("GuildConfigId"); + + b.Property("RoleId"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.ToTable("SlowmodeIgnoredRole"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.SlowmodeIgnoredUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("GuildConfigId"); + + b.Property("UserId"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.ToTable("SlowmodeIgnoredUser"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.StartupCommand", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("BotConfigId"); + + b.Property("ChannelId"); + + b.Property("ChannelName"); + + b.Property("CommandText"); + + b.Property("DateAdded"); + + b.Property("GuildId"); + + b.Property("GuildName"); + + b.Property("Index"); + + b.Property("VoiceChannelId"); + + b.Property("VoiceChannelName"); + + b.HasKey("Id"); + + b.HasIndex("BotConfigId"); + + b.ToTable("StartupCommand"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.StreamRoleBlacklistedUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("StreamRoleSettingsId"); + + b.Property("UserId"); + + b.Property("Username"); + + b.HasKey("Id"); + + b.HasIndex("StreamRoleSettingsId"); + + b.ToTable("StreamRoleBlacklistedUser"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.StreamRoleSettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AddRoleId"); + + b.Property("DateAdded"); + + b.Property("Enabled"); + + b.Property("FromRoleId"); + + b.Property("GuildConfigId"); + + b.Property("Keyword"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId") + .IsUnique(); + + b.ToTable("StreamRoleSettings"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.StreamRoleWhitelistedUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("StreamRoleSettingsId"); + + b.Property("UserId"); + + b.Property("Username"); + + b.HasKey("Id"); + + b.HasIndex("StreamRoleSettingsId"); + + b.ToTable("StreamRoleWhitelistedUser"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.UnmuteTimer", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("GuildConfigId"); + + b.Property("UnmuteAt"); + + b.Property("UserId"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.ToTable("UnmuteTimer"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.UserPokeTypes", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("UserId"); + + b.Property("type"); + + b.HasKey("Id"); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("PokeGame"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.UserXpStats", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AwardedXp"); + + b.Property("DateAdded"); + + b.Property("GuildId"); + + b.Property("LastLevelUp") + .ValueGeneratedOnAdd() + .HasDefaultValue(new DateTime(2017, 9, 13, 4, 26, 53, 910, DateTimeKind.Local)); + + b.Property("NotifyOnLevelUp"); + + b.Property("UserId"); + + b.Property("Xp"); + + b.HasKey("Id"); + + b.HasIndex("UserId", "GuildId") + .IsUnique(); + + b.ToTable("UserXpStats"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.VcRoleInfo", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("GuildConfigId"); + + b.Property("RoleId"); + + b.Property("VoiceChannelId"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.ToTable("VcRoleInfo"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.WaifuInfo", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AffinityId"); + + b.Property("ClaimerId"); + + b.Property("DateAdded"); + + b.Property("Price"); + + b.Property("WaifuId"); + + b.HasKey("Id"); + + b.HasIndex("AffinityId"); + + b.HasIndex("ClaimerId"); + + b.HasIndex("WaifuId") + .IsUnique(); + + b.ToTable("WaifuInfo"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.WaifuItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("Item"); + + b.Property("ItemEmoji"); + + b.Property("Price"); + + b.Property("WaifuInfoId"); + + b.HasKey("Id"); + + b.HasIndex("WaifuInfoId"); + + b.ToTable("WaifuItem"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.WaifuUpdate", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("NewId"); + + b.Property("OldId"); + + b.Property("UpdateType"); + + b.Property("UserId"); + + b.HasKey("Id"); + + b.HasIndex("NewId"); + + b.HasIndex("OldId"); + + b.HasIndex("UserId"); + + b.ToTable("WaifuUpdates"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.Warning", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("Forgiven"); + + b.Property("ForgivenBy"); + + b.Property("GuildId"); + + b.Property("Moderator"); + + b.Property("Reason"); + + b.Property("UserId"); + + b.HasKey("Id"); + + b.ToTable("Warnings"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.WarningPunishment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("Count"); + + b.Property("DateAdded"); + + b.Property("GuildConfigId"); + + b.Property("Punishment"); + + b.Property("Time"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.ToTable("WarningPunishment"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.XpRoleReward", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("Level"); + + b.Property("RoleId"); + + b.Property("XpSettingsId"); + + b.HasKey("Id"); + + b.HasAlternateKey("Level"); + + b.HasIndex("XpSettingsId"); + + b.ToTable("XpRoleReward"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.XpSettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("GuildConfigId"); + + b.Property("NotifyMessage"); + + b.Property("ServerExcluded"); + + b.Property("XpRoleRewardExclusive"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId") + .IsUnique(); + + b.ToTable("XpSettings"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.AntiRaidSetting", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig", "GuildConfig") + .WithOne("AntiRaidSetting") + .HasForeignKey("NadekoBot.Services.Database.Models.AntiRaidSetting", "GuildConfigId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.AntiSpamIgnore", b => + { + b.HasOne("NadekoBot.Services.Database.Models.AntiSpamSetting") + .WithMany("IgnoredChannels") + .HasForeignKey("AntiSpamSettingId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.AntiSpamSetting", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig", "GuildConfig") + .WithOne("AntiSpamSetting") + .HasForeignKey("NadekoBot.Services.Database.Models.AntiSpamSetting", "GuildConfigId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.BlacklistItem", b => + { + b.HasOne("NadekoBot.Services.Database.Models.BotConfig") + .WithMany("Blacklist") + .HasForeignKey("BotConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.BlockedCmdOrMdl", b => + { + b.HasOne("NadekoBot.Services.Database.Models.BotConfig") + .WithMany("BlockedCommands") + .HasForeignKey("BotConfigId"); + + b.HasOne("NadekoBot.Services.Database.Models.BotConfig") + .WithMany("BlockedModules") + .HasForeignKey("BotConfigId1"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.ClashCaller", b => + { + b.HasOne("NadekoBot.Services.Database.Models.ClashWar", "ClashWar") + .WithMany("Bases") + .HasForeignKey("ClashWarId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.ClubApplicants", b => + { + b.HasOne("NadekoBot.Services.Database.Models.ClubInfo", "Club") + .WithMany("Applicants") + .HasForeignKey("ClubId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("NadekoBot.Services.Database.Models.DiscordUser", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.ClubBans", b => + { + b.HasOne("NadekoBot.Services.Database.Models.ClubInfo", "Club") + .WithMany("Bans") + .HasForeignKey("ClubId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("NadekoBot.Services.Database.Models.DiscordUser", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.ClubInfo", b => + { + b.HasOne("NadekoBot.Services.Database.Models.DiscordUser", "Owner") + .WithOne() + .HasForeignKey("NadekoBot.Services.Database.Models.ClubInfo", "OwnerId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.CommandAlias", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig") + .WithMany("CommandAliases") + .HasForeignKey("GuildConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.CommandCooldown", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig") + .WithMany("CommandCooldowns") + .HasForeignKey("GuildConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.CommandPrice", b => + { + b.HasOne("NadekoBot.Services.Database.Models.BotConfig") + .WithMany("CommandPrices") + .HasForeignKey("BotConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.DiscordUser", b => + { + b.HasOne("NadekoBot.Services.Database.Models.ClubInfo", "Club") + .WithMany("Users") + .HasForeignKey("ClubId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.EightBallResponse", b => + { + b.HasOne("NadekoBot.Services.Database.Models.BotConfig") + .WithMany("EightBallResponses") + .HasForeignKey("BotConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.ExcludedItem", b => + { + b.HasOne("NadekoBot.Services.Database.Models.XpSettings") + .WithMany("ExclusionList") + .HasForeignKey("XpSettingsId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.FilterChannelId", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig") + .WithMany("FilterInvitesChannelIds") + .HasForeignKey("GuildConfigId"); + + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig") + .WithMany("FilterWordsChannelIds") + .HasForeignKey("GuildConfigId1"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.FilteredWord", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig") + .WithMany("FilteredWords") + .HasForeignKey("GuildConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.FollowedStream", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig") + .WithMany("FollowedStreams") + .HasForeignKey("GuildConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.GCChannelId", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig") + .WithMany("GenerateCurrencyChannelIds") + .HasForeignKey("GuildConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.GuildConfig", b => + { + b.HasOne("NadekoBot.Services.Database.Models.LogSetting", "LogSetting") + .WithMany() + .HasForeignKey("LogSettingId"); + + b.HasOne("NadekoBot.Services.Database.Models.Permission", "RootPermission") + .WithMany() + .HasForeignKey("RootPermissionId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.GuildRepeater", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig") + .WithMany("GuildRepeaters") + .HasForeignKey("GuildConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.IgnoredLogChannel", b => + { + b.HasOne("NadekoBot.Services.Database.Models.LogSetting", "LogSetting") + .WithMany("IgnoredChannels") + .HasForeignKey("LogSettingId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.IgnoredVoicePresenceChannel", b => + { + b.HasOne("NadekoBot.Services.Database.Models.LogSetting", "LogSetting") + .WithMany("IgnoredVoicePresenceChannelIds") + .HasForeignKey("LogSettingId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.ModulePrefix", b => + { + b.HasOne("NadekoBot.Services.Database.Models.BotConfig") + .WithMany("ModulePrefixes") + .HasForeignKey("BotConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.MutedUserId", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig") + .WithMany("MutedUsers") + .HasForeignKey("GuildConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.NsfwBlacklitedTag", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig") + .WithMany("NsfwBlacklistedTags") + .HasForeignKey("GuildConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.Permission", b => + { + b.HasOne("NadekoBot.Services.Database.Models.Permission", "Next") + .WithOne("Previous") + .HasForeignKey("NadekoBot.Services.Database.Models.Permission", "NextId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.Permissionv2", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig") + .WithMany("Permissions") + .HasForeignKey("GuildConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.PlayingStatus", b => + { + b.HasOne("NadekoBot.Services.Database.Models.BotConfig") + .WithMany("RotatingStatusMessages") + .HasForeignKey("BotConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.PlaylistSong", b => + { + b.HasOne("NadekoBot.Services.Database.Models.MusicPlaylist") + .WithMany("Songs") + .HasForeignKey("MusicPlaylistId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.RaceAnimal", b => + { + b.HasOne("NadekoBot.Services.Database.Models.BotConfig") + .WithMany("RaceAnimals") + .HasForeignKey("BotConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.ShopEntry", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig") + .WithMany("ShopEntries") + .HasForeignKey("GuildConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.ShopEntryItem", b => + { + b.HasOne("NadekoBot.Services.Database.Models.ShopEntry") + .WithMany("Items") + .HasForeignKey("ShopEntryId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.SlowmodeIgnoredRole", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig") + .WithMany("SlowmodeIgnoredRoles") + .HasForeignKey("GuildConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.SlowmodeIgnoredUser", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig") + .WithMany("SlowmodeIgnoredUsers") + .HasForeignKey("GuildConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.StartupCommand", b => + { + b.HasOne("NadekoBot.Services.Database.Models.BotConfig") + .WithMany("StartupCommands") + .HasForeignKey("BotConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.StreamRoleBlacklistedUser", b => + { + b.HasOne("NadekoBot.Services.Database.Models.StreamRoleSettings") + .WithMany("Blacklist") + .HasForeignKey("StreamRoleSettingsId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.StreamRoleSettings", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig", "GuildConfig") + .WithOne("StreamRole") + .HasForeignKey("NadekoBot.Services.Database.Models.StreamRoleSettings", "GuildConfigId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.StreamRoleWhitelistedUser", b => + { + b.HasOne("NadekoBot.Services.Database.Models.StreamRoleSettings") + .WithMany("Whitelist") + .HasForeignKey("StreamRoleSettingsId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.UnmuteTimer", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig") + .WithMany("UnmuteTimers") + .HasForeignKey("GuildConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.VcRoleInfo", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig") + .WithMany("VcRoleInfos") + .HasForeignKey("GuildConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.WaifuInfo", b => + { + b.HasOne("NadekoBot.Services.Database.Models.DiscordUser", "Affinity") + .WithMany() + .HasForeignKey("AffinityId"); + + b.HasOne("NadekoBot.Services.Database.Models.DiscordUser", "Claimer") + .WithMany() + .HasForeignKey("ClaimerId"); + + b.HasOne("NadekoBot.Services.Database.Models.DiscordUser", "Waifu") + .WithOne() + .HasForeignKey("NadekoBot.Services.Database.Models.WaifuInfo", "WaifuId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.WaifuItem", b => + { + b.HasOne("NadekoBot.Services.Database.Models.WaifuInfo") + .WithMany("Items") + .HasForeignKey("WaifuInfoId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.WaifuUpdate", b => + { + b.HasOne("NadekoBot.Services.Database.Models.DiscordUser", "New") + .WithMany() + .HasForeignKey("NewId"); + + b.HasOne("NadekoBot.Services.Database.Models.DiscordUser", "Old") + .WithMany() + .HasForeignKey("OldId"); + + b.HasOne("NadekoBot.Services.Database.Models.DiscordUser", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.WarningPunishment", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig") + .WithMany("WarnPunishments") + .HasForeignKey("GuildConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.XpRoleReward", b => + { + b.HasOne("NadekoBot.Services.Database.Models.XpSettings") + .WithMany("RoleRewards") + .HasForeignKey("XpSettingsId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.XpSettings", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig", "GuildConfig") + .WithOne("XpSettings") + .HasForeignKey("NadekoBot.Services.Database.Models.XpSettings", "GuildConfigId") + .OnDelete(DeleteBehavior.Cascade); + }); + } + } +} diff --git a/src/NadekoBot/Migrations/20170913022654_total-xp.cs b/src/NadekoBot/Migrations/20170913022654_total-xp.cs new file mode 100644 index 00000000..f608e696 --- /dev/null +++ b/src/NadekoBot/Migrations/20170913022654_total-xp.cs @@ -0,0 +1,27 @@ +using System; +using System.Collections.Generic; +using Microsoft.EntityFrameworkCore.Migrations; + +namespace NadekoBot.Migrations +{ + public partial class totalxp : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "TotalXp", + table: "DiscordUser", + nullable: false, + defaultValue: 0); + + migrationBuilder.Sql(MigrationQueries.TotalXp); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "TotalXp", + table: "DiscordUser"); + } + } +} diff --git a/src/NadekoBot/Migrations/MigrationQueries.cs b/src/NadekoBot/Migrations/MigrationQueries.cs index 201f0c34..d89a1685 100644 --- a/src/NadekoBot/Migrations/MigrationQueries.cs +++ b/src/NadekoBot/Migrations/MigrationQueries.cs @@ -34,5 +34,9 @@ INSERT INTO DiscordUser FROM DiscordUser_tmp; DROP TABLE DiscordUser_tmp;"; + public static string TotalXp { get; } = +@"UPDATE DiscordUser +SET TotalXp = (SELECT SUM(Xp) FROM UserXpStats WHERE UserId = DiscordUser.UserId)"; + } } \ No newline at end of file diff --git a/src/NadekoBot/Migrations/NadekoSqliteContextModelSnapshot.cs b/src/NadekoBot/Migrations/NadekoSqliteContextModelSnapshot.cs index 25117604..5f522e7b 100644 --- a/src/NadekoBot/Migrations/NadekoSqliteContextModelSnapshot.cs +++ b/src/NadekoBot/Migrations/NadekoSqliteContextModelSnapshot.cs @@ -464,12 +464,14 @@ namespace NadekoBot.Migrations b.Property("LastLevelUp") .ValueGeneratedOnAdd() - .HasDefaultValue(new DateTime(2017, 9, 11, 22, 0, 31, 236, DateTimeKind.Local)); + .HasDefaultValue(new DateTime(2017, 9, 13, 4, 26, 53, 906, DateTimeKind.Local)); b.Property("LastXpGain"); b.Property("NotifyOnLevelUp"); + b.Property("TotalXp"); + b.Property("UserId"); b.Property("Username"); @@ -1362,7 +1364,7 @@ namespace NadekoBot.Migrations b.Property("LastLevelUp") .ValueGeneratedOnAdd() - .HasDefaultValue(new DateTime(2017, 9, 11, 22, 0, 31, 238, DateTimeKind.Local)); + .HasDefaultValue(new DateTime(2017, 9, 13, 4, 26, 53, 910, DateTimeKind.Local)); b.Property("NotifyOnLevelUp"); diff --git a/src/NadekoBot/Modules/Xp/Services/XpService.cs b/src/NadekoBot/Modules/Xp/Services/XpService.cs index 9bc1969b..ad701ed9 100644 --- a/src/NadekoBot/Modules/Xp/Services/XpService.cs +++ b/src/NadekoBot/Modules/Xp/Services/XpService.cs @@ -150,6 +150,7 @@ namespace NadekoBot.Modules.Xp.Services var oldGuildLevelData = new LevelStats(usr.Xp + usr.AwardedXp); usr.Xp += xp; + du.TotalXp += xp; if (du.Club != null) du.Club.Xp += xp; var newGuildLevelData = new LevelStats(usr.Xp + usr.AwardedXp); @@ -311,7 +312,7 @@ namespace NadekoBot.Modules.Xp.Services { using (var uow = _db.UnitOfWork) { - return uow.Xp.GetUsersFor(page); + return uow.DiscordUsers.GetUsersXpLeaderboardFor(page); } } @@ -424,8 +425,8 @@ namespace NadekoBot.Modules.Xp.Services { du = uow.DiscordUsers.GetOrCreate(user); stats = uow.Xp.GetOrCreateUser(user.GuildId, user.Id); - totalXp = uow.Xp.GetTotalUserXp(user.Id); - globalRank = uow.Xp.GetUserGlobalRanking(user.Id); + totalXp = du.TotalXp; + globalRank = uow.DiscordUsers.GetUserGlobalRanking(user.Id); guildRank = uow.Xp.GetUserGuildRanking(user.Id, user.GuildId); } diff --git a/src/NadekoBot/Modules/Xp/Xp.cs b/src/NadekoBot/Modules/Xp/Xp.cs index 31a02b35..f44bc483 100644 --- a/src/NadekoBot/Modules/Xp/Xp.cs +++ b/src/NadekoBot/Modules/Xp/Xp.cs @@ -1,10 +1,12 @@ using Discord; using Discord.Commands; using Discord.WebSocket; +using NadekoBot.Common; using NadekoBot.Common.Attributes; using NadekoBot.Extensions; using NadekoBot.Modules.Xp.Common; using NadekoBot.Modules.Xp.Services; +using NadekoBot.Services; using NadekoBot.Services.Database.Models; using System.Diagnostics; using System.Linq; @@ -15,14 +17,42 @@ namespace NadekoBot.Modules.Xp public partial class Xp : NadekoTopLevelModule { private readonly DiscordSocketClient _client; + private readonly DbService _db; - public Xp(DiscordSocketClient client) + public Xp(DiscordSocketClient client,DbService db) { _client = client; + _db = db; } + //[NadekoCommand, Usage, Description, Aliases] + //[RequireContext(ContextType.Guild)] + //[OwnerOnly] + //public async Task Populate() + //{ + // var rng = new NadekoRandom(); + // using (var uow = _db.UnitOfWork) + // { + // for (var i = 0ul; i < 1000000; i++) + // { + // uow.DiscordUsers.Add(new DiscordUser() + // { + // AvatarId = i.ToString(), + // Discriminator = "1234", + // UserId = i, + // Username = i.ToString(), + // Club = null, + // }); + // var xp = uow.Xp.GetOrCreateUser(Context.Guild.Id, i); + // xp.Xp = rng.Next(100, 100000); + // } + // uow.Complete(); + // } + //} + [NadekoCommand, Usage, Description, Aliases] [RequireContext(ContextType.Guild)] + //[Ratelimit(30)] public async Task Experience([Remainder]IUser user = null) { user = user ?? Context.User; @@ -44,7 +74,7 @@ namespace NadekoBot.Modules.Xp { page--; - if (page < 0) + if (page < 0 || page > 100) return Task.CompletedTask; var roles = _service.GetRoleRewards(Context.Guild.Id) @@ -173,7 +203,7 @@ namespace NadekoBot.Modules.Xp [RequireContext(ContextType.Guild)] public Task XpLeaderboard(int page = 1) { - if (--page < 0) + if (--page < 0 || page > 100) return Task.CompletedTask; return Context.Channel.SendPaginatedConfirmAsync(_client, page, async (curPage) => @@ -214,32 +244,28 @@ namespace NadekoBot.Modules.Xp [RequireContext(ContextType.Guild)] public async Task XpGlobalLeaderboard(int page = 1) { - if (--page < 0) + if (--page < 0 || page > 100) return; + var users = _service.GetUserXps(page); - await Context.Channel.SendPaginatedConfirmAsync(_client, page, async (curPage) => + var embed = new EmbedBuilder() + .WithTitle(GetText("global_leaderboard")) + .WithOkColor(); + + if (!users.Any()) + embed.WithDescription("-"); + else { - var users = _service.GetUserXps(curPage); - - var embed = new EmbedBuilder() - .WithTitle(GetText("global_leaderboard")) - .WithOkColor(); - - if (!users.Any()) - return embed.WithDescription("-"); - else + for (int i = 0; i < users.Length; i++) { - for (int i = 0; i < users.Length; i++) - { - var user = await Context.Guild.GetUserAsync(users[i].UserId).ConfigureAwait(false); - embed.AddField( - $"#{(i + 1 + curPage * 9)} {(user?.ToString() ?? users[i].UserId.ToString())}", - $"{GetText("level_x", LevelStats.FromXp(users[i].TotalXp).Level)} - {users[i].TotalXp}xp"); - } - - return embed; + var user = await Context.Guild.GetUserAsync(users[i].UserId).ConfigureAwait(false); + embed.AddField( + $"#{(i + 1 + page * 9)} {(user?.ToString() ?? users[i].UserId.ToString())}", + $"{GetText("level_x", LevelStats.FromXp(users[i].TotalXp).Level)} - {users[i].TotalXp}xp"); } - }, addPaginatedFooter: false); + } + + await Context.Channel.EmbedAsync(embed); } [NadekoCommand, Usage, Description, Aliases] diff --git a/src/NadekoBot/Resources/CommandStrings.resx b/src/NadekoBot/Resources/CommandStrings.resx index 079f5fc4..587d643e 100644 --- a/src/NadekoBot/Resources/CommandStrings.resx +++ b/src/NadekoBot/Resources/CommandStrings.resx @@ -3589,7 +3589,7 @@ `{0}xpex Role Excluded-Role` `{0}xpex Server` - Exclude a user or a role from the xp system, or whole current server. + Exclude a channel, role or current server from the xp system. xpnotify xpn diff --git a/src/NadekoBot/Services/Database/Models/DiscordUser.cs b/src/NadekoBot/Services/Database/Models/DiscordUser.cs index 6c02388d..77a573b6 100644 --- a/src/NadekoBot/Services/Database/Models/DiscordUser.cs +++ b/src/NadekoBot/Services/Database/Models/DiscordUser.cs @@ -10,6 +10,8 @@ namespace NadekoBot.Services.Database.Models public string AvatarId { get; set; } public ClubInfo Club { get; set; } + + public int TotalXp { get; set; } public DateTime LastLevelUp { get; set; } = DateTime.UtcNow; public DateTime LastXpGain { get; set; } = DateTime.MinValue; public XpNotificationType NotifyOnLevelUp { get; set; } diff --git a/src/NadekoBot/Services/Database/Repositories/IDiscordUserRepository.cs b/src/NadekoBot/Services/Database/Repositories/IDiscordUserRepository.cs index 480d0723..2d149d85 100644 --- a/src/NadekoBot/Services/Database/Repositories/IDiscordUserRepository.cs +++ b/src/NadekoBot/Services/Database/Repositories/IDiscordUserRepository.cs @@ -6,5 +6,7 @@ namespace NadekoBot.Services.Database.Repositories public interface IDiscordUserRepository : IRepository { DiscordUser GetOrCreate(IUser original); + int GetUserGlobalRanking(ulong id); + (ulong UserId, int TotalXp)[] GetUsersXpLeaderboardFor(int page); } } diff --git a/src/NadekoBot/Services/Database/Repositories/IXpRepository.cs b/src/NadekoBot/Services/Database/Repositories/IXpRepository.cs index 667dd315..10345d5d 100644 --- a/src/NadekoBot/Services/Database/Repositories/IXpRepository.cs +++ b/src/NadekoBot/Services/Database/Repositories/IXpRepository.cs @@ -6,9 +6,7 @@ namespace NadekoBot.Services.Database.Repositories { UserXpStats GetOrCreateUser(ulong guildId, ulong userId); int GetTotalUserXp(ulong userId); - UserXpStats[] GetUsersFor(ulong guildId, int page); - (ulong UserId, int TotalXp)[] GetUsersFor(int page); - int GetUserGlobalRanking(ulong userId); int GetUserGuildRanking(ulong userId, ulong guildId); + UserXpStats[] GetUsersFor(ulong guildId, int page); } } diff --git a/src/NadekoBot/Services/Database/Repositories/Impl/DiscordUserRepository.cs b/src/NadekoBot/Services/Database/Repositories/Impl/DiscordUserRepository.cs index 6fa22ebc..034fbe7f 100644 --- a/src/NadekoBot/Services/Database/Repositories/Impl/DiscordUserRepository.cs +++ b/src/NadekoBot/Services/Database/Repositories/Impl/DiscordUserRepository.cs @@ -37,5 +37,24 @@ namespace NadekoBot.Services.Database.Repositories.Impl return toReturn; } + + public int GetUserGlobalRanking(ulong id) + { + return _set.Count(x => x.TotalXp > + _set.Where(y => y.UserId == id) + .DefaultIfEmpty() + .Sum(y => y.TotalXp)); + } + + public (ulong UserId, int TotalXp)[] GetUsersXpLeaderboardFor(int page) + { + return _set + .OrderByDescending(x => x.TotalXp) + .Skip(page * 9) + .Take(9) + .AsEnumerable() + .Select(y => (y.UserId, y.TotalXp)) + .ToArray(); + } } } diff --git a/src/NadekoBot/Services/Database/Repositories/Impl/XpRepository.cs b/src/NadekoBot/Services/Database/Repositories/Impl/XpRepository.cs index 43d35202..cc026b6a 100644 --- a/src/NadekoBot/Services/Database/Repositories/Impl/XpRepository.cs +++ b/src/NadekoBot/Services/Database/Repositories/Impl/XpRepository.cs @@ -43,15 +43,6 @@ namespace NadekoBot.Services.Database.Repositories.Impl .ToArray(); } - public int GetUserGlobalRanking(ulong userId) - { - return _set - .GroupBy(x => x.UserId) - .Count(x => x.Sum(y => y.Xp) > _set - .Where(y => y.UserId == userId) - .Sum(y => y.Xp)) + 1; - } - public int GetUserGuildRanking(ulong userId, ulong guildId) { return _set @@ -62,16 +53,5 @@ namespace NadekoBot.Services.Database.Repositories.Impl .DefaultIfEmpty() .Sum())) + 1; } - - public (ulong UserId, int TotalXp)[] GetUsersFor(int page) - { - return _set.GroupBy(x => x.UserId) - .OrderByDescending(x => x.Sum(y => y.Xp)) - .Skip(page * 9) - .Take(9) - .AsEnumerable() - .Select(x => (x.Key, x.Sum(y => y.Xp))) - .ToArray(); - } } } \ No newline at end of file From 0a5267604200f764b1cbdfbe6613231c9bfb566d Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Wed, 13 Sep 2017 08:12:07 +0200 Subject: [PATCH 308/346] possible fix for null migration error --- src/NadekoBot/Migrations/MigrationQueries.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/NadekoBot/Migrations/MigrationQueries.cs b/src/NadekoBot/Migrations/MigrationQueries.cs index d89a1685..b0d8413a 100644 --- a/src/NadekoBot/Migrations/MigrationQueries.cs +++ b/src/NadekoBot/Migrations/MigrationQueries.cs @@ -36,7 +36,7 @@ INSERT INTO DiscordUser DROP TABLE DiscordUser_tmp;"; public static string TotalXp { get; } = @"UPDATE DiscordUser -SET TotalXp = (SELECT SUM(Xp) FROM UserXpStats WHERE UserId = DiscordUser.UserId)"; +SET TotalXp = ifnull((SELECT SUM(Xp) FROM UserXpStats WHERE UserId = DiscordUser.UserId), 0)"; } } \ No newline at end of file From 37412e4e730e3be1be1902a3d6fb335c416deeac Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Wed, 13 Sep 2017 21:15:49 +0200 Subject: [PATCH 309/346] Fixed shop role name. Fixed .xpglb (it will now show usernames and discriminators) --- src/NadekoBot/Modules/Gambling/FlowerShopCommands.cs | 4 ++-- src/NadekoBot/Modules/Xp/Services/XpService.cs | 2 +- src/NadekoBot/Modules/Xp/Xp.cs | 4 ++-- .../Services/Database/Repositories/IDiscordUserRepository.cs | 2 +- .../Database/Repositories/Impl/DiscordUserRepository.cs | 3 +-- 5 files changed, 7 insertions(+), 8 deletions(-) diff --git a/src/NadekoBot/Modules/Gambling/FlowerShopCommands.cs b/src/NadekoBot/Modules/Gambling/FlowerShopCommands.cs index 660b0d33..6f50b7c3 100644 --- a/src/NadekoBot/Modules/Gambling/FlowerShopCommands.cs +++ b/src/NadekoBot/Modules/Gambling/FlowerShopCommands.cs @@ -331,7 +331,7 @@ namespace NadekoBot.Modules.Gambling var embed = new EmbedBuilder().WithOkColor(); if (entry.Type == ShopEntryType.Role) - return embed.AddField(efb => efb.WithName(GetText("name")).WithValue(GetText("shop_role", Format.Bold(entry.RoleName))).WithIsInline(true)) + return embed.AddField(efb => efb.WithName(GetText("name")).WithValue(GetText("shop_role", Format.Bold(Context.Guild.GetRole(entry.RoleId)?.Name ?? "MISSING_ROLE"))).WithIsInline(true)) .AddField(efb => efb.WithName(GetText("price")).WithValue(entry.Price.ToString()).WithIsInline(true)) .AddField(efb => efb.WithName(GetText("type")).WithValue(entry.Type.ToString()).WithIsInline(true)); else if (entry.Type == ShopEntryType.List) @@ -349,7 +349,7 @@ namespace NadekoBot.Modules.Gambling { if (entry.Type == ShopEntryType.Role) { - return GetText("shop_role", Format.Bold(entry.RoleName)); + return GetText("shop_role", Format.Bold(Context.Guild.GetRole(entry.RoleId)?.Name ?? "MISSING_ROLE")); } else if (entry.Type == ShopEntryType.List) { diff --git a/src/NadekoBot/Modules/Xp/Services/XpService.cs b/src/NadekoBot/Modules/Xp/Services/XpService.cs index ad701ed9..537953e2 100644 --- a/src/NadekoBot/Modules/Xp/Services/XpService.cs +++ b/src/NadekoBot/Modules/Xp/Services/XpService.cs @@ -308,7 +308,7 @@ namespace NadekoBot.Modules.Xp.Services } } - public (ulong UserId, int TotalXp)[] GetUserXps(int page) + public DiscordUser[] GetUserXps(int page) { using (var uow = _db.UnitOfWork) { diff --git a/src/NadekoBot/Modules/Xp/Xp.cs b/src/NadekoBot/Modules/Xp/Xp.cs index f44bc483..a3356372 100644 --- a/src/NadekoBot/Modules/Xp/Xp.cs +++ b/src/NadekoBot/Modules/Xp/Xp.cs @@ -258,9 +258,9 @@ namespace NadekoBot.Modules.Xp { for (int i = 0; i < users.Length; i++) { - var user = await Context.Guild.GetUserAsync(users[i].UserId).ConfigureAwait(false); + var user = users[i]; embed.AddField( - $"#{(i + 1 + page * 9)} {(user?.ToString() ?? users[i].UserId.ToString())}", + $"#{(i + 1 + page * 9)} {(user.ToString())}", $"{GetText("level_x", LevelStats.FromXp(users[i].TotalXp).Level)} - {users[i].TotalXp}xp"); } } diff --git a/src/NadekoBot/Services/Database/Repositories/IDiscordUserRepository.cs b/src/NadekoBot/Services/Database/Repositories/IDiscordUserRepository.cs index 2d149d85..fb9360b7 100644 --- a/src/NadekoBot/Services/Database/Repositories/IDiscordUserRepository.cs +++ b/src/NadekoBot/Services/Database/Repositories/IDiscordUserRepository.cs @@ -7,6 +7,6 @@ namespace NadekoBot.Services.Database.Repositories { DiscordUser GetOrCreate(IUser original); int GetUserGlobalRanking(ulong id); - (ulong UserId, int TotalXp)[] GetUsersXpLeaderboardFor(int page); + DiscordUser[] GetUsersXpLeaderboardFor(int page); } } diff --git a/src/NadekoBot/Services/Database/Repositories/Impl/DiscordUserRepository.cs b/src/NadekoBot/Services/Database/Repositories/Impl/DiscordUserRepository.cs index 034fbe7f..16938a0d 100644 --- a/src/NadekoBot/Services/Database/Repositories/Impl/DiscordUserRepository.cs +++ b/src/NadekoBot/Services/Database/Repositories/Impl/DiscordUserRepository.cs @@ -46,14 +46,13 @@ namespace NadekoBot.Services.Database.Repositories.Impl .Sum(y => y.TotalXp)); } - public (ulong UserId, int TotalXp)[] GetUsersXpLeaderboardFor(int page) + public DiscordUser[] GetUsersXpLeaderboardFor(int page) { return _set .OrderByDescending(x => x.TotalXp) .Skip(page * 9) .Take(9) .AsEnumerable() - .Select(y => (y.UserId, y.TotalXp)) .ToArray(); } } From 25258a0c6174f16eea3c60203468b493bd1b93dc Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Thu, 14 Sep 2017 19:37:41 +0200 Subject: [PATCH 310/346] Possible fix for redis on linux. Setgame/SetStream and rotating statuses will now properly work across shards. --- .../Modules/Administration/SelfCommands.cs | 6 +- .../Services/PlayingRotateService.cs | 68 +++++++++++-------- src/NadekoBot/NadekoBot.cs | 49 +++++++++++++ src/NadekoBot/Services/Impl/RedisCache.cs | 2 +- 4 files changed, 94 insertions(+), 31 deletions(-) diff --git a/src/NadekoBot/Modules/Administration/SelfCommands.cs b/src/NadekoBot/Modules/Administration/SelfCommands.cs index 622e1b6b..32d902df 100644 --- a/src/NadekoBot/Modules/Administration/SelfCommands.cs +++ b/src/NadekoBot/Modules/Administration/SelfCommands.cs @@ -30,8 +30,9 @@ namespace NadekoBot.Modules.Administration private readonly IImagesService _images; private readonly MusicService _music; private readonly IBotConfigProvider _bc; + private readonly NadekoBot _bot; - public SelfCommands(DbService db, DiscordSocketClient client, + public SelfCommands(DbService db, NadekoBot bot, DiscordSocketClient client, MusicService music, IImagesService images, IBotConfigProvider bc) { _db = db; @@ -39,6 +40,7 @@ namespace NadekoBot.Modules.Administration _images = images; _music = music; _bc = bc; + _bot = bot; } [NadekoCommand, Usage, Description, Aliases] @@ -349,7 +351,7 @@ namespace NadekoBot.Modules.Administration [OwnerOnly] public async Task SetGame([Remainder] string game = null) { - await _client.SetGameAsync(game).ConfigureAwait(false); + await _bot.SetGameAsync(game).ConfigureAwait(false); await ReplyConfirmLocalized("set_game").ConfigureAwait(false); } diff --git a/src/NadekoBot/Modules/Administration/Services/PlayingRotateService.cs b/src/NadekoBot/Modules/Administration/Services/PlayingRotateService.cs index 9786d085..d0cd5eef 100644 --- a/src/NadekoBot/Modules/Administration/Services/PlayingRotateService.cs +++ b/src/NadekoBot/Modules/Administration/Services/PlayingRotateService.cs @@ -7,6 +7,7 @@ using NadekoBot.Modules.Music.Services; using NadekoBot.Services; using NadekoBot.Services.Database.Models; using NLog; +using System.Threading.Tasks; namespace NadekoBot.Modules.Administration.Services { @@ -16,6 +17,7 @@ namespace NadekoBot.Modules.Administration.Services private readonly DiscordSocketClient _client; private readonly MusicService _music; private readonly Logger _log; + private readonly IDataCache _cache; private readonly Replacer _rep; private readonly DbService _db; private readonly IBotConfigProvider _bcp; @@ -27,50 +29,60 @@ namespace NadekoBot.Modules.Administration.Services public int Index { get; set; } } - public PlayingRotateService(DiscordSocketClient client, IBotConfigProvider bcp, MusicService music, DbService db) + public PlayingRotateService(DiscordSocketClient client, IBotConfigProvider bcp, + MusicService music, DbService db, IDataCache cache, NadekoBot bot) { _client = client; _bcp = bcp; _music = music; _db = db; _log = LogManager.GetCurrentClassLogger(); - _rep = new ReplacementBuilder() - .WithClient(client) - .WithStats(client) - .WithMusic(music) - .Build(); + _cache = cache; - _t = new Timer(async (objState) => + if (client.ShardId == 0) { - try + + _rep = new ReplacementBuilder() + .WithClient(client) + .WithStats(client) + .WithMusic(music) + .Build(); + + _t = new Timer(async (objState) => { - bcp.Reload(); + try + { + bcp.Reload(); - var state = (TimerState)objState; - if (!BotConfig.RotatingStatuses) - return; - if (state.Index >= BotConfig.RotatingStatusMessages.Count) - state.Index = 0; + var state = (TimerState)objState; + if (!BotConfig.RotatingStatuses) + return; + if (state.Index >= BotConfig.RotatingStatusMessages.Count) + state.Index = 0; - if (!BotConfig.RotatingStatusMessages.Any()) - return; - var status = BotConfig.RotatingStatusMessages[state.Index++].Status; - if (string.IsNullOrWhiteSpace(status)) - return; + if (!BotConfig.RotatingStatusMessages.Any()) + return; + var status = BotConfig.RotatingStatusMessages[state.Index++].Status; + if (string.IsNullOrWhiteSpace(status)) + return; - status = _rep.Replace(status); + status = _rep.Replace(status); - try { await client.SetGameAsync(status).ConfigureAwait(false); } + try + { + await bot.SetGameAsync(status).ConfigureAwait(false); + } + catch (Exception ex) + { + _log.Warn(ex); + } + } catch (Exception ex) { - _log.Warn(ex); + _log.Warn("Rotating playing status errored.\n" + ex); } - } - catch (Exception ex) - { - _log.Warn("Rotating playing status errored.\n" + ex); - } - }, new TimerState(), TimeSpan.FromMinutes(1), TimeSpan.FromMinutes(1)); + }, new TimerState(), TimeSpan.FromMinutes(1), TimeSpan.FromMinutes(1)); + } } } } diff --git a/src/NadekoBot/NadekoBot.cs b/src/NadekoBot/NadekoBot.cs index a85da294..5fd01eba 100644 --- a/src/NadekoBot/NadekoBot.cs +++ b/src/NadekoBot/NadekoBot.cs @@ -20,6 +20,8 @@ using NadekoBot.Common.ShardCom; using NadekoBot.Common.TypeReaders; using NadekoBot.Common.TypeReaders.Models; using NadekoBot.Services.Database; +using StackExchange.Redis; +using Newtonsoft.Json; namespace NadekoBot { @@ -257,6 +259,7 @@ namespace NadekoBot .ForEach(x => CommandService.RemoveModuleAsync(x)); Ready.TrySetResult(true); + HandleStatusChanges(); _log.Info($"Shard {Client.ShardId} ready."); //_log.Info(await stats.Print().ConfigureAwait(false)); } @@ -319,5 +322,51 @@ namespace NadekoBot } })).Start(); } + + private void HandleStatusChanges() + { + var sub = Services.GetService().Redis.GetSubscriber(); + sub.Subscribe("status.game_set", async (ch, game) => + { + try + { + var obj = new { Name = default(string) }; + obj = JsonConvert.DeserializeAnonymousType(game, obj); + await Client.SetGameAsync(obj.Name).ConfigureAwait(false); + } + catch (Exception ex) + { + _log.Warn(ex); + } + }, CommandFlags.FireAndForget); + + sub.Subscribe("status.stream_set", async (ch, streamData) => + { + try + { + var obj = new { Name = "", Url = "" }; + obj = JsonConvert.DeserializeAnonymousType(streamData, obj); + await Client.SetGameAsync(obj.Name, obj.Url, StreamType.Twitch).ConfigureAwait(false); + } + catch (Exception ex) + { + _log.Warn(ex); + } + }, CommandFlags.FireAndForget); + } + + public Task SetGameAsync(string game) + { + var obj = new { Name = game }; + var sub = Services.GetService().Redis.GetSubscriber(); + return sub.PublishAsync("status.game_set", JsonConvert.SerializeObject(obj)); + } + + public Task SetStreamAsync(string name, string url) + { + var obj = new { Name = name, Url = url }; + var sub = Services.GetService().Redis.GetSubscriber(); + return sub.PublishAsync("status.game_set", JsonConvert.SerializeObject(obj)); + } } } diff --git a/src/NadekoBot/Services/Impl/RedisCache.cs b/src/NadekoBot/Services/Impl/RedisCache.cs index 5e54979d..716b46ea 100644 --- a/src/NadekoBot/Services/Impl/RedisCache.cs +++ b/src/NadekoBot/Services/Impl/RedisCache.cs @@ -10,7 +10,7 @@ namespace NadekoBot.Services.Impl public RedisCache() { - Redis = ConnectionMultiplexer.Connect("localhost"); + Redis = ConnectionMultiplexer.Connect("127.0.0.1"); Redis.PreserveAsyncOrder = false; _db = Redis.GetDatabase(); } From 16fd835d4b8a743d15ad7080ad7b7df9322f484e Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Fri, 15 Sep 2017 02:42:51 +0200 Subject: [PATCH 311/346] Caching anime and manga seraches. Club disband error message fixed. --- .../Searches/Services/AnimeSearchService.cs | 30 ++++++++++++++----- src/NadekoBot/Modules/Xp/Club.cs | 2 +- src/NadekoBot/NadekoBot.cs | 7 ----- src/NadekoBot/Services/IDataCache.cs | 2 ++ src/NadekoBot/Services/Impl/RedisCache.cs | 11 +++++++ 5 files changed, 36 insertions(+), 16 deletions(-) diff --git a/src/NadekoBot/Modules/Searches/Services/AnimeSearchService.cs b/src/NadekoBot/Modules/Searches/Services/AnimeSearchService.cs index 638db338..1a372d8c 100644 --- a/src/NadekoBot/Modules/Searches/Services/AnimeSearchService.cs +++ b/src/NadekoBot/Modules/Searches/Services/AnimeSearchService.cs @@ -11,10 +11,14 @@ namespace NadekoBot.Modules.Searches.Services public class AnimeSearchService : INService { private readonly Logger _log; + private readonly IDataCache _cache; + private readonly HttpClient _http; - public AnimeSearchService() + public AnimeSearchService(IDataCache cache) { _log = LogManager.GetCurrentClassLogger(); + _cache = cache; + _http = new HttpClient(); } public async Task GetAnimeData(string query) @@ -25,11 +29,16 @@ namespace NadekoBot.Modules.Searches.Services { var link = "https://aniapi.nadekobot.me/anime/" + Uri.EscapeDataString(query.Replace("/", " ")); - using (var http = new HttpClient()) + link = link.ToLowerInvariant(); + var (ok, data) = await _cache.TryGetAnimeDataAsync(link).ConfigureAwait(false); + if (!ok) { - var res = await http.GetStringAsync(link).ConfigureAwait(false); - return JsonConvert.DeserializeObject(res); + data = await _http.GetStringAsync(link).ConfigureAwait(false); + await _cache.SetAnimeDataAsync(link, data).ConfigureAwait(false); } + + + return JsonConvert.DeserializeObject(data); } catch { @@ -44,12 +53,17 @@ namespace NadekoBot.Modules.Searches.Services try { - var link = "https://aniapi.nadekobot.me/manga/" + Uri.EscapeDataString(query.Replace("/", " ")); - using (var http = new HttpClient()) + var link = "https://aniapi.nadekobot.me/manga/" + Uri.EscapeDataString(query.Replace("/", " ")); + link = link.ToLowerInvariant(); + var (ok, data) = await _cache.TryGetAnimeDataAsync(link).ConfigureAwait(false); + if (!ok) { - var res = await http.GetStringAsync(link).ConfigureAwait(false); - return JsonConvert.DeserializeObject(res); + data = await _http.GetStringAsync(link).ConfigureAwait(false); + await _cache.SetAnimeDataAsync(link, data).ConfigureAwait(false); } + + + return JsonConvert.DeserializeObject(data); } catch { diff --git a/src/NadekoBot/Modules/Xp/Club.cs b/src/NadekoBot/Modules/Xp/Club.cs index a911ab4f..8c4ce4c5 100644 --- a/src/NadekoBot/Modules/Xp/Club.cs +++ b/src/NadekoBot/Modules/Xp/Club.cs @@ -282,7 +282,7 @@ namespace NadekoBot.Modules.Xp } else { - await ReplyErrorLocalized("club_disaband_error").ConfigureAwait(false); + await ReplyErrorLocalized("club_disband_error").ConfigureAwait(false); } } diff --git a/src/NadekoBot/NadekoBot.cs b/src/NadekoBot/NadekoBot.cs index 5fd01eba..95fd9574 100644 --- a/src/NadekoBot/NadekoBot.cs +++ b/src/NadekoBot/NadekoBot.cs @@ -242,13 +242,6 @@ namespace NadekoBot #if GLOBAL_NADEKO isPublicNadeko = true; #endif - //_log.Info(string.Join(", ", CommandService.Commands - // .Distinct(x => x.Name + x.Module.Name) - // .SelectMany(x => x.Aliases) - // .GroupBy(x => x) - // .Where(x => x.Count() > 1) - // .Select(x => x.Key + $"({x.Count()})"))); - //unload modules which are not available on the public bot if(isPublicNadeko) diff --git a/src/NadekoBot/Services/IDataCache.cs b/src/NadekoBot/Services/IDataCache.cs index b708a66b..3be7ad35 100644 --- a/src/NadekoBot/Services/IDataCache.cs +++ b/src/NadekoBot/Services/IDataCache.cs @@ -11,6 +11,8 @@ namespace NadekoBot.Services { ConnectionMultiplexer Redis { get; } Task<(bool Success, byte[] Data)> TryGetImageDataAsync(string key); + Task<(bool Success, string Data)> TryGetAnimeDataAsync(string key); Task SetImageDataAsync(string key, byte[] data); + Task SetAnimeDataAsync(string link, string data); } } diff --git a/src/NadekoBot/Services/Impl/RedisCache.cs b/src/NadekoBot/Services/Impl/RedisCache.cs index 716b46ea..84083ef8 100644 --- a/src/NadekoBot/Services/Impl/RedisCache.cs +++ b/src/NadekoBot/Services/Impl/RedisCache.cs @@ -25,5 +25,16 @@ namespace NadekoBot.Services.Impl { return _db.StringSetAsync("image_" + key, data); } + + public async Task<(bool Success, string Data)> TryGetAnimeDataAsync(string key) + { + string x = await _db.StringGetAsync("anime_" + key); + return (x != null, x); + } + + public Task SetAnimeDataAsync(string key, string data) + { + return _db.StringSetAsync("anime_" + key, data); + } } } From 4841418cff47c7e898f44bb8d57160338125230c Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Fri, 15 Sep 2017 22:17:31 +0200 Subject: [PATCH 312/346] Added `.clubadmin`, `.autoboobs` and `.autobutts`, cleaned up SongBuffer. --- .../20170915034808_club-admins.Designer.cs | 1951 +++++++++++++++++ .../Migrations/20170915034808_club-admins.cs | 25 + .../NadekoSqliteContextModelSnapshot.cs | 6 +- .../Modules/Music/Common/SongBuffer.cs | 326 +-- src/NadekoBot/Modules/NSFW/NSFW.cs | 120 +- src/NadekoBot/Modules/Xp/Club.cs | 40 +- .../Modules/Xp/Services/ClubService.cs | 52 +- .../Modules/Xp/Services/XpService.cs | 13 +- src/NadekoBot/Resources/CommandStrings.resx | 27 + .../Services/Database/Models/DiscordUser.cs | 1 + .../Services/Database/NadekoContext.cs | 4 +- .../Database/Repositories/IClubRepository.cs | 1 + .../Database/Repositories/IXpRepository.cs | 1 - .../Repositories/Impl/ClubRepository.cs | 24 + .../Repositories/Impl/XpRepository.cs | 5 - .../_strings/ResponseStrings.en-US.json | 8 +- 16 files changed, 2225 insertions(+), 379 deletions(-) create mode 100644 src/NadekoBot/Migrations/20170915034808_club-admins.Designer.cs create mode 100644 src/NadekoBot/Migrations/20170915034808_club-admins.cs diff --git a/src/NadekoBot/Migrations/20170915034808_club-admins.Designer.cs b/src/NadekoBot/Migrations/20170915034808_club-admins.Designer.cs new file mode 100644 index 00000000..d7b4c4d9 --- /dev/null +++ b/src/NadekoBot/Migrations/20170915034808_club-admins.Designer.cs @@ -0,0 +1,1951 @@ +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using NadekoBot.Services.Database; +using NadekoBot.Services.Database.Models; + +namespace NadekoBot.Migrations +{ + [DbContext(typeof(NadekoContext))] + [Migration("20170915034808_club-admins")] + partial class clubadmins + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { + modelBuilder + .HasAnnotation("ProductVersion", "1.1.1"); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.AntiRaidSetting", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("Action"); + + b.Property("DateAdded"); + + b.Property("GuildConfigId"); + + b.Property("Seconds"); + + b.Property("UserThreshold"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId") + .IsUnique(); + + b.ToTable("AntiRaidSetting"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.AntiSpamIgnore", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AntiSpamSettingId"); + + b.Property("ChannelId"); + + b.Property("DateAdded"); + + b.HasKey("Id"); + + b.HasIndex("AntiSpamSettingId"); + + b.ToTable("AntiSpamIgnore"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.AntiSpamSetting", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("Action"); + + b.Property("DateAdded"); + + b.Property("GuildConfigId"); + + b.Property("MessageThreshold"); + + b.Property("MuteTime"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId") + .IsUnique(); + + b.ToTable("AntiSpamSetting"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.BlacklistItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("BotConfigId"); + + b.Property("DateAdded"); + + b.Property("ItemId"); + + b.Property("Type"); + + b.HasKey("Id"); + + b.HasIndex("BotConfigId"); + + b.ToTable("BlacklistItem"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.BlockedCmdOrMdl", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("BotConfigId"); + + b.Property("BotConfigId1"); + + b.Property("DateAdded"); + + b.Property("Name"); + + b.HasKey("Id"); + + b.HasIndex("BotConfigId"); + + b.HasIndex("BotConfigId1"); + + b.ToTable("BlockedCmdOrMdl"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.BotConfig", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("BetflipMultiplier"); + + b.Property("Betroll100Multiplier"); + + b.Property("Betroll67Multiplier"); + + b.Property("Betroll91Multiplier"); + + b.Property("BufferSize"); + + b.Property("CurrencyDropAmount"); + + b.Property("CurrencyDropAmountMax"); + + b.Property("CurrencyGenerationChance"); + + b.Property("CurrencyGenerationCooldown"); + + b.Property("CurrencyName"); + + b.Property("CurrencyPluralName"); + + b.Property("CurrencySign"); + + b.Property("CustomReactionsStartWith"); + + b.Property("DMHelpString"); + + b.Property("DateAdded"); + + b.Property("DefaultPrefix"); + + b.Property("ErrorColor"); + + b.Property("ForwardMessages"); + + b.Property("ForwardToAllOwners"); + + b.Property("HelpString"); + + b.Property("Locale"); + + b.Property("MigrationVersion"); + + b.Property("MinimumBetAmount"); + + b.Property("OkColor"); + + b.Property("PermissionVersion"); + + b.Property("RemindMessageFormat"); + + b.Property("RotatingStatuses"); + + b.Property("TriviaCurrencyReward"); + + b.Property("XpMinutesTimeout") + .ValueGeneratedOnAdd() + .HasDefaultValue(5); + + b.Property("XpPerMessage") + .ValueGeneratedOnAdd() + .HasDefaultValue(3); + + b.HasKey("Id"); + + b.ToTable("BotConfig"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.ClashCaller", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("BaseDestroyed"); + + b.Property("CallUser"); + + b.Property("ClashWarId"); + + b.Property("DateAdded"); + + b.Property("SequenceNumber"); + + b.Property("Stars"); + + b.Property("TimeAdded"); + + b.HasKey("Id"); + + b.HasIndex("ClashWarId"); + + b.ToTable("ClashCallers"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.ClashWar", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ChannelId"); + + b.Property("DateAdded"); + + b.Property("EnemyClan"); + + b.Property("GuildId"); + + b.Property("Size"); + + b.Property("StartedAt"); + + b.Property("WarState"); + + b.HasKey("Id"); + + b.ToTable("ClashOfClans"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.ClubApplicants", b => + { + b.Property("ClubId"); + + b.Property("UserId"); + + b.HasKey("ClubId", "UserId"); + + b.HasIndex("UserId"); + + b.ToTable("ClubApplicants"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.ClubBans", b => + { + b.Property("ClubId"); + + b.Property("UserId"); + + b.HasKey("ClubId", "UserId"); + + b.HasIndex("UserId"); + + b.ToTable("ClubBans"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.ClubInfo", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("Discrim"); + + b.Property("ImageUrl"); + + b.Property("MinimumLevelReq"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(20); + + b.Property("OwnerId"); + + b.Property("Xp"); + + b.HasKey("Id"); + + b.HasAlternateKey("Name", "Discrim"); + + b.HasIndex("OwnerId") + .IsUnique(); + + b.ToTable("Clubs"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.CommandAlias", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("GuildConfigId"); + + b.Property("Mapping"); + + b.Property("Trigger"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.ToTable("CommandAlias"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.CommandCooldown", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("CommandName"); + + b.Property("DateAdded"); + + b.Property("GuildConfigId"); + + b.Property("Seconds"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.ToTable("CommandCooldown"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.CommandPrice", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("BotConfigId"); + + b.Property("CommandName"); + + b.Property("DateAdded"); + + b.Property("Price"); + + b.HasKey("Id"); + + b.HasIndex("BotConfigId"); + + b.HasIndex("Price") + .IsUnique(); + + b.ToTable("CommandPrice"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.ConvertUnit", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("InternalTrigger"); + + b.Property("Modifier"); + + b.Property("UnitType"); + + b.HasKey("Id"); + + b.ToTable("ConversionUnits"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.Currency", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("Amount"); + + b.Property("DateAdded"); + + b.Property("UserId"); + + b.HasKey("Id"); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("Currency"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.CurrencyTransaction", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("Amount"); + + b.Property("DateAdded"); + + b.Property("Reason"); + + b.Property("UserId"); + + b.HasKey("Id"); + + b.ToTable("CurrencyTransactions"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.CustomReaction", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AutoDeleteTrigger"); + + b.Property("ContainsAnywhere"); + + b.Property("DateAdded"); + + b.Property("DmResponse"); + + b.Property("GuildId"); + + b.Property("IsRegex"); + + b.Property("OwnerOnly"); + + b.Property("Response"); + + b.Property("Trigger"); + + b.HasKey("Id"); + + b.ToTable("CustomReactions"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.DiscordUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AvatarId"); + + b.Property("ClubId"); + + b.Property("DateAdded"); + + b.Property("Discriminator"); + + b.Property("IsClubAdmin"); + + b.Property("LastLevelUp") + .ValueGeneratedOnAdd() + .HasDefaultValue(new DateTime(2017, 9, 15, 5, 48, 8, 660, DateTimeKind.Local)); + + b.Property("LastXpGain"); + + b.Property("NotifyOnLevelUp"); + + b.Property("TotalXp"); + + b.Property("UserId"); + + b.Property("Username"); + + b.HasKey("Id"); + + b.HasAlternateKey("UserId"); + + b.HasIndex("ClubId"); + + b.ToTable("DiscordUser"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.Donator", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("Amount"); + + b.Property("DateAdded"); + + b.Property("Name"); + + b.Property("UserId"); + + b.HasKey("Id"); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("Donators"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.EightBallResponse", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("BotConfigId"); + + b.Property("DateAdded"); + + b.Property("Text"); + + b.HasKey("Id"); + + b.HasIndex("BotConfigId"); + + b.ToTable("EightBallResponses"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.ExcludedItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("ItemId"); + + b.Property("ItemType"); + + b.Property("XpSettingsId"); + + b.HasKey("Id"); + + b.HasIndex("XpSettingsId"); + + b.ToTable("ExcludedItem"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.FilterChannelId", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ChannelId"); + + b.Property("DateAdded"); + + b.Property("GuildConfigId"); + + b.Property("GuildConfigId1"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.HasIndex("GuildConfigId1"); + + b.ToTable("FilterChannelId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.FilteredWord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("GuildConfigId"); + + b.Property("Word"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.ToTable("FilteredWord"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.FollowedStream", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ChannelId"); + + b.Property("DateAdded"); + + b.Property("GuildConfigId"); + + b.Property("GuildId"); + + b.Property("Type"); + + b.Property("Username"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.ToTable("FollowedStream"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.GCChannelId", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ChannelId"); + + b.Property("DateAdded"); + + b.Property("GuildConfigId"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.ToTable("GCChannelId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.GuildConfig", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AutoAssignRoleId"); + + b.Property("AutoDeleteByeMessages"); + + b.Property("AutoDeleteByeMessagesTimer"); + + b.Property("AutoDeleteGreetMessages"); + + b.Property("AutoDeleteGreetMessagesTimer"); + + b.Property("AutoDeleteSelfAssignedRoleMessages"); + + b.Property("ByeMessageChannelId"); + + b.Property("ChannelByeMessageText"); + + b.Property("ChannelGreetMessageText"); + + b.Property("CleverbotEnabled"); + + b.Property("DateAdded"); + + b.Property("DefaultMusicVolume"); + + b.Property("DeleteMessageOnCommand"); + + b.Property("DmGreetMessageText"); + + b.Property("ExclusiveSelfAssignedRoles"); + + b.Property("FilterInvites"); + + b.Property("FilterWords"); + + b.Property("GameVoiceChannel"); + + b.Property("GreetMessageChannelId"); + + b.Property("GuildId"); + + b.Property("Locale"); + + b.Property("LogSettingId"); + + b.Property("MuteRoleName"); + + b.Property("PermissionRole"); + + b.Property("Prefix"); + + b.Property("RootPermissionId"); + + b.Property("SendChannelByeMessage"); + + b.Property("SendChannelGreetMessage"); + + b.Property("SendDmGreetMessage"); + + b.Property("TimeZoneId"); + + b.Property("VerboseErrors"); + + b.Property("VerbosePermissions"); + + b.Property("VoicePlusTextEnabled"); + + b.Property("WarningsInitialized"); + + b.HasKey("Id"); + + b.HasIndex("GuildId") + .IsUnique(); + + b.HasIndex("LogSettingId"); + + b.HasIndex("RootPermissionId"); + + b.ToTable("GuildConfigs"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.GuildRepeater", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ChannelId"); + + b.Property("DateAdded"); + + b.Property("GuildConfigId"); + + b.Property("GuildId"); + + b.Property("Interval"); + + b.Property("Message"); + + b.Property("StartTimeOfDay"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.ToTable("GuildRepeater"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.IgnoredLogChannel", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ChannelId"); + + b.Property("DateAdded"); + + b.Property("LogSettingId"); + + b.HasKey("Id"); + + b.HasIndex("LogSettingId"); + + b.ToTable("IgnoredLogChannels"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.IgnoredVoicePresenceChannel", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ChannelId"); + + b.Property("DateAdded"); + + b.Property("LogSettingId"); + + b.HasKey("Id"); + + b.HasIndex("LogSettingId"); + + b.ToTable("IgnoredVoicePresenceCHannels"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.LogSetting", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ChannelCreated"); + + b.Property("ChannelCreatedId"); + + b.Property("ChannelDestroyed"); + + b.Property("ChannelDestroyedId"); + + b.Property("ChannelId"); + + b.Property("ChannelUpdated"); + + b.Property("ChannelUpdatedId"); + + b.Property("DateAdded"); + + b.Property("IsLogging"); + + b.Property("LogOtherId"); + + b.Property("LogUserPresence"); + + b.Property("LogUserPresenceId"); + + b.Property("LogVoicePresence"); + + b.Property("LogVoicePresenceId"); + + b.Property("LogVoicePresenceTTSId"); + + b.Property("MessageDeleted"); + + b.Property("MessageDeletedId"); + + b.Property("MessageUpdated"); + + b.Property("MessageUpdatedId"); + + b.Property("UserBanned"); + + b.Property("UserBannedId"); + + b.Property("UserJoined"); + + b.Property("UserJoinedId"); + + b.Property("UserLeft"); + + b.Property("UserLeftId"); + + b.Property("UserMutedId"); + + b.Property("UserPresenceChannelId"); + + b.Property("UserUnbanned"); + + b.Property("UserUnbannedId"); + + b.Property("UserUpdated"); + + b.Property("UserUpdatedId"); + + b.Property("VoicePresenceChannelId"); + + b.HasKey("Id"); + + b.ToTable("LogSettings"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.ModulePrefix", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("BotConfigId"); + + b.Property("DateAdded"); + + b.Property("ModuleName"); + + b.Property("Prefix"); + + b.HasKey("Id"); + + b.HasIndex("BotConfigId"); + + b.ToTable("ModulePrefixes"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.MusicPlaylist", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("Author"); + + b.Property("AuthorId"); + + b.Property("DateAdded"); + + b.Property("Name"); + + b.HasKey("Id"); + + b.ToTable("MusicPlaylists"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.MutedUserId", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("GuildConfigId"); + + b.Property("UserId"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.ToTable("MutedUserId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.NsfwBlacklitedTag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("GuildConfigId"); + + b.Property("Tag"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.ToTable("NsfwBlacklitedTag"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.Permission", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("NextId"); + + b.Property("PrimaryTarget"); + + b.Property("PrimaryTargetId"); + + b.Property("SecondaryTarget"); + + b.Property("SecondaryTargetName"); + + b.Property("State"); + + b.HasKey("Id"); + + b.HasIndex("NextId") + .IsUnique(); + + b.ToTable("Permission"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.Permissionv2", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("GuildConfigId"); + + b.Property("Index"); + + b.Property("PrimaryTarget"); + + b.Property("PrimaryTargetId"); + + b.Property("SecondaryTarget"); + + b.Property("SecondaryTargetName"); + + b.Property("State"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.ToTable("Permissionv2"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.PlayingStatus", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("BotConfigId"); + + b.Property("DateAdded"); + + b.Property("Status"); + + b.HasKey("Id"); + + b.HasIndex("BotConfigId"); + + b.ToTable("PlayingStatus"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.PlaylistSong", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("MusicPlaylistId"); + + b.Property("Provider"); + + b.Property("ProviderType"); + + b.Property("Query"); + + b.Property("Title"); + + b.Property("Uri"); + + b.HasKey("Id"); + + b.HasIndex("MusicPlaylistId"); + + b.ToTable("PlaylistSong"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.Quote", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AuthorId"); + + b.Property("AuthorName") + .IsRequired(); + + b.Property("DateAdded"); + + b.Property("GuildId"); + + b.Property("Keyword") + .IsRequired(); + + b.Property("Text") + .IsRequired(); + + b.HasKey("Id"); + + b.ToTable("Quotes"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.RaceAnimal", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("BotConfigId"); + + b.Property("DateAdded"); + + b.Property("Icon"); + + b.Property("Name"); + + b.HasKey("Id"); + + b.HasIndex("BotConfigId"); + + b.ToTable("RaceAnimals"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.Reminder", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ChannelId"); + + b.Property("DateAdded"); + + b.Property("IsPrivate"); + + b.Property("Message"); + + b.Property("ServerId"); + + b.Property("UserId"); + + b.Property("When"); + + b.HasKey("Id"); + + b.ToTable("Reminders"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.RewardedUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AmountRewardedThisMonth"); + + b.Property("DateAdded"); + + b.Property("LastReward"); + + b.Property("PatreonUserId"); + + b.Property("UserId"); + + b.HasKey("Id"); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("RewardedUsers"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.SelfAssignedRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("GuildId"); + + b.Property("RoleId"); + + b.HasKey("Id"); + + b.HasIndex("GuildId", "RoleId") + .IsUnique(); + + b.ToTable("SelfAssignableRoles"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.ShopEntry", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AuthorId"); + + b.Property("DateAdded"); + + b.Property("GuildConfigId"); + + b.Property("Index"); + + b.Property("Name"); + + b.Property("Price"); + + b.Property("RoleId"); + + b.Property("RoleName"); + + b.Property("Type"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.ToTable("ShopEntry"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.ShopEntryItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("ShopEntryId"); + + b.Property("Text"); + + b.HasKey("Id"); + + b.HasIndex("ShopEntryId"); + + b.ToTable("ShopEntryItem"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.SlowmodeIgnoredRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("GuildConfigId"); + + b.Property("RoleId"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.ToTable("SlowmodeIgnoredRole"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.SlowmodeIgnoredUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("GuildConfigId"); + + b.Property("UserId"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.ToTable("SlowmodeIgnoredUser"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.StartupCommand", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("BotConfigId"); + + b.Property("ChannelId"); + + b.Property("ChannelName"); + + b.Property("CommandText"); + + b.Property("DateAdded"); + + b.Property("GuildId"); + + b.Property("GuildName"); + + b.Property("Index"); + + b.Property("VoiceChannelId"); + + b.Property("VoiceChannelName"); + + b.HasKey("Id"); + + b.HasIndex("BotConfigId"); + + b.ToTable("StartupCommand"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.StreamRoleBlacklistedUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("StreamRoleSettingsId"); + + b.Property("UserId"); + + b.Property("Username"); + + b.HasKey("Id"); + + b.HasIndex("StreamRoleSettingsId"); + + b.ToTable("StreamRoleBlacklistedUser"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.StreamRoleSettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AddRoleId"); + + b.Property("DateAdded"); + + b.Property("Enabled"); + + b.Property("FromRoleId"); + + b.Property("GuildConfigId"); + + b.Property("Keyword"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId") + .IsUnique(); + + b.ToTable("StreamRoleSettings"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.StreamRoleWhitelistedUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("StreamRoleSettingsId"); + + b.Property("UserId"); + + b.Property("Username"); + + b.HasKey("Id"); + + b.HasIndex("StreamRoleSettingsId"); + + b.ToTable("StreamRoleWhitelistedUser"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.UnmuteTimer", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("GuildConfigId"); + + b.Property("UnmuteAt"); + + b.Property("UserId"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.ToTable("UnmuteTimer"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.UserPokeTypes", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("UserId"); + + b.Property("type"); + + b.HasKey("Id"); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("PokeGame"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.UserXpStats", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AwardedXp"); + + b.Property("DateAdded"); + + b.Property("GuildId"); + + b.Property("LastLevelUp") + .ValueGeneratedOnAdd() + .HasDefaultValue(new DateTime(2017, 9, 15, 5, 48, 8, 665, DateTimeKind.Local)); + + b.Property("NotifyOnLevelUp"); + + b.Property("UserId"); + + b.Property("Xp"); + + b.HasKey("Id"); + + b.HasIndex("UserId", "GuildId") + .IsUnique(); + + b.ToTable("UserXpStats"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.VcRoleInfo", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("GuildConfigId"); + + b.Property("RoleId"); + + b.Property("VoiceChannelId"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.ToTable("VcRoleInfo"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.WaifuInfo", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AffinityId"); + + b.Property("ClaimerId"); + + b.Property("DateAdded"); + + b.Property("Price"); + + b.Property("WaifuId"); + + b.HasKey("Id"); + + b.HasIndex("AffinityId"); + + b.HasIndex("ClaimerId"); + + b.HasIndex("WaifuId") + .IsUnique(); + + b.ToTable("WaifuInfo"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.WaifuItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("Item"); + + b.Property("ItemEmoji"); + + b.Property("Price"); + + b.Property("WaifuInfoId"); + + b.HasKey("Id"); + + b.HasIndex("WaifuInfoId"); + + b.ToTable("WaifuItem"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.WaifuUpdate", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("NewId"); + + b.Property("OldId"); + + b.Property("UpdateType"); + + b.Property("UserId"); + + b.HasKey("Id"); + + b.HasIndex("NewId"); + + b.HasIndex("OldId"); + + b.HasIndex("UserId"); + + b.ToTable("WaifuUpdates"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.Warning", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("Forgiven"); + + b.Property("ForgivenBy"); + + b.Property("GuildId"); + + b.Property("Moderator"); + + b.Property("Reason"); + + b.Property("UserId"); + + b.HasKey("Id"); + + b.ToTable("Warnings"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.WarningPunishment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("Count"); + + b.Property("DateAdded"); + + b.Property("GuildConfigId"); + + b.Property("Punishment"); + + b.Property("Time"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.ToTable("WarningPunishment"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.XpRoleReward", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("Level"); + + b.Property("RoleId"); + + b.Property("XpSettingsId"); + + b.HasKey("Id"); + + b.HasAlternateKey("Level"); + + b.HasIndex("XpSettingsId"); + + b.ToTable("XpRoleReward"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.XpSettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("GuildConfigId"); + + b.Property("NotifyMessage"); + + b.Property("ServerExcluded"); + + b.Property("XpRoleRewardExclusive"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId") + .IsUnique(); + + b.ToTable("XpSettings"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.AntiRaidSetting", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig", "GuildConfig") + .WithOne("AntiRaidSetting") + .HasForeignKey("NadekoBot.Services.Database.Models.AntiRaidSetting", "GuildConfigId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.AntiSpamIgnore", b => + { + b.HasOne("NadekoBot.Services.Database.Models.AntiSpamSetting") + .WithMany("IgnoredChannels") + .HasForeignKey("AntiSpamSettingId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.AntiSpamSetting", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig", "GuildConfig") + .WithOne("AntiSpamSetting") + .HasForeignKey("NadekoBot.Services.Database.Models.AntiSpamSetting", "GuildConfigId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.BlacklistItem", b => + { + b.HasOne("NadekoBot.Services.Database.Models.BotConfig") + .WithMany("Blacklist") + .HasForeignKey("BotConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.BlockedCmdOrMdl", b => + { + b.HasOne("NadekoBot.Services.Database.Models.BotConfig") + .WithMany("BlockedCommands") + .HasForeignKey("BotConfigId"); + + b.HasOne("NadekoBot.Services.Database.Models.BotConfig") + .WithMany("BlockedModules") + .HasForeignKey("BotConfigId1"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.ClashCaller", b => + { + b.HasOne("NadekoBot.Services.Database.Models.ClashWar", "ClashWar") + .WithMany("Bases") + .HasForeignKey("ClashWarId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.ClubApplicants", b => + { + b.HasOne("NadekoBot.Services.Database.Models.ClubInfo", "Club") + .WithMany("Applicants") + .HasForeignKey("ClubId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("NadekoBot.Services.Database.Models.DiscordUser", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.ClubBans", b => + { + b.HasOne("NadekoBot.Services.Database.Models.ClubInfo", "Club") + .WithMany("Bans") + .HasForeignKey("ClubId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("NadekoBot.Services.Database.Models.DiscordUser", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.ClubInfo", b => + { + b.HasOne("NadekoBot.Services.Database.Models.DiscordUser", "Owner") + .WithOne() + .HasForeignKey("NadekoBot.Services.Database.Models.ClubInfo", "OwnerId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.CommandAlias", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig") + .WithMany("CommandAliases") + .HasForeignKey("GuildConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.CommandCooldown", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig") + .WithMany("CommandCooldowns") + .HasForeignKey("GuildConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.CommandPrice", b => + { + b.HasOne("NadekoBot.Services.Database.Models.BotConfig") + .WithMany("CommandPrices") + .HasForeignKey("BotConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.DiscordUser", b => + { + b.HasOne("NadekoBot.Services.Database.Models.ClubInfo", "Club") + .WithMany("Users") + .HasForeignKey("ClubId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.EightBallResponse", b => + { + b.HasOne("NadekoBot.Services.Database.Models.BotConfig") + .WithMany("EightBallResponses") + .HasForeignKey("BotConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.ExcludedItem", b => + { + b.HasOne("NadekoBot.Services.Database.Models.XpSettings") + .WithMany("ExclusionList") + .HasForeignKey("XpSettingsId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.FilterChannelId", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig") + .WithMany("FilterInvitesChannelIds") + .HasForeignKey("GuildConfigId"); + + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig") + .WithMany("FilterWordsChannelIds") + .HasForeignKey("GuildConfigId1"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.FilteredWord", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig") + .WithMany("FilteredWords") + .HasForeignKey("GuildConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.FollowedStream", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig") + .WithMany("FollowedStreams") + .HasForeignKey("GuildConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.GCChannelId", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig") + .WithMany("GenerateCurrencyChannelIds") + .HasForeignKey("GuildConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.GuildConfig", b => + { + b.HasOne("NadekoBot.Services.Database.Models.LogSetting", "LogSetting") + .WithMany() + .HasForeignKey("LogSettingId"); + + b.HasOne("NadekoBot.Services.Database.Models.Permission", "RootPermission") + .WithMany() + .HasForeignKey("RootPermissionId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.GuildRepeater", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig") + .WithMany("GuildRepeaters") + .HasForeignKey("GuildConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.IgnoredLogChannel", b => + { + b.HasOne("NadekoBot.Services.Database.Models.LogSetting", "LogSetting") + .WithMany("IgnoredChannels") + .HasForeignKey("LogSettingId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.IgnoredVoicePresenceChannel", b => + { + b.HasOne("NadekoBot.Services.Database.Models.LogSetting", "LogSetting") + .WithMany("IgnoredVoicePresenceChannelIds") + .HasForeignKey("LogSettingId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.ModulePrefix", b => + { + b.HasOne("NadekoBot.Services.Database.Models.BotConfig") + .WithMany("ModulePrefixes") + .HasForeignKey("BotConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.MutedUserId", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig") + .WithMany("MutedUsers") + .HasForeignKey("GuildConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.NsfwBlacklitedTag", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig") + .WithMany("NsfwBlacklistedTags") + .HasForeignKey("GuildConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.Permission", b => + { + b.HasOne("NadekoBot.Services.Database.Models.Permission", "Next") + .WithOne("Previous") + .HasForeignKey("NadekoBot.Services.Database.Models.Permission", "NextId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.Permissionv2", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig") + .WithMany("Permissions") + .HasForeignKey("GuildConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.PlayingStatus", b => + { + b.HasOne("NadekoBot.Services.Database.Models.BotConfig") + .WithMany("RotatingStatusMessages") + .HasForeignKey("BotConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.PlaylistSong", b => + { + b.HasOne("NadekoBot.Services.Database.Models.MusicPlaylist") + .WithMany("Songs") + .HasForeignKey("MusicPlaylistId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.RaceAnimal", b => + { + b.HasOne("NadekoBot.Services.Database.Models.BotConfig") + .WithMany("RaceAnimals") + .HasForeignKey("BotConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.ShopEntry", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig") + .WithMany("ShopEntries") + .HasForeignKey("GuildConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.ShopEntryItem", b => + { + b.HasOne("NadekoBot.Services.Database.Models.ShopEntry") + .WithMany("Items") + .HasForeignKey("ShopEntryId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.SlowmodeIgnoredRole", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig") + .WithMany("SlowmodeIgnoredRoles") + .HasForeignKey("GuildConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.SlowmodeIgnoredUser", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig") + .WithMany("SlowmodeIgnoredUsers") + .HasForeignKey("GuildConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.StartupCommand", b => + { + b.HasOne("NadekoBot.Services.Database.Models.BotConfig") + .WithMany("StartupCommands") + .HasForeignKey("BotConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.StreamRoleBlacklistedUser", b => + { + b.HasOne("NadekoBot.Services.Database.Models.StreamRoleSettings") + .WithMany("Blacklist") + .HasForeignKey("StreamRoleSettingsId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.StreamRoleSettings", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig", "GuildConfig") + .WithOne("StreamRole") + .HasForeignKey("NadekoBot.Services.Database.Models.StreamRoleSettings", "GuildConfigId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.StreamRoleWhitelistedUser", b => + { + b.HasOne("NadekoBot.Services.Database.Models.StreamRoleSettings") + .WithMany("Whitelist") + .HasForeignKey("StreamRoleSettingsId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.UnmuteTimer", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig") + .WithMany("UnmuteTimers") + .HasForeignKey("GuildConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.VcRoleInfo", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig") + .WithMany("VcRoleInfos") + .HasForeignKey("GuildConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.WaifuInfo", b => + { + b.HasOne("NadekoBot.Services.Database.Models.DiscordUser", "Affinity") + .WithMany() + .HasForeignKey("AffinityId"); + + b.HasOne("NadekoBot.Services.Database.Models.DiscordUser", "Claimer") + .WithMany() + .HasForeignKey("ClaimerId"); + + b.HasOne("NadekoBot.Services.Database.Models.DiscordUser", "Waifu") + .WithOne() + .HasForeignKey("NadekoBot.Services.Database.Models.WaifuInfo", "WaifuId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.WaifuItem", b => + { + b.HasOne("NadekoBot.Services.Database.Models.WaifuInfo") + .WithMany("Items") + .HasForeignKey("WaifuInfoId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.WaifuUpdate", b => + { + b.HasOne("NadekoBot.Services.Database.Models.DiscordUser", "New") + .WithMany() + .HasForeignKey("NewId"); + + b.HasOne("NadekoBot.Services.Database.Models.DiscordUser", "Old") + .WithMany() + .HasForeignKey("OldId"); + + b.HasOne("NadekoBot.Services.Database.Models.DiscordUser", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.WarningPunishment", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig") + .WithMany("WarnPunishments") + .HasForeignKey("GuildConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.XpRoleReward", b => + { + b.HasOne("NadekoBot.Services.Database.Models.XpSettings") + .WithMany("RoleRewards") + .HasForeignKey("XpSettingsId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.XpSettings", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig", "GuildConfig") + .WithOne("XpSettings") + .HasForeignKey("NadekoBot.Services.Database.Models.XpSettings", "GuildConfigId") + .OnDelete(DeleteBehavior.Cascade); + }); + } + } +} diff --git a/src/NadekoBot/Migrations/20170915034808_club-admins.cs b/src/NadekoBot/Migrations/20170915034808_club-admins.cs new file mode 100644 index 00000000..b1c0d3e6 --- /dev/null +++ b/src/NadekoBot/Migrations/20170915034808_club-admins.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; +using Microsoft.EntityFrameworkCore.Migrations; + +namespace NadekoBot.Migrations +{ + public partial class clubadmins : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "IsClubAdmin", + table: "DiscordUser", + nullable: false, + defaultValue: false); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "IsClubAdmin", + table: "DiscordUser"); + } + } +} diff --git a/src/NadekoBot/Migrations/NadekoSqliteContextModelSnapshot.cs b/src/NadekoBot/Migrations/NadekoSqliteContextModelSnapshot.cs index 5f522e7b..cb6b041e 100644 --- a/src/NadekoBot/Migrations/NadekoSqliteContextModelSnapshot.cs +++ b/src/NadekoBot/Migrations/NadekoSqliteContextModelSnapshot.cs @@ -462,9 +462,11 @@ namespace NadekoBot.Migrations b.Property("Discriminator"); + b.Property("IsClubAdmin"); + b.Property("LastLevelUp") .ValueGeneratedOnAdd() - .HasDefaultValue(new DateTime(2017, 9, 13, 4, 26, 53, 906, DateTimeKind.Local)); + .HasDefaultValue(new DateTime(2017, 9, 15, 5, 48, 8, 660, DateTimeKind.Local)); b.Property("LastXpGain"); @@ -1364,7 +1366,7 @@ namespace NadekoBot.Migrations b.Property("LastLevelUp") .ValueGeneratedOnAdd() - .HasDefaultValue(new DateTime(2017, 9, 13, 4, 26, 53, 910, DateTimeKind.Local)); + .HasDefaultValue(new DateTime(2017, 9, 15, 5, 48, 8, 665, DateTimeKind.Local)); b.Property("NotifyOnLevelUp"); diff --git a/src/NadekoBot/Modules/Music/Common/SongBuffer.cs b/src/NadekoBot/Modules/Music/Common/SongBuffer.cs index 8f76be79..2193877f 100644 --- a/src/NadekoBot/Modules/Music/Common/SongBuffer.cs +++ b/src/NadekoBot/Modules/Music/Common/SongBuffer.cs @@ -3,7 +3,6 @@ using System; using System.Diagnostics; using System.IO; using System.Threading; -using System.Threading.Tasks; namespace NadekoBot.Modules.Music.Common { @@ -18,27 +17,16 @@ namespace NadekoBot.Modules.Music.Common public string SongUri { get; private set; } - //private volatile bool restart = false; - public SongBuffer(string songUri, string skipTo, bool isLocal) { _log = LogManager.GetCurrentClassLogger(); - //_log.Warn(songUri); this.SongUri = songUri; this._isLocal = isLocal; try { this.p = StartFFmpegProcess(SongUri, 0); - var t = Task.Run(() => - { - this.p.BeginErrorReadLine(); - this.p.ErrorDataReceived += P_ErrorDataReceived; - this.p.WaitForExit(); - }); - this._outStream = this.p.StandardOutput.BaseStream; - } catch (System.ComponentModel.Win32Exception) { @@ -68,113 +56,14 @@ Check the guides for your platform on how to setup ffmpeg correctly: Arguments = args, UseShellExecute = false, RedirectStandardOutput = true, - RedirectStandardError = true, + RedirectStandardError = false, CreateNoWindow = true, }); } - private void P_ErrorDataReceived(object sender, DataReceivedEventArgs e) - { - if (string.IsNullOrWhiteSpace(e.Data)) - return; - _log.Error(">>> " + e.Data); - if (e.Data?.Contains("Error in the pull function") == true) - { - _log.Error("Ignore this."); - //restart = true; - } - } - private readonly object locker = new object(); private readonly bool _isLocal; - public Task StartBuffering(CancellationToken cancelToken) - { - var toReturn = new TaskCompletionSource(); - var _ = Task.Run(() => - { - try - { - - ////int maxLoopsPerSec = 25; - //var sw = Stopwatch.StartNew(); - ////var delay = 1000 / maxLoopsPerSec; - //int currentLoops = 0; - //int _bytesSent = 0; - //try - //{ - // //do - // //{ - // // if (restart) - // // { - // // var cur = _bytesSent / 3840 / (1000 / 20.0f); - // // _log.Info("Restarting"); - // // try { this.p.StandardOutput.Dispose(); } catch { } - // // try { this.p.Dispose(); } catch { } - // // this.p = StartFFmpegProcess(SongUri, cur); - // // } - // // restart = false; - // ++currentLoops; - // byte[] buffer = new byte[readSize]; - // int bytesRead = 1; - // while (!cancelToken.IsCancellationRequested && !this.p.HasExited) - // { - // bytesRead = await p.StandardOutput.BaseStream.ReadAsync(buffer, 0, readSize, cancelToken).ConfigureAwait(false); - // _bytesSent += bytesRead; - // if (bytesRead == 0) - // break; - // bool written; - // do - // { - // lock (locker) - // written = _outStream.Write(buffer, 0, bytesRead); - // if (!written) - // await Task.Delay(2000, cancelToken); - // } - // while (!written && !cancelToken.IsCancellationRequested); - // lock (locker) - // if (_outStream.Length > 200_000 || bytesRead == 0) - // if (toReturn.TrySetResult(true)) - // _log.Info("Prebuffering finished in {0}", sw.Elapsed.TotalSeconds.ToString("F2")); - - // //_log.Info(_outStream.Length); - // await Task.Delay(10); - // } - // //if (cancelToken.IsCancellationRequested) - // // _log.Info("Song canceled"); - // //else if (p.HasExited) - // // _log.Info("Song buffered completely (FFmpeg exited)"); - // //else if (bytesRead == 0) - // // _log.Info("Nothing read"); - // //} - // //while (restart && !cancelToken.IsCancellationRequested); - //return Task.CompletedTask; - toReturn.TrySetResult(true); - } - catch (System.ComponentModel.Win32Exception) - { - _log.Error(@"You have not properly installed or configured FFMPEG. -Please install and configure FFMPEG to play music. -Check the guides for your platform on how to setup ffmpeg correctly: - Windows Guide: https://goo.gl/OjKk8F - Linux Guide: https://goo.gl/ShjCUo"); - } - catch (OperationCanceledException) { } - catch (InvalidOperationException) { } // when ffmpeg is disposed - catch (Exception ex) - { - _log.Info(ex); - } - finally - { - if (toReturn.TrySetResult(false)) - _log.Info("Prebuffering failed"); - } - }, cancelToken); - - return toReturn.Task; - } - public int Read(byte[] b, int offset, int toRead) { lock (locker) @@ -203,215 +92,4 @@ Check the guides for your platform on how to setup ffmpeg correctly: this.p.Dispose(); } } -} - -//namespace NadekoBot.Services.Music -//{ -// ///

-// /// Create a buffer for a song file. It will create multiples files to ensure, that radio don't fill up disk space. -// /// It also help for large music by deleting files that are already seen. -// /// -// class SongBuffer : Stream -// { -// public SongBuffer(MusicPlayer musicPlayer, string basename, SongInfo songInfo, int skipTo, int maxFileSize) -// { -// MusicPlayer = musicPlayer; -// Basename = basename; -// SongInfo = songInfo; -// SkipTo = skipTo; -// MaxFileSize = maxFileSize; -// CurrentFileStream = new FileStream(this.GetNextFile(), FileMode.OpenOrCreate, FileAccess.Read, FileShare.Write); -// _log = LogManager.GetCurrentClassLogger(); -// } - -// MusicPlayer MusicPlayer { get; } - -// private string Basename { get; } - -// private SongInfo SongInfo { get; } - -// private int SkipTo { get; } - -// private int MaxFileSize { get; } = 2.MiB(); - -// private long FileNumber = -1; - -// private long NextFileToRead = 0; - -// public bool BufferingCompleted { get; private set; } = false; - -// private ulong CurrentBufferSize = 0; - -// private FileStream CurrentFileStream; -// private Logger _log; - -// public Task BufferSong(CancellationToken cancelToken) => -// Task.Run(async () => -// { -// Process p = null; -// FileStream outStream = null; -// try -// { -// p = Process.Start(new ProcessStartInfo -// { -// FileName = "ffmpeg", -// Arguments = $"-ss {SkipTo} -i {SongInfo.Uri} -f s16le -ar 48000 -vn -ac 2 pipe:1 -loglevel quiet", -// UseShellExecute = false, -// RedirectStandardOutput = true, -// RedirectStandardError = false, -// CreateNoWindow = true, -// }); - -// byte[] buffer = new byte[81920]; -// int currentFileSize = 0; -// ulong prebufferSize = 100ul.MiB(); - -// outStream = new FileStream(Basename + "-" + ++FileNumber, FileMode.Append, FileAccess.Write, FileShare.Read); -// while (!p.HasExited) //Also fix low bandwidth -// { -// int bytesRead = await p.StandardOutput.BaseStream.ReadAsync(buffer, 0, buffer.Length, cancelToken).ConfigureAwait(false); -// if (currentFileSize >= MaxFileSize) -// { -// try -// { -// outStream.Dispose(); -// } -// catch { } -// outStream = new FileStream(Basename + "-" + ++FileNumber, FileMode.Append, FileAccess.Write, FileShare.Read); -// currentFileSize = bytesRead; -// } -// else -// { -// currentFileSize += bytesRead; -// } -// CurrentBufferSize += Convert.ToUInt64(bytesRead); -// await outStream.WriteAsync(buffer, 0, bytesRead, cancelToken).ConfigureAwait(false); -// while (CurrentBufferSize > prebufferSize) -// await Task.Delay(100, cancelToken); -// } -// BufferingCompleted = true; -// } -// catch (System.ComponentModel.Win32Exception) -// { -// var oldclr = Console.ForegroundColor; -// Console.ForegroundColor = ConsoleColor.Red; -// Console.WriteLine(@"You have not properly installed or configured FFMPEG. -//Please install and configure FFMPEG to play music. -//Check the guides for your platform on how to setup ffmpeg correctly: -// Windows Guide: https://goo.gl/OjKk8F -// Linux Guide: https://goo.gl/ShjCUo"); -// Console.ForegroundColor = oldclr; -// } -// catch (Exception ex) -// { -// Console.WriteLine($"Buffering stopped: {ex.Message}"); -// } -// finally -// { -// if (outStream != null) -// outStream.Dispose(); -// if (p != null) -// { -// try -// { -// p.Kill(); -// } -// catch { } -// p.Dispose(); -// } -// } -// }); - -// /// -// /// Return the next file to read, and delete the old one -// /// -// /// Name of the file to read -// private string GetNextFile() -// { -// string filename = Basename + "-" + NextFileToRead; - -// if (NextFileToRead != 0) -// { -// try -// { -// CurrentBufferSize -= Convert.ToUInt64(new FileInfo(Basename + "-" + (NextFileToRead - 1)).Length); -// File.Delete(Basename + "-" + (NextFileToRead - 1)); -// } -// catch { } -// } -// NextFileToRead++; -// return filename; -// } - -// private bool IsNextFileReady() -// { -// return NextFileToRead <= FileNumber; -// } - -// private void CleanFiles() -// { -// for (long i = NextFileToRead - 1; i <= FileNumber; i++) -// { -// try -// { -// File.Delete(Basename + "-" + i); -// } -// catch { } -// } -// } - -// //Stream part - -// public override bool CanRead => true; - -// public override bool CanSeek => false; - -// public override bool CanWrite => false; - -// public override long Length => (long)CurrentBufferSize; - -// public override long Position { get; set; } = 0; - -// public override void Flush() { } - -// public override int Read(byte[] buffer, int offset, int count) -// { -// int read = CurrentFileStream.Read(buffer, offset, count); -// if (read < count) -// { -// if (!BufferingCompleted || IsNextFileReady()) -// { -// CurrentFileStream.Dispose(); -// CurrentFileStream = new FileStream(GetNextFile(), FileMode.OpenOrCreate, FileAccess.Read, FileShare.Write); -// read += CurrentFileStream.Read(buffer, read + offset, count - read); -// } -// if (read < count) -// Array.Clear(buffer, read, count - read); -// } -// return read; -// } - -// public override long Seek(long offset, SeekOrigin origin) -// { -// throw new NotImplementedException(); -// } - -// public override void SetLength(long value) -// { -// throw new NotImplementedException(); -// } - -// public override void Write(byte[] buffer, int offset, int count) -// { -// throw new NotImplementedException(); -// } - -// public new void Dispose() -// { -// CurrentFileStream.Dispose(); -// MusicPlayer.SongCancelSource.Cancel(); -// CleanFiles(); -// base.Dispose(); -// } -// } -//} \ No newline at end of file +} \ No newline at end of file diff --git a/src/NadekoBot/Modules/NSFW/NSFW.cs b/src/NadekoBot/Modules/NSFW/NSFW.cs index 5e3591db..1ca27691 100644 --- a/src/NadekoBot/Modules/NSFW/NSFW.cs +++ b/src/NadekoBot/Modules/NSFW/NSFW.cs @@ -16,9 +16,13 @@ using NadekoBot.Modules.NSFW.Exceptions; namespace NadekoBot.Modules.NSFW { + // thanks to halitalf for adding autoboob and autobutt features :D public class NSFW : NadekoTopLevelModule { private static readonly ConcurrentDictionary _autoHentaiTimers = new ConcurrentDictionary(); + private static readonly ConcurrentDictionary _autoBoobTimers = new ConcurrentDictionary(); + private static readonly ConcurrentDictionary _autoButtTimers = new ConcurrentDictionary(); + private static readonly ConcurrentHashSet _hentaiBombBlacklist = new ConcurrentHashSet(); private async Task InternalHentai(IMessageChannel channel, string tag, bool noError) @@ -49,10 +53,34 @@ namespace NadekoBot.Modules.NSFW .WithDescription($"[{GetText("tag")}: {tag}]({img})")) .ConfigureAwait(false); } + private async Task InternalBoobs(IMessageChannel Channel) + { + try + { + JToken obj; + obj = JArray.Parse(await _service.Http.GetStringAsync($"http://api.oboobs.ru/boobs/{new NadekoRandom().Next(0, 10330)}").ConfigureAwait(false))[0]; + await Channel.SendMessageAsync($"http://media.oboobs.ru/{obj["preview"]}").ConfigureAwait(false); + } + catch (Exception ex) + { + await Channel.SendErrorAsync(ex.Message).ConfigureAwait(false); + } + } + + private async Task InternalButts(IMessageChannel Channel) + { + try + { + JToken obj; + obj = JArray.Parse(await _service.Http.GetStringAsync($"http://api.obutts.ru/butts/{new NadekoRandom().Next(0, 4335)}").ConfigureAwait(false))[0]; + await Channel.SendMessageAsync($"http://media.obutts.ru/{obj["preview"]}").ConfigureAwait(false); + } + catch (Exception ex) + { + await Channel.SendErrorAsync(ex.Message).ConfigureAwait(false); + } + } - [NadekoCommand, Usage, Description, Aliases] - public Task Hentai([Remainder] string tag = null) => - InternalHentai(Context.Channel, tag, false); #if !GLOBAL_NADEKO [NadekoCommand, Usage, Description, Aliases] [RequireUserPermission(ChannelPermission.ManageMessages)] @@ -65,7 +93,7 @@ namespace NadekoBot.Modules.NSFW if (!_autoHentaiTimers.TryRemove(Context.Channel.Id, out t)) return; t.Change(Timeout.Infinite, Timeout.Infinite); //proper way to disable the timer - await ReplyConfirmLocalized("autohentai_stopped").ConfigureAwait(false); + await ReplyConfirmLocalized("stopped").ConfigureAwait(false); return; } @@ -99,8 +127,90 @@ namespace NadekoBot.Modules.NSFW interval, string.Join(", ", tagsArr)).ConfigureAwait(false); } + + [NadekoCommand, Usage, Description, Aliases] + [RequireUserPermission(ChannelPermission.ManageMessages)] + public async Task AutoBoobs(int interval = 0) + { + Timer t; + + if (interval == 0) + { + if (!_autoBoobTimers.TryRemove(Context.Channel.Id, out t)) return; + + t.Change(Timeout.Infinite, Timeout.Infinite); //proper way to disable the timer + await ReplyConfirmLocalized("stopped").ConfigureAwait(false); + return; + } + + if (interval < 20) + return; + + t = new Timer(async (state) => + { + try + { + await InternalBoobs(Context.Channel).ConfigureAwait(false); + } + catch + { + // ignored + } + }, null, interval * 1000, interval * 1000); + + _autoBoobTimers.AddOrUpdate(Context.Channel.Id, t, (key, old) => + { + old.Change(Timeout.Infinite, Timeout.Infinite); + return t; + }); + + await ReplyConfirmLocalized("started", interval).ConfigureAwait(false); + } + + [NadekoCommand, Usage, Description, Aliases] + [RequireUserPermission(ChannelPermission.ManageMessages)] + public async Task AutoButts(int interval = 0) + { + Timer t; + + if (interval == 0) + { + if (!_autoButtTimers.TryRemove(Context.Channel.Id, out t)) return; + + t.Change(Timeout.Infinite, Timeout.Infinite); //proper way to disable the timer + await ReplyConfirmLocalized("stopped").ConfigureAwait(false); + return; + } + + if (interval < 20) + return; + + t = new Timer(async (state) => + { + try + { + await InternalButts(Context.Channel).ConfigureAwait(false); + } + catch + { + // ignored + } + }, null, interval * 1000, interval * 1000); + + _autoButtTimers.AddOrUpdate(Context.Channel.Id, t, (key, old) => + { + old.Change(Timeout.Infinite, Timeout.Infinite); + return t; + }); + + await ReplyConfirmLocalized("started", interval).ConfigureAwait(false); + } #endif + [NadekoCommand, Usage, Description, Aliases] + public Task Hentai([Remainder] string tag = null) => + InternalHentai(Context.Channel, tag, false); + [NadekoCommand, Usage, Description, Aliases] public async Task HentaiBomb([Remainder] string tag = null) { @@ -199,7 +309,7 @@ namespace NadekoBot.Modules.NSFW tag = tag.Trim().ToLowerInvariant(); var added = _service.ToggleBlacklistedTag(Context.Guild.Id, tag); - if(added) + if (added) await ReplyConfirmLocalized("blacklisted_tag_add", tag).ConfigureAwait(false); else await ReplyConfirmLocalized("blacklisted_tag_remove", tag).ConfigureAwait(false); diff --git a/src/NadekoBot/Modules/Xp/Club.cs b/src/NadekoBot/Modules/Xp/Club.cs index 8c4ce4c5..06fd5336 100644 --- a/src/NadekoBot/Modules/Xp/Club.cs +++ b/src/NadekoBot/Modules/Xp/Club.cs @@ -26,6 +26,27 @@ namespace NadekoBot.Modules.Xp _client = client; } + [NadekoCommand, Usage, Description, Aliases] + public async Task ClubAdmin([Remainder]IUser toAdmin) + { + bool admin; + try + { + admin = _service.ToggleAdmin(Context.User, toAdmin); + } + catch (InvalidOperationException) + { + await ReplyErrorLocalized("club_admin_error").ConfigureAwait(false); + return; + } + + if(admin) + await ReplyConfirmLocalized("club_admin_add", Format.Bold(toAdmin.ToString())).ConfigureAwait(false); + else + await ReplyConfirmLocalized("club_admin_remove", Format.Bold(toAdmin.ToString())).ConfigureAwait(false); + + } + [NadekoCommand, Usage, Description, Aliases] public async Task ClubCreate([Remainder]string clubName) { @@ -97,7 +118,14 @@ namespace NadekoBot.Modules.Xp .AddField("Level Req.", club.MinimumLevelReq.ToString(), true) .AddField("Members", string.Join("\n", club.Users .Skip(page * 10) - .Take(10)), false); + .Take(10) + .OrderByDescending(x => x.IsClubAdmin) + .Select(x => + { + if (x.IsClubAdmin) + return x.ToString() + "⭐"; + return x.ToString(); + })), false); if (Uri.IsWellFormedUriString(club.ImageUrl, UriKind.Absolute)) return embed.WithThumbnailUrl(club.ImageUrl); @@ -112,7 +140,7 @@ namespace NadekoBot.Modules.Xp if (--page < 0) return Task.CompletedTask; - var club = _service.GetBansAndApplications(Context.User.Id); + var club = _service.GetClubWithBansAndApplications(Context.User.Id); if (club == null) return ReplyErrorLocalized("club_not_exists"); @@ -143,11 +171,11 @@ namespace NadekoBot.Modules.Xp if (--page < 0) return Task.CompletedTask; - var club = _service.GetBansAndApplications(Context.User.Id); + var club = _service.GetClubWithBansAndApplications(Context.User.Id); if (club == null) return ReplyErrorLocalized("club_not_exists"); - var bans = club + var apps = club .Applicants .Select(x => x.User) .ToArray(); @@ -155,7 +183,7 @@ namespace NadekoBot.Modules.Xp return Context.Channel.SendPaginatedConfirmAsync(_client, page, curPage => { - var toShow = string.Join("\n", bans + var toShow = string.Join("\n", apps .Skip(page * 10) .Take(10) .Select(x => x.ToString())); @@ -164,7 +192,7 @@ namespace NadekoBot.Modules.Xp .WithTitle(GetText("club_apps_for", club.ToString())) .WithDescription(toShow); - }, bans.Length / 10); + }, apps.Length / 10); } [NadekoCommand, Usage, Description, Aliases] diff --git a/src/NadekoBot/Modules/Xp/Services/ClubService.cs b/src/NadekoBot/Modules/Xp/Services/ClubService.cs index f8d08f66..362067eb 100644 --- a/src/NadekoBot/Modules/Xp/Services/ClubService.cs +++ b/src/NadekoBot/Modules/Xp/Services/ClubService.cs @@ -27,10 +27,11 @@ namespace NadekoBot.Modules.Xp.Services { var du = uow.DiscordUsers.GetOrCreate(user); uow._context.SaveChanges(); - var xp = new LevelStats(uow.Xp.GetTotalUserXp(user.Id)); + var xp = new LevelStats(du.TotalXp); if (xp.Level >= 5 && du.Club == null) { + du.IsClubAdmin = true; du.Club = new ClubInfo() { Name = clubName, @@ -52,6 +53,27 @@ namespace NadekoBot.Modules.Xp.Services return true; } + public bool ToggleAdmin(IUser owner, IUser toAdmin) + { + bool newState; + using (var uow = _db.UnitOfWork) + { + var club = uow.Clubs.GetByOwner(owner.Id); + var adminUser = uow.DiscordUsers.GetOrCreate(toAdmin); + + if (club.OwnerId == adminUser.Id) + return true; + + if (club == null || club.Owner.UserId != owner.Id || + !club.Users.Contains(adminUser)) + throw new InvalidOperationException(); + + newState = adminUser.IsClubAdmin = !adminUser.IsClubAdmin; + uow.Complete(); + } + return newState; + } + public ClubInfo GetClubByMember(IUser user) { using (var uow = _db.UnitOfWork) @@ -107,7 +129,7 @@ namespace NadekoBot.Modules.Xp.Services uow._context.SaveChanges(); if (du.Club != null - || new LevelStats(uow.Xp.GetTotalUserXp(user.Id)).Level < club.MinimumLevelReq + || new LevelStats(du.TotalXp).Level < club.MinimumLevelReq || club.Bans.Any(x => x.UserId == du.Id) || club.Applicants.Any(x => x.UserId == du.Id)) { @@ -134,11 +156,7 @@ namespace NadekoBot.Modules.Xp.Services discordUser = null; using (var uow = _db.UnitOfWork) { - var club = uow.Clubs.GetByOwner(clubOwnerUserId, - set => set.Include(x => x.Applicants) - .ThenInclude(x => x.Club) - .Include(x => x.Applicants) - .ThenInclude(x => x.User)); + var club = uow.Clubs.GetByOwnerOrAdmin(clubOwnerUserId); if (club == null) return false; @@ -147,6 +165,7 @@ namespace NadekoBot.Modules.Xp.Services return false; applicant.User.Club = club; + applicant.User.IsClubAdmin = false; club.Applicants.Remove(applicant); //remove that user's all other applications @@ -159,15 +178,11 @@ namespace NadekoBot.Modules.Xp.Services return true; } - public ClubInfo GetBansAndApplications(ulong ownerUserId) + public ClubInfo GetClubWithBansAndApplications(ulong ownerUserId) { using (var uow = _db.UnitOfWork) { - return uow.Clubs.GetByOwner(ownerUserId, - x => x.Include(y => y.Bans) - .ThenInclude(y => y.User) - .Include(y => y.Applicants) - .ThenInclude(y => y.User)); + return uow.Clubs.GetByOwnerOrAdmin(ownerUserId); } } @@ -180,6 +195,7 @@ namespace NadekoBot.Modules.Xp.Services return false; du.Club = null; + du.IsClubAdmin = false; uow.Complete(); } return true; @@ -221,9 +237,7 @@ namespace NadekoBot.Modules.Xp.Services { using (var uow = _db.UnitOfWork) { - club = uow.Clubs.GetByOwner(ownerUserId, - set => set.Include(x => x.Applicants) - .ThenInclude(x => x.User)); + club = uow.Clubs.GetByOwnerOrAdmin(ownerUserId); if (club == null) return false; @@ -256,9 +270,7 @@ namespace NadekoBot.Modules.Xp.Services { using (var uow = _db.UnitOfWork) { - club = uow.Clubs.GetByOwner(ownerUserId, - set => set.Include(x => x.Bans) - .ThenInclude(x => x.User)); + club = uow.Clubs.GetByOwnerOrAdmin(ownerUserId); if (club == null) return false; @@ -277,7 +289,7 @@ namespace NadekoBot.Modules.Xp.Services { using (var uow = _db.UnitOfWork) { - club = uow.Clubs.GetByOwner(ownerUserId); + club = uow.Clubs.GetByOwnerOrAdmin(ownerUserId); if (club == null) return false; diff --git a/src/NadekoBot/Modules/Xp/Services/XpService.cs b/src/NadekoBot/Modules/Xp/Services/XpService.cs index 537953e2..09fa7ec9 100644 --- a/src/NadekoBot/Modules/Xp/Services/XpService.cs +++ b/src/NadekoBot/Modules/Xp/Services/XpService.cs @@ -144,7 +144,7 @@ namespace NadekoBot.Modules.Xp.Services du.LastXpGain = DateTime.UtcNow; - var globalXp = uow.Xp.GetTotalUserXp(item.Key.User.Id); + var globalXp = du.TotalXp; var oldGlobalLevelData = new LevelStats(globalXp); var newGlobalLevelData = new LevelStats(globalXp + xp); @@ -403,17 +403,6 @@ namespace NadekoBot.Modules.Xp.Services return _rewardedUsers.Add(userId); } - public LevelStats GetGlobalUserStats(ulong userId) - { - int totalXp; - using (var uow = _db.UnitOfWork) - { - totalXp = uow.Xp.GetTotalUserXp(userId); - } - - return new LevelStats(totalXp); - } - public FullUserStats GetUserStats(IGuildUser user) { DiscordUser du; diff --git a/src/NadekoBot/Resources/CommandStrings.resx b/src/NadekoBot/Resources/CommandStrings.resx index 587d643e..957e8059 100644 --- a/src/NadekoBot/Resources/CommandStrings.resx +++ b/src/NadekoBot/Resources/CommandStrings.resx @@ -3780,4 +3780,31 @@ Clears nsfw cache. + + clubadmin + + + `{0}clubadmin` + + + Assigns (or unassigns) staff role to the member of the club. Admins can ban, kick and accept applications. + + + autoboobs + + + Posts a boobs every X seconds. 20 seconds minimum. Provide no arguments to disable. + + + `{0}autoboobs 30` or `{0}autoboobs` + + + autobutts + + + Posts a butts every X seconds. 20 seconds minimum. Provide no arguments to disable. + + + `{0}autobutts 30` or `{0}autobutts` + diff --git a/src/NadekoBot/Services/Database/Models/DiscordUser.cs b/src/NadekoBot/Services/Database/Models/DiscordUser.cs index 77a573b6..654408e2 100644 --- a/src/NadekoBot/Services/Database/Models/DiscordUser.cs +++ b/src/NadekoBot/Services/Database/Models/DiscordUser.cs @@ -10,6 +10,7 @@ namespace NadekoBot.Services.Database.Models public string AvatarId { get; set; } public ClubInfo Club { get; set; } + public bool IsClubAdmin { get; set; } public int TotalXp { get; set; } public DateTime LastLevelUp { get; set; } = DateTime.UtcNow; diff --git a/src/NadekoBot/Services/Database/NadekoContext.cs b/src/NadekoBot/Services/Database/NadekoContext.cs index 5c04c816..0df96909 100644 --- a/src/NadekoBot/Services/Database/NadekoContext.cs +++ b/src/NadekoBot/Services/Database/NadekoContext.cs @@ -329,7 +329,7 @@ namespace NadekoBot.Services.Database #region ClubManytoMany modelBuilder.Entity() - .HasKey(t => new { t.ClubId, t.UserId }); + .HasKey(t => new { t.ClubId, t.UserId }); modelBuilder.Entity() .HasOne(pt => pt.User) @@ -340,7 +340,7 @@ namespace NadekoBot.Services.Database .WithMany(x => x.Applicants); modelBuilder.Entity() - .HasKey(t => new { t.ClubId, t.UserId }); + .HasKey(t => new { t.ClubId, t.UserId }); modelBuilder.Entity() .HasOne(pt => pt.User) diff --git a/src/NadekoBot/Services/Database/Repositories/IClubRepository.cs b/src/NadekoBot/Services/Database/Repositories/IClubRepository.cs index 086d638b..66ad3d92 100644 --- a/src/NadekoBot/Services/Database/Repositories/IClubRepository.cs +++ b/src/NadekoBot/Services/Database/Repositories/IClubRepository.cs @@ -10,6 +10,7 @@ namespace NadekoBot.Services.Database.Repositories int GetNextDiscrim(string clubName); ClubInfo GetByName(string v, int discrim, Func, IQueryable> func = null); ClubInfo GetByOwner(ulong userId, Func, IQueryable> func = null); + ClubInfo GetByOwnerOrAdmin(ulong userId); ClubInfo GetByMember(ulong userId, Func, IQueryable> func = null); ClubInfo[] GetClubLeaderboardPage(int page); } diff --git a/src/NadekoBot/Services/Database/Repositories/IXpRepository.cs b/src/NadekoBot/Services/Database/Repositories/IXpRepository.cs index 10345d5d..f62394d7 100644 --- a/src/NadekoBot/Services/Database/Repositories/IXpRepository.cs +++ b/src/NadekoBot/Services/Database/Repositories/IXpRepository.cs @@ -5,7 +5,6 @@ namespace NadekoBot.Services.Database.Repositories public interface IXpRepository : IRepository { UserXpStats GetOrCreateUser(ulong guildId, ulong userId); - int GetTotalUserXp(ulong userId); int GetUserGuildRanking(ulong userId, ulong guildId); UserXpStats[] GetUsersFor(ulong guildId, int page); } diff --git a/src/NadekoBot/Services/Database/Repositories/Impl/ClubRepository.cs b/src/NadekoBot/Services/Database/Repositories/Impl/ClubRepository.cs index eb09e8b0..7eabbb1c 100644 --- a/src/NadekoBot/Services/Database/Repositories/Impl/ClubRepository.cs +++ b/src/NadekoBot/Services/Database/Repositories/Impl/ClubRepository.cs @@ -24,6 +24,30 @@ namespace NadekoBot.Services.Database.Repositories.Impl return func(_set).FirstOrDefault(x => x.Owner.UserId == userId); } + public ClubInfo GetByOwnerOrAdmin(ulong userId) + { + return _set + .Include(x => x.Bans) + .ThenInclude(x => x.User) + .Include(x => x.Applicants) + .ThenInclude(x => x.User) + .Include(x => x.Owner) + .FirstOrDefault(x => x.Owner.UserId == userId) ?? + _context.Set() + .Include(x => x.Club) + .ThenInclude(x => x.Users) + .Include(x => x.Club) + .ThenInclude(x => x.Bans) + .ThenInclude(x => x.User) + .Include(x => x.Club) + .ThenInclude(x => x.Applicants) + .ThenInclude(x => x.User) + .Include(x => x.Club) + .ThenInclude(x => x.Owner) + .FirstOrDefault(x => x.UserId == userId && x.IsClubAdmin) + ?.Club; + } + public ClubInfo GetByName(string name, int discrim, Func, IQueryable> func = null) { if (func == null) diff --git a/src/NadekoBot/Services/Database/Repositories/Impl/XpRepository.cs b/src/NadekoBot/Services/Database/Repositories/Impl/XpRepository.cs index cc026b6a..b411cdad 100644 --- a/src/NadekoBot/Services/Database/Repositories/Impl/XpRepository.cs +++ b/src/NadekoBot/Services/Database/Repositories/Impl/XpRepository.cs @@ -29,11 +29,6 @@ namespace NadekoBot.Services.Database.Repositories.Impl return usr; } - public int GetTotalUserXp(ulong userId) - { - return _set.Where(x => x.UserId == userId).Sum(x => x.Xp); - } - public UserXpStats[] GetUsersFor(ulong guildId, int page) { return _set.Where(x => x.GuildId == guildId) diff --git a/src/NadekoBot/_strings/ResponseStrings.en-US.json b/src/NadekoBot/_strings/ResponseStrings.en-US.json index 1ee47eed..ebd9c435 100644 --- a/src/NadekoBot/_strings/ResponseStrings.en-US.json +++ b/src/NadekoBot/_strings/ResponseStrings.en-US.json @@ -13,7 +13,6 @@ "customreactions_stats_not_found": "No stats for that trigger found, no action taken.", "customreactions_trigger": "Trigger", "customreactions_redacted_too_long": "Redecated because it's too long.", - "nsfw_autohentai_stopped": "Autohentai stopped.", "nsfw_not_found": "No results found.", "nsfw_blacklisted_tag_list": "List of blacklisted tags:", "nsfw_blacklisted_tag": "One or more tags you've used are blacklisted", @@ -868,5 +867,10 @@ "xp_club_icon_set": "New club icon set.", "xp_club_bans_for": "Bans for {0} club", "xp_club_apps_for": "Applicants for {0} club", - "xp_club_leaderboard": "Club leaderboard - page {0}" + "xp_club_leaderboard": "Club leaderboard - page {0}", + "xp_club_admin_add": "{0} is now a club admin.", + "xp_club_admin_remove": "{0} is no longer club admin.", + "xp_club_admin_error": "Error. You are either not the owner of the club, or that user is not in your club.", + "nsfw_started": "Started. Reposting every {0}s.", + "nsfw_stopped": "Stopped reposting." } \ No newline at end of file From 9cff3b59c1965a6a280176e61cff622a7db07fe3 Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Fri, 15 Sep 2017 22:19:31 +0200 Subject: [PATCH 313/346] clubapps and clubbans now have ok color line --- src/NadekoBot/Modules/Xp/Club.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/NadekoBot/Modules/Xp/Club.cs b/src/NadekoBot/Modules/Xp/Club.cs index 06fd5336..6ef7b5b5 100644 --- a/src/NadekoBot/Modules/Xp/Club.cs +++ b/src/NadekoBot/Modules/Xp/Club.cs @@ -159,7 +159,8 @@ namespace NadekoBot.Modules.Xp return new EmbedBuilder() .WithTitle(GetText("club_bans_for", club.ToString())) - .WithDescription(toShow); + .WithDescription(toShow) + .WithOkColor(); }, bans.Length / 10); } @@ -190,7 +191,8 @@ namespace NadekoBot.Modules.Xp return new EmbedBuilder() .WithTitle(GetText("club_apps_for", club.ToString())) - .WithDescription(toShow); + .WithDescription(toShow) + .WithOkColor(); }, apps.Length / 10); } From a127e43dc0b137986bef355decd5af58d19062f5 Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Fri, 15 Sep 2017 22:22:03 +0200 Subject: [PATCH 314/346] Version upped --- src/NadekoBot/Services/Impl/StatsService.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/NadekoBot/Services/Impl/StatsService.cs b/src/NadekoBot/Services/Impl/StatsService.cs index 8e94ceec..9b65c5f7 100644 --- a/src/NadekoBot/Services/Impl/StatsService.cs +++ b/src/NadekoBot/Services/Impl/StatsService.cs @@ -20,7 +20,7 @@ namespace NadekoBot.Services.Impl private readonly IBotCredentials _creds; private readonly DateTime _started; - public const string BotVersion = "1.8.4"; + public const string BotVersion = "1.8.5"; public string Author => "Kwoth#2560"; public string Library => "Discord.Net"; From 6bea4b9f029f19d7d48e9460b786d534ed908f18 Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Sat, 16 Sep 2017 02:10:22 +0200 Subject: [PATCH 315/346] Upgraded to .net core 2.0 --- global.json | 2 +- src/NadekoBot/NadekoBot.csproj | 73 ++++++------------- src/NadekoBot/NadekoBot.nuspec | 15 ---- src/NadekoBot/NadekoBot.xproj.DotSettings | 7 -- src/NadekoBot/Properties/AssemblyInfo.cs | 19 ----- .../Services/Database/NadekoContext.cs | 23 ++---- src/NadekoBot/Services/DbService.cs | 8 +- src/NadekoBot/Services/LogSetup.cs | 2 +- 8 files changed, 36 insertions(+), 113 deletions(-) delete mode 100644 src/NadekoBot/NadekoBot.nuspec delete mode 100644 src/NadekoBot/NadekoBot.xproj.DotSettings delete mode 100644 src/NadekoBot/Properties/AssemblyInfo.cs diff --git a/global.json b/global.json index 3b965cc4..68be1221 100644 --- a/global.json +++ b/global.json @@ -1,3 +1,3 @@ { - "sdk": { "version": "1.0.1" } + "sdk": { "version": "2.0.0" } } \ No newline at end of file diff --git a/src/NadekoBot/NadekoBot.csproj b/src/NadekoBot/NadekoBot.csproj index 1a5bf786..3834ef6e 100644 --- a/src/NadekoBot/NadekoBot.csproj +++ b/src/NadekoBot/NadekoBot.csproj @@ -1,31 +1,18 @@  - General purpose Discord bot written in C#. - Kwoth - Kwoth - Kwoth - netcoreapp1.1 - true - NadekoBot - Exe - NadekoBot - 1.1.1 - $(PackageTargetFallback);dnxcore50;portable-net45+win8+wpa81 - false - false - false - false - False - 1.0.0.0 - 1.0.0.0 + netcoreapp2.0 + 2.0.0 + exe + $(AssetTargetFallback);dnxcore50;portable-net45+win8+wpa81 nadeko_icon.ico win7-x64 Debug;Release;global_nadeko + latest - 1.4.1 + 1.9.1 $(VersionPrefix).$(VersionSuffix) $(VersionPrefix) @@ -60,28 +47,24 @@ - - - - - + + + + - - - - - - - - - - - - + + + + + + + + + + - - + @@ -89,21 +72,13 @@ $(NoWarn);CS1573;CS1591 - - latest - - - latest - - - - latest + true - - + + diff --git a/src/NadekoBot/NadekoBot.nuspec b/src/NadekoBot/NadekoBot.nuspec deleted file mode 100644 index 109f83d5..00000000 --- a/src/NadekoBot/NadekoBot.nuspec +++ /dev/null @@ -1,15 +0,0 @@ - - - - NadekoBot - 1.4.0-2$suffix$ - NadekoBot - Kwoth - Kwoth - General purpose discord chat bot written in C#. - nadeko;bot;nadekobot;discord bot - https://github.com/Kwoth/NadekoBot - https://choosealicense.com/licenses/unlicense/ - false - - \ No newline at end of file diff --git a/src/NadekoBot/NadekoBot.xproj.DotSettings b/src/NadekoBot/NadekoBot.xproj.DotSettings deleted file mode 100644 index c5dd7773..00000000 --- a/src/NadekoBot/NadekoBot.xproj.DotSettings +++ /dev/null @@ -1,7 +0,0 @@ - - True - True - True - True - True - True \ No newline at end of file diff --git a/src/NadekoBot/Properties/AssemblyInfo.cs b/src/NadekoBot/Properties/AssemblyInfo.cs deleted file mode 100644 index ca3bd293..00000000 --- a/src/NadekoBot/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,19 +0,0 @@ -using System.Reflection; -using System.Runtime.InteropServices; - -// General Information about an assembly is controlled through the following -// set of attributes. Change these attribute values to modify the information -// associated with an assembly. -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("")] -[assembly: AssemblyProduct("NadekoBot")] -[assembly: AssemblyTrademark("")] -[assembly: AssemblyInformationalVersion("1.0")] - -// Setting ComVisible to false makes the types in this assembly not visible -// to COM components. If you need to access a type in this assembly from -// COM, set the ComVisible attribute to true on that type. -[assembly: ComVisible(false)] - -// The following GUID is for the ID of the typelib if this project is exposed to COM -[assembly: Guid("f8225ac4-3cbc-40b4-bcf3-1cacf276bf29")] diff --git a/src/NadekoBot/Services/Database/NadekoContext.cs b/src/NadekoBot/Services/Database/NadekoContext.cs index 0df96909..7fd511de 100644 --- a/src/NadekoBot/Services/Database/NadekoContext.cs +++ b/src/NadekoBot/Services/Database/NadekoContext.cs @@ -3,22 +3,16 @@ using System.Collections.Generic; using System.Linq; using NadekoBot.Services.Database.Models; using NadekoBot.Extensions; -using Microsoft.EntityFrameworkCore.Infrastructure; using System; +using Microsoft.EntityFrameworkCore.Design; namespace NadekoBot.Services.Database { - - public class NadekoContextFactory : IDbContextFactory + public class NadekoContextFactory : IDesignTimeDbContextFactory { - /// - /// :\ Used for migrations - /// - /// - /// - public NadekoContext Create(DbContextFactoryOptions options) + public NadekoContext CreateDbContext(string[] args) { - var optionsBuilder = new DbContextOptionsBuilder(); + var optionsBuilder = new DbContextOptionsBuilder(); optionsBuilder.UseSqlite("Filename=./data/NadekoBot.db"); var ctx = new NadekoContext(optionsBuilder.Options); ctx.Database.SetCommandTimeout(60); @@ -58,12 +52,7 @@ namespace NadekoBot.Services.Database public DbSet ModulePrefixes { get; set; } public DbSet RewardedUsers { get; set; } - public NadekoContext() : base() - { - - } - - public NadekoContext(DbContextOptions options) : base(options) + public NadekoContext(DbContextOptions options) : base(options) { } @@ -231,7 +220,7 @@ namespace NadekoBot.Services.Database musicPlaylistEntity .HasMany(p => p.Songs) .WithOne() - .OnDelete(Microsoft.EntityFrameworkCore.Metadata.DeleteBehavior.Cascade); + .OnDelete(DeleteBehavior.Cascade); #endregion diff --git a/src/NadekoBot/Services/DbService.cs b/src/NadekoBot/Services/DbService.cs index 5bc9f6bf..277bc33d 100644 --- a/src/NadekoBot/Services/DbService.cs +++ b/src/NadekoBot/Services/DbService.cs @@ -6,19 +6,19 @@ namespace NadekoBot.Services { public class DbService { - private readonly DbContextOptions options; - private readonly DbContextOptions migrateOptions; + private readonly DbContextOptions options; + private readonly DbContextOptions migrateOptions; private readonly string _connectionString; public DbService(IBotCredentials creds) { _connectionString = creds.Db.ConnectionString; - var optionsBuilder = new DbContextOptionsBuilder(); + var optionsBuilder = new DbContextOptionsBuilder(); optionsBuilder.UseSqlite(creds.Db.ConnectionString); options = optionsBuilder.Options; - optionsBuilder = new DbContextOptionsBuilder(); + optionsBuilder = new DbContextOptionsBuilder(); optionsBuilder.UseSqlite(creds.Db.ConnectionString, x => x.SuppressForeignKeyEnforcement()); migrateOptions = optionsBuilder.Options; } diff --git a/src/NadekoBot/Services/LogSetup.cs b/src/NadekoBot/Services/LogSetup.cs index 0d3234ee..9cf85799 100644 --- a/src/NadekoBot/Services/LogSetup.cs +++ b/src/NadekoBot/Services/LogSetup.cs @@ -11,7 +11,7 @@ namespace NadekoBot.Services var logConfig = new LoggingConfiguration(); var consoleTarget = new ColoredConsoleTarget() { - Layout = @"${date:format=HH\:mm\:ss} ${logger} | ${message}" + Layout = @"${date:format=HH\:mm\:ss} ${logger:shortName=True} | ${message}" }; logConfig.AddTarget("Console", consoleTarget); From 9f2d9d6710a2f84eead7fa8e55913ecd512e056b Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Sat, 16 Sep 2017 04:35:02 +0200 Subject: [PATCH 316/346] version upped --- src/NadekoBot/Services/Impl/StatsService.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/NadekoBot/Services/Impl/StatsService.cs b/src/NadekoBot/Services/Impl/StatsService.cs index 9b65c5f7..24bc273e 100644 --- a/src/NadekoBot/Services/Impl/StatsService.cs +++ b/src/NadekoBot/Services/Impl/StatsService.cs @@ -20,7 +20,7 @@ namespace NadekoBot.Services.Impl private readonly IBotCredentials _creds; private readonly DateTime _started; - public const string BotVersion = "1.8.5"; + public const string BotVersion = "1.9.0"; public string Author => "Kwoth#2560"; public string Library => "Discord.Net"; From bdc6974451f88e1bc2334bdc0ba6bd047dcb7f38 Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Sun, 17 Sep 2017 07:28:48 +0200 Subject: [PATCH 317/346] Commands strings are now in data/command_strings.json. Database path no longer has ./ prefix and filename -> Data Source. Fixed permissions migration due to new EF behaviour. Although commands will now error if they don't have their entry in the command strings - needs fixing. --- src/NadekoBot/Common/Attributes/Aliases.cs | 4 +- .../Common/Attributes/Description.cs | 2 +- .../Common/Attributes/NadekoCommand.cs | 2 +- src/NadekoBot/Common/Attributes/Usage.cs | 2 +- src/NadekoBot/Common/CommandData.cs | 9 + src/NadekoBot/Modules/Games/Games.cs | 2 +- .../Services/PermissionsService.cs | 4 +- src/NadekoBot/NadekoBot.csproj | 6 - .../Services/Database/NadekoContext.cs | 8 +- src/NadekoBot/Services/DbService.cs | 15 +- src/NadekoBot/Services/Impl/BotCredentials.cs | 4 +- src/NadekoBot/Services/Impl/Localization.cs | 17 +- src/NadekoBot/credentials_example.json | 2 +- src/NadekoBot/data/command_strings.json | 2057 +++++++++++++++++ 14 files changed, 2107 insertions(+), 27 deletions(-) create mode 100644 src/NadekoBot/Common/CommandData.cs create mode 100644 src/NadekoBot/data/command_strings.json diff --git a/src/NadekoBot/Common/Attributes/Aliases.cs b/src/NadekoBot/Common/Attributes/Aliases.cs index 7f365078..b6ebbf81 100644 --- a/src/NadekoBot/Common/Attributes/Aliases.cs +++ b/src/NadekoBot/Common/Attributes/Aliases.cs @@ -3,11 +3,13 @@ using System.Runtime.CompilerServices; using Discord.Commands; using NadekoBot.Services.Impl; +//todo what if it doesn't exist + namespace NadekoBot.Common.Attributes { public class Aliases : AliasAttribute { - public Aliases([CallerMemberName] string memberName = "") : base(Localization.LoadCommandString(memberName.ToLowerInvariant() + "_cmd").Split(' ').Skip(1).ToArray()) + public Aliases([CallerMemberName] string memberName = "") : base(Localization.LoadCommand(memberName.ToLowerInvariant()).Cmd.Split(' ').Skip(1).ToArray()) { } } diff --git a/src/NadekoBot/Common/Attributes/Description.cs b/src/NadekoBot/Common/Attributes/Description.cs index 1b0e7957..7ebbac47 100644 --- a/src/NadekoBot/Common/Attributes/Description.cs +++ b/src/NadekoBot/Common/Attributes/Description.cs @@ -6,7 +6,7 @@ namespace NadekoBot.Common.Attributes { public class Description : SummaryAttribute { - public Description([CallerMemberName] string memberName="") : base(Localization.LoadCommandString(memberName.ToLowerInvariant() + "_desc")) + public Description([CallerMemberName] string memberName="") : base(Localization.LoadCommand(memberName.ToLowerInvariant()).Desc) { } diff --git a/src/NadekoBot/Common/Attributes/NadekoCommand.cs b/src/NadekoBot/Common/Attributes/NadekoCommand.cs index a471e007..eda997fd 100644 --- a/src/NadekoBot/Common/Attributes/NadekoCommand.cs +++ b/src/NadekoBot/Common/Attributes/NadekoCommand.cs @@ -6,7 +6,7 @@ namespace NadekoBot.Common.Attributes { public class NadekoCommand : CommandAttribute { - public NadekoCommand([CallerMemberName] string memberName="") : base(Localization.LoadCommandString(memberName.ToLowerInvariant() + "_cmd").Split(' ')[0]) + public NadekoCommand([CallerMemberName] string memberName="") : base(Localization.LoadCommand(memberName.ToLowerInvariant()).Cmd) { } diff --git a/src/NadekoBot/Common/Attributes/Usage.cs b/src/NadekoBot/Common/Attributes/Usage.cs index 2991c6aa..97d342ee 100644 --- a/src/NadekoBot/Common/Attributes/Usage.cs +++ b/src/NadekoBot/Common/Attributes/Usage.cs @@ -6,7 +6,7 @@ namespace NadekoBot.Common.Attributes { public class Usage : RemarksAttribute { - public Usage([CallerMemberName] string memberName="") : base(Localization.LoadCommandString(memberName.ToLowerInvariant()+"_usage")) + public Usage([CallerMemberName] string memberName="") : base(Localization.LoadCommand(memberName.ToLowerInvariant()).Usage) { } diff --git a/src/NadekoBot/Common/CommandData.cs b/src/NadekoBot/Common/CommandData.cs new file mode 100644 index 00000000..59712eae --- /dev/null +++ b/src/NadekoBot/Common/CommandData.cs @@ -0,0 +1,9 @@ +namespace NadekoBot.Common +{ + public class CommandData + { + public string Cmd { get; set; } + public string Usage { get; set; } + public string Desc { get; set; } + } +} diff --git a/src/NadekoBot/Modules/Games/Games.cs b/src/NadekoBot/Modules/Games/Games.cs index 4d7a0ea3..07fd450e 100644 --- a/src/NadekoBot/Modules/Games/Games.cs +++ b/src/NadekoBot/Modules/Games/Games.cs @@ -38,7 +38,7 @@ namespace NadekoBot.Modules.Games } [NadekoCommand, Usage, Description, Aliases] - public async Task _8Ball([Remainder] string question = null) + public async Task EightBall([Remainder] string question = null) { if (string.IsNullOrWhiteSpace(question)) return; diff --git a/src/NadekoBot/Modules/Permissions/Services/PermissionsService.cs b/src/NadekoBot/Modules/Permissions/Services/PermissionsService.cs index 4329850c..9b218758 100644 --- a/src/NadekoBot/Modules/Permissions/Services/PermissionsService.cs +++ b/src/NadekoBot/Modules/Permissions/Services/PermissionsService.cs @@ -135,11 +135,11 @@ namespace NadekoBot.Modules.Permissions.Services { var oldPrefixes = new[] { ".", ";", "!!", "!m", "!", "+", "-", "$", ">" }; uow._context.Database.ExecuteSqlCommand( - $@"UPDATE {nameof(Permissionv2)} + @"UPDATE Permissionv2 SET secondaryTargetName=trim(substr(secondaryTargetName, 3)) WHERE secondaryTargetName LIKE '!!%' OR secondaryTargetName LIKE '!m%'; -UPDATE {nameof(Permissionv2)} +UPDATE Permissionv2 SET secondaryTargetName=substr(secondaryTargetName, 2) WHERE secondaryTargetName LIKE '.%' OR secondaryTargetName LIKE '~%' OR diff --git a/src/NadekoBot/NadekoBot.csproj b/src/NadekoBot/NadekoBot.csproj index 3834ef6e..da1a1e22 100644 --- a/src/NadekoBot/NadekoBot.csproj +++ b/src/NadekoBot/NadekoBot.csproj @@ -80,10 +80,4 @@ - - - - Designer - - diff --git a/src/NadekoBot/Services/Database/NadekoContext.cs b/src/NadekoBot/Services/Database/NadekoContext.cs index 7fd511de..5142e8e5 100644 --- a/src/NadekoBot/Services/Database/NadekoContext.cs +++ b/src/NadekoBot/Services/Database/NadekoContext.cs @@ -5,15 +5,19 @@ using NadekoBot.Services.Database.Models; using NadekoBot.Extensions; using System; using Microsoft.EntityFrameworkCore.Design; +using Microsoft.Data.Sqlite; +using System.IO; namespace NadekoBot.Services.Database { public class NadekoContextFactory : IDesignTimeDbContextFactory - { + { public NadekoContext CreateDbContext(string[] args) { var optionsBuilder = new DbContextOptionsBuilder(); - optionsBuilder.UseSqlite("Filename=./data/NadekoBot.db"); + var builder = new SqliteConnectionStringBuilder("Data Source=data/NadekoBot.db"); + builder.DataSource = Path.Combine(AppContext.BaseDirectory, builder.DataSource); + optionsBuilder.UseSqlite(builder.ToString()); var ctx = new NadekoContext(optionsBuilder.Options); ctx.Database.SetCommandTimeout(60); return ctx; diff --git a/src/NadekoBot/Services/DbService.cs b/src/NadekoBot/Services/DbService.cs index 277bc33d..56c6940a 100644 --- a/src/NadekoBot/Services/DbService.cs +++ b/src/NadekoBot/Services/DbService.cs @@ -1,5 +1,8 @@ -using Microsoft.EntityFrameworkCore; +using Microsoft.Data.Sqlite; +using Microsoft.EntityFrameworkCore; using NadekoBot.Services.Database; +using System; +using System.IO; using System.Linq; namespace NadekoBot.Services @@ -9,17 +12,17 @@ namespace NadekoBot.Services private readonly DbContextOptions options; private readonly DbContextOptions migrateOptions; - private readonly string _connectionString; - public DbService(IBotCredentials creds) { - _connectionString = creds.Db.ConnectionString; + var builder = new SqliteConnectionStringBuilder(creds.Db.ConnectionString); + builder.DataSource = Path.Combine(AppContext.BaseDirectory, builder.DataSource); + var optionsBuilder = new DbContextOptionsBuilder(); - optionsBuilder.UseSqlite(creds.Db.ConnectionString); + optionsBuilder.UseSqlite(builder.ToString()); options = optionsBuilder.Options; optionsBuilder = new DbContextOptionsBuilder(); - optionsBuilder.UseSqlite(creds.Db.ConnectionString, x => x.SuppressForeignKeyEnforcement()); + optionsBuilder.UseSqlite(builder.ToString(), x => x.SuppressForeignKeyEnforcement()); migrateOptions = optionsBuilder.Options; } diff --git a/src/NadekoBot/Services/Impl/BotCredentials.cs b/src/NadekoBot/Services/Impl/BotCredentials.cs index e8a96de5..341c947e 100644 --- a/src/NadekoBot/Services/Impl/BotCredentials.cs +++ b/src/NadekoBot/Services/Impl/BotCredentials.cs @@ -101,7 +101,7 @@ namespace NadekoBot.Services.Impl ? "sqlite" : dbSection["Type"], string.IsNullOrWhiteSpace(dbSection["ConnectionString"]) - ? "Filename=./data/NadekoBot.db" + ? "Data Source=data/NadekoBot.db" : dbSection["ConnectionString"]); } catch (Exception ex) @@ -125,7 +125,7 @@ namespace NadekoBot.Services.Impl public string SoundCloudClientId { get; set; } = ""; public string CleverbotApiKey { get; } = ""; public string CarbonKey { get; set; } = ""; - public DBConfig Db { get; set; } = new DBConfig("sqlite", "Filename=./data/NadekoBot.db"); + public DBConfig Db { get; set; } = new DBConfig("sqlite", "Data Source=data/NadekoBot.db"); public int TotalShards { get; set; } = 1; public string PatreonAccessToken { get; set; } = ""; public string PatreonCampaignId { get; set; } = "334038"; diff --git a/src/NadekoBot/Services/Impl/Localization.cs b/src/NadekoBot/Services/Impl/Localization.cs index d781054b..b8a977ec 100644 --- a/src/NadekoBot/Services/Impl/Localization.cs +++ b/src/NadekoBot/Services/Impl/Localization.cs @@ -5,6 +5,9 @@ using System.Linq; using Discord; using NLog; using NadekoBot.Services.Database.Models; +using NadekoBot.Common; +using Newtonsoft.Json; +using System.IO; namespace NadekoBot.Services.Impl { @@ -16,6 +19,14 @@ namespace NadekoBot.Services.Impl public ConcurrentDictionary GuildCultureInfos { get; } public CultureInfo DefaultCultureInfo { get; private set; } = CultureInfo.CurrentCulture; + private static readonly Dictionary _commandData; + + static Localization() + { + _commandData = JsonConvert.DeserializeObject>( + File.ReadAllText("./data/command_strings.json")); + } + private Localization() { } public Localization(IBotConfigProvider bcp, IEnumerable gcs, DbService db) { @@ -117,10 +128,10 @@ namespace NadekoBot.Services.Impl return info ?? DefaultCultureInfo; } - public static string LoadCommandString(string key) + public static CommandData LoadCommand(string key) { - string toReturn = Resources.CommandStrings.ResourceManager.GetString(key); - return string.IsNullOrWhiteSpace(toReturn) ? key : toReturn; + _commandData.TryGetValue(key, out var toReturn); + return toReturn; } } } diff --git a/src/NadekoBot/credentials_example.json b/src/NadekoBot/credentials_example.json index 5260d8c0..5074f8e0 100644 --- a/src/NadekoBot/credentials_example.json +++ b/src/NadekoBot/credentials_example.json @@ -13,7 +13,7 @@ "CarbonKey": "", "Db": { "Type": "sqlite", - "ConnectionString": "Filename=./data/NadekoBot.db" + "ConnectionString": "Data Source=data/NadekoBot.db" }, "TotalShards": 1, "PatreonAccessToken": "", diff --git a/src/NadekoBot/data/command_strings.json b/src/NadekoBot/data/command_strings.json new file mode 100644 index 00000000..d8f0ccd2 --- /dev/null +++ b/src/NadekoBot/data/command_strings.json @@ -0,0 +1,2057 @@ +{ + "h": { + "Cmd": "help h", + "Desc": "Either shows a help for a single command, or DMs you help link if no arguments are specified.", + "Usage": "`{0}h {0}cmds` or `{0}h`" + }, + "hgit": { + "Cmd": "hgit", + "Desc": "Generates the commandlist.md file.", + "Usage": "`{0}hgit`" + }, + "donate": { + "Cmd": "donate", + "Desc": "Instructions for helping the project financially.", + "Usage": "`{0}donate`" + }, + "modules": { + "Cmd": "modules mdls", + "Desc": "Lists all bot modules.", + "Usage": "`{0}modules`" + }, + "commands": { + "Cmd": "commands cmds", + "Desc": "List all of the bot's commands from a certain module. You can either specify the full name or only the first few letters of the module name.", + "Usage": "`{0}commands Administration` or `{0}cmds Admin`" + }, + "greetdel": { + "Cmd": "greetdel grdel", + "Desc": "Sets the time it takes (in seconds) for greet messages to be auto-deleted. Set it to 0 to disable automatic deletion.", + "Usage": "`{0}greetdel 0` or `{0}greetdel 30`" + }, + "greet": { + "Cmd": "greet", + "Desc": "Toggles anouncements on the current channel when someone joins the server.", + "Usage": "`{0}greet`" + }, + "greetmsg": { + "Cmd": "greetmsg", + "Desc": "Sets a new join announcement message which will be shown in the server's channel. Type `%user%` if you want to mention the new member. Using it with no message will show the current greet message. You can use embed json from instead of a regular text, if you want the message to be embedded.", + "Usage": "`{0}greetmsg Welcome, %user%.`" + }, + "bye": { + "Cmd": "bye", + "Desc": "Toggles anouncements on the current channel when someone leaves the server.", + "Usage": "`{0}bye`" + }, + "byemsg": { + "Cmd": "byemsg", + "Desc": "Sets a new leave announcement message. Type `%user%` if you want to show the name the user who left. Type `%id%` to show id. Using this command with no message will show the current bye message. You can use embed json from instead of a regular text, if you want the message to be embedded.", + "Usage": "`{0}byemsg %user% has left.`" + }, + "byedel": { + "Cmd": "byedel", + "Desc": "Sets the time it takes (in seconds) for bye messages to be auto-deleted. Set it to `0` to disable automatic deletion.", + "Usage": "`{0}byedel 0` or `{0}byedel 30`" + }, + "greetdm": { + "Cmd": "greetdm", + "Desc": "Toggles whether the greet messages will be sent in a DM (This is separate from greet - you can have both, any or neither enabled).", + "Usage": "`{0}greetdm`" + }, + "logserver": { + "Cmd": "logserver", + "Desc": "Enables or Disables ALL log events. If enabled, all log events will log to this channel.", + "Usage": "`{0}logserver enable` or `{0}logserver disable`" + }, + "logignore": { + "Cmd": "logignore", + "Desc": "Toggles whether the `.logserver` command ignores this channel. Useful if you have hidden admin channel and public log channel.", + "Usage": "`{0}logignore`" + }, + "userpresence": { + "Cmd": "userpresence", + "Desc": "Starts logging to this channel when someone from the server goes online/offline/idle.", + "Usage": "`{0}userpresence`" + }, + "voicepresence": { + "Cmd": "voicepresence", + "Desc": "Toggles logging to this channel whenever someone joins or leaves a voice channel you are currently in.", + "Usage": "`{0}voicepresence`" + }, + "repeatinvoke": { + "Cmd": "repeatinvoke repinv", + "Desc": "Immediately shows the repeat message on a certain index and restarts its timer.", + "Usage": "`{0}repinv 1`" + }, + "repeat": { + "Cmd": "repeat", + "Desc": "Repeat a message every `X` minutes in the current channel. You can instead specify time of day for the message to be repeated at daily (make sure you've set your server's timezone). You can have up to 5 repeating messages on the server in total.", + "Usage": "`{0}repeat 5 Hello there` or `{0}repeat 17:30 tea time`" + }, + "rotateplaying": { + "Cmd": "rotateplaying ropl", + "Desc": "Toggles rotation of playing status of the dynamic strings you previously specified.", + "Usage": "`{0}ropl`" + }, + "addplaying": { + "Cmd": "addplaying adpl", + "Desc": "Adds a specified string to the list of playing strings to rotate. Supported placeholders: `%servers%`, `%users%`, `%playing%`, `%queued%`, `%time%`, `%shardid%`, `%shardcount%`, `%shardguilds%`.", + "Usage": "`{0}adpl`" + }, + "listplaying": { + "Cmd": "listplaying lipl", + "Desc": "Lists all playing statuses with their corresponding number.", + "Usage": "`{0}lipl`" + }, + "removeplaying": { + "Cmd": "removeplaying rmpl repl", + "Desc": "Removes a playing string on a given number.", + "Usage": "`{0}rmpl`" + }, + "slowmode": { + "Cmd": "slowmode", + "Desc": "Toggles slowmode. Disable by specifying no parameters. To enable, specify a number of messages each user can send, and an interval in seconds. For example 1 message every 5 seconds.", + "Usage": "`{0}slowmode 1 5` or `{0}slowmode`" + }, + "cleanvplust": { + "Cmd": "cleanvplust cv+t", + "Desc": "Deletes all text channels ending in `-voice` for which voicechannels are not found. Use at your own risk.", + "Usage": "`{0}cleanv+t`" + }, + "voiceplustext": { + "Cmd": "voice+text v+t", + "Desc": "Creates a text channel for each voice channel only users in that voice channel can see. If you are server owner, keep in mind you will see them all the time regardless.", + "Usage": "`{0}v+t`" + }, + "scsc": { + "Cmd": "scsc", + "Desc": "Starts an instance of cross server channel. You will get a token as a DM that other people will use to tune in to the same instance.", + "Usage": "`{0}scsc`" + }, + "jcsc": { + "Cmd": "jcsc", + "Desc": "Joins current channel to an instance of cross server channel using the token.", + "Usage": "`{0}jcsc TokenHere`" + }, + "lcsc": { + "Cmd": "lcsc", + "Desc": "Leaves a cross server channel instance from this channel.", + "Usage": "`{0}lcsc`" + }, + "asar": { + "Cmd": "asar", + "Desc": "Adds a role to the list of self-assignable roles.", + "Usage": "`{0}asar Gamer`" + }, + "rsar": { + "Cmd": "rsar", + "Desc": "Removes a specified role from the list of self-assignable roles.", + "Usage": "`{0}rsar`" + }, + "lsar": { + "Cmd": "lsar", + "Desc": "Lists all self-assignable roles.", + "Usage": "`{0}lsar`" + }, + "tesar": { + "Cmd": "togglexclsar tesar", + "Desc": "Toggles whether the self-assigned roles are exclusive. (So that any person can have only one of the self assignable roles)", + "Usage": "`{0}tesar`" + }, + "iam": { + "Cmd": "iam", + "Desc": "Adds a role to you that you choose. Role must be on a list of self-assignable roles.", + "Usage": "`{0}iam Gamer`" + }, + "iamnot": { + "Cmd": "iamnot iamn", + "Desc": "Removes a specified role from you. Role must be on a list of self-assignable roles.", + "Usage": "`{0}iamn Gamer`" + }, + "addcustreact": { + "Cmd": "addcustreact acr", + "Desc": "Add a custom reaction with a trigger and a response. Running this command in server requires the Administration permission. Running this command in DM is Bot Owner only and adds a new global custom reaction. Guide here: ", + "Usage": "`{0}acr \"hello\" Hi there %user%`" + }, + "listcustreact": { + "Cmd": "listcustreact lcr", + "Desc": "Lists global or server custom reactions (20 commands per page). Running the command in DM will list global custom reactions, while running it in server will list that server's custom reactions. Specifying `all` argument instead of the number will DM you a text file with a list of all custom reactions.", + "Usage": "`{0}lcr 1` or `{0}lcr all`" + }, + "listcustreactg": { + "Cmd": "listcustreactg lcrg", + "Desc": "Lists global or server custom reactions (20 commands per page) grouped by trigger, and show a number of responses for each. Running the command in DM will list global custom reactions, while running it in server will list that server's custom reactions.", + "Usage": "`{0}lcrg 1`" + }, + "showcustreact": { + "Cmd": "showcustreact scr", + "Desc": "Shows a custom reaction's response on a given ID.", + "Usage": "`{0}scr 1`" + }, + "delcustreact": { + "Cmd": "delcustreact dcr", + "Desc": "Deletes a custom reaction on a specific index. If ran in DM, it is bot owner only and deletes a global custom reaction. If ran in a server, it requires Administration privileges and removes server custom reaction.", + "Usage": "`{0}dcr 5`" + }, + "autoassignrole": { + "Cmd": "autoassignrole aar", + "Desc": "Automaticaly assigns a specified role to every user who joins the server.", + "Usage": "`{0}aar` to disable, `{0}aar Role Name` to enable" + }, + "leave": { + "Cmd": "leave", + "Desc": "Makes Nadeko leave the server. Either server name or server ID is required.", + "Usage": "`{0}leave 123123123331`" + }, + "delmsgoncmd": { + "Cmd": "delmsgoncmd", + "Desc": "Toggles the automatic deletion of the user's successful command message to prevent chat flood.", + "Usage": "`{0}delmsgoncmd`" + }, + "restart": { + "Cmd": "restart", + "Desc": "Restarts the bot. Might not work.", + "Usage": "`{0}restart`" + }, + "setrole": { + "Cmd": "setrole sr", + "Desc": "Sets a role for a given user.", + "Usage": "`{0}sr @User Guest`" + }, + "removerole": { + "Cmd": "removerole rr", + "Desc": "Removes a role from a given user.", + "Usage": "`{0}rr @User Admin`" + }, + "renamerole": { + "Cmd": "renamerole renr", + "Desc": "Renames a role. The role you are renaming must be lower than bot's highest role.", + "Usage": "`{0}renr \"First role\" SecondRole`" + }, + "removeallroles": { + "Cmd": "removeallroles rar", + "Desc": "Removes all roles from a mentioned user.", + "Usage": "`{0}rar @User`" + }, + "createrole": { + "Cmd": "createrole cr", + "Desc": "Creates a role with a given name.", + "Usage": "`{0}cr Awesome Role`" + }, + "rolecolor": { + "Cmd": "rolecolor roleclr", + "Desc": "Set a role's color to the hex or 0-255 rgb color value provided.", + "Usage": "`{0}roleclr Admin 255 200 100` or `{0}roleclr Admin ffba55`" + }, + "ban": { + "Cmd": "ban b", + "Desc": "Bans a user by ID or name with an optional message.", + "Usage": "`{0}b \"@some Guy\" Your behaviour is toxic.`" + }, + "softban": { + "Cmd": "softban sb", + "Desc": "Bans and then unbans a user by ID or name with an optional message.", + "Usage": "`{0}sb \"@some Guy\" Your behaviour is toxic.`" + }, + "kick": { + "Cmd": "kick k", + "Desc": "Kicks a mentioned user.", + "Usage": "`{0}k \"@some Guy\" Your behaviour is toxic.`" + }, + "mute": { + "Cmd": "mute", + "Desc": "Mutes a mentioned user both from speaking and chatting. You can also specify time in minutes (up to 1440) for how long the user should be muted.", + "Usage": "`{0}mute @Someone` or `{0}mute 30 @Someone`" + }, + "voiceunmute": { + "Cmd": "voiceunmute", + "Desc": "Gives a previously voice-muted user a permission to speak.", + "Usage": "`{0}voiceunmute @Someguy`" + }, + "deafen": { + "Cmd": "deafen deaf", + "Desc": "Deafens mentioned user or users.", + "Usage": "`{0}deaf \"@Someguy\"` or `{0}deaf \"@Someguy\" \"@Someguy\"`" + }, + "undeafen": { + "Cmd": "undeafen undef", + "Desc": "Undeafens mentioned user or users.", + "Usage": "`{0}undef \"@Someguy\"` or `{0}undef \"@Someguy\" \"@Someguy\"`" + }, + "delvoichanl": { + "Cmd": "delvoichanl dvch", + "Desc": "Deletes a voice channel with a given name.", + "Usage": "`{0}dvch VoiceChannelName`" + }, + "creatvoichanl": { + "Cmd": "creatvoichanl cvch", + "Desc": "Creates a new voice channel with a given name.", + "Usage": "`{0}cvch VoiceChannelName`" + }, + "deltxtchanl": { + "Cmd": "deltxtchanl dtch", + "Desc": "Deletes a text channel with a given name.", + "Usage": "`{0}dtch TextChannelName`" + }, + "creatxtchanl": { + "Cmd": "creatxtchanl ctch", + "Desc": "Creates a new text channel with a given name.", + "Usage": "`{0}ctch TextChannelName`" + }, + "settopic": { + "Cmd": "settopic st", + "Desc": "Sets a topic on the current channel.", + "Usage": "`{0}st My new topic`" + }, + "setchanlname": { + "Cmd": "setchanlname schn", + "Desc": "Changes the name of the current channel.", + "Usage": "`{0}schn NewName`" + }, + "prune": { + "Cmd": "prune clear", + "Desc": "`{0}prune` removes all Nadeko's messages in the last 100 messages. `{0}prune X` removes last `X` number of messages from the channel (up to 100). `{0}prune @Someone` removes all Someone's messages in the last 100 messages. `{0}prune @Someone X` removes last `X` number of 'Someone's' messages in the channel.", + "Usage": "`{0}prune` or `{0}prune 5` or `{0}prune @Someone` or `{0}prune @Someone X`" + }, + "die": { + "Cmd": "die", + "Desc": "Shuts the bot down.", + "Usage": "`{0}die`" + }, + "setname": { + "Cmd": "setname newnm", + "Desc": "Gives the bot a new name.", + "Usage": "`{0}newnm BotName`" + }, + "setnick": { + "Cmd": "setnick", + "Desc": "Changes the nickname of the bot on this server. You can also target other users to change their nickname.", + "Usage": "`{0}setnick BotNickname` or `{0}setnick @SomeUser New Nickname`" + }, + "setavatar": { + "Cmd": "setavatar setav", + "Desc": "Sets a new avatar image for the NadekoBot. Argument is a direct link to an image.", + "Usage": "`{0}setav http://i.imgur.com/xTG3a1I.jpg`" + }, + "setgame": { + "Cmd": "setgame", + "Desc": "Sets the bots game.", + "Usage": "`{0}setgame with snakes`" + }, + "send": { + "Cmd": "send", + "Desc": "Sends a message to someone on a different server through the bot. Separate server and channel/user ids with `|` and prefix the channel id with `c:` and the user id with `u:`.", + "Usage": "`{0}send serverid|c:channelid message` or `{0}send serverid|u:userid message`" + }, + "mentionrole": { + "Cmd": "mentionrole menro", + "Desc": "Mentions every person from the provided role or roles (separated by a ',') on this server.", + "Usage": "`{0}menro RoleName`" + }, + "unstuck": { + "Cmd": "unstuck", + "Desc": "Clears the message queue.", + "Usage": "`{0}unstuck`" + }, + "donators": { + "Cmd": "donators", + "Desc": "List of the lovely people who donated to keep this project alive.", + "Usage": "`{0}donators`" + }, + "donadd": { + "Cmd": "donadd", + "Desc": "Add a donator to the database.", + "Usage": "`{0}donadd Donate Amount`" + }, + "savechat": { + "Cmd": "savechat", + "Desc": "Saves a number of messages to a text file and sends it to you.", + "Usage": "`{0}savechat 150`" + }, + "remind": { + "Cmd": "remind", + "Desc": "Sends a message to you or a channel after certain amount of time. First argument is `me`/`here`/'channelname'. Second argument is time in a descending order (mo>w>d>h>m) example: 1w5d3h10m. Third argument is a (multiword) message.", + "Usage": "`{0}remind me 1d5h Do something` or `{0}remind #general 1m Start now!`" + }, + "remindtemplate": { + "Cmd": "remindtemplate", + "Desc": "Sets message for when the remind is triggered. Available placeholders are `%user%` - user who ran the command, `%message%` - Message specified in the remind, `%target%` - target channel of the remind.", + "Usage": "`{0}remindtemplate %user%, do %message%!`" + }, + "serverinfo": { + "Cmd": "serverinfo sinfo", + "Desc": "Shows info about the server the bot is on. If no server is supplied, it defaults to current one.", + "Usage": "`{0}sinfo Some Server`" + }, + "channelinfo": { + "Cmd": "channelinfo cinfo", + "Desc": "Shows info about the channel. If no channel is supplied, it defaults to current one.", + "Usage": "`{0}cinfo #some-channel`" + }, + "userinfo": { + "Cmd": "userinfo uinfo", + "Desc": "Shows info about the user. If no user is supplied, it defaults a user running the command.", + "Usage": "`{0}uinfo @SomeUser`" + }, + "whosplaying": { + "Cmd": "whosplaying whpl", + "Desc": "Shows a list of users who are playing the specified game.", + "Usage": "`{0}whpl Overwatch`" + }, + "inrole": { + "Cmd": "inrole", + "Desc": "Lists every person from the specified role on this server. You can use role ID, role name.", + "Usage": "`{0}inrole Some Role`" + }, + "checkmyperms": { + "Cmd": "checkmyperms", + "Desc": "Checks your user-specific permissions on this channel.", + "Usage": "`{0}checkmyperms`" + }, + "stats": { + "Cmd": "stats", + "Desc": "Shows some basic stats for Nadeko.", + "Usage": "`{0}stats`" + }, + "userid": { + "Cmd": "userid uid", + "Desc": "Shows user ID.", + "Usage": "`{0}uid` or `{0}uid @SomeGuy`" + }, + "channelid": { + "Cmd": "channelid cid", + "Desc": "Shows current channel ID.", + "Usage": "`{0}cid`" + }, + "serverid": { + "Cmd": "serverid sid", + "Desc": "Shows current server ID.", + "Usage": "`{0}sid`" + }, + "roles": { + "Cmd": "roles", + "Desc": "List roles on this server or a roles of a specific user if specified. Paginated, 20 roles per page.", + "Usage": "`{0}roles 2` or `{0}roles @Someone`" + }, + "channeltopic": { + "Cmd": "channeltopic ct", + "Desc": "Sends current channel's topic as a message.", + "Usage": "`{0}ct`" + }, + "chnlfilterinv": { + "Cmd": "chnlfilterinv cfi", + "Desc": "Toggles automatic deletion of invites posted in the channel. Does not negate the `{0}srvrfilterinv` enabled setting. Does not affect the Bot Owner.", + "Usage": "`{0}cfi`" + }, + "srvrfilterinv": { + "Cmd": "srvrfilterinv sfi", + "Desc": "Toggles automatic deletion of invites posted in the server. Does not affect the Bot Owner.", + "Usage": "`{0}sfi`" + }, + "chnlfilterwords": { + "Cmd": "chnlfilterwords cfw", + "Desc": "Toggles automatic deletion of messages containing filtered words on the channel. Does not negate the `{0}srvrfilterwords` enabled setting. Does not affect the Bot Owner.", + "Usage": "`{0}cfw`" + }, + "filterword": { + "Cmd": "fw", + "Desc": "Adds or removes (if it exists) a word from the list of filtered words. Use`{0}sfw` or `{0}cfw` to toggle filtering.", + "Usage": "`{0}fw poop`" + }, + "lstfilterwords": { + "Cmd": "lstfilterwords lfw", + "Desc": "Shows a list of filtered words.", + "Usage": "`{0}lfw`" + }, + "srvrfilterwords": { + "Cmd": "srvrfilterwords sfw", + "Desc": "Toggles automatic deletion of messages containing filtered words on the server. Does not affect the Bot Owner.", + "Usage": "`{0}sfw`" + }, + "permrole": { + "Cmd": "permrole pr", + "Desc": "Sets a role which can change permissions. Supply no parameters to see the current one. Default is 'Nadeko'.", + "Usage": "`{0}pr role`" + }, + "verbose": { + "Cmd": "verbose v", + "Desc": "Sets whether to show when a command/module is blocked.", + "Usage": "`{0}verbose true`" + }, + "srvrmdl": { + "Cmd": "srvrmdl sm", + "Desc": "Sets a module's permission at the server level.", + "Usage": "`{0}sm ModuleName enable`" + }, + "srvrcmd": { + "Cmd": "srvrcmd sc", + "Desc": "Sets a command's permission at the server level.", + "Usage": "`{0}sc \"command name\" disable`" + }, + "rolemdl": { + "Cmd": "rolemdl rm", + "Desc": "Sets a module's permission at the role level.", + "Usage": "`{0}rm ModuleName enable MyRole`" + }, + "rolecmd": { + "Cmd": "rolecmd rc", + "Desc": "Sets a command's permission at the role level.", + "Usage": "`{0}rc \"command name\" disable MyRole`" + }, + "chnlmdl": { + "Cmd": "chnlmdl cm", + "Desc": "Sets a module's permission at the channel level.", + "Usage": "`{0}cm ModuleName enable SomeChannel`" + }, + "chnlcmd": { + "Cmd": "chnlcmd cc", + "Desc": "Sets a command's permission at the channel level.", + "Usage": "`{0}cc \"command name\" enable SomeChannel`" + }, + "usrmdl": { + "Cmd": "usrmdl um", + "Desc": "Sets a module's permission at the user level.", + "Usage": "`{0}um ModuleName enable SomeUsername`" + }, + "usrcmd": { + "Cmd": "usrcmd uc", + "Desc": "Sets a command's permission at the user level.", + "Usage": "`{0}uc \"command name\" enable SomeUsername`" + }, + "allsrvrmdls": { + "Cmd": "allsrvrmdls asm", + "Desc": "Enable or disable all modules for your server.", + "Usage": "`{0}asm [enable/disable]`" + }, + "allchnlmdls": { + "Cmd": "allchnlmdls acm", + "Desc": "Enable or disable all modules in a specified channel.", + "Usage": "`{0}acm enable #SomeChannel`" + }, + "allrolemdls": { + "Cmd": "allrolemdls arm", + "Desc": "Enable or disable all modules for a specific role.", + "Usage": "`{0}arm [enable/disable] MyRole`" + }, + "userblacklist": { + "Cmd": "ubl", + "Desc": "Either [add]s or [rem]oves a user specified by a Mention or an ID from a blacklist.", + "Usage": "`{0}ubl add @SomeUser` or `{0}ubl rem 12312312313`" + }, + "channelblacklist": { + "Cmd": "cbl", + "Desc": "Either [add]s or [rem]oves a channel specified by an ID from a blacklist.", + "Usage": "`{0}cbl rem 12312312312`" + }, + "serverblacklist": { + "Cmd": "sbl", + "Desc": "Either [add]s or [rem]oves a server specified by a Name or an ID from a blacklist.", + "Usage": "`{0}sbl add 12312321312` or `{0}sbl rem SomeTrashServer`" + }, + "cmdcooldown": { + "Cmd": "cmdcooldown cmdcd", + "Desc": "Sets a cooldown per user for a command. Set it to 0 to remove the cooldown.", + "Usage": "`{0}cmdcd \"some cmd\" 5`" + }, + "allcmdcooldowns": { + "Cmd": "allcmdcooldowns acmdcds", + "Desc": "Shows a list of all commands and their respective cooldowns.", + "Usage": "`{0}acmdcds`" + }, + "addquote": { + "Cmd": ".", + "Desc": "Adds a new quote with the specified name and message.", + "Usage": "`{0}. sayhi Hi`" + }, + "showquote": { + "Cmd": "..", + "Desc": "Shows a random quote with a specified name.", + "Usage": "`{0}.. abc`" + }, + "quotesearch": { + "Cmd": "qsearch", + "Desc": "Shows a random quote for a keyword that contains any text specified in the search.", + "Usage": "`{0}qsearch keyword text`" + }, + "quoteid": { + "Cmd": "quoteid qid", + "Desc": "Displays the quote with the specified ID number. Quote ID numbers can be found by typing `.liqu [num]` where `[num]` is a number of a page which contains 15 quotes.", + "Usage": "`{0}qid 123456`" + }, + "quotedelete": { + "Cmd": "quotedel qdel", + "Desc": "Deletes a quote with the specified ID. You have to be either server Administrator or the creator of the quote to delete it.", + "Usage": "`{0}qdel 123456`" + }, + "draw": { + "Cmd": "draw", + "Desc": "Draws a card from this server's deck. You can draw up to 10 cards by supplying a number of cards to draw.", + "Usage": "`{0}draw` or `{0}draw 5`" + }, + "drawnew": { + "Cmd": "drawnew", + "Desc": "Draws a card from the NEW deck of cards. You can draw up to 10 cards by supplying a number of cards to draw.", + "Usage": "`{0}drawnew` or `{0}drawnew 5`" + }, + "shuffleplaylist": { + "Cmd": "shuffle sh plsh", + "Desc": "Shuffles the current playlist.", + "Usage": "`{0}plsh`" + }, + "flip": { + "Cmd": "flip", + "Desc": "Flips coin(s) - heads or tails, and shows an image.", + "Usage": "`{0}flip` or `{0}flip 3`" + }, + "betflip": { + "Cmd": "betflip bf", + "Desc": "Bet to guess will the result be heads or tails. Guessing awards you 1.95x the currency you've bet (rounded up). Multiplier can be changed by the bot owner.", + "Usage": "`{0}bf 5 heads` or `{0}bf 3 t`" + }, + "roll": { + "Cmd": "roll", + "Desc": "Rolls 0-100. If you supply a number `X` it rolls up to 30 normal dice. If you split 2 numbers with letter `d` (`xdy`) it will roll `X` dice from 1 to `y`. `Y` can be a letter 'F' if you want to roll fate dice instead of dnd.", + "Usage": "`{0}roll` or `{0}roll 7` or `{0}roll 3d5` or `{0}roll 5dF`" + }, + "rolluo": { + "Cmd": "rolluo", + "Desc": "Rolls `X` normal dice (up to 30) unordered. If you split 2 numbers with letter `d` (`xdy`) it will roll `X` dice from 1 to `y`.", + "Usage": "`{0}rolluo` or `{0}rolluo 7` or `{0}rolluo 3d5`" + }, + "nroll": { + "Cmd": "nroll", + "Desc": "Rolls in a given range.", + "Usage": "`{0}nroll 5` (rolls 0-5) or `{0}nroll 5-15`" + }, + "race": { + "Cmd": "race", + "Desc": "Starts a new animal race.", + "Usage": "`{0}race`" + }, + "joinrace": { + "Cmd": "joinrace jr", + "Desc": "Joins a new race. You can specify an amount of currency for betting (optional). You will get YourBet*(participants-1) back if you win.", + "Usage": "`{0}jr` or `{0}jr 5`" + }, + "nunchi": { + "Cmd": "nunchi", + "Desc": "Creates or joins an existing nunchi game. Users have to count up by 1 from the starting number shown by the bot. If someone makes a mistake (types an incorrent number, or repeats the same number) they are out of the game and a new round starts without them. Minimum 3 users required.", + "Usage": "`{0}nunchi`" + }, + "connect4": { + "Cmd": "connect4 con4", + "Desc": "Creates or joins an existing connect4 game. 2 players are required for the game. Objective of the game is to get 4 of your pieces next to each other in a vertical, horizontal or diagonal line.", + "Usage": "`{0}connect4`" + }, + "raffle": { + "Cmd": "raffle", + "Desc": "Prints a name and ID of a random user from the online list from the (optional) role.", + "Usage": "`{0}raffle` or `{0}raffle RoleName`" + }, + "give": { + "Cmd": "give", + "Desc": "Give someone a certain amount of currency.", + "Usage": "`{0}give 1 @SomeGuy`" + }, + "award": { + "Cmd": "award", + "Desc": "Awards someone a certain amount of currency. You can also specify a role name to award currency to all users in a role.", + "Usage": "`{0}award 100 @person` or `{0}award 5 Role Of Gamblers`" + }, + "take": { + "Cmd": "take", + "Desc": "Takes a certain amount of currency from someone.", + "Usage": "`{0}take 1 @SomeGuy`" + }, + "betroll": { + "Cmd": "betroll br", + "Desc": "Bets a certain amount of currency and rolls a dice. Rolling over 66 yields x2 of your currency, over 90 - x4 and 100 x10.", + "Usage": "`{0}br 5`" + }, + "wheeloffortune": { + "Cmd": "wheeloffortune wheel", + "Desc": "Bets a certain amount of currency on the wheel of fortune. Wheel can stop on one of many different multipliers. Won amount is rounded down to the nearest whole number.", + "Usage": "`{0}wheel 10`" + }, + "leaderboard": { + "Cmd": "leaderboard lb", + "Desc": "Displays the bot's currency leaderboard.", + "Usage": "`{0}lb`" + }, + "trivia": { + "Cmd": "trivia t", + "Desc": "Starts a game of trivia. You can add `nohint` to prevent hints. First player to get to 10 points wins by default. You can specify a different number. 30 seconds per question.", + "Usage": "`{0}t` or `{0}t 5 nohint`" + }, + "tl": { + "Cmd": "tl", + "Desc": "Shows a current trivia leaderboard.", + "Usage": "`{0}tl`" + }, + "tq": { + "Cmd": "tq", + "Desc": "Quits current trivia after current question.", + "Usage": "`{0}tq`" + }, + "typestart": { + "Cmd": "typestart", + "Desc": "Starts a typing contest.", + "Usage": "`{0}typestart`" + }, + "typestop": { + "Cmd": "typestop", + "Desc": "Stops a typing contest on the current channel.", + "Usage": "`{0}typestop`" + }, + "typeadd": { + "Cmd": "typeadd", + "Desc": "Adds a new article to the typing contest.", + "Usage": "`{0}typeadd wordswords`" + }, + "pollend": { + "Cmd": "pollend", + "Desc": "Stops active poll on this server and prints the results in this channel.", + "Usage": "`{0}pollend`" + }, + "pick": { + "Cmd": "pick", + "Desc": "Picks the currency planted in this channel. 60 seconds cooldown.", + "Usage": "`{0}pick`" + }, + "plant": { + "Cmd": "plant", + "Desc": "Spend an amount of currency to plant it in this channel. Default is 1. (If bot is restarted or crashes, the currency will be lost)", + "Usage": "`{0}plant` or `{0}plant 5`" + }, + "gencurrency": { + "Cmd": "gencurrency gc", + "Desc": "Toggles currency generation on this channel. Every posted message will have chance to spawn currency. Chance is specified by the Bot Owner. (default is 2%)", + "Usage": "`{0}gc`" + }, + "leet": { + "Cmd": "leet", + "Desc": "Converts a text to leetspeak with 6 (1-6) severity levels", + "Usage": "`{0}leet 3 Hello`" + }, + "choose": { + "Cmd": "choose", + "Desc": "Chooses a thing from a list of things", + "Usage": "`{0}choose Get up;Sleep;Sleep more`" + }, + "": { + "Cmd": null, + "Desc": null, + "Usage": null + }, + "rps": { + "Cmd": "rps", + "Desc": "Play a game of Rocket-Paperclip-Scissors with Nadeko.", + "Usage": "`{0}rps scissors`" + }, + "linux": { + "Cmd": "linux", + "Desc": "Prints a customizable Linux interjection", + "Usage": "`{0}linux Spyware Windows`" + }, + "next": { + "Cmd": "next n", + "Desc": "Goes to the next song in the queue. You have to be in the same voice channel as the bot. You can skip multiple songs, but in that case songs will not be requeued if {0}rcs or {0}rpl is enabled.", + "Usage": "`{0}n` or `{0}n 5`" + }, + "play": { + "Cmd": "play start", + "Desc": "If no arguments are specified, acts as `{0}next 1` command. If you specify a song number, it will jump to that song. If you specify a search query, acts as a `{0}q` command", + "Usage": "`{0}play` or `{0}play 5` or `{0}play Dream Of Venice`" + }, + "stop": { + "Cmd": "stop s", + "Desc": "Stops the music and preserves the current song index. Stays in the channel.", + "Usage": "`{0}s`" + }, + "destroy": { + "Cmd": "destroy d", + "Desc": "Completely stops the music and unbinds the bot from the channel. (may cause weird behaviour)", + "Usage": "`{0}d`" + }, + "pause": { + "Cmd": "pause p", + "Desc": "Pauses or Unpauses the song.", + "Usage": "`{0}p`" + }, + "queue": { + "Cmd": "queue q yq", + "Desc": "Queue a song using keywords or a link. Bot will join your voice channel. **You must be in a voice channel**.", + "Usage": "`{0}q Dream Of Venice`" + }, + "queuenext": { + "Cmd": "queuenext qn", + "Desc": "Works the same as `{0}queue` command, except it enqueues the new song after the current one. **You must be in a voice channel**.", + "Usage": "`{0}qn Dream Of Venice`" + }, + "queuesearch": { + "Cmd": "queuesearch qs yqs", + "Desc": "Search for top 5 youtube song result using keywords, and type the index of the song to play that song. Bot will join your voice channel. **You must be in a voice channel**.", + "Usage": "`{0}qs Dream Of Venice`" + }, + "soundcloudqueue": { + "Cmd": "soundcloudqueue sq", + "Desc": "Queue a soundcloud song using keywords. Bot will join your voice channel. **You must be in a voice channel**.", + "Usage": "`{0}sq Dream Of Venice`" + }, + "listqueue": { + "Cmd": "listqueue lq", + "Desc": "Lists 10 currently queued songs per page. Default page is 1.", + "Usage": "`{0}lq` or `{0}lq 2`" + }, + "nowplaying": { + "Cmd": "nowplaying np", + "Desc": "Shows the song that the bot is currently playing.", + "Usage": "`{0}np`" + }, + "volume": { + "Cmd": "volume vol", + "Desc": "Sets the music playback volume (0-100%)", + "Usage": "`{0}vol 50`" + }, + "defvol": { + "Cmd": "defvol dv", + "Desc": "Sets the default music volume when music playback is started (0-100). Persists through restarts.", + "Usage": "`{0}dv 80`" + }, + "max": { + "Cmd": "max", + "Desc": "Sets the music playback volume to 100%.", + "Usage": "`{0}max`" + }, + "half": { + "Cmd": "half", + "Desc": "Sets the music playback volume to 50%.", + "Usage": "`{0}half`" + }, + "playlist": { + "Cmd": "playlist pl", + "Desc": "Queues up to 500 songs from a youtube playlist specified by a link, or keywords.", + "Usage": "`{0}pl playlist link or name`" + }, + "soundcloudpl": { + "Cmd": "soundcloudpl scpl", + "Desc": "Queue a Soundcloud playlist using a link.", + "Usage": "`{0}scpl soundcloudseturl`" + }, + "localpl": { + "Cmd": "localplaylst lopl", + "Desc": "Queues all songs from a directory.", + "Usage": "`{0}lopl C:/music/classical`" + }, + "radio": { + "Cmd": "radio ra", + "Desc": "Queues a radio stream from a link. It can be a direct mp3 radio stream, .m3u, .pls .asx or .xspf (Usage Video: )", + "Usage": "`{0}ra radio link here`" + }, + "local": { + "Cmd": "local lo", + "Desc": "Queues a local file by specifying a full path.", + "Usage": "`{0}lo C:/music/mysong.mp3`" + }, + "move": { + "Cmd": "move mv", + "Desc": "Moves the bot to your voice channel. (works only if music is already playing)", + "Usage": "`{0}mv`" + }, + "songremove": { + "Cmd": "songremove srm", + "Desc": "Remove a song by its # in the queue, or 'all' to remove all songs from the queue and reset the song index.", + "Usage": "`{0}srm 5`" + }, + "movesong": { + "Cmd": "movesong ms", + "Desc": "Moves a song from one position to another.", + "Usage": "`{0}ms 5>3`" + }, + "setmaxqueue": { + "Cmd": "setmaxqueue smq", + "Desc": "Sets a maximum queue size. Supply 0 or no argument to have no limit.", + "Usage": "`{0}smq 50` or `{0}smq`" + }, + "cleanup": { + "Cmd": "cleanup", + "Desc": "Cleans up hanging voice connections.", + "Usage": "`{0}cleanup`" + }, + "reptcursong": { + "Cmd": "reptcursong rcs", + "Desc": "Toggles repeat of current song.", + "Usage": "`{0}rcs`" + }, + "repeatpl": { + "Cmd": "rpeatplaylst rpl", + "Desc": "Toggles repeat of all songs in the queue (every song that finishes is added to the end of the queue).", + "Usage": "`{0}rpl`" + }, + "save": { + "Cmd": "save", + "Desc": "Saves a playlist under a certain name. Playlist name must be no longer than 20 characters and must not contain dashes.", + "Usage": "`{0}save classical1`" + }, + "streamrole": { + "Cmd": "streamrole", + "Desc": "Sets a role which is monitored for streamers (FromRole), and a role to add if a user from 'FromRole' is streaming (AddRole). When a user from 'FromRole' starts streaming, they will receive an 'AddRole'. Provide no arguments to disable", + "Usage": "`{0}streamrole \"Eligible Streamers\" \"Featured Streams\"`" + }, + "load": { + "Cmd": "load", + "Desc": "Loads a saved playlist using its ID. Use `{0}pls` to list all saved playlists and `{0}save` to save new ones.", + "Usage": "`{0}load 5`" + }, + "playlists": { + "Cmd": "playlists pls", + "Desc": "Lists all playlists. Paginated, 20 per page. Default page is 0.", + "Usage": "`{0}pls 1`" + }, + "deleteplaylist": { + "Cmd": "deleteplaylist delpls", + "Desc": "Deletes a saved playlist. Works only if you made it or if you are the bot owner.", + "Usage": "`{0}delpls animu-5`" + }, + "goto": { + "Cmd": "goto", + "Desc": "Goes to a specific time in seconds in a song.", + "Usage": "`{0}goto 30`" + }, + "autoplay": { + "Cmd": "autoplay ap", + "Desc": "Toggles autoplay - When the song is finished, automatically queue a related Youtube song. (Works only for Youtube songs and when queue is empty)", + "Usage": "`{0}ap`" + }, + "lolchamp": { + "Cmd": "lolchamp", + "Desc": "Shows League Of Legends champion statistics. If there are spaces/apostrophes or in the name - omit them. Optional second parameter is a role.", + "Usage": "`{0}lolchamp Riven` or `{0}lolchamp Annie sup`" + }, + "lolban": { + "Cmd": "lolban", + "Desc": "Shows top banned champions ordered by ban rate.", + "Usage": "`{0}lolban`" + }, + "smashcast": { + "Cmd": "smashcast hb", + "Desc": "Notifies this channel when a certain user starts streaming.", + "Usage": "`{0}smashcast SomeStreamer`" + }, + "twitch": { + "Cmd": "twitch tw", + "Desc": "Notifies this channel when a certain user starts streaming.", + "Usage": "`{0}twitch SomeStreamer`" + }, + "mixer": { + "Cmd": "mixer bm", + "Desc": "Notifies this channel when a certain user starts streaming.", + "Usage": "`{0}mixer SomeStreamer`" + }, + "removestream": { + "Cmd": "removestream rms", + "Desc": "Removes notifications of a certain streamer from a certain platform on this channel.", + "Usage": "`{0}rms Twitch SomeGuy` or `{0}rms mixer SomeOtherGuy`" + }, + "liststreams": { + "Cmd": "liststreams ls", + "Desc": "Lists all streams you are following on this server.", + "Usage": "`{0}ls`" + }, + "convert": { + "Cmd": "convert", + "Desc": "Convert quantities. Use `{0}convertlist` to see supported dimensions and currencies.", + "Usage": "`{0}convert m km 1000`" + }, + "convertlist": { + "Cmd": "convertlist", + "Desc": "List of the convertible dimensions and currencies.", + "Usage": "`{0}convertlist`" + }, + "wowjoke": { + "Cmd": "wowjoke", + "Desc": "Get one of Kwoth's penultimate WoW jokes.", + "Usage": "`{0}wowjoke`" + }, + "calculate": { + "Cmd": "calculate calc", + "Desc": "Evaluate a mathematical expression.", + "Usage": "`{0}calc 1+1`" + }, + "osu": { + "Cmd": "osu", + "Desc": "Shows osu stats for a player.", + "Usage": "`{0}osu Name` or `{0}osu Name taiko`" + }, + "osub": { + "Cmd": "osub", + "Desc": "Shows information about an osu beatmap.", + "Usage": "`{0}osub https://osu.ppy.sh/s/127712`" + }, + "osu5": { + "Cmd": "osu5", + "Desc": "Displays a user's top 5 plays.", + "Usage": "`{0}osu5 Name`" + }, + "pokemon": { + "Cmd": "pokemon poke", + "Desc": "Searches for a pokemon.", + "Usage": "`{0}poke Sylveon`" + }, + "pokemonability": { + "Cmd": "pokemonability pokeab", + "Desc": "Searches for a pokemon ability.", + "Usage": "`{0}pokeab overgrow`" + }, + "memelist": { + "Cmd": "memelist", + "Desc": "Pulls a list of memes you can use with `{0}memegen` from http://memegen.link/templates/", + "Usage": "`{0}memelist`" + }, + "memegen": { + "Cmd": "memegen", + "Desc": "Generates a meme from memelist with top and bottom text.", + "Usage": "`{0}memegen biw \"gets iced coffee\" \"in the winter\"`" + }, + "weather": { + "Cmd": "weather we", + "Desc": "Shows weather data for a specified city. You can also specify a country after a comma.", + "Usage": "`{0}we Moscow, RU`" + }, + "youtube": { + "Cmd": "youtube yt", + "Desc": "Searches youtubes and shows the first result", + "Usage": "`{0}yt query`" + }, + "anime": { + "Cmd": "anime ani aq", + "Desc": "Queries anilist for an anime and shows the first result.", + "Usage": "`{0}ani aquarion evol`" + }, + "imdb": { + "Cmd": "imdb omdb", + "Desc": "Queries omdb for movies or series, show first result.", + "Usage": "`{0}imdb Batman vs Superman`" + }, + "manga": { + "Cmd": "manga mang mq", + "Desc": "Queries anilist for a manga and shows the first result.", + "Usage": "`{0}mq Shingeki no kyojin`" + }, + "randomcat": { + "Cmd": "randomcat meow", + "Desc": "Shows a random cat image.", + "Usage": "`{0}meow`" + }, + "randomdog": { + "Cmd": "randomdog woof", + "Desc": "Shows a random dog image.", + "Usage": "`{0}woof`" + }, + "image": { + "Cmd": "image img", + "Desc": "Pulls the first image found using a search parameter. Use `{0}rimg` for different results.", + "Usage": "`{0}img cute kitten`" + }, + "randomimage": { + "Cmd": "randomimage rimg", + "Desc": "Pulls a random image using a search parameter.", + "Usage": "`{0}rimg cute kitten`" + }, + "lmgtfy": { + "Cmd": "lmgtfy", + "Desc": "Google something for an idiot.", + "Usage": "`{0}lmgtfy query`" + }, + "google": { + "Cmd": "google g", + "Desc": "Get a Google search link for some terms.", + "Usage": "`{0}google query`" + }, + "hearthstone": { + "Cmd": "hearthstone hs", + "Desc": "Searches for a Hearthstone card and shows its image. Takes a while to complete.", + "Usage": "`{0}hs Ysera`" + }, + "urbandict": { + "Cmd": "urbandict ud", + "Desc": "Searches Urban Dictionary for a word.", + "Usage": "`{0}ud Pineapple`" + }, + "hashtag": { + "Cmd": "#", + "Desc": "Searches Tagdef.com for a hashtag.", + "Usage": "`{0}# ff`" + }, + "catfact": { + "Cmd": "catfact", + "Desc": "Shows a random catfact from ", + "Usage": "`{0}catfact`" + }, + "yomama": { + "Cmd": "yomama ym", + "Desc": "Shows a random joke from ", + "Usage": "`{0}ym`" + }, + "randjoke": { + "Cmd": "randjoke rj", + "Desc": "Shows a random joke from ", + "Usage": "`{0}rj`" + }, + "chucknorris": { + "Cmd": "chucknorris cn", + "Desc": "Shows a random Chuck Norris joke from ", + "Usage": "`{0}cn`" + }, + "magicitem": { + "Cmd": "magicitem mi", + "Desc": "Shows a random magic item from ", + "Usage": "`{0}mi`" + }, + "revav": { + "Cmd": "revav", + "Desc": "Returns a Google reverse image search for someone's avatar.", + "Usage": "`{0}revav @SomeGuy`" + }, + "revimg": { + "Cmd": "revimg", + "Desc": "Returns a Google reverse image search for an image from a link.", + "Usage": "`{0}revimg Image link`" + }, + "safebooru": { + "Cmd": "safebooru", + "Desc": "Shows a random image from safebooru with a given tag. Tag is optional but preferred. (multiple tags are appended with +)", + "Usage": "`{0}safebooru yuri+kissing`" + }, + "wiki": { + "Cmd": "wikipedia wiki", + "Desc": "Gives you back a wikipedia link", + "Usage": "`{0}wiki query`" + }, + "color": { + "Cmd": "color", + "Desc": "Shows you what color corresponds to that hex.", + "Usage": "`{0}color 00ff00`" + }, + "videocall": { + "Cmd": "videocall", + "Desc": "Creates a private video call link for you and other mentioned people. The link is sent to mentioned people via a private message.", + "Usage": "`{0}videocall \"@the First\" \"@Xyz\"`" + }, + "avatar": { + "Cmd": "avatar av", + "Desc": "Shows a mentioned person's avatar.", + "Usage": "`{0}av @SomeGuy`" + }, + "hentai": { + "Cmd": "hentai", + "Desc": "Shows a hentai image from a random website (gelbooru or danbooru or konachan or atfbooru or yandere) with a given tag. Tag is optional but preferred. Only 1 tag allowed.", + "Usage": "`{0}hentai yuri`" + }, + "danbooru": { + "Cmd": "danbooru", + "Desc": "Shows a random hentai image from danbooru with a given tag. Tag is optional but preferred. (multiple tags are appended with +)", + "Usage": "`{0}danbooru yuri+kissing`" + }, + "atfbooru": { + "Cmd": "atfbooru atf", + "Desc": "Shows a random hentai image from atfbooru with a given tag. Tag is optional but preferred.", + "Usage": "`{0}atfbooru yuri+kissing`" + }, + "gelbooru": { + "Cmd": "gelbooru", + "Desc": "Shows a random hentai image from gelbooru with a given tag. Tag is optional but preferred. (multiple tags are appended with +)", + "Usage": "`{0}gelbooru yuri+kissing`" + }, + "rule34": { + "Cmd": "rule34", + "Desc": "Shows a random image from rule34.xx with a given tag. Tag is optional but preferred. (multiple tags are appended with +)", + "Usage": "`{0}rule34 yuri+kissing`" + }, + "e621": { + "Cmd": "e621", + "Desc": "Shows a random hentai image from e621.net with a given tag. Tag is optional but preferred. Use spaces for multiple tags.", + "Usage": "`{0}e621 yuri kissing`" + }, + "boobs": { + "Cmd": "boobs", + "Desc": "Real adult content.", + "Usage": "`{0}boobs`" + }, + "butts": { + "Cmd": "butts ass butt", + "Desc": "Real adult content.", + "Usage": "`{0}butts` or `{0}ass`" + }, + "translate": { + "Cmd": "translate trans", + "Desc": "Translates from>to text. From the given language to the destination language.", + "Usage": "`{0}trans en>fr Hello`" + }, + "translangs": { + "Cmd": "translangs", + "Desc": "Lists the valid languages for translation.", + "Usage": "`{0}translangs`" + }, + "guide": { + "Cmd": "readme guide", + "Desc": "Sends a readme and a guide links to the channel.", + "Usage": "`{0}readme` or `{0}guide`" + }, + "calcops": { + "Cmd": "calcops", + "Desc": "Shows all available operations in the `{0}calc` command", + "Usage": "`{0}calcops`" + }, + "delallquotes": { + "Cmd": "delallq daq", + "Desc": "Deletes all quotes on a specified keyword.", + "Usage": "`{0}delallq kek`" + }, + "greetdmmsg": { + "Cmd": "greetdmmsg", + "Desc": "Sets a new join announcement message which will be sent to the user who joined. Type `%user%` if you want to mention the new member. Using it with no message will show the current DM greet message. You can use embed json from instead of a regular text, if you want the message to be embedded.", + "Usage": "`{0}greetdmmsg Welcome to the server, %user%`." + }, + "cash": { + "Cmd": "$ currency $$ $$$ cash cur", + "Desc": "Check how much currency a person has. (Defaults to yourself)", + "Usage": "`{0}$` or `{0}$ @SomeGuy`" + }, + "listperms": { + "Cmd": "listperms lp", + "Desc": "Lists whole permission chain with their indexes. You can specify an optional page number if there are a lot of permissions.", + "Usage": "`{0}lp` or `{0}lp 3`" + }, + "allusrmdls": { + "Cmd": "allusrmdls aum", + "Desc": "Enable or disable all modules for a specific user.", + "Usage": "`{0}aum enable @someone`" + }, + "moveperm": { + "Cmd": "moveperm mp", + "Desc": "Moves permission from one position to another in the Permissions list.", + "Usage": "`{0}mp 2 4`" + }, + "removeperm": { + "Cmd": "removeperm rp", + "Desc": "Removes a permission from a given position in the Permissions list.", + "Usage": "`{0}rp 1`" + }, + "migratedata": { + "Cmd": "migratedata", + "Desc": "Migrate data from old bot configuration", + "Usage": "`{0}migratedata`" + }, + "checkstream": { + "Cmd": "checkstream cs", + "Desc": "Checks if a user is online on a certain streaming platform.", + "Usage": "`{0}cs twitch MyFavStreamer`" + }, + "showemojis": { + "Cmd": "showemojis se", + "Desc": "Shows a name and a link to every SPECIAL emoji in the message.", + "Usage": "`{0}se A message full of SPECIAL emojis`" + }, + "deckshuffle": { + "Cmd": "deckshuffle dsh", + "Desc": "Reshuffles all cards back into the deck.", + "Usage": "`{0}dsh`" + }, + "forwardmessages": { + "Cmd": "fwmsgs", + "Desc": "Toggles forwarding of non-command messages sent to bot's DM to the bot owners", + "Usage": "`{0}fwmsgs`" + }, + "forwardtoall": { + "Cmd": "fwtoall", + "Desc": "Toggles whether messages will be forwarded to all bot owners or only to the first one specified in the credentials.json file", + "Usage": "`{0}fwtoall`" + }, + "resetpermissions": { + "Cmd": "resetperms", + "Desc": "Resets the bot's permissions module on this server to the default value.", + "Usage": "`{0}resetperms`" + }, + "antiraid": { + "Cmd": "antiraid", + "Desc": "Sets an anti-raid protection on the server. First argument is number of people which will trigger the protection. Second one is a time interval in which that number of people needs to join in order to trigger the protection, and third argument is punishment for those people (Kick, Ban, Mute)", + "Usage": "`{0}antiraid 5 20 Kick`" + }, + "antispam": { + "Cmd": "antispam", + "Desc": "Stops people from repeating same message X times in a row. You can specify to either mute, kick or ban the offenders. Max message count is 10.", + "Usage": "`{0}antispam 3 Mute` or `{0}antispam 4 Kick` or `{0}antispam 6 Ban`" + }, + "chatmute": { + "Cmd": "chatmute", + "Desc": "Prevents a mentioned user from chatting in text channels.", + "Usage": "`{0}chatmute @Someone`" + }, + "voicemute": { + "Cmd": "voicemute", + "Desc": "Prevents a mentioned user from speaking in voice channels.", + "Usage": "`{0}voicemute @Someone`" + }, + "konachan": { + "Cmd": "konachan", + "Desc": "Shows a random hentai image from konachan with a given tag. Tag is optional but preferred.", + "Usage": "`{0}konachan yuri`" + }, + "setmuterole": { + "Cmd": "setmuterole", + "Desc": "Sets a name of the role which will be assigned to people who should be muted. Default is nadeko-mute.", + "Usage": "`{0}setmuterole Silenced`" + }, + "adsarm": { + "Cmd": "adsarm", + "Desc": "Toggles the automatic deletion of confirmations for `{0}iam` and `{0}iamn` commands.", + "Usage": "`{0}adsarm`" + }, + "setstream": { + "Cmd": "setstream", + "Desc": "Sets the bots stream. First argument is the twitch link, second argument is stream name.", + "Usage": "`{0}setstream TWITCHLINK Hello`" + }, + "chatunmute": { + "Cmd": "chatunmute", + "Desc": "Removes a mute role previously set on a mentioned user with `{0}chatmute` which prevented him from chatting in text channels.", + "Usage": "`{0}chatunmute @Someone`" + }, + "unmute": { + "Cmd": "unmute", + "Desc": "Unmutes a mentioned user previously muted with `{0}mute` command.", + "Usage": "`{0}unmute @Someone`" + }, + "xkcd": { + "Cmd": "xkcd", + "Desc": "Shows a XKCD comic. No arguments will retrieve random one. Number argument will retrieve a specific comic, and \"latest\" will get the latest one.", + "Usage": "`{0}xkcd` or `{0}xkcd 1400` or `{0}xkcd latest`" + }, + "placelist": { + "Cmd": "placelist", + "Desc": "Shows the list of available tags for the `{0}place` command.", + "Usage": "`{0}placelist`" + }, + "place": { + "Cmd": "place", + "Desc": "Shows a placeholder image of a given tag. Use `{0}placelist` to see all available tags. You can specify the width and height of the image as the last two optional arguments.", + "Usage": "`{0}place Cage` or `{0}place steven 500 400`" + }, + "togethertube": { + "Cmd": "togethertube totube", + "Desc": "Creates a new room on and shows the link in the chat.", + "Usage": "`{0}totube`" + }, + "poll": { + "Cmd": "poll ppoll", + "Desc": "Creates a public poll which requires users to type a number of the voting option in the channel command is ran in.", + "Usage": "`{0}ppoll Question?;Answer1;Answ 2;A_3`" + }, + "autotranslang": { + "Cmd": "autotranslang atl", + "Desc": "Sets your source and target language to be used with `{0}at`. Specify no arguments to remove previously set value.", + "Usage": "`{0}atl en>fr`" + }, + "autotranslate": { + "Cmd": "autotrans at", + "Desc": "Starts automatic translation of all messages by users who set their `{0}atl` in this channel. You can set \"del\" argument to automatically delete all translated user messages.", + "Usage": "`{0}at` or `{0}at del`" + }, + "listquotes": { + "Cmd": "listquotes liqu", + "Desc": "Lists all quotes on the server ordered alphabetically. 15 Per page.", + "Usage": "`{0}liqu` or `{0}liqu 3`" + }, + "typedel": { + "Cmd": "typedel", + "Desc": "Deletes a typing article given the ID.", + "Usage": "`{0}typedel 3`" + }, + "typelist": { + "Cmd": "typelist", + "Desc": "Lists added typing articles with their IDs. 15 per page.", + "Usage": "`{0}typelist` or `{0}typelist 3`" + }, + "listservers": { + "Cmd": "listservers", + "Desc": "Lists servers the bot is on with some basic info. 15 per page.", + "Usage": "`{0}listservers 3`" + }, + "hentaibomb": { + "Cmd": "hentaibomb", + "Desc": "Shows a total 5 images (from gelbooru, danbooru, konachan, yandere and atfbooru). Tag is optional but preferred.", + "Usage": "`{0}hentaibomb yuri`" + }, + "cleverbot": { + "Cmd": "cleverbot", + "Desc": "Toggles cleverbot session. When enabled, the bot will reply to messages starting with bot mention in the server. Custom reactions starting with %mention% won't work if cleverbot is enabled.", + "Usage": "`{0}cleverbot`" + }, + "shorten": { + "Cmd": "shorten", + "Desc": "Attempts to shorten an URL, if it fails, returns the input URL.", + "Usage": "`{0}shorten https://google.com`" + }, + "mcping": { + "Cmd": "minecraftping mcping", + "Desc": "Pings a minecraft server.", + "Usage": "`{0}mcping 127.0.0.1:25565`" + }, + "mcq": { + "Cmd": "minecraftquery mcq", + "Desc": "Finds information about a minecraft server.", + "Usage": "`{0}mcq server:ip`" + }, + "wikia": { + "Cmd": "wikia", + "Desc": "Gives you back a wikia link", + "Usage": "`{0}wikia mtg Vigilance` or `{0}wikia mlp Dashy`" + }, + "yandere": { + "Cmd": "yandere", + "Desc": "Shows a random image from yandere with a given tag. Tag is optional but preferred. (multiple tags are appended with +)", + "Usage": "`{0}yandere tag1+tag2`" + }, + "magicthegathering": { + "Cmd": "magicthegathering mtg", + "Desc": "Searches for a Magic The Gathering card.", + "Usage": "`{0}magicthegathering about face` or `{0}mtg about face`" + }, + "yodify": { + "Cmd": "yodify yoda", + "Desc": "Translates your normal sentences into Yoda styled sentences!", + "Usage": "`{0}yoda my feelings hurt`" + }, + "attack": { + "Cmd": "attack", + "Desc": "Attacks a target with the given move. Use `{0}movelist` to see a list of moves your type can use.", + "Usage": "`{0}attack \"vine whip\" @someguy`" + }, + "heal": { + "Cmd": "heal", + "Desc": "Heals someone. Revives those who fainted. Costs a NadekoFlower. ", + "Usage": "`{0}heal @someone`" + }, + "movelist": { + "Cmd": "movelist ml", + "Desc": "Lists the moves you are able to use", + "Usage": "`{0}ml`" + }, + "settype": { + "Cmd": "settype", + "Desc": "Set your poketype. Costs a NadekoFlower. Provide no arguments to see a list of available types.", + "Usage": "`{0}settype fire` or `{0}settype`" + }, + "type": { + "Cmd": "type", + "Desc": "Get the poketype of the target.", + "Usage": "`{0}type @someone`" + }, + "hangmanlist": { + "Cmd": "hangmanlist", + "Desc": "Shows a list of hangman term types.", + "Usage": "`{0}hangmanlist`" + }, + "hangman": { + "Cmd": "hangman", + "Desc": "Starts a game of hangman in the channel. Use `{0}hangmanlist` to see a list of available term types. Defaults to 'all'.", + "Usage": "`{0}hangman` or `{0}hangman movies`" + }, + "hangmanstop": { + "Cmd": "hangmanstop", + "Desc": "Stops the active hangman game on this channel if it exists.", + "Usage": "`{0}hangmanstop`" + }, + "crstatsclear": { + "Cmd": "crstatsclear", + "Desc": "Resets the counters on `{0}crstats`. You can specify a trigger to clear stats only for that trigger.", + "Usage": "`{0}crstatsclear` or `{0}crstatsclear rng`" + }, + "crstats": { + "Cmd": "crstats", + "Desc": "Shows a list of custom reactions and the number of times they have been executed. Paginated with 10 per page. Use `{0}crstatsclear` to reset the counters.", + "Usage": "`{0}crstats` or `{0}crstats 3`" + }, + "overwatch": { + "Cmd": "overwatch ow", + "Desc": "Show's basic stats on a player (competitive rank, playtime, level etc) Region codes are: `eu` `us` `cn` `kr`", + "Usage": "`{0}ow us Battletag#1337` or `{0}overwatch eu Battletag#2016`" + }, + "acro": { + "Cmd": "acrophobia acro", + "Desc": "Starts an Acrophobia game. Second argument is optional round length in seconds. (default is 60)", + "Usage": "`{0}acro` or `{0}acro 30`" + }, + "logevents": { + "Cmd": "logevents", + "Desc": "Shows a list of all events you can subscribe to with `{0}log`", + "Usage": "`{0}logevents`" + }, + "log": { + "Cmd": "log", + "Desc": "Toggles logging event. Disables it if it is active anywhere on the server. Enables if it isn't active. Use `{0}logevents` to see a list of all events you can subscribe to.", + "Usage": "`{0}log userpresence` or `{0}log userbanned`" + }, + "fairplay": { + "Cmd": "fairplay fp", + "Desc": "Toggles fairplay. While enabled, the bot will prioritize songs from users who didn't have their song recently played instead of the song's position in the queue.", + "Usage": "`{0}fp`" + }, + "songautodelete": { + "Cmd": "songautodelete sad", + "Desc": "Toggles whether the song should be automatically removed from the music queue when it finishes playing.", + "Usage": "`{0}sad`" + }, + "define": { + "Cmd": "define def", + "Desc": "Finds a definition of a word.", + "Usage": "`{0}def heresy`" + }, + "setmaxplaytime": { + "Cmd": "setmaxplaytime smp", + "Desc": "Sets a maximum number of seconds (>14) a song can run before being skipped automatically. Set 0 to have no limit.", + "Usage": "`{0}smp 0` or `{0}smp 270`" + }, + "activity": { + "Cmd": "activity", + "Desc": "Checks for spammers.", + "Usage": "`{0}activity`" + }, + "autohentai": { + "Cmd": "autohentai", + "Desc": "Posts a hentai every X seconds with a random tag from the provided tags. Use `|` to separate tags. 20 seconds minimum. Provide no arguments to disable.", + "Usage": "`{0}autohentai 30 yuri|tail|long_hair` or `{0}autohentai`" + }, + "setstatus": { + "Cmd": "setstatus", + "Desc": "Sets the bot's status. (Online/Idle/Dnd/Invisible)", + "Usage": "`{0}setstatus Idle`" + }, + "rotaterolecolor": { + "Cmd": "rotaterolecolor rrc", + "Desc": "Rotates a roles color on an interval with a list of supplied colors. First argument is interval in seconds (Minimum 60). Second argument is a role, followed by a space-separated list of colors in hex. Provide a rolename with a 0 interval to disable.", + "Usage": "`{0}rrc 60 MyLsdRole #ff0000 #00ff00 #0000ff` or `{0}rrc 0 MyLsdRole`" + }, + "createinvite": { + "Cmd": "createinvite crinv", + "Desc": "Creates a new invite which has infinite max uses and never expires.", + "Usage": "`{0}crinv`" + }, + "pollstats": { + "Cmd": "pollstats", + "Desc": "Shows the poll results without stopping the poll on this server.", + "Usage": "`{0}pollstats`" + }, + "repeatlist": { + "Cmd": "repeatlist replst", + "Desc": "Shows currently repeating messages and their indexes.", + "Usage": "`{0}repeatlist`" + }, + "repeatremove": { + "Cmd": "repeatremove reprm", + "Desc": "Removes a repeating message on a specified index. Use `{0}repeatlist` to see indexes.", + "Usage": "`{0}reprm 2`" + }, + "antilist": { + "Cmd": "antilist antilst", + "Desc": "Shows currently enabled protection features.", + "Usage": "`{0}antilist`" + }, + "antispamignore": { + "Cmd": "antispamignore", + "Desc": "Toggles whether antispam ignores current channel. Antispam must be enabled.", + "Usage": "`{0}antispamignore`" + }, + "cmdcosts": { + "Cmd": "cmdcosts", + "Desc": "Shows a list of command costs. Paginated with 9 commands per page.", + "Usage": "`{0}cmdcosts` or `{0}cmdcosts 2`" + }, + "commandcost": { + "Cmd": "commandcost cmdcost", + "Desc": "Sets a price for a command. Running that command will take currency from users. Set 0 to remove the price.", + "Usage": "`{0}cmdcost 0 !!q` or `{0}cmdcost 1 {0}8ball`" + }, + "startevent": { + "Cmd": "startevent", + "Desc": "Starts one of the events seen on public nadeko.", + "Usage": "`{0}startevent flowerreaction`" + }, + "slotstats": { + "Cmd": "slotstats", + "Desc": "Shows the total stats of the slot command for this bot's session.", + "Usage": "`{0}slotstats`" + }, + "slottest": { + "Cmd": "slottest", + "Desc": "Tests to see how much slots payout for X number of plays.", + "Usage": "`{0}slottest 1000`" + }, + "slot": { + "Cmd": "slot", + "Desc": "Play Nadeko slots. Max bet is 9999. 1.5 second cooldown per user.", + "Usage": "`{0}slot 5`" + }, + "waifuclaimeraffinity": { + "Cmd": "affinity", + "Desc": "Sets your affinity towards someone you want to be claimed by. Setting affinity will reduce their `{0}claim` on you by 20%. You can leave second argument empty to clear your affinity. 30 minutes cooldown.", + "Usage": "`{0}affinity @MyHusband` or `{0}affinity`" + }, + "waifuclaim": { + "Cmd": "claimwaifu claim", + "Desc": "Claim a waifu for yourself by spending currency. You must spend at least 10% more than her current value unless she set `{0}affinity` towards you.", + "Usage": "`{0}claim 50 @Himesama`" + }, + "waifugift": { + "Cmd": "waifugift gift gifts", + "Desc": "Gift an item to someone. This will increase their waifu value by 50% of the gifted item's value if they don't have affinity set towards you, or 100% if they do. Provide no arguments to see a list of items that you can gift.", + "Usage": "`{0}gifts` or `{0}gift Rose @Himesama`" + }, + "waifuleaderboard": { + "Cmd": "waifus waifulb", + "Desc": "Shows top 9 waifus. You can specify another page to show other waifus.", + "Usage": "`{0}waifus` or `{0}waifulb 3`" + }, + "divorce": { + "Cmd": "divorce", + "Desc": "Releases your claim on a specific waifu. You will get some of the money you've spent back unless that waifu has an affinity towards you. 6 hours cooldown.", + "Usage": "`{0}divorce @CheatingSloot`" + }, + "waifuinfo": { + "Cmd": "waifuinfo waifustats", + "Desc": "Shows waifu stats for a target person. Defaults to you if no user is provided.", + "Usage": "`{0}waifuinfo @MyCrush` or `{0}waifuinfo`" + }, + "mal": { + "Cmd": "mal", + "Desc": "Shows basic info from a MyAnimeList profile.", + "Usage": "`{0}mal straysocks`" + }, + "setmusicchannel": { + "Cmd": "setmusicchannel smch", + "Desc": "Sets the current channel as the default music output channel. This will output playing, finished, paused and removed songs to that channel instead of the channel where the first song was queued in.", + "Usage": "`{0}smch`" + }, + "reloadimages": { + "Cmd": "reloadimages", + "Desc": "Reloads images bot is using. Safe to use even when bot is being used heavily.", + "Usage": "`{0}reloadimages`" + }, + "shardstats": { + "Cmd": "shardstats", + "Desc": "Stats for shards. Paginated with 25 shards per page.", + "Usage": "`{0}shardstats` or `{0}shardstats 2`" + }, + "restartshard": { + "Cmd": "restartshard", + "Desc": "Try (re)connecting a shard with a certain shardid when it dies. No one knows will it work. Keep an eye on the console for errors.", + "Usage": "`{0}restartshard 2`" + }, + "shardid": { + "Cmd": "shardid", + "Desc": "Shows which shard is a certain guild on, by guildid.", + "Usage": "`{0}shardid 117523346618318850`" + }, + "tictactoe": { + "Cmd": "tictactoe ttt", + "Desc": "Starts a game of tic tac toe. Another user must run the command in the same channel in order to accept the challenge. Use numbers 1-9 to play. 15 seconds per move.", + "Usage": "{0}ttt" + }, + "timezones": { + "Cmd": "timezones", + "Desc": "Lists all timezones available on the system to be used with `{0}timezone`.", + "Usage": "`{0}timezones`" + }, + "timezone": { + "Cmd": "timezone", + "Desc": "Sets this guilds timezone. This affects bot's time output in this server (logs, etc..)", + "Usage": "`{0}timezone` or `{0}timezone GMT Standard Time`" + }, + "languagesetdefault": { + "Cmd": "langsetdefault langsetd", + "Desc": "Sets the bot's default response language. All servers which use a default locale will use this one. Setting to `default` will use the host's current culture. Provide no arguments to see currently set language.", + "Usage": "`{0}langsetd en-US` or `{0}langsetd default`" + }, + "languageset": { + "Cmd": "languageset langset", + "Desc": "Sets this server's response language. If bot's response strings have been translated to that language, bot will use that language in this server. Reset by using `default` as the locale name. Provide no arguments to see currently set language.", + "Usage": "`{0}langset de-DE ` or `{0}langset default`" + }, + "languageslist": { + "Cmd": "languageslist langli", + "Desc": "List of languages for which translation (or part of it) exist atm.", + "Usage": "`{0}langli`" + }, + "rategirl": { + "Cmd": "rategirl", + "Desc": "Use the universal hot-crazy wife zone matrix to determine the girl's worth. It is everything young men need to know about women. At any moment in time, any woman you have previously located on this chart can vanish from that location and appear anywhere else on the chart.", + "Usage": "`{0}rategirl @SomeGurl`" + }, + "lucky7test": { + "Cmd": "lucky7test l7t", + "Desc": "Tests the l7 command.", + "Usage": "`{0}l7t 10000`" + }, + "lucky7": { + "Cmd": "lucky7 l7", + "Desc": "Bet currency on the game and start rolling 3 sided dice. At any point you can choose to [m]ove (roll again) or [s]tay (get the amount bet times the current multiplier).", + "Usage": "`{0}l7 10` or `{0}l7 move` or `{0}l7 s`" + }, + "vcrolelist": { + "Cmd": "vcrolelist", + "Desc": "Shows a list of currently set voice channel roles.", + "Usage": "`{0}vcrolelist`" + }, + "vcrole": { + "Cmd": "vcrole", + "Desc": "Sets or resets a role which will be given to users who join the voice channel you're in when you run this command. Provide no role name to disable. You must be in a voice channel to run this command.", + "Usage": "`{0}vcrole SomeRole` or `{0}vcrole`" + }, + "crad": { + "Cmd": "crad", + "Desc": "Toggles whether the message triggering the custom reaction will be automatically deleted.", + "Usage": "`{0}crad 59`" + }, + "crdm": { + "Cmd": "crdm", + "Desc": "Toggles whether the response message of the custom reaction will be sent as a direct message.", + "Usage": "`{0}crdm 44`" + }, + "crca": { + "Cmd": "crca", + "Desc": "Toggles whether the custom reaction will trigger if the triggering message contains the keyword (instead of only starting with it).", + "Usage": "`{0}crca 44`" + }, + "aliaslist": { + "Cmd": "aliaslist cmdmaplist aliases", + "Desc": "Shows the list of currently set aliases. Paginated.", + "Usage": "`{0}aliaslist` or `{0}aliaslist 3`" + }, + "alias": { + "Cmd": "alias cmdmap", + "Desc": "Create a custom alias for a certain Nadeko command. Provide no alias to remove the existing one.", + "Usage": "`{0}alias allin $bf 100 h` or `{0}alias \"linux thingy\" >loonix Spyware Windows`" + }, + "warnlog": { + "Cmd": "warnlog", + "Desc": "See a list of warnings of a certain user.", + "Usage": "`{0}warnlog @b1nzy`" + }, + "warnlogall": { + "Cmd": "warnlogall", + "Desc": "See a list of all warnings on the server. 15 users per page.", + "Usage": "`{0}warnlogall` or `{0}warnlogall 2`" + }, + "warn": { + "Cmd": "warn", + "Desc": "Warns a user.", + "Usage": "`{0}warn @b1nzy Very rude person`" + }, + "startupcommandadd": { + "Cmd": "scadd", + "Desc": "Adds a command to the list of commands which will be executed automatically in the current channel, in the order they were added in, by the bot when it startups up.", + "Usage": "`{0}scadd .stats`" + }, + "startupcommandremove": { + "Cmd": "scrm", + "Desc": "Removes a startup command with the provided command text.", + "Usage": "`{0}scrm .stats`" + }, + "startupcommandsclear": { + "Cmd": "scclr", + "Desc": "Removes all startup commands.", + "Usage": "`{0}scclr`" + }, + "startupcommands": { + "Cmd": "sclist", + "Desc": "Lists all startup commands in the order they will be executed in.", + "Usage": "`{0}sclist`" + }, + "unban": { + "Cmd": "unban", + "Desc": "Unbans a user with the provided user#discrim or id.", + "Usage": "`{0}unban kwoth#1234` or `{0}unban 123123123`" + }, + "wait": { + "Cmd": "wait", + "Desc": "Used only as a startup command. Waits a certain number of miliseconds before continuing the execution of the following startup commands.", + "Usage": "`{0}wait 3000`" + }, + "warnclear": { + "Cmd": "warnclear warnc", + "Desc": "Clears all warnings from a certain user.", + "Usage": "`{0}warnclear @PoorDude`" + }, + "warnpunishlist": { + "Cmd": "warnpunishlist warnpl", + "Desc": "Lists punishments for warnings.", + "Usage": "`{0}warnpunishlist`" + }, + "warnpunish": { + "Cmd": "warnpunish warnp", + "Desc": "Sets a punishment for a certain number of warnings. Provide no punishment to remove.", + "Usage": "`{0}warnpunish 5 Ban` or `{0}warnpunish 3`" + }, + "claimpatreonrewards": { + "Cmd": "clparew", + "Desc": "Claim patreon rewards. If you're subscribed to bot owner's patreon you can use this command to claim your rewards - assuming bot owner did setup has their patreon key.", + "Usage": "`{0}clparew`" + }, + "ping": { + "Cmd": "ping", + "Desc": "Ping the bot to see if there are latency issues.", + "Usage": "`{0}ping`" + }, + "slowmodewhitelist": { + "Cmd": "slowmodewl", + "Desc": "Ignores a role or a user from the slowmode feature.", + "Usage": "`{0}slowmodewl SomeRole` or `{0}slowmodewl AdminDude`" + }, + "time": { + "Cmd": "time", + "Desc": "Shows the current time and timezone in the specified location.", + "Usage": "`{0}time London, UK`" + }, + "patreonrewardsreload": { + "Cmd": "parewrel", + "Desc": "Forces the update of the list of patrons who are eligible for the reward.", + "Usage": "`{0}parewrel`" + }, + "shopadd": { + "Cmd": "shopadd", + "Desc": "Adds an item to the shop by specifying type price and name. Available types are role and list.", + "Usage": "`{0}shopadd role 1000 Rich`" + }, + "shopremove": { + "Cmd": "shoprem shoprm", + "Desc": "Removes an item from the shop by its ID.", + "Usage": "`{0}shoprm 1`" + }, + "shop": { + "Cmd": "shop", + "Desc": "Lists this server's administrators' shop. Paginated.", + "Usage": "`{0}shop` or `{0}shop 2`" + }, + "rolehoist": { + "Cmd": "rolehoist rh", + "Desc": "Toggles whether this role is displayed in the sidebar or not.", + "Usage": "`{0}rh Guests` or `{0}rh \"Space Wizards\"`" + }, + "buy": { + "Cmd": "buy", + "Desc": "Buys an item from the shop on a given index. If buying items, make sure that the bot can DM you.", + "Usage": "`{0}buy 2`" + }, + "gamevoicechannel": { + "Cmd": "gvc", + "Desc": "Toggles game voice channel feature in the voice channel you're currently in. Users who join the game voice channel will get automatically redirected to the voice channel with the name of their current game, if it exists. Can't move users to channels that the bot has no connect permission for. One per server.", + "Usage": "`{0}gvc`" + }, + "shoplistadd": { + "Cmd": "shoplistadd", + "Desc": "Adds an item to the list of items for sale in the shop entry given the index. You usually want to run this command in the secret channel, so that the unique items are not leaked.", + "Usage": "`{0}shoplistadd 1 Uni-que-Steam-Key`" + }, + "gcmd": { + "Cmd": "globalcommand gcmd", + "Desc": "Toggles whether a command can be used on any server.", + "Usage": "`{0}gcmd .stats`" + }, + "gmod": { + "Cmd": "globalmodule gmod", + "Desc": "Toggles whether a module can be used on any server.", + "Usage": "`{0}gmod nsfw`" + }, + "lgp": { + "Cmd": "listglobalperms lgp", + "Desc": "Lists global permissions set by the bot owner.", + "Usage": "`{0}lgp`" + }, + "resetglobalpermissions": { + "Cmd": "resetglobalperms", + "Desc": "Resets global permissions set by bot owner.", + "Usage": "`{0}resetglobalperms`" + }, + "prefix": { + "Cmd": "prefix", + "Desc": "Sets this server's prefix for all bot commands. Provide no arguments to see the current server prefix.", + "Usage": "`{0}prefix +`" + }, + "defprefix": { + "Cmd": "defprefix", + "Desc": "Sets bot's default prefix for all bot commands. Provide no arguments to see the current default prefix. This will not change this server's current prefix.", + "Usage": "`{0}defprefix +`" + }, + "verboseerror": { + "Cmd": "verboseerror ve", + "Desc": "Toggles whether the bot should print command errors when a command is incorrectly used.", + "Usage": "`{0}ve`" + }, + "streamrolekeyword": { + "Cmd": "streamrolekw srkw", + "Desc": "Sets keyword which is required in the stream's title in order for the streamrole to apply. Provide no keyword in order to reset.", + "Usage": "`{0}srkw` or `{0}srkw PUBG`" + }, + "streamroleblacklist": { + "Cmd": "streamrolebl srbl", + "Desc": "Adds or removes a blacklisted user. Blacklisted users will never receive the stream role.", + "Usage": "`{0}srbl add @b1nzy#1234` or `{0}srbl rem @b1nzy#1234`" + }, + "streamrolewhitelist": { + "Cmd": "streamrolewl srwl", + "Desc": "Adds or removes a whitelisted user. Whitelisted users will receive the stream role even if they don't have the specified keyword in their stream title.", + "Usage": "`{0}srwl add @b1nzy#1234` or `{0}srwl rem @b1nzy#1234`" + }, + "botconfigedit": { + "Cmd": "botconfigedit bce", + "Desc": "Sets one of available bot config settings to a specified value. Use the command without any parameters to get a list of available settings.", + "Usage": "`{0}bce CurrencyName b1nzy` or `{0}bce`" + }, + "nsfwtagblacklist": { + "Cmd": "nsfwtagbl nsfwtbl", + "Desc": "Toggles whether the tag is blacklisted or not in nsfw searches. Provide no parameters to see the list of blacklisted tags.", + "Usage": "`{0}nsfwtbl poop`" + }, + "experience": { + "Cmd": "experience xp", + "Desc": "Shows your xp stats. Specify the user to show that user's stats instead.", + "Usage": "`{0}xp`" + }, + "xpexclusionlist": { + "Cmd": "xpexclusionlist xpexl", + "Desc": "Shows the roles and channels excluded from the XP system on this server, as well as whether the whole server is excluded.", + "Usage": "`{0}xpexl`" + }, + "xpexclude": { + "Cmd": "xpexclude xpex", + "Desc": "Exclude a channel, role or current server from the xp system.", + "Usage": "`{0}xpex Role Excluded-Role` `{0}xpex Server`" + }, + "xpnotify": { + "Cmd": "xpnotify xpn", + "Desc": "Sets how the bot should notify you when you get a `server` or `global` level. You can set `dm` (for the bot to send a direct message), `channel` (to get notified in the channel you sent the last message in) or `none` to disable.", + "Usage": "`{0}xpn global dm` `{0}xpn server channel`" + }, + "xprolerewards": { + "Cmd": "xprolerewards xprrs", + "Desc": "Shows currently set role rewards.", + "Usage": "`{0}xprrs`" + }, + "xprolereward": { + "Cmd": "xprolereward xprr", + "Desc": "Sets a role reward on a specified level. Provide no role name in order to remove the role reward.", + "Usage": "`{0}xprr 3 Social`" + }, + "xpleaderboard": { + "Cmd": "xpleaderboard xplb", + "Desc": "Shows current server's xp leaderboard.", + "Usage": "`{0}xplb`" + }, + "xpgloballeaderboard": { + "Cmd": "xpgleaderboard xpglb", + "Desc": "Shows the global xp leaderboard.", + "Usage": "`{0}xpglb`" + }, + "xpadd": { + "Cmd": "xpadd", + "Desc": "Adds xp to a user on the server. This does not affect their global ranking. You can use negative values.", + "Usage": "`{0}xpadd 100 @b1nzy`" + }, + "clubcreate": { + "Cmd": "clubcreate", + "Desc": "Creates a club. You must be atleast level 5 and not be in the club already.", + "Usage": "`{0}clubcreate b1nzy's friends`" + }, + "clubinformation": { + "Cmd": "clubinfo", + "Desc": "Shows information about the club.", + "Usage": "`{0}clubinfo b1nzy's friends#123`" + }, + "clubapply": { + "Cmd": "clubapply", + "Desc": "Apply to join a club. You must meet that club's minimum level requirement, and not be on its ban list.", + "Usage": "`{0}clubapply b1nzy's friends#123`" + }, + "clubaccept": { + "Cmd": "clubaccept", + "Desc": "Accept a user who applied to your club.", + "Usage": "`{0}clubaccept b1nzy#1337`" + }, + "clubleave": { + "Cmd": "clubleave", + "Desc": "Leaves the club you're currently in.", + "Usage": "`{0}clubleave`" + }, + "clubdisband": { + "Cmd": "clubdisband", + "Desc": "Disbands the club you're the owner of. This action is irreversible.", + "Usage": "`{0}clubdisband`" + }, + "clubkick": { + "Cmd": "clubkick", + "Desc": "Kicks the user from the club. You must be the club owner. They will be able to apply again.", + "Usage": "`{0}clubkick b1nzy#1337`" + }, + "clubban": { + "Cmd": "clubban", + "Desc": "Bans the user from the club. You must be the club owner. They will not be able to apply again.", + "Usage": "`{0}clubban b1nzy#1337`" + }, + "clubunban": { + "Cmd": "clubunban", + "Desc": "Unbans the previously banned user from the club. You must be the club owner.", + "Usage": "`{0}clubunban b1nzy#1337`" + }, + "clublevelreq": { + "Cmd": "clublevelreq", + "Desc": "Sets the club required level to apply to join the club. You must be club owner. You can't set this number below 5.", + "Usage": "`{0}clublevelreq 7`" + }, + "clubicon": { + "Cmd": "clubicon", + "Desc": "Sets the club icon.", + "Usage": "`{0}clubicon https://i.imgur.com/htfDMfU.png`" + }, + "clubapps": { + "Cmd": "clubapps", + "Desc": "Shows the list of users who have applied to your club. Paginated. You must be club owner to use this command.", + "Usage": "`{0}clubapps 2`" + }, + "clubbans": { + "Cmd": "clubbans", + "Desc": "Shows the list of users who have banned from your club. Paginated. You must be club owner to use this command.", + "Usage": "`{0}clubbans 2`" + }, + "clubleaderboard": { + "Cmd": "clublb", + "Desc": "Shows club rankings on the specified page.", + "Usage": "`{0}clublb 2`" + }, + "nsfwclearcache": { + "Cmd": "nsfwcc", + "Desc": "Clears nsfw cache.", + "Usage": "`{0}nsfwcc`" + }, + "clubadmin": { + "Cmd": "clubadmin", + "Desc": "Assigns (or unassigns) staff role to the member of the club. Admins can ban, kick and accept applications.", + "Usage": "`{0}clubadmin`" + }, + "autoboobs": { + "Cmd": "autoboobs", + "Desc": "Posts a boobs every X seconds. 20 seconds minimum. Provide no arguments to disable.", + "Usage": "`{0}autoboobs 30` or `{0}autoboobs`" + }, + "autobutts": { + "Cmd": "autobutts", + "Desc": "Posts a butts every X seconds. 20 seconds minimum. Provide no arguments to disable.", + "Usage": "`{0}autobutts 30` or `{0}autobutts`" + }, + "eightball": { + "cmd": "8ball", + "desc": "Ask the 8ball a yes/no question.", + "usage": "`{0}8ball`" + } +} \ No newline at end of file From 038f17c3a438f5cdcc7d7a8e8f96e89a3d942dd1 Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Sun, 17 Sep 2017 08:33:16 +0200 Subject: [PATCH 318/346] Commands which have no command strings should no longer throw an exception --- src/NadekoBot/Common/Attributes/Aliases.cs | 3 --- src/NadekoBot/Services/Impl/BotCredentials.cs | 5 ----- src/NadekoBot/Services/Impl/Localization.cs | 9 +++++++++ 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/src/NadekoBot/Common/Attributes/Aliases.cs b/src/NadekoBot/Common/Attributes/Aliases.cs index b6ebbf81..e6c95f67 100644 --- a/src/NadekoBot/Common/Attributes/Aliases.cs +++ b/src/NadekoBot/Common/Attributes/Aliases.cs @@ -2,9 +2,6 @@ using System.Runtime.CompilerServices; using Discord.Commands; using NadekoBot.Services.Impl; - -//todo what if it doesn't exist - namespace NadekoBot.Common.Attributes { public class Aliases : AliasAttribute diff --git a/src/NadekoBot/Services/Impl/BotCredentials.cs b/src/NadekoBot/Services/Impl/BotCredentials.cs index 341c947e..0ee0dea9 100644 --- a/src/NadekoBot/Services/Impl/BotCredentials.cs +++ b/src/NadekoBot/Services/Impl/BotCredentials.cs @@ -90,11 +90,6 @@ namespace NadekoBot.Services.Impl ulong.TryParse(data[nameof(ClientId)], out ulong clId); ClientId = clId; - //var scId = data[nameof(SoundCloudClientId)]; - //SoundCloudClientId = scId; - //SoundCloudClientId = string.IsNullOrWhiteSpace(scId) - // ? - // : scId; CarbonKey = data[nameof(CarbonKey)]; var dbSection = data.GetSection("db"); Db = new DBConfig(string.IsNullOrWhiteSpace(dbSection["Type"]) diff --git a/src/NadekoBot/Services/Impl/Localization.cs b/src/NadekoBot/Services/Impl/Localization.cs index b8a977ec..438153a0 100644 --- a/src/NadekoBot/Services/Impl/Localization.cs +++ b/src/NadekoBot/Services/Impl/Localization.cs @@ -131,6 +131,15 @@ namespace NadekoBot.Services.Impl public static CommandData LoadCommand(string key) { _commandData.TryGetValue(key, out var toReturn); + + if (toReturn == null) + return new CommandData + { + Cmd = key, + Desc = key, + Usage = key, + }; + return toReturn; } } From acccfbd960d3aa608d930b8b01d97958988d0985 Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Mon, 18 Sep 2017 00:02:55 +0200 Subject: [PATCH 319/346] cleanup --- .../Services/Database/Repositories/Impl/XpRepository.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/NadekoBot/Services/Database/Repositories/Impl/XpRepository.cs b/src/NadekoBot/Services/Database/Repositories/Impl/XpRepository.cs index b411cdad..b833cb79 100644 --- a/src/NadekoBot/Services/Database/Repositories/Impl/XpRepository.cs +++ b/src/NadekoBot/Services/Database/Repositories/Impl/XpRepository.cs @@ -2,7 +2,6 @@ using System.Linq; using Microsoft.EntityFrameworkCore; -//todo add pagination to .lb namespace NadekoBot.Services.Database.Repositories.Impl { public class XpRepository : Repository, IXpRepository From 64523b95e6a4ebb09e42a097e6fd2d57dc36b379 Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Mon, 18 Sep 2017 01:41:43 +0200 Subject: [PATCH 320/346] Flowerreaction renamed to reaction. It will use your CurrencySign, instead of flower. --- .../Common/Attributes/NadekoCommand.cs | 2 +- .../Gambling/CurrencyEventsCommands.cs | 38 ++++++++++++------- .../_strings/ResponseStrings.en-US.json | 6 +-- src/NadekoBot/data/command_strings.json | 8 ++-- 4 files changed, 32 insertions(+), 22 deletions(-) diff --git a/src/NadekoBot/Common/Attributes/NadekoCommand.cs b/src/NadekoBot/Common/Attributes/NadekoCommand.cs index eda997fd..ee8b9d58 100644 --- a/src/NadekoBot/Common/Attributes/NadekoCommand.cs +++ b/src/NadekoBot/Common/Attributes/NadekoCommand.cs @@ -6,7 +6,7 @@ namespace NadekoBot.Common.Attributes { public class NadekoCommand : CommandAttribute { - public NadekoCommand([CallerMemberName] string memberName="") : base(Localization.LoadCommand(memberName.ToLowerInvariant()).Cmd) + public NadekoCommand([CallerMemberName] string memberName="") : base(Localization.LoadCommand(memberName.ToLowerInvariant()).Cmd.Split(' ')[0]) { } diff --git a/src/NadekoBot/Modules/Gambling/CurrencyEventsCommands.cs b/src/NadekoBot/Modules/Gambling/CurrencyEventsCommands.cs index 6fbee42e..4546ea72 100644 --- a/src/NadekoBot/Modules/Gambling/CurrencyEventsCommands.cs +++ b/src/NadekoBot/Modules/Gambling/CurrencyEventsCommands.cs @@ -13,6 +13,7 @@ using NadekoBot.Common.Collections; using NLog; using System.Collections.Concurrent; using System.Collections.Generic; +using NadekoBot.Services.Database.Models; namespace NadekoBot.Modules.Gambling { @@ -23,7 +24,7 @@ namespace NadekoBot.Modules.Gambling { public enum CurrencyEvent { - FlowerReaction, + Reaction, SneakyGameStatus } //flower reaction event @@ -54,8 +55,8 @@ namespace NadekoBot.Modules.Gambling { switch (e) { - case CurrencyEvent.FlowerReaction: - await FlowerReactionEvent(Context, arg).ConfigureAwait(false); + case CurrencyEvent.Reaction: + await reactionEvent(Context, arg).ConfigureAwait(false); break; case CurrencyEvent.SneakyGameStatus: await SneakyGameStatusEvent(Context, arg).ConfigureAwait(false); @@ -127,19 +128,19 @@ namespace NadekoBot.Modules.Gambling return Task.CompletedTask; } - public async Task FlowerReactionEvent(ICommandContext context, int amount) + public async Task reactionEvent(ICommandContext context, int amount) { if (amount <= 0) amount = 100; - var title = GetText("flowerreaction_title"); - var desc = GetText("flowerreaction_desc", "🌸", Format.Bold(amount.ToString()) + _bc.BotConfig.CurrencySign); - var footer = GetText("flowerreaction_footer", 24); + var title = GetText("reaction_title"); + var desc = GetText("reaction_desc", _bc.BotConfig.CurrencySign, Format.Bold(amount.ToString()) + _bc.BotConfig.CurrencySign); + var footer = GetText("reaction_footer", 24); var msg = await context.Channel.SendConfirmAsync(title, desc, footer: footer) .ConfigureAwait(false); - await new FlowerReactionEvent(_client, _cs, amount).Start(msg, context); + await new ReactionEvent(_bc.BotConfig, _client, _cs, amount).Start(msg, context); } } } @@ -149,9 +150,10 @@ namespace NadekoBot.Modules.Gambling public abstract Task Start(IUserMessage msg, ICommandContext channel); } - public class FlowerReactionEvent : CurrencyEvent + public class ReactionEvent : CurrencyEvent { - private readonly ConcurrentHashSet _flowerReactionAwardedUsers = new ConcurrentHashSet(); + private readonly ConcurrentHashSet _reactionAwardedUsers = new ConcurrentHashSet(); + private readonly BotConfig _bc; private readonly Logger _log; private readonly DiscordSocketClient _client; private readonly CurrencyService _cs; @@ -165,8 +167,9 @@ namespace NadekoBot.Modules.Gambling private readonly ConcurrentQueue _toGiveTo = new ConcurrentQueue(); private readonly int _amount; - public FlowerReactionEvent(DiscordSocketClient client, CurrencyService cs, int amount) + public ReactionEvent(BotConfig bc, DiscordSocketClient client, CurrencyService cs, int amount) { + _bc = bc; _log = LogManager.GetCurrentClassLogger(); _client = client; _cs = cs; @@ -223,10 +226,17 @@ namespace NadekoBot.Modules.Gambling StartingMessage = umsg; _client.MessageDeleted += MessageDeletedEventHandler; - try { await StartingMessage.AddReactionAsync(new Emoji("🌸")).ConfigureAwait(false); } + IEmote iemote; + if (Emote.TryParse(_bc.CurrencySign, out var emote)) + { + iemote = emote; + } + else + iemote = new Emoji(_bc.CurrencySign); + try { await StartingMessage.AddReactionAsync(iemote).ConfigureAwait(false); } catch { - try { await StartingMessage.AddReactionAsync(new Emoji("🌸")).ConfigureAwait(false); } + try { await StartingMessage.AddReactionAsync(iemote).ConfigureAwait(false); } catch { try { await StartingMessage.DeleteAsync().ConfigureAwait(false); } @@ -240,7 +250,7 @@ namespace NadekoBot.Modules.Gambling if (r.UserId == _botUser.Id) return; - if (r.Emote.Name == "🌸" && r.User.IsSpecified && ((DateTime.UtcNow - r.User.Value.CreatedAt).TotalDays > 5) && _flowerReactionAwardedUsers.Add(r.User.Value.Id)) + if (r.Emote.Name == iemote.Name && r.User.IsSpecified && ((DateTime.UtcNow - r.User.Value.CreatedAt).TotalDays > 5) && _reactionAwardedUsers.Add(r.User.Value.Id)) { _toGiveTo.Enqueue(r.UserId); } diff --git a/src/NadekoBot/_strings/ResponseStrings.en-US.json b/src/NadekoBot/_strings/ResponseStrings.en-US.json index ebd9c435..80389b23 100644 --- a/src/NadekoBot/_strings/ResponseStrings.en-US.json +++ b/src/NadekoBot/_strings/ResponseStrings.en-US.json @@ -240,9 +240,9 @@ "gambling_flipped": "flipped {0}.", "gambling_flip_guess": "You guessed it! You won {0}", "gambling_flip_invalid": "Invalid number specified. You can flip 1 to {0} coins.", - "gambling_flowerreaction_desc": "Add {0} reaction to this message to get {1} ", - "gambling_flowerreaction_footer": "This event is active for up to {0} hours.", - "gambling_flowerreaction_title": "Flower reaction event started!", + "gambling_reaction_desc": "Add {0} reaction to this message to get {1} ", + "gambling_reaction_footer": "This event is active for up to {0} hours.", + "gambling_reaction_title": "Reaction event started!", "gambling_gifted": "has gifted {0} to {1}", "gambling_has": "{0} has {1}", "gambling_heads": "Head", diff --git a/src/NadekoBot/data/command_strings.json b/src/NadekoBot/data/command_strings.json index d8f0ccd2..fdd51cd3 100644 --- a/src/NadekoBot/data/command_strings.json +++ b/src/NadekoBot/data/command_strings.json @@ -1431,7 +1431,7 @@ }, "heal": { "Cmd": "heal", - "Desc": "Heals someone. Revives those who fainted. Costs a NadekoFlower. ", + "Desc": "Heals someone. Revives those who fainted. Costs one Currency. ", "Usage": "`{0}heal @someone`" }, "movelist": { @@ -1441,7 +1441,7 @@ }, "settype": { "Cmd": "settype", - "Desc": "Set your poketype. Costs a NadekoFlower. Provide no arguments to see a list of available types.", + "Desc": "Set your poketype. Costs one Currency. Provide no arguments to see a list of available types.", "Usage": "`{0}settype fire` or `{0}settype`" }, "type": { @@ -1576,8 +1576,8 @@ }, "startevent": { "Cmd": "startevent", - "Desc": "Starts one of the events seen on public nadeko.", - "Usage": "`{0}startevent flowerreaction`" + "Desc": "Starts one of the events seen on public nadeko. `reaction` and `sneakygamestatus` are the only 2 available now.", + "Usage": "`{0}startevent reaction`" }, "slotstats": { "Cmd": "slotstats", From e12c29dda5797009b0c816ce7d2942f5d89edcfd Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Mon, 18 Sep 2017 09:24:42 +0200 Subject: [PATCH 321/346] New usage format for command strings. --- src/NadekoBot/Common/Attributes/Usage.cs | 12 +- src/NadekoBot/Common/CommandData.cs | 2 +- .../Gambling/CurrencyEventsCommands.cs | 4 +- src/NadekoBot/NadekoBot.cs | 11 + src/NadekoBot/Services/Impl/Localization.cs | 2 +- src/NadekoBot/data/command_strings.json | 1746 +++++++++++++---- 6 files changed, 1355 insertions(+), 422 deletions(-) diff --git a/src/NadekoBot/Common/Attributes/Usage.cs b/src/NadekoBot/Common/Attributes/Usage.cs index 97d342ee..391de638 100644 --- a/src/NadekoBot/Common/Attributes/Usage.cs +++ b/src/NadekoBot/Common/Attributes/Usage.cs @@ -1,14 +1,24 @@ using System.Runtime.CompilerServices; using Discord.Commands; using NadekoBot.Services.Impl; +using System.Linq; +using Discord; namespace NadekoBot.Common.Attributes { public class Usage : RemarksAttribute { - public Usage([CallerMemberName] string memberName="") : base(Localization.LoadCommand(memberName.ToLowerInvariant()).Usage) + public Usage([CallerMemberName] string memberName="") : base(Usage.GetUsage(memberName)) { } + + public static string GetUsage(string memberName) + { + var usage = Localization.LoadCommand(memberName.ToLowerInvariant()).Usage; + return string.Join(" or ", usage + .Select(x => Format.Code(x))); + + } } } diff --git a/src/NadekoBot/Common/CommandData.cs b/src/NadekoBot/Common/CommandData.cs index 59712eae..b53ab2ea 100644 --- a/src/NadekoBot/Common/CommandData.cs +++ b/src/NadekoBot/Common/CommandData.cs @@ -3,7 +3,7 @@ public class CommandData { public string Cmd { get; set; } - public string Usage { get; set; } public string Desc { get; set; } + public string[] Usage { get; set; } } } diff --git a/src/NadekoBot/Modules/Gambling/CurrencyEventsCommands.cs b/src/NadekoBot/Modules/Gambling/CurrencyEventsCommands.cs index 4546ea72..0ad3ddf9 100644 --- a/src/NadekoBot/Modules/Gambling/CurrencyEventsCommands.cs +++ b/src/NadekoBot/Modules/Gambling/CurrencyEventsCommands.cs @@ -56,7 +56,7 @@ namespace NadekoBot.Modules.Gambling switch (e) { case CurrencyEvent.Reaction: - await reactionEvent(Context, arg).ConfigureAwait(false); + await ReactionEvent(Context, arg).ConfigureAwait(false); break; case CurrencyEvent.SneakyGameStatus: await SneakyGameStatusEvent(Context, arg).ConfigureAwait(false); @@ -128,7 +128,7 @@ namespace NadekoBot.Modules.Gambling return Task.CompletedTask; } - public async Task reactionEvent(ICommandContext context, int amount) + public async Task ReactionEvent(ICommandContext context, int amount) { if (amount <= 0) amount = 100; diff --git a/src/NadekoBot/NadekoBot.cs b/src/NadekoBot/NadekoBot.cs index 95fd9574..f6a87c11 100644 --- a/src/NadekoBot/NadekoBot.cs +++ b/src/NadekoBot/NadekoBot.cs @@ -61,6 +61,17 @@ namespace NadekoBot if (shardId < 0) throw new ArgumentOutOfRangeException(nameof(shardId)); + //var obj = JsonConvert.DeserializeObject>(File.ReadAllText("./data/command_strings.json")) + // .ToDictionary(x => x.Key, x => new CommandData2 + // { + // Cmd = x.Value.Cmd, + // Desc = x.Value.Desc, + // Usage = x.Value.Usage.Select(y => y.Substring(1, y.Length - 2)).ToArray(), + // }); + + //File.WriteAllText("./data/command_strings.json", JsonConvert.SerializeObject(obj, Formatting.Indented)); + + LogSetup.SetupLogger(); _log = LogManager.GetCurrentClassLogger(); TerribleElevatedPermissionCheck(); diff --git a/src/NadekoBot/Services/Impl/Localization.cs b/src/NadekoBot/Services/Impl/Localization.cs index 438153a0..dc3829ce 100644 --- a/src/NadekoBot/Services/Impl/Localization.cs +++ b/src/NadekoBot/Services/Impl/Localization.cs @@ -137,7 +137,7 @@ namespace NadekoBot.Services.Impl { Cmd = key, Desc = key, - Usage = key, + Usage = new[] { key }, }; return toReturn; diff --git a/src/NadekoBot/data/command_strings.json b/src/NadekoBot/data/command_strings.json index fdd51cd3..ca64fa24 100644 --- a/src/NadekoBot/data/command_strings.json +++ b/src/NadekoBot/data/command_strings.json @@ -2,2056 +2,2968 @@ "h": { "Cmd": "help h", "Desc": "Either shows a help for a single command, or DMs you help link if no arguments are specified.", - "Usage": "`{0}h {0}cmds` or `{0}h`" + "Usage": [ + "{0}h {0}cmds", + "{0}h" + ] }, "hgit": { "Cmd": "hgit", "Desc": "Generates the commandlist.md file.", - "Usage": "`{0}hgit`" + "Usage": [ + "{0}hgit" + ] }, "donate": { "Cmd": "donate", "Desc": "Instructions for helping the project financially.", - "Usage": "`{0}donate`" + "Usage": [ + "{0}donate" + ] }, "modules": { "Cmd": "modules mdls", "Desc": "Lists all bot modules.", - "Usage": "`{0}modules`" + "Usage": [ + "{0}modules" + ] }, "commands": { "Cmd": "commands cmds", "Desc": "List all of the bot's commands from a certain module. You can either specify the full name or only the first few letters of the module name.", - "Usage": "`{0}commands Administration` or `{0}cmds Admin`" + "Usage": [ + "{0}commands Administration", + "{0}cmds Admin" + ] }, "greetdel": { "Cmd": "greetdel grdel", "Desc": "Sets the time it takes (in seconds) for greet messages to be auto-deleted. Set it to 0 to disable automatic deletion.", - "Usage": "`{0}greetdel 0` or `{0}greetdel 30`" + "Usage": [ + "{0}greetdel 0", + "{0}greetdel 30" + ] }, "greet": { "Cmd": "greet", "Desc": "Toggles anouncements on the current channel when someone joins the server.", - "Usage": "`{0}greet`" + "Usage": [ + "{0}greet" + ] }, "greetmsg": { "Cmd": "greetmsg", "Desc": "Sets a new join announcement message which will be shown in the server's channel. Type `%user%` if you want to mention the new member. Using it with no message will show the current greet message. You can use embed json from instead of a regular text, if you want the message to be embedded.", - "Usage": "`{0}greetmsg Welcome, %user%.`" + "Usage": [ + "{0}greetmsg Welcome, %user%." + ] }, "bye": { "Cmd": "bye", "Desc": "Toggles anouncements on the current channel when someone leaves the server.", - "Usage": "`{0}bye`" + "Usage": [ + "{0}bye" + ] }, "byemsg": { "Cmd": "byemsg", "Desc": "Sets a new leave announcement message. Type `%user%` if you want to show the name the user who left. Type `%id%` to show id. Using this command with no message will show the current bye message. You can use embed json from instead of a regular text, if you want the message to be embedded.", - "Usage": "`{0}byemsg %user% has left.`" + "Usage": [ + "{0}byemsg %user% has left." + ] }, "byedel": { "Cmd": "byedel", "Desc": "Sets the time it takes (in seconds) for bye messages to be auto-deleted. Set it to `0` to disable automatic deletion.", - "Usage": "`{0}byedel 0` or `{0}byedel 30`" + "Usage": [ + "{0}byedel 0", + "{0}byedel 30" + ] }, "greetdm": { "Cmd": "greetdm", "Desc": "Toggles whether the greet messages will be sent in a DM (This is separate from greet - you can have both, any or neither enabled).", - "Usage": "`{0}greetdm`" + "Usage": [ + "{0}greetdm" + ] }, "logserver": { "Cmd": "logserver", "Desc": "Enables or Disables ALL log events. If enabled, all log events will log to this channel.", - "Usage": "`{0}logserver enable` or `{0}logserver disable`" + "Usage": [ + "{0}logserver enable", + "{0}logserver disable" + ] }, "logignore": { "Cmd": "logignore", "Desc": "Toggles whether the `.logserver` command ignores this channel. Useful if you have hidden admin channel and public log channel.", - "Usage": "`{0}logignore`" + "Usage": [ + "{0}logignore" + ] }, "userpresence": { "Cmd": "userpresence", "Desc": "Starts logging to this channel when someone from the server goes online/offline/idle.", - "Usage": "`{0}userpresence`" + "Usage": [ + "{0}userpresence" + ] }, "voicepresence": { "Cmd": "voicepresence", "Desc": "Toggles logging to this channel whenever someone joins or leaves a voice channel you are currently in.", - "Usage": "`{0}voicepresence`" + "Usage": [ + "{0}voicepresence" + ] }, "repeatinvoke": { "Cmd": "repeatinvoke repinv", "Desc": "Immediately shows the repeat message on a certain index and restarts its timer.", - "Usage": "`{0}repinv 1`" + "Usage": [ + "{0}repinv 1" + ] }, "repeat": { "Cmd": "repeat", "Desc": "Repeat a message every `X` minutes in the current channel. You can instead specify time of day for the message to be repeated at daily (make sure you've set your server's timezone). You can have up to 5 repeating messages on the server in total.", - "Usage": "`{0}repeat 5 Hello there` or `{0}repeat 17:30 tea time`" + "Usage": [ + "{0}repeat 5 Hello there", + "{0}repeat 17:30 tea time" + ] }, "rotateplaying": { "Cmd": "rotateplaying ropl", "Desc": "Toggles rotation of playing status of the dynamic strings you previously specified.", - "Usage": "`{0}ropl`" + "Usage": [ + "{0}ropl" + ] }, "addplaying": { "Cmd": "addplaying adpl", "Desc": "Adds a specified string to the list of playing strings to rotate. Supported placeholders: `%servers%`, `%users%`, `%playing%`, `%queued%`, `%time%`, `%shardid%`, `%shardcount%`, `%shardguilds%`.", - "Usage": "`{0}adpl`" + "Usage": [ + "{0}adpl" + ] }, "listplaying": { "Cmd": "listplaying lipl", "Desc": "Lists all playing statuses with their corresponding number.", - "Usage": "`{0}lipl`" + "Usage": [ + "{0}lipl" + ] }, "removeplaying": { "Cmd": "removeplaying rmpl repl", "Desc": "Removes a playing string on a given number.", - "Usage": "`{0}rmpl`" + "Usage": [ + "{0}rmpl" + ] }, "slowmode": { "Cmd": "slowmode", "Desc": "Toggles slowmode. Disable by specifying no parameters. To enable, specify a number of messages each user can send, and an interval in seconds. For example 1 message every 5 seconds.", - "Usage": "`{0}slowmode 1 5` or `{0}slowmode`" + "Usage": [ + "{0}slowmode 1 5", + "{0}slowmode" + ] }, "cleanvplust": { "Cmd": "cleanvplust cv+t", "Desc": "Deletes all text channels ending in `-voice` for which voicechannels are not found. Use at your own risk.", - "Usage": "`{0}cleanv+t`" + "Usage": [ + "{0}cleanv+t" + ] }, "voiceplustext": { "Cmd": "voice+text v+t", "Desc": "Creates a text channel for each voice channel only users in that voice channel can see. If you are server owner, keep in mind you will see them all the time regardless.", - "Usage": "`{0}v+t`" + "Usage": [ + "{0}v+t" + ] }, "scsc": { "Cmd": "scsc", "Desc": "Starts an instance of cross server channel. You will get a token as a DM that other people will use to tune in to the same instance.", - "Usage": "`{0}scsc`" + "Usage": [ + "{0}scsc" + ] }, "jcsc": { "Cmd": "jcsc", "Desc": "Joins current channel to an instance of cross server channel using the token.", - "Usage": "`{0}jcsc TokenHere`" + "Usage": [ + "{0}jcsc TokenHere" + ] }, "lcsc": { "Cmd": "lcsc", "Desc": "Leaves a cross server channel instance from this channel.", - "Usage": "`{0}lcsc`" + "Usage": [ + "{0}lcsc" + ] }, "asar": { "Cmd": "asar", "Desc": "Adds a role to the list of self-assignable roles.", - "Usage": "`{0}asar Gamer`" + "Usage": [ + "{0}asar Gamer" + ] }, "rsar": { "Cmd": "rsar", "Desc": "Removes a specified role from the list of self-assignable roles.", - "Usage": "`{0}rsar`" + "Usage": [ + "{0}rsar" + ] }, "lsar": { "Cmd": "lsar", "Desc": "Lists all self-assignable roles.", - "Usage": "`{0}lsar`" + "Usage": [ + "{0}lsar" + ] }, "tesar": { "Cmd": "togglexclsar tesar", "Desc": "Toggles whether the self-assigned roles are exclusive. (So that any person can have only one of the self assignable roles)", - "Usage": "`{0}tesar`" + "Usage": [ + "{0}tesar" + ] }, "iam": { "Cmd": "iam", "Desc": "Adds a role to you that you choose. Role must be on a list of self-assignable roles.", - "Usage": "`{0}iam Gamer`" + "Usage": [ + "{0}iam Gamer" + ] }, "iamnot": { "Cmd": "iamnot iamn", "Desc": "Removes a specified role from you. Role must be on a list of self-assignable roles.", - "Usage": "`{0}iamn Gamer`" + "Usage": [ + "{0}iamn Gamer" + ] }, "addcustreact": { "Cmd": "addcustreact acr", "Desc": "Add a custom reaction with a trigger and a response. Running this command in server requires the Administration permission. Running this command in DM is Bot Owner only and adds a new global custom reaction. Guide here: ", - "Usage": "`{0}acr \"hello\" Hi there %user%`" + "Usage": [ + "{0}acr \"hello\" Hi there %user%" + ] }, "listcustreact": { "Cmd": "listcustreact lcr", "Desc": "Lists global or server custom reactions (20 commands per page). Running the command in DM will list global custom reactions, while running it in server will list that server's custom reactions. Specifying `all` argument instead of the number will DM you a text file with a list of all custom reactions.", - "Usage": "`{0}lcr 1` or `{0}lcr all`" + "Usage": [ + "{0}lcr 1", + "{0}lcr all" + ] }, "listcustreactg": { "Cmd": "listcustreactg lcrg", "Desc": "Lists global or server custom reactions (20 commands per page) grouped by trigger, and show a number of responses for each. Running the command in DM will list global custom reactions, while running it in server will list that server's custom reactions.", - "Usage": "`{0}lcrg 1`" + "Usage": [ + "{0}lcrg 1" + ] }, "showcustreact": { "Cmd": "showcustreact scr", "Desc": "Shows a custom reaction's response on a given ID.", - "Usage": "`{0}scr 1`" + "Usage": [ + "{0}scr 1" + ] }, "delcustreact": { "Cmd": "delcustreact dcr", "Desc": "Deletes a custom reaction on a specific index. If ran in DM, it is bot owner only and deletes a global custom reaction. If ran in a server, it requires Administration privileges and removes server custom reaction.", - "Usage": "`{0}dcr 5`" + "Usage": [ + "{0}dcr 5" + ] }, "autoassignrole": { "Cmd": "autoassignrole aar", "Desc": "Automaticaly assigns a specified role to every user who joins the server.", - "Usage": "`{0}aar` to disable, `{0}aar Role Name` to enable" + "Usage": [ + "{0}aar` to disable, `{0}aar Role Name` to enabl" + ] }, "leave": { "Cmd": "leave", "Desc": "Makes Nadeko leave the server. Either server name or server ID is required.", - "Usage": "`{0}leave 123123123331`" + "Usage": [ + "{0}leave 123123123331" + ] }, "delmsgoncmd": { "Cmd": "delmsgoncmd", "Desc": "Toggles the automatic deletion of the user's successful command message to prevent chat flood.", - "Usage": "`{0}delmsgoncmd`" + "Usage": [ + "{0}delmsgoncmd" + ] }, "restart": { "Cmd": "restart", "Desc": "Restarts the bot. Might not work.", - "Usage": "`{0}restart`" + "Usage": [ + "{0}restart" + ] }, "setrole": { "Cmd": "setrole sr", "Desc": "Sets a role for a given user.", - "Usage": "`{0}sr @User Guest`" + "Usage": [ + "{0}sr @User Guest" + ] }, "removerole": { "Cmd": "removerole rr", "Desc": "Removes a role from a given user.", - "Usage": "`{0}rr @User Admin`" + "Usage": [ + "{0}rr @User Admin" + ] }, "renamerole": { "Cmd": "renamerole renr", "Desc": "Renames a role. The role you are renaming must be lower than bot's highest role.", - "Usage": "`{0}renr \"First role\" SecondRole`" + "Usage": [ + "{0}renr \"First role\" SecondRole" + ] }, "removeallroles": { "Cmd": "removeallroles rar", "Desc": "Removes all roles from a mentioned user.", - "Usage": "`{0}rar @User`" + "Usage": [ + "{0}rar @User" + ] }, "createrole": { "Cmd": "createrole cr", "Desc": "Creates a role with a given name.", - "Usage": "`{0}cr Awesome Role`" + "Usage": [ + "{0}cr Awesome Role" + ] }, "rolecolor": { "Cmd": "rolecolor roleclr", "Desc": "Set a role's color to the hex or 0-255 rgb color value provided.", - "Usage": "`{0}roleclr Admin 255 200 100` or `{0}roleclr Admin ffba55`" + "Usage": [ + "{0}roleclr Admin 255 200 100", + "{0}roleclr Admin ffba55" + ] }, "ban": { "Cmd": "ban b", "Desc": "Bans a user by ID or name with an optional message.", - "Usage": "`{0}b \"@some Guy\" Your behaviour is toxic.`" + "Usage": [ + "{0}b \"@some Guy\" Your behaviour is toxic." + ] }, "softban": { "Cmd": "softban sb", "Desc": "Bans and then unbans a user by ID or name with an optional message.", - "Usage": "`{0}sb \"@some Guy\" Your behaviour is toxic.`" + "Usage": [ + "{0}sb \"@some Guy\" Your behaviour is toxic." + ] }, "kick": { "Cmd": "kick k", "Desc": "Kicks a mentioned user.", - "Usage": "`{0}k \"@some Guy\" Your behaviour is toxic.`" + "Usage": [ + "{0}k \"@some Guy\" Your behaviour is toxic." + ] }, "mute": { "Cmd": "mute", "Desc": "Mutes a mentioned user both from speaking and chatting. You can also specify time in minutes (up to 1440) for how long the user should be muted.", - "Usage": "`{0}mute @Someone` or `{0}mute 30 @Someone`" + "Usage": [ + "{0}mute @Someone", + "{0}mute 30 @Someone" + ] }, "voiceunmute": { "Cmd": "voiceunmute", "Desc": "Gives a previously voice-muted user a permission to speak.", - "Usage": "`{0}voiceunmute @Someguy`" + "Usage": [ + "{0}voiceunmute @Someguy" + ] }, "deafen": { "Cmd": "deafen deaf", "Desc": "Deafens mentioned user or users.", - "Usage": "`{0}deaf \"@Someguy\"` or `{0}deaf \"@Someguy\" \"@Someguy\"`" + "Usage": [ + "{0}deaf \"@Someguy\"", + "{0}deaf \"@Someguy\" \"@Someguy\"" + ] }, "undeafen": { "Cmd": "undeafen undef", "Desc": "Undeafens mentioned user or users.", - "Usage": "`{0}undef \"@Someguy\"` or `{0}undef \"@Someguy\" \"@Someguy\"`" + "Usage": [ + "{0}undef \"@Someguy\"", + "{0}undef \"@Someguy\" \"@Someguy\"" + ] }, "delvoichanl": { "Cmd": "delvoichanl dvch", "Desc": "Deletes a voice channel with a given name.", - "Usage": "`{0}dvch VoiceChannelName`" + "Usage": [ + "{0}dvch VoiceChannelName" + ] }, "creatvoichanl": { "Cmd": "creatvoichanl cvch", "Desc": "Creates a new voice channel with a given name.", - "Usage": "`{0}cvch VoiceChannelName`" + "Usage": [ + "{0}cvch VoiceChannelName" + ] }, "deltxtchanl": { "Cmd": "deltxtchanl dtch", "Desc": "Deletes a text channel with a given name.", - "Usage": "`{0}dtch TextChannelName`" + "Usage": [ + "{0}dtch TextChannelName" + ] }, "creatxtchanl": { "Cmd": "creatxtchanl ctch", "Desc": "Creates a new text channel with a given name.", - "Usage": "`{0}ctch TextChannelName`" + "Usage": [ + "{0}ctch TextChannelName" + ] }, "settopic": { "Cmd": "settopic st", "Desc": "Sets a topic on the current channel.", - "Usage": "`{0}st My new topic`" + "Usage": [ + "{0}st My new topic" + ] }, "setchanlname": { "Cmd": "setchanlname schn", "Desc": "Changes the name of the current channel.", - "Usage": "`{0}schn NewName`" + "Usage": [ + "{0}schn NewName" + ] }, "prune": { "Cmd": "prune clear", "Desc": "`{0}prune` removes all Nadeko's messages in the last 100 messages. `{0}prune X` removes last `X` number of messages from the channel (up to 100). `{0}prune @Someone` removes all Someone's messages in the last 100 messages. `{0}prune @Someone X` removes last `X` number of 'Someone's' messages in the channel.", - "Usage": "`{0}prune` or `{0}prune 5` or `{0}prune @Someone` or `{0}prune @Someone X`" + "Usage": [ + "{0}prune", + "{0}prune 5", + "{0}prune @Someone", + "{0}prune @Someone X" + ] }, "die": { "Cmd": "die", "Desc": "Shuts the bot down.", - "Usage": "`{0}die`" + "Usage": [ + "{0}die" + ] }, "setname": { "Cmd": "setname newnm", "Desc": "Gives the bot a new name.", - "Usage": "`{0}newnm BotName`" + "Usage": [ + "{0}newnm BotName" + ] }, "setnick": { "Cmd": "setnick", "Desc": "Changes the nickname of the bot on this server. You can also target other users to change their nickname.", - "Usage": "`{0}setnick BotNickname` or `{0}setnick @SomeUser New Nickname`" + "Usage": [ + "{0}setnick BotNickname", + "{0}setnick @SomeUser New Nickname" + ] }, "setavatar": { "Cmd": "setavatar setav", "Desc": "Sets a new avatar image for the NadekoBot. Argument is a direct link to an image.", - "Usage": "`{0}setav http://i.imgur.com/xTG3a1I.jpg`" + "Usage": [ + "{0}setav http://i.imgur.com/xTG3a1I.jpg" + ] }, "setgame": { "Cmd": "setgame", "Desc": "Sets the bots game.", - "Usage": "`{0}setgame with snakes`" + "Usage": [ + "{0}setgame with snakes" + ] }, "send": { "Cmd": "send", "Desc": "Sends a message to someone on a different server through the bot. Separate server and channel/user ids with `|` and prefix the channel id with `c:` and the user id with `u:`.", - "Usage": "`{0}send serverid|c:channelid message` or `{0}send serverid|u:userid message`" + "Usage": [ + "{0}send serverid|c:channelid message", + "{0}send serverid|u:userid message" + ] }, "mentionrole": { "Cmd": "mentionrole menro", "Desc": "Mentions every person from the provided role or roles (separated by a ',') on this server.", - "Usage": "`{0}menro RoleName`" + "Usage": [ + "{0}menro RoleName" + ] }, "unstuck": { "Cmd": "unstuck", "Desc": "Clears the message queue.", - "Usage": "`{0}unstuck`" + "Usage": [ + "{0}unstuck" + ] }, "donators": { "Cmd": "donators", "Desc": "List of the lovely people who donated to keep this project alive.", - "Usage": "`{0}donators`" + "Usage": [ + "{0}donators" + ] }, "donadd": { "Cmd": "donadd", "Desc": "Add a donator to the database.", - "Usage": "`{0}donadd Donate Amount`" + "Usage": [ + "{0}donadd Donate Amount" + ] }, "savechat": { "Cmd": "savechat", "Desc": "Saves a number of messages to a text file and sends it to you.", - "Usage": "`{0}savechat 150`" + "Usage": [ + "{0}savechat 150" + ] }, "remind": { "Cmd": "remind", "Desc": "Sends a message to you or a channel after certain amount of time. First argument is `me`/`here`/'channelname'. Second argument is time in a descending order (mo>w>d>h>m) example: 1w5d3h10m. Third argument is a (multiword) message.", - "Usage": "`{0}remind me 1d5h Do something` or `{0}remind #general 1m Start now!`" + "Usage": [ + "{0}remind me 1d5h Do something", + "{0}remind #general 1m Start now!" + ] }, "remindtemplate": { "Cmd": "remindtemplate", "Desc": "Sets message for when the remind is triggered. Available placeholders are `%user%` - user who ran the command, `%message%` - Message specified in the remind, `%target%` - target channel of the remind.", - "Usage": "`{0}remindtemplate %user%, do %message%!`" + "Usage": [ + "{0}remindtemplate %user%, do %message%!" + ] }, "serverinfo": { "Cmd": "serverinfo sinfo", "Desc": "Shows info about the server the bot is on. If no server is supplied, it defaults to current one.", - "Usage": "`{0}sinfo Some Server`" + "Usage": [ + "{0}sinfo Some Server" + ] }, "channelinfo": { "Cmd": "channelinfo cinfo", "Desc": "Shows info about the channel. If no channel is supplied, it defaults to current one.", - "Usage": "`{0}cinfo #some-channel`" + "Usage": [ + "{0}cinfo #some-channel" + ] }, "userinfo": { "Cmd": "userinfo uinfo", "Desc": "Shows info about the user. If no user is supplied, it defaults a user running the command.", - "Usage": "`{0}uinfo @SomeUser`" + "Usage": [ + "{0}uinfo @SomeUser" + ] }, "whosplaying": { "Cmd": "whosplaying whpl", "Desc": "Shows a list of users who are playing the specified game.", - "Usage": "`{0}whpl Overwatch`" + "Usage": [ + "{0}whpl Overwatch" + ] }, "inrole": { "Cmd": "inrole", "Desc": "Lists every person from the specified role on this server. You can use role ID, role name.", - "Usage": "`{0}inrole Some Role`" + "Usage": [ + "{0}inrole Some Role" + ] }, "checkmyperms": { "Cmd": "checkmyperms", "Desc": "Checks your user-specific permissions on this channel.", - "Usage": "`{0}checkmyperms`" + "Usage": [ + "{0}checkmyperms" + ] }, "stats": { "Cmd": "stats", "Desc": "Shows some basic stats for Nadeko.", - "Usage": "`{0}stats`" + "Usage": [ + "{0}stats" + ] }, "userid": { "Cmd": "userid uid", "Desc": "Shows user ID.", - "Usage": "`{0}uid` or `{0}uid @SomeGuy`" + "Usage": [ + "{0}uid", + "{0}uid @SomeGuy" + ] }, "channelid": { "Cmd": "channelid cid", "Desc": "Shows current channel ID.", - "Usage": "`{0}cid`" + "Usage": [ + "{0}cid" + ] }, "serverid": { "Cmd": "serverid sid", "Desc": "Shows current server ID.", - "Usage": "`{0}sid`" + "Usage": [ + "{0}sid" + ] }, "roles": { "Cmd": "roles", "Desc": "List roles on this server or a roles of a specific user if specified. Paginated, 20 roles per page.", - "Usage": "`{0}roles 2` or `{0}roles @Someone`" + "Usage": [ + "{0}roles 2", + "{0}roles @Someone" + ] }, "channeltopic": { "Cmd": "channeltopic ct", "Desc": "Sends current channel's topic as a message.", - "Usage": "`{0}ct`" + "Usage": [ + "{0}ct" + ] }, "chnlfilterinv": { "Cmd": "chnlfilterinv cfi", "Desc": "Toggles automatic deletion of invites posted in the channel. Does not negate the `{0}srvrfilterinv` enabled setting. Does not affect the Bot Owner.", - "Usage": "`{0}cfi`" + "Usage": [ + "{0}cfi" + ] }, "srvrfilterinv": { "Cmd": "srvrfilterinv sfi", "Desc": "Toggles automatic deletion of invites posted in the server. Does not affect the Bot Owner.", - "Usage": "`{0}sfi`" + "Usage": [ + "{0}sfi" + ] }, "chnlfilterwords": { "Cmd": "chnlfilterwords cfw", "Desc": "Toggles automatic deletion of messages containing filtered words on the channel. Does not negate the `{0}srvrfilterwords` enabled setting. Does not affect the Bot Owner.", - "Usage": "`{0}cfw`" + "Usage": [ + "{0}cfw" + ] }, "filterword": { "Cmd": "fw", "Desc": "Adds or removes (if it exists) a word from the list of filtered words. Use`{0}sfw` or `{0}cfw` to toggle filtering.", - "Usage": "`{0}fw poop`" + "Usage": [ + "{0}fw poop" + ] }, "lstfilterwords": { "Cmd": "lstfilterwords lfw", "Desc": "Shows a list of filtered words.", - "Usage": "`{0}lfw`" + "Usage": [ + "{0}lfw" + ] }, "srvrfilterwords": { "Cmd": "srvrfilterwords sfw", "Desc": "Toggles automatic deletion of messages containing filtered words on the server. Does not affect the Bot Owner.", - "Usage": "`{0}sfw`" + "Usage": [ + "{0}sfw" + ] }, "permrole": { "Cmd": "permrole pr", "Desc": "Sets a role which can change permissions. Supply no parameters to see the current one. Default is 'Nadeko'.", - "Usage": "`{0}pr role`" + "Usage": [ + "{0}pr role" + ] }, "verbose": { "Cmd": "verbose v", "Desc": "Sets whether to show when a command/module is blocked.", - "Usage": "`{0}verbose true`" + "Usage": [ + "{0}verbose true" + ] }, "srvrmdl": { "Cmd": "srvrmdl sm", "Desc": "Sets a module's permission at the server level.", - "Usage": "`{0}sm ModuleName enable`" + "Usage": [ + "{0}sm ModuleName enable" + ] }, "srvrcmd": { "Cmd": "srvrcmd sc", "Desc": "Sets a command's permission at the server level.", - "Usage": "`{0}sc \"command name\" disable`" + "Usage": [ + "{0}sc \"command name\" disable" + ] }, "rolemdl": { "Cmd": "rolemdl rm", "Desc": "Sets a module's permission at the role level.", - "Usage": "`{0}rm ModuleName enable MyRole`" + "Usage": [ + "{0}rm ModuleName enable MyRole" + ] }, "rolecmd": { "Cmd": "rolecmd rc", "Desc": "Sets a command's permission at the role level.", - "Usage": "`{0}rc \"command name\" disable MyRole`" + "Usage": [ + "{0}rc \"command name\" disable MyRole" + ] }, "chnlmdl": { "Cmd": "chnlmdl cm", "Desc": "Sets a module's permission at the channel level.", - "Usage": "`{0}cm ModuleName enable SomeChannel`" + "Usage": [ + "{0}cm ModuleName enable SomeChannel" + ] }, "chnlcmd": { "Cmd": "chnlcmd cc", "Desc": "Sets a command's permission at the channel level.", - "Usage": "`{0}cc \"command name\" enable SomeChannel`" + "Usage": [ + "{0}cc \"command name\" enable SomeChannel" + ] }, "usrmdl": { "Cmd": "usrmdl um", "Desc": "Sets a module's permission at the user level.", - "Usage": "`{0}um ModuleName enable SomeUsername`" + "Usage": [ + "{0}um ModuleName enable SomeUsername" + ] }, "usrcmd": { "Cmd": "usrcmd uc", "Desc": "Sets a command's permission at the user level.", - "Usage": "`{0}uc \"command name\" enable SomeUsername`" + "Usage": [ + "{0}uc \"command name\" enable SomeUsername" + ] }, "allsrvrmdls": { "Cmd": "allsrvrmdls asm", "Desc": "Enable or disable all modules for your server.", - "Usage": "`{0}asm [enable/disable]`" + "Usage": [ + "{0}asm [enable/disable]" + ] }, "allchnlmdls": { "Cmd": "allchnlmdls acm", "Desc": "Enable or disable all modules in a specified channel.", - "Usage": "`{0}acm enable #SomeChannel`" + "Usage": [ + "{0}acm enable #SomeChannel" + ] }, "allrolemdls": { "Cmd": "allrolemdls arm", "Desc": "Enable or disable all modules for a specific role.", - "Usage": "`{0}arm [enable/disable] MyRole`" + "Usage": [ + "{0}arm [enable/disable] MyRole" + ] }, "userblacklist": { "Cmd": "ubl", "Desc": "Either [add]s or [rem]oves a user specified by a Mention or an ID from a blacklist.", - "Usage": "`{0}ubl add @SomeUser` or `{0}ubl rem 12312312313`" + "Usage": [ + "{0}ubl add @SomeUser", + "{0}ubl rem 12312312313" + ] }, "channelblacklist": { "Cmd": "cbl", "Desc": "Either [add]s or [rem]oves a channel specified by an ID from a blacklist.", - "Usage": "`{0}cbl rem 12312312312`" + "Usage": [ + "{0}cbl rem 12312312312" + ] }, "serverblacklist": { "Cmd": "sbl", "Desc": "Either [add]s or [rem]oves a server specified by a Name or an ID from a blacklist.", - "Usage": "`{0}sbl add 12312321312` or `{0}sbl rem SomeTrashServer`" + "Usage": [ + "{0}sbl add 12312321312", + "{0}sbl rem SomeTrashServer" + ] }, "cmdcooldown": { "Cmd": "cmdcooldown cmdcd", "Desc": "Sets a cooldown per user for a command. Set it to 0 to remove the cooldown.", - "Usage": "`{0}cmdcd \"some cmd\" 5`" + "Usage": [ + "{0}cmdcd \"some cmd\" 5" + ] }, "allcmdcooldowns": { "Cmd": "allcmdcooldowns acmdcds", "Desc": "Shows a list of all commands and their respective cooldowns.", - "Usage": "`{0}acmdcds`" + "Usage": [ + "{0}acmdcds" + ] }, "addquote": { "Cmd": ".", "Desc": "Adds a new quote with the specified name and message.", - "Usage": "`{0}. sayhi Hi`" + "Usage": [ + "{0}. sayhi Hi" + ] }, "showquote": { "Cmd": "..", "Desc": "Shows a random quote with a specified name.", - "Usage": "`{0}.. abc`" + "Usage": [ + "{0}.. abc" + ] }, "quotesearch": { "Cmd": "qsearch", "Desc": "Shows a random quote for a keyword that contains any text specified in the search.", - "Usage": "`{0}qsearch keyword text`" + "Usage": [ + "{0}qsearch keyword text" + ] }, "quoteid": { "Cmd": "quoteid qid", "Desc": "Displays the quote with the specified ID number. Quote ID numbers can be found by typing `.liqu [num]` where `[num]` is a number of a page which contains 15 quotes.", - "Usage": "`{0}qid 123456`" + "Usage": [ + "{0}qid 123456" + ] }, "quotedelete": { "Cmd": "quotedel qdel", "Desc": "Deletes a quote with the specified ID. You have to be either server Administrator or the creator of the quote to delete it.", - "Usage": "`{0}qdel 123456`" + "Usage": [ + "{0}qdel 123456" + ] }, "draw": { "Cmd": "draw", "Desc": "Draws a card from this server's deck. You can draw up to 10 cards by supplying a number of cards to draw.", - "Usage": "`{0}draw` or `{0}draw 5`" + "Usage": [ + "{0}draw", + "{0}draw 5" + ] }, "drawnew": { "Cmd": "drawnew", "Desc": "Draws a card from the NEW deck of cards. You can draw up to 10 cards by supplying a number of cards to draw.", - "Usage": "`{0}drawnew` or `{0}drawnew 5`" + "Usage": [ + "{0}drawnew", + "{0}drawnew 5" + ] }, "shuffleplaylist": { "Cmd": "shuffle sh plsh", "Desc": "Shuffles the current playlist.", - "Usage": "`{0}plsh`" + "Usage": [ + "{0}plsh" + ] }, "flip": { "Cmd": "flip", "Desc": "Flips coin(s) - heads or tails, and shows an image.", - "Usage": "`{0}flip` or `{0}flip 3`" + "Usage": [ + "{0}flip", + "{0}flip 3" + ] }, "betflip": { "Cmd": "betflip bf", "Desc": "Bet to guess will the result be heads or tails. Guessing awards you 1.95x the currency you've bet (rounded up). Multiplier can be changed by the bot owner.", - "Usage": "`{0}bf 5 heads` or `{0}bf 3 t`" + "Usage": [ + "{0}bf 5 heads", + "{0}bf 3 t" + ] }, "roll": { "Cmd": "roll", "Desc": "Rolls 0-100. If you supply a number `X` it rolls up to 30 normal dice. If you split 2 numbers with letter `d` (`xdy`) it will roll `X` dice from 1 to `y`. `Y` can be a letter 'F' if you want to roll fate dice instead of dnd.", - "Usage": "`{0}roll` or `{0}roll 7` or `{0}roll 3d5` or `{0}roll 5dF`" + "Usage": [ + "{0}roll", + "{0}roll 7", + "{0}roll 3d5", + "{0}roll 5dF" + ] }, "rolluo": { "Cmd": "rolluo", "Desc": "Rolls `X` normal dice (up to 30) unordered. If you split 2 numbers with letter `d` (`xdy`) it will roll `X` dice from 1 to `y`.", - "Usage": "`{0}rolluo` or `{0}rolluo 7` or `{0}rolluo 3d5`" + "Usage": [ + "{0}rolluo", + "{0}rolluo 7", + "{0}rolluo 3d5" + ] }, "nroll": { "Cmd": "nroll", "Desc": "Rolls in a given range.", - "Usage": "`{0}nroll 5` (rolls 0-5) or `{0}nroll 5-15`" + "Usage": [ + "{0}nroll 5` (rolls 0-5", + "{0}nroll 5-15" + ] }, "race": { "Cmd": "race", "Desc": "Starts a new animal race.", - "Usage": "`{0}race`" + "Usage": [ + "{0}race" + ] }, "joinrace": { "Cmd": "joinrace jr", "Desc": "Joins a new race. You can specify an amount of currency for betting (optional). You will get YourBet*(participants-1) back if you win.", - "Usage": "`{0}jr` or `{0}jr 5`" + "Usage": [ + "{0}jr", + "{0}jr 5" + ] }, "nunchi": { "Cmd": "nunchi", "Desc": "Creates or joins an existing nunchi game. Users have to count up by 1 from the starting number shown by the bot. If someone makes a mistake (types an incorrent number, or repeats the same number) they are out of the game and a new round starts without them. Minimum 3 users required.", - "Usage": "`{0}nunchi`" + "Usage": [ + "{0}nunchi" + ] }, "connect4": { "Cmd": "connect4 con4", "Desc": "Creates or joins an existing connect4 game. 2 players are required for the game. Objective of the game is to get 4 of your pieces next to each other in a vertical, horizontal or diagonal line.", - "Usage": "`{0}connect4`" + "Usage": [ + "{0}connect4" + ] }, "raffle": { "Cmd": "raffle", "Desc": "Prints a name and ID of a random user from the online list from the (optional) role.", - "Usage": "`{0}raffle` or `{0}raffle RoleName`" + "Usage": [ + "{0}raffle", + "{0}raffle RoleName" + ] }, "give": { "Cmd": "give", "Desc": "Give someone a certain amount of currency.", - "Usage": "`{0}give 1 @SomeGuy`" + "Usage": [ + "{0}give 1 @SomeGuy" + ] }, "award": { "Cmd": "award", "Desc": "Awards someone a certain amount of currency. You can also specify a role name to award currency to all users in a role.", - "Usage": "`{0}award 100 @person` or `{0}award 5 Role Of Gamblers`" + "Usage": [ + "{0}award 100 @person", + "{0}award 5 Role Of Gamblers" + ] }, "take": { "Cmd": "take", "Desc": "Takes a certain amount of currency from someone.", - "Usage": "`{0}take 1 @SomeGuy`" + "Usage": [ + "{0}take 1 @SomeGuy" + ] }, "betroll": { "Cmd": "betroll br", "Desc": "Bets a certain amount of currency and rolls a dice. Rolling over 66 yields x2 of your currency, over 90 - x4 and 100 x10.", - "Usage": "`{0}br 5`" + "Usage": [ + "{0}br 5" + ] }, "wheeloffortune": { "Cmd": "wheeloffortune wheel", "Desc": "Bets a certain amount of currency on the wheel of fortune. Wheel can stop on one of many different multipliers. Won amount is rounded down to the nearest whole number.", - "Usage": "`{0}wheel 10`" + "Usage": [ + "{0}wheel 10" + ] }, "leaderboard": { "Cmd": "leaderboard lb", "Desc": "Displays the bot's currency leaderboard.", - "Usage": "`{0}lb`" + "Usage": [ + "{0}lb" + ] }, "trivia": { "Cmd": "trivia t", "Desc": "Starts a game of trivia. You can add `nohint` to prevent hints. First player to get to 10 points wins by default. You can specify a different number. 30 seconds per question.", - "Usage": "`{0}t` or `{0}t 5 nohint`" + "Usage": [ + "{0}t", + "{0}t 5 nohint" + ] }, "tl": { "Cmd": "tl", "Desc": "Shows a current trivia leaderboard.", - "Usage": "`{0}tl`" + "Usage": [ + "{0}tl" + ] }, "tq": { "Cmd": "tq", "Desc": "Quits current trivia after current question.", - "Usage": "`{0}tq`" + "Usage": [ + "{0}tq" + ] }, "typestart": { "Cmd": "typestart", "Desc": "Starts a typing contest.", - "Usage": "`{0}typestart`" + "Usage": [ + "{0}typestart" + ] }, "typestop": { "Cmd": "typestop", "Desc": "Stops a typing contest on the current channel.", - "Usage": "`{0}typestop`" + "Usage": [ + "{0}typestop" + ] }, "typeadd": { "Cmd": "typeadd", "Desc": "Adds a new article to the typing contest.", - "Usage": "`{0}typeadd wordswords`" + "Usage": [ + "{0}typeadd wordswords" + ] }, "pollend": { "Cmd": "pollend", "Desc": "Stops active poll on this server and prints the results in this channel.", - "Usage": "`{0}pollend`" + "Usage": [ + "{0}pollend" + ] }, "pick": { "Cmd": "pick", "Desc": "Picks the currency planted in this channel. 60 seconds cooldown.", - "Usage": "`{0}pick`" + "Usage": [ + "{0}pick" + ] }, "plant": { "Cmd": "plant", "Desc": "Spend an amount of currency to plant it in this channel. Default is 1. (If bot is restarted or crashes, the currency will be lost)", - "Usage": "`{0}plant` or `{0}plant 5`" + "Usage": [ + "{0}plant", + "{0}plant 5" + ] }, "gencurrency": { "Cmd": "gencurrency gc", "Desc": "Toggles currency generation on this channel. Every posted message will have chance to spawn currency. Chance is specified by the Bot Owner. (default is 2%)", - "Usage": "`{0}gc`" + "Usage": [ + "{0}gc" + ] }, "leet": { "Cmd": "leet", "Desc": "Converts a text to leetspeak with 6 (1-6) severity levels", - "Usage": "`{0}leet 3 Hello`" + "Usage": [ + "{0}leet 3 Hello" + ] }, "choose": { "Cmd": "choose", "Desc": "Chooses a thing from a list of things", - "Usage": "`{0}choose Get up;Sleep;Sleep more`" - }, - "": { - "Cmd": null, - "Desc": null, - "Usage": null + "Usage": [ + "{0}choose Get up;Sleep;Sleep more" + ] }, "rps": { "Cmd": "rps", "Desc": "Play a game of Rocket-Paperclip-Scissors with Nadeko.", - "Usage": "`{0}rps scissors`" + "Usage": [ + "{0}rps scissors" + ] }, "linux": { "Cmd": "linux", "Desc": "Prints a customizable Linux interjection", - "Usage": "`{0}linux Spyware Windows`" + "Usage": [ + "{0}linux Spyware Windows" + ] }, "next": { "Cmd": "next n", "Desc": "Goes to the next song in the queue. You have to be in the same voice channel as the bot. You can skip multiple songs, but in that case songs will not be requeued if {0}rcs or {0}rpl is enabled.", - "Usage": "`{0}n` or `{0}n 5`" + "Usage": [ + "{0}n", + "{0}n 5" + ] }, "play": { "Cmd": "play start", "Desc": "If no arguments are specified, acts as `{0}next 1` command. If you specify a song number, it will jump to that song. If you specify a search query, acts as a `{0}q` command", - "Usage": "`{0}play` or `{0}play 5` or `{0}play Dream Of Venice`" + "Usage": [ + "{0}play", + "{0}play 5", + "{0}play Dream Of Venice" + ] }, "stop": { "Cmd": "stop s", "Desc": "Stops the music and preserves the current song index. Stays in the channel.", - "Usage": "`{0}s`" + "Usage": [ + "{0}s" + ] }, "destroy": { "Cmd": "destroy d", "Desc": "Completely stops the music and unbinds the bot from the channel. (may cause weird behaviour)", - "Usage": "`{0}d`" + "Usage": [ + "{0}d" + ] }, "pause": { "Cmd": "pause p", "Desc": "Pauses or Unpauses the song.", - "Usage": "`{0}p`" + "Usage": [ + "{0}p" + ] }, "queue": { "Cmd": "queue q yq", "Desc": "Queue a song using keywords or a link. Bot will join your voice channel. **You must be in a voice channel**.", - "Usage": "`{0}q Dream Of Venice`" + "Usage": [ + "{0}q Dream Of Venice" + ] }, "queuenext": { "Cmd": "queuenext qn", "Desc": "Works the same as `{0}queue` command, except it enqueues the new song after the current one. **You must be in a voice channel**.", - "Usage": "`{0}qn Dream Of Venice`" + "Usage": [ + "{0}qn Dream Of Venice" + ] }, "queuesearch": { "Cmd": "queuesearch qs yqs", "Desc": "Search for top 5 youtube song result using keywords, and type the index of the song to play that song. Bot will join your voice channel. **You must be in a voice channel**.", - "Usage": "`{0}qs Dream Of Venice`" + "Usage": [ + "{0}qs Dream Of Venice" + ] }, "soundcloudqueue": { "Cmd": "soundcloudqueue sq", "Desc": "Queue a soundcloud song using keywords. Bot will join your voice channel. **You must be in a voice channel**.", - "Usage": "`{0}sq Dream Of Venice`" + "Usage": [ + "{0}sq Dream Of Venice" + ] }, "listqueue": { "Cmd": "listqueue lq", "Desc": "Lists 10 currently queued songs per page. Default page is 1.", - "Usage": "`{0}lq` or `{0}lq 2`" + "Usage": [ + "{0}lq", + "{0}lq 2" + ] }, "nowplaying": { "Cmd": "nowplaying np", "Desc": "Shows the song that the bot is currently playing.", - "Usage": "`{0}np`" + "Usage": [ + "{0}np" + ] }, "volume": { "Cmd": "volume vol", "Desc": "Sets the music playback volume (0-100%)", - "Usage": "`{0}vol 50`" + "Usage": [ + "{0}vol 50" + ] }, "defvol": { "Cmd": "defvol dv", "Desc": "Sets the default music volume when music playback is started (0-100). Persists through restarts.", - "Usage": "`{0}dv 80`" + "Usage": [ + "{0}dv 80" + ] }, "max": { "Cmd": "max", "Desc": "Sets the music playback volume to 100%.", - "Usage": "`{0}max`" + "Usage": [ + "{0}max" + ] }, "half": { "Cmd": "half", "Desc": "Sets the music playback volume to 50%.", - "Usage": "`{0}half`" + "Usage": [ + "{0}half" + ] }, "playlist": { "Cmd": "playlist pl", "Desc": "Queues up to 500 songs from a youtube playlist specified by a link, or keywords.", - "Usage": "`{0}pl playlist link or name`" + "Usage": [ + "{0}pl playlist lin", + "ame" + ] }, "soundcloudpl": { "Cmd": "soundcloudpl scpl", "Desc": "Queue a Soundcloud playlist using a link.", - "Usage": "`{0}scpl soundcloudseturl`" + "Usage": [ + "{0}scpl soundcloudseturl" + ] }, "localpl": { "Cmd": "localplaylst lopl", "Desc": "Queues all songs from a directory.", - "Usage": "`{0}lopl C:/music/classical`" + "Usage": [ + "{0}lopl C:/music/classical" + ] }, "radio": { "Cmd": "radio ra", "Desc": "Queues a radio stream from a link. It can be a direct mp3 radio stream, .m3u, .pls .asx or .xspf (Usage Video: )", - "Usage": "`{0}ra radio link here`" + "Usage": [ + "{0}ra radio link here" + ] }, "local": { "Cmd": "local lo", "Desc": "Queues a local file by specifying a full path.", - "Usage": "`{0}lo C:/music/mysong.mp3`" + "Usage": [ + "{0}lo C:/music/mysong.mp3" + ] }, "move": { "Cmd": "move mv", "Desc": "Moves the bot to your voice channel. (works only if music is already playing)", - "Usage": "`{0}mv`" + "Usage": [ + "{0}mv" + ] }, "songremove": { "Cmd": "songremove srm", "Desc": "Remove a song by its # in the queue, or 'all' to remove all songs from the queue and reset the song index.", - "Usage": "`{0}srm 5`" + "Usage": [ + "{0}srm 5" + ] }, "movesong": { "Cmd": "movesong ms", "Desc": "Moves a song from one position to another.", - "Usage": "`{0}ms 5>3`" + "Usage": [ + "{0}ms 5>3" + ] }, "setmaxqueue": { "Cmd": "setmaxqueue smq", "Desc": "Sets a maximum queue size. Supply 0 or no argument to have no limit.", - "Usage": "`{0}smq 50` or `{0}smq`" + "Usage": [ + "{0}smq 50", + "{0}smq" + ] }, "cleanup": { "Cmd": "cleanup", "Desc": "Cleans up hanging voice connections.", - "Usage": "`{0}cleanup`" + "Usage": [ + "{0}cleanup" + ] }, "reptcursong": { "Cmd": "reptcursong rcs", "Desc": "Toggles repeat of current song.", - "Usage": "`{0}rcs`" + "Usage": [ + "{0}rcs" + ] }, "repeatpl": { "Cmd": "rpeatplaylst rpl", "Desc": "Toggles repeat of all songs in the queue (every song that finishes is added to the end of the queue).", - "Usage": "`{0}rpl`" + "Usage": [ + "{0}rpl" + ] }, "save": { "Cmd": "save", "Desc": "Saves a playlist under a certain name. Playlist name must be no longer than 20 characters and must not contain dashes.", - "Usage": "`{0}save classical1`" + "Usage": [ + "{0}save classical1" + ] }, "streamrole": { "Cmd": "streamrole", "Desc": "Sets a role which is monitored for streamers (FromRole), and a role to add if a user from 'FromRole' is streaming (AddRole). When a user from 'FromRole' starts streaming, they will receive an 'AddRole'. Provide no arguments to disable", - "Usage": "`{0}streamrole \"Eligible Streamers\" \"Featured Streams\"`" + "Usage": [ + "{0}streamrole \"Eligible Streamers\" \"Featured Streams\"" + ] }, "load": { "Cmd": "load", "Desc": "Loads a saved playlist using its ID. Use `{0}pls` to list all saved playlists and `{0}save` to save new ones.", - "Usage": "`{0}load 5`" + "Usage": [ + "{0}load 5" + ] }, "playlists": { "Cmd": "playlists pls", "Desc": "Lists all playlists. Paginated, 20 per page. Default page is 0.", - "Usage": "`{0}pls 1`" + "Usage": [ + "{0}pls 1" + ] }, "deleteplaylist": { "Cmd": "deleteplaylist delpls", "Desc": "Deletes a saved playlist. Works only if you made it or if you are the bot owner.", - "Usage": "`{0}delpls animu-5`" + "Usage": [ + "{0}delpls animu-5" + ] }, "goto": { "Cmd": "goto", "Desc": "Goes to a specific time in seconds in a song.", - "Usage": "`{0}goto 30`" + "Usage": [ + "{0}goto 30" + ] }, "autoplay": { "Cmd": "autoplay ap", "Desc": "Toggles autoplay - When the song is finished, automatically queue a related Youtube song. (Works only for Youtube songs and when queue is empty)", - "Usage": "`{0}ap`" + "Usage": [ + "{0}ap" + ] }, "lolchamp": { "Cmd": "lolchamp", "Desc": "Shows League Of Legends champion statistics. If there are spaces/apostrophes or in the name - omit them. Optional second parameter is a role.", - "Usage": "`{0}lolchamp Riven` or `{0}lolchamp Annie sup`" + "Usage": [ + "{0}lolchamp Riven", + "{0}lolchamp Annie sup" + ] }, "lolban": { "Cmd": "lolban", "Desc": "Shows top banned champions ordered by ban rate.", - "Usage": "`{0}lolban`" + "Usage": [ + "{0}lolban" + ] }, "smashcast": { "Cmd": "smashcast hb", "Desc": "Notifies this channel when a certain user starts streaming.", - "Usage": "`{0}smashcast SomeStreamer`" + "Usage": [ + "{0}smashcast SomeStreamer" + ] }, "twitch": { "Cmd": "twitch tw", "Desc": "Notifies this channel when a certain user starts streaming.", - "Usage": "`{0}twitch SomeStreamer`" + "Usage": [ + "{0}twitch SomeStreamer" + ] }, "mixer": { "Cmd": "mixer bm", "Desc": "Notifies this channel when a certain user starts streaming.", - "Usage": "`{0}mixer SomeStreamer`" + "Usage": [ + "{0}mixer SomeStreamer" + ] }, "removestream": { "Cmd": "removestream rms", "Desc": "Removes notifications of a certain streamer from a certain platform on this channel.", - "Usage": "`{0}rms Twitch SomeGuy` or `{0}rms mixer SomeOtherGuy`" + "Usage": [ + "{0}rms Twitch SomeGuy", + "{0}rms mixer SomeOtherGuy" + ] }, "liststreams": { "Cmd": "liststreams ls", "Desc": "Lists all streams you are following on this server.", - "Usage": "`{0}ls`" + "Usage": [ + "{0}ls" + ] }, "convert": { "Cmd": "convert", "Desc": "Convert quantities. Use `{0}convertlist` to see supported dimensions and currencies.", - "Usage": "`{0}convert m km 1000`" + "Usage": [ + "{0}convert m km 1000" + ] }, "convertlist": { "Cmd": "convertlist", "Desc": "List of the convertible dimensions and currencies.", - "Usage": "`{0}convertlist`" + "Usage": [ + "{0}convertlist" + ] }, "wowjoke": { "Cmd": "wowjoke", "Desc": "Get one of Kwoth's penultimate WoW jokes.", - "Usage": "`{0}wowjoke`" + "Usage": [ + "{0}wowjoke" + ] }, "calculate": { "Cmd": "calculate calc", "Desc": "Evaluate a mathematical expression.", - "Usage": "`{0}calc 1+1`" + "Usage": [ + "{0}calc 1+1" + ] }, "osu": { "Cmd": "osu", "Desc": "Shows osu stats for a player.", - "Usage": "`{0}osu Name` or `{0}osu Name taiko`" + "Usage": [ + "{0}osu Name", + "{0}osu Name taiko" + ] }, "osub": { "Cmd": "osub", "Desc": "Shows information about an osu beatmap.", - "Usage": "`{0}osub https://osu.ppy.sh/s/127712`" + "Usage": [ + "{0}osub https://osu.ppy.sh/s/127712" + ] }, "osu5": { "Cmd": "osu5", "Desc": "Displays a user's top 5 plays.", - "Usage": "`{0}osu5 Name`" + "Usage": [ + "{0}osu5 Name" + ] }, "pokemon": { "Cmd": "pokemon poke", "Desc": "Searches for a pokemon.", - "Usage": "`{0}poke Sylveon`" + "Usage": [ + "{0}poke Sylveon" + ] }, "pokemonability": { "Cmd": "pokemonability pokeab", "Desc": "Searches for a pokemon ability.", - "Usage": "`{0}pokeab overgrow`" + "Usage": [ + "{0}pokeab overgrow" + ] }, "memelist": { "Cmd": "memelist", "Desc": "Pulls a list of memes you can use with `{0}memegen` from http://memegen.link/templates/", - "Usage": "`{0}memelist`" + "Usage": [ + "{0}memelist" + ] }, "memegen": { "Cmd": "memegen", "Desc": "Generates a meme from memelist with top and bottom text.", - "Usage": "`{0}memegen biw \"gets iced coffee\" \"in the winter\"`" + "Usage": [ + "{0}memegen biw \"gets iced coffee\" \"in the winter\"" + ] }, "weather": { "Cmd": "weather we", "Desc": "Shows weather data for a specified city. You can also specify a country after a comma.", - "Usage": "`{0}we Moscow, RU`" + "Usage": [ + "{0}we Moscow, RU" + ] }, "youtube": { "Cmd": "youtube yt", "Desc": "Searches youtubes and shows the first result", - "Usage": "`{0}yt query`" + "Usage": [ + "{0}yt query" + ] }, "anime": { "Cmd": "anime ani aq", "Desc": "Queries anilist for an anime and shows the first result.", - "Usage": "`{0}ani aquarion evol`" + "Usage": [ + "{0}ani aquarion evol" + ] }, "imdb": { "Cmd": "imdb omdb", "Desc": "Queries omdb for movies or series, show first result.", - "Usage": "`{0}imdb Batman vs Superman`" + "Usage": [ + "{0}imdb Batman vs Superman" + ] }, "manga": { "Cmd": "manga mang mq", "Desc": "Queries anilist for a manga and shows the first result.", - "Usage": "`{0}mq Shingeki no kyojin`" + "Usage": [ + "{0}mq Shingeki no kyojin" + ] }, "randomcat": { "Cmd": "randomcat meow", "Desc": "Shows a random cat image.", - "Usage": "`{0}meow`" + "Usage": [ + "{0}meow" + ] }, "randomdog": { "Cmd": "randomdog woof", "Desc": "Shows a random dog image.", - "Usage": "`{0}woof`" + "Usage": [ + "{0}woof" + ] }, "image": { "Cmd": "image img", "Desc": "Pulls the first image found using a search parameter. Use `{0}rimg` for different results.", - "Usage": "`{0}img cute kitten`" + "Usage": [ + "{0}img cute kitten" + ] }, "randomimage": { "Cmd": "randomimage rimg", "Desc": "Pulls a random image using a search parameter.", - "Usage": "`{0}rimg cute kitten`" + "Usage": [ + "{0}rimg cute kitten" + ] }, "lmgtfy": { "Cmd": "lmgtfy", "Desc": "Google something for an idiot.", - "Usage": "`{0}lmgtfy query`" + "Usage": [ + "{0}lmgtfy query" + ] }, "google": { "Cmd": "google g", "Desc": "Get a Google search link for some terms.", - "Usage": "`{0}google query`" + "Usage": [ + "{0}google query" + ] }, "hearthstone": { "Cmd": "hearthstone hs", "Desc": "Searches for a Hearthstone card and shows its image. Takes a while to complete.", - "Usage": "`{0}hs Ysera`" + "Usage": [ + "{0}hs Ysera" + ] }, "urbandict": { "Cmd": "urbandict ud", "Desc": "Searches Urban Dictionary for a word.", - "Usage": "`{0}ud Pineapple`" + "Usage": [ + "{0}ud Pineapple" + ] }, "hashtag": { "Cmd": "#", "Desc": "Searches Tagdef.com for a hashtag.", - "Usage": "`{0}# ff`" + "Usage": [ + "{0}# ff" + ] }, "catfact": { "Cmd": "catfact", "Desc": "Shows a random catfact from ", - "Usage": "`{0}catfact`" + "Usage": [ + "{0}catfact" + ] }, "yomama": { "Cmd": "yomama ym", "Desc": "Shows a random joke from ", - "Usage": "`{0}ym`" + "Usage": [ + "{0}ym" + ] }, "randjoke": { "Cmd": "randjoke rj", "Desc": "Shows a random joke from ", - "Usage": "`{0}rj`" + "Usage": [ + "{0}rj" + ] }, "chucknorris": { "Cmd": "chucknorris cn", "Desc": "Shows a random Chuck Norris joke from ", - "Usage": "`{0}cn`" + "Usage": [ + "{0}cn" + ] }, "magicitem": { "Cmd": "magicitem mi", "Desc": "Shows a random magic item from ", - "Usage": "`{0}mi`" + "Usage": [ + "{0}mi" + ] }, "revav": { "Cmd": "revav", "Desc": "Returns a Google reverse image search for someone's avatar.", - "Usage": "`{0}revav @SomeGuy`" + "Usage": [ + "{0}revav @SomeGuy" + ] }, "revimg": { "Cmd": "revimg", "Desc": "Returns a Google reverse image search for an image from a link.", - "Usage": "`{0}revimg Image link`" + "Usage": [ + "{0}revimg Image link" + ] }, "safebooru": { "Cmd": "safebooru", "Desc": "Shows a random image from safebooru with a given tag. Tag is optional but preferred. (multiple tags are appended with +)", - "Usage": "`{0}safebooru yuri+kissing`" + "Usage": [ + "{0}safebooru yuri+kissing" + ] }, "wiki": { "Cmd": "wikipedia wiki", "Desc": "Gives you back a wikipedia link", - "Usage": "`{0}wiki query`" + "Usage": [ + "{0}wiki query" + ] }, "color": { "Cmd": "color", "Desc": "Shows you what color corresponds to that hex.", - "Usage": "`{0}color 00ff00`" + "Usage": [ + "{0}color 00ff00" + ] }, "videocall": { "Cmd": "videocall", "Desc": "Creates a private video call link for you and other mentioned people. The link is sent to mentioned people via a private message.", - "Usage": "`{0}videocall \"@the First\" \"@Xyz\"`" + "Usage": [ + "{0}videocall \"@the First\" \"@Xyz\"" + ] }, "avatar": { "Cmd": "avatar av", "Desc": "Shows a mentioned person's avatar.", - "Usage": "`{0}av @SomeGuy`" + "Usage": [ + "{0}av @SomeGuy" + ] }, "hentai": { "Cmd": "hentai", "Desc": "Shows a hentai image from a random website (gelbooru or danbooru or konachan or atfbooru or yandere) with a given tag. Tag is optional but preferred. Only 1 tag allowed.", - "Usage": "`{0}hentai yuri`" + "Usage": [ + "{0}hentai yuri" + ] }, "danbooru": { "Cmd": "danbooru", "Desc": "Shows a random hentai image from danbooru with a given tag. Tag is optional but preferred. (multiple tags are appended with +)", - "Usage": "`{0}danbooru yuri+kissing`" + "Usage": [ + "{0}danbooru yuri+kissing" + ] }, "atfbooru": { "Cmd": "atfbooru atf", "Desc": "Shows a random hentai image from atfbooru with a given tag. Tag is optional but preferred.", - "Usage": "`{0}atfbooru yuri+kissing`" + "Usage": [ + "{0}atfbooru yuri+kissing" + ] }, "gelbooru": { "Cmd": "gelbooru", "Desc": "Shows a random hentai image from gelbooru with a given tag. Tag is optional but preferred. (multiple tags are appended with +)", - "Usage": "`{0}gelbooru yuri+kissing`" + "Usage": [ + "{0}gelbooru yuri+kissing" + ] }, "rule34": { "Cmd": "rule34", "Desc": "Shows a random image from rule34.xx with a given tag. Tag is optional but preferred. (multiple tags are appended with +)", - "Usage": "`{0}rule34 yuri+kissing`" + "Usage": [ + "{0}rule34 yuri+kissing" + ] }, "e621": { "Cmd": "e621", "Desc": "Shows a random hentai image from e621.net with a given tag. Tag is optional but preferred. Use spaces for multiple tags.", - "Usage": "`{0}e621 yuri kissing`" + "Usage": [ + "{0}e621 yuri kissing" + ] }, "boobs": { "Cmd": "boobs", "Desc": "Real adult content.", - "Usage": "`{0}boobs`" + "Usage": [ + "{0}boobs" + ] }, "butts": { "Cmd": "butts ass butt", "Desc": "Real adult content.", - "Usage": "`{0}butts` or `{0}ass`" + "Usage": [ + "{0}butts", + "{0}ass" + ] }, "translate": { "Cmd": "translate trans", "Desc": "Translates from>to text. From the given language to the destination language.", - "Usage": "`{0}trans en>fr Hello`" + "Usage": [ + "{0}trans en>fr Hello" + ] }, "translangs": { "Cmd": "translangs", "Desc": "Lists the valid languages for translation.", - "Usage": "`{0}translangs`" + "Usage": [ + "{0}translangs" + ] }, "guide": { "Cmd": "readme guide", "Desc": "Sends a readme and a guide links to the channel.", - "Usage": "`{0}readme` or `{0}guide`" + "Usage": [ + "{0}readme", + "{0}guide" + ] }, "calcops": { "Cmd": "calcops", "Desc": "Shows all available operations in the `{0}calc` command", - "Usage": "`{0}calcops`" + "Usage": [ + "{0}calcops" + ] }, "delallquotes": { "Cmd": "delallq daq", "Desc": "Deletes all quotes on a specified keyword.", - "Usage": "`{0}delallq kek`" + "Usage": [ + "{0}delallq kek" + ] }, "greetdmmsg": { "Cmd": "greetdmmsg", "Desc": "Sets a new join announcement message which will be sent to the user who joined. Type `%user%` if you want to mention the new member. Using it with no message will show the current DM greet message. You can use embed json from instead of a regular text, if you want the message to be embedded.", - "Usage": "`{0}greetdmmsg Welcome to the server, %user%`." + "Usage": [ + "{0}greetdmmsg Welcome to the server, %user%`" + ] }, "cash": { "Cmd": "$ currency $$ $$$ cash cur", "Desc": "Check how much currency a person has. (Defaults to yourself)", - "Usage": "`{0}$` or `{0}$ @SomeGuy`" + "Usage": [ + "{0}$", + "{0}$ @SomeGuy" + ] }, "listperms": { "Cmd": "listperms lp", "Desc": "Lists whole permission chain with their indexes. You can specify an optional page number if there are a lot of permissions.", - "Usage": "`{0}lp` or `{0}lp 3`" + "Usage": [ + "{0}lp", + "{0}lp 3" + ] }, "allusrmdls": { "Cmd": "allusrmdls aum", "Desc": "Enable or disable all modules for a specific user.", - "Usage": "`{0}aum enable @someone`" + "Usage": [ + "{0}aum enable @someone" + ] }, "moveperm": { "Cmd": "moveperm mp", "Desc": "Moves permission from one position to another in the Permissions list.", - "Usage": "`{0}mp 2 4`" + "Usage": [ + "{0}mp 2 4" + ] }, "removeperm": { "Cmd": "removeperm rp", "Desc": "Removes a permission from a given position in the Permissions list.", - "Usage": "`{0}rp 1`" + "Usage": [ + "{0}rp 1" + ] }, "migratedata": { "Cmd": "migratedata", "Desc": "Migrate data from old bot configuration", - "Usage": "`{0}migratedata`" + "Usage": [ + "{0}migratedata" + ] }, "checkstream": { "Cmd": "checkstream cs", "Desc": "Checks if a user is online on a certain streaming platform.", - "Usage": "`{0}cs twitch MyFavStreamer`" + "Usage": [ + "{0}cs twitch MyFavStreamer" + ] }, "showemojis": { "Cmd": "showemojis se", "Desc": "Shows a name and a link to every SPECIAL emoji in the message.", - "Usage": "`{0}se A message full of SPECIAL emojis`" + "Usage": [ + "{0}se A message full of SPECIAL emojis" + ] }, "deckshuffle": { "Cmd": "deckshuffle dsh", "Desc": "Reshuffles all cards back into the deck.", - "Usage": "`{0}dsh`" + "Usage": [ + "{0}dsh" + ] }, "forwardmessages": { "Cmd": "fwmsgs", "Desc": "Toggles forwarding of non-command messages sent to bot's DM to the bot owners", - "Usage": "`{0}fwmsgs`" + "Usage": [ + "{0}fwmsgs" + ] }, "forwardtoall": { "Cmd": "fwtoall", "Desc": "Toggles whether messages will be forwarded to all bot owners or only to the first one specified in the credentials.json file", - "Usage": "`{0}fwtoall`" + "Usage": [ + "{0}fwtoall" + ] }, "resetpermissions": { "Cmd": "resetperms", "Desc": "Resets the bot's permissions module on this server to the default value.", - "Usage": "`{0}resetperms`" + "Usage": [ + "{0}resetperms" + ] }, "antiraid": { "Cmd": "antiraid", "Desc": "Sets an anti-raid protection on the server. First argument is number of people which will trigger the protection. Second one is a time interval in which that number of people needs to join in order to trigger the protection, and third argument is punishment for those people (Kick, Ban, Mute)", - "Usage": "`{0}antiraid 5 20 Kick`" + "Usage": [ + "{0}antiraid 5 20 Kick" + ] }, "antispam": { "Cmd": "antispam", "Desc": "Stops people from repeating same message X times in a row. You can specify to either mute, kick or ban the offenders. Max message count is 10.", - "Usage": "`{0}antispam 3 Mute` or `{0}antispam 4 Kick` or `{0}antispam 6 Ban`" + "Usage": [ + "{0}antispam 3 Mute", + "{0}antispam 4 Kick", + "{0}antispam 6 Ban" + ] }, "chatmute": { "Cmd": "chatmute", "Desc": "Prevents a mentioned user from chatting in text channels.", - "Usage": "`{0}chatmute @Someone`" + "Usage": [ + "{0}chatmute @Someone" + ] }, "voicemute": { "Cmd": "voicemute", "Desc": "Prevents a mentioned user from speaking in voice channels.", - "Usage": "`{0}voicemute @Someone`" + "Usage": [ + "{0}voicemute @Someone" + ] }, "konachan": { "Cmd": "konachan", "Desc": "Shows a random hentai image from konachan with a given tag. Tag is optional but preferred.", - "Usage": "`{0}konachan yuri`" + "Usage": [ + "{0}konachan yuri" + ] }, "setmuterole": { "Cmd": "setmuterole", "Desc": "Sets a name of the role which will be assigned to people who should be muted. Default is nadeko-mute.", - "Usage": "`{0}setmuterole Silenced`" + "Usage": [ + "{0}setmuterole Silenced" + ] }, "adsarm": { "Cmd": "adsarm", "Desc": "Toggles the automatic deletion of confirmations for `{0}iam` and `{0}iamn` commands.", - "Usage": "`{0}adsarm`" + "Usage": [ + "{0}adsarm" + ] }, "setstream": { "Cmd": "setstream", "Desc": "Sets the bots stream. First argument is the twitch link, second argument is stream name.", - "Usage": "`{0}setstream TWITCHLINK Hello`" + "Usage": [ + "{0}setstream TWITCHLINK Hello" + ] }, "chatunmute": { "Cmd": "chatunmute", "Desc": "Removes a mute role previously set on a mentioned user with `{0}chatmute` which prevented him from chatting in text channels.", - "Usage": "`{0}chatunmute @Someone`" + "Usage": [ + "{0}chatunmute @Someone" + ] }, "unmute": { "Cmd": "unmute", "Desc": "Unmutes a mentioned user previously muted with `{0}mute` command.", - "Usage": "`{0}unmute @Someone`" + "Usage": [ + "{0}unmute @Someone" + ] }, "xkcd": { "Cmd": "xkcd", "Desc": "Shows a XKCD comic. No arguments will retrieve random one. Number argument will retrieve a specific comic, and \"latest\" will get the latest one.", - "Usage": "`{0}xkcd` or `{0}xkcd 1400` or `{0}xkcd latest`" + "Usage": [ + "{0}xkcd", + "{0}xkcd 1400", + "{0}xkcd latest" + ] }, "placelist": { "Cmd": "placelist", "Desc": "Shows the list of available tags for the `{0}place` command.", - "Usage": "`{0}placelist`" + "Usage": [ + "{0}placelist" + ] }, "place": { "Cmd": "place", "Desc": "Shows a placeholder image of a given tag. Use `{0}placelist` to see all available tags. You can specify the width and height of the image as the last two optional arguments.", - "Usage": "`{0}place Cage` or `{0}place steven 500 400`" + "Usage": [ + "{0}place Cage", + "{0}place steven 500 400" + ] }, "togethertube": { "Cmd": "togethertube totube", "Desc": "Creates a new room on and shows the link in the chat.", - "Usage": "`{0}totube`" + "Usage": [ + "{0}totube" + ] }, "poll": { "Cmd": "poll ppoll", "Desc": "Creates a public poll which requires users to type a number of the voting option in the channel command is ran in.", - "Usage": "`{0}ppoll Question?;Answer1;Answ 2;A_3`" + "Usage": [ + "{0}ppoll Question?;Answer1;Answ 2;A_3" + ] }, "autotranslang": { "Cmd": "autotranslang atl", "Desc": "Sets your source and target language to be used with `{0}at`. Specify no arguments to remove previously set value.", - "Usage": "`{0}atl en>fr`" + "Usage": [ + "{0}atl en>fr" + ] }, "autotranslate": { "Cmd": "autotrans at", "Desc": "Starts automatic translation of all messages by users who set their `{0}atl` in this channel. You can set \"del\" argument to automatically delete all translated user messages.", - "Usage": "`{0}at` or `{0}at del`" + "Usage": [ + "{0}at", + "{0}at del" + ] }, "listquotes": { "Cmd": "listquotes liqu", "Desc": "Lists all quotes on the server ordered alphabetically. 15 Per page.", - "Usage": "`{0}liqu` or `{0}liqu 3`" + "Usage": [ + "{0}liqu", + "{0}liqu 3" + ] }, "typedel": { "Cmd": "typedel", "Desc": "Deletes a typing article given the ID.", - "Usage": "`{0}typedel 3`" + "Usage": [ + "{0}typedel 3" + ] }, "typelist": { "Cmd": "typelist", "Desc": "Lists added typing articles with their IDs. 15 per page.", - "Usage": "`{0}typelist` or `{0}typelist 3`" + "Usage": [ + "{0}typelist", + "{0}typelist 3" + ] }, "listservers": { "Cmd": "listservers", "Desc": "Lists servers the bot is on with some basic info. 15 per page.", - "Usage": "`{0}listservers 3`" + "Usage": [ + "{0}listservers 3" + ] }, "hentaibomb": { "Cmd": "hentaibomb", "Desc": "Shows a total 5 images (from gelbooru, danbooru, konachan, yandere and atfbooru). Tag is optional but preferred.", - "Usage": "`{0}hentaibomb yuri`" + "Usage": [ + "{0}hentaibomb yuri" + ] }, "cleverbot": { "Cmd": "cleverbot", "Desc": "Toggles cleverbot session. When enabled, the bot will reply to messages starting with bot mention in the server. Custom reactions starting with %mention% won't work if cleverbot is enabled.", - "Usage": "`{0}cleverbot`" + "Usage": [ + "{0}cleverbot" + ] }, "shorten": { "Cmd": "shorten", "Desc": "Attempts to shorten an URL, if it fails, returns the input URL.", - "Usage": "`{0}shorten https://google.com`" + "Usage": [ + "{0}shorten https://google.com" + ] }, "mcping": { "Cmd": "minecraftping mcping", "Desc": "Pings a minecraft server.", - "Usage": "`{0}mcping 127.0.0.1:25565`" + "Usage": [ + "{0}mcping 127.0.0.1:25565" + ] }, "mcq": { "Cmd": "minecraftquery mcq", "Desc": "Finds information about a minecraft server.", - "Usage": "`{0}mcq server:ip`" + "Usage": [ + "{0}mcq server:ip" + ] }, "wikia": { "Cmd": "wikia", "Desc": "Gives you back a wikia link", - "Usage": "`{0}wikia mtg Vigilance` or `{0}wikia mlp Dashy`" + "Usage": [ + "{0}wikia mtg Vigilance", + "{0}wikia mlp Dashy" + ] }, "yandere": { "Cmd": "yandere", "Desc": "Shows a random image from yandere with a given tag. Tag is optional but preferred. (multiple tags are appended with +)", - "Usage": "`{0}yandere tag1+tag2`" + "Usage": [ + "{0}yandere tag1+tag2" + ] }, "magicthegathering": { "Cmd": "magicthegathering mtg", "Desc": "Searches for a Magic The Gathering card.", - "Usage": "`{0}magicthegathering about face` or `{0}mtg about face`" + "Usage": [ + "{0}magicthegathering about face", + "{0}mtg about face" + ] }, "yodify": { "Cmd": "yodify yoda", "Desc": "Translates your normal sentences into Yoda styled sentences!", - "Usage": "`{0}yoda my feelings hurt`" + "Usage": [ + "{0}yoda my feelings hurt" + ] }, "attack": { "Cmd": "attack", "Desc": "Attacks a target with the given move. Use `{0}movelist` to see a list of moves your type can use.", - "Usage": "`{0}attack \"vine whip\" @someguy`" + "Usage": [ + "{0}attack \"vine whip\" @someguy" + ] }, "heal": { "Cmd": "heal", "Desc": "Heals someone. Revives those who fainted. Costs one Currency. ", - "Usage": "`{0}heal @someone`" + "Usage": [ + "{0}heal @someone" + ] }, "movelist": { "Cmd": "movelist ml", "Desc": "Lists the moves you are able to use", - "Usage": "`{0}ml`" + "Usage": [ + "{0}ml" + ] }, "settype": { "Cmd": "settype", "Desc": "Set your poketype. Costs one Currency. Provide no arguments to see a list of available types.", - "Usage": "`{0}settype fire` or `{0}settype`" + "Usage": [ + "{0}settype fire", + "{0}settype" + ] }, "type": { "Cmd": "type", "Desc": "Get the poketype of the target.", - "Usage": "`{0}type @someone`" + "Usage": [ + "{0}type @someone" + ] }, "hangmanlist": { "Cmd": "hangmanlist", "Desc": "Shows a list of hangman term types.", - "Usage": "`{0}hangmanlist`" + "Usage": [ + "{0}hangmanlist" + ] }, "hangman": { "Cmd": "hangman", "Desc": "Starts a game of hangman in the channel. Use `{0}hangmanlist` to see a list of available term types. Defaults to 'all'.", - "Usage": "`{0}hangman` or `{0}hangman movies`" + "Usage": [ + "{0}hangman", + "{0}hangman movies" + ] }, "hangmanstop": { "Cmd": "hangmanstop", "Desc": "Stops the active hangman game on this channel if it exists.", - "Usage": "`{0}hangmanstop`" + "Usage": [ + "{0}hangmanstop" + ] }, "crstatsclear": { "Cmd": "crstatsclear", "Desc": "Resets the counters on `{0}crstats`. You can specify a trigger to clear stats only for that trigger.", - "Usage": "`{0}crstatsclear` or `{0}crstatsclear rng`" + "Usage": [ + "{0}crstatsclear", + "{0}crstatsclear rng" + ] }, "crstats": { "Cmd": "crstats", "Desc": "Shows a list of custom reactions and the number of times they have been executed. Paginated with 10 per page. Use `{0}crstatsclear` to reset the counters.", - "Usage": "`{0}crstats` or `{0}crstats 3`" + "Usage": [ + "{0}crstats", + "{0}crstats 3" + ] }, "overwatch": { "Cmd": "overwatch ow", "Desc": "Show's basic stats on a player (competitive rank, playtime, level etc) Region codes are: `eu` `us` `cn` `kr`", - "Usage": "`{0}ow us Battletag#1337` or `{0}overwatch eu Battletag#2016`" + "Usage": [ + "{0}ow us Battletag#1337", + "`{0}overwatch eu Battletag#2016" + ] }, "acro": { "Cmd": "acrophobia acro", "Desc": "Starts an Acrophobia game. Second argument is optional round length in seconds. (default is 60)", - "Usage": "`{0}acro` or `{0}acro 30`" + "Usage": [ + "{0}acro", + "{0}acro 30" + ] }, "logevents": { "Cmd": "logevents", "Desc": "Shows a list of all events you can subscribe to with `{0}log`", - "Usage": "`{0}logevents`" + "Usage": [ + "{0}logevents" + ] }, "log": { "Cmd": "log", "Desc": "Toggles logging event. Disables it if it is active anywhere on the server. Enables if it isn't active. Use `{0}logevents` to see a list of all events you can subscribe to.", - "Usage": "`{0}log userpresence` or `{0}log userbanned`" + "Usage": [ + "{0}log userpresence", + "{0}log userbanned" + ] }, "fairplay": { "Cmd": "fairplay fp", "Desc": "Toggles fairplay. While enabled, the bot will prioritize songs from users who didn't have their song recently played instead of the song's position in the queue.", - "Usage": "`{0}fp`" + "Usage": [ + "{0}fp" + ] }, "songautodelete": { "Cmd": "songautodelete sad", "Desc": "Toggles whether the song should be automatically removed from the music queue when it finishes playing.", - "Usage": "`{0}sad`" + "Usage": [ + "{0}sad" + ] }, "define": { "Cmd": "define def", "Desc": "Finds a definition of a word.", - "Usage": "`{0}def heresy`" + "Usage": [ + "{0}def heresy" + ] }, "setmaxplaytime": { "Cmd": "setmaxplaytime smp", "Desc": "Sets a maximum number of seconds (>14) a song can run before being skipped automatically. Set 0 to have no limit.", - "Usage": "`{0}smp 0` or `{0}smp 270`" + "Usage": [ + "{0}smp 0", + "{0}smp 270" + ] }, "activity": { "Cmd": "activity", "Desc": "Checks for spammers.", - "Usage": "`{0}activity`" + "Usage": [ + "{0}activity" + ] }, "autohentai": { "Cmd": "autohentai", "Desc": "Posts a hentai every X seconds with a random tag from the provided tags. Use `|` to separate tags. 20 seconds minimum. Provide no arguments to disable.", - "Usage": "`{0}autohentai 30 yuri|tail|long_hair` or `{0}autohentai`" + "Usage": [ + "{0}autohentai 30 yuri|tail|long_hair", + "{0}autohentai" + ] }, "setstatus": { "Cmd": "setstatus", "Desc": "Sets the bot's status. (Online/Idle/Dnd/Invisible)", - "Usage": "`{0}setstatus Idle`" + "Usage": [ + "{0}setstatus Idle" + ] }, "rotaterolecolor": { "Cmd": "rotaterolecolor rrc", "Desc": "Rotates a roles color on an interval with a list of supplied colors. First argument is interval in seconds (Minimum 60). Second argument is a role, followed by a space-separated list of colors in hex. Provide a rolename with a 0 interval to disable.", - "Usage": "`{0}rrc 60 MyLsdRole #ff0000 #00ff00 #0000ff` or `{0}rrc 0 MyLsdRole`" + "Usage": [ + "{0}rrc 60 MyLsdRole #ff0000 #00ff00 #0000ff", + "{0}rrc 0 MyLsdRole" + ] }, "createinvite": { "Cmd": "createinvite crinv", "Desc": "Creates a new invite which has infinite max uses and never expires.", - "Usage": "`{0}crinv`" + "Usage": [ + "{0}crinv" + ] }, "pollstats": { "Cmd": "pollstats", "Desc": "Shows the poll results without stopping the poll on this server.", - "Usage": "`{0}pollstats`" + "Usage": [ + "{0}pollstats" + ] }, "repeatlist": { "Cmd": "repeatlist replst", "Desc": "Shows currently repeating messages and their indexes.", - "Usage": "`{0}repeatlist`" + "Usage": [ + "{0}repeatlist" + ] }, "repeatremove": { "Cmd": "repeatremove reprm", "Desc": "Removes a repeating message on a specified index. Use `{0}repeatlist` to see indexes.", - "Usage": "`{0}reprm 2`" + "Usage": [ + "{0}reprm 2" + ] }, "antilist": { "Cmd": "antilist antilst", "Desc": "Shows currently enabled protection features.", - "Usage": "`{0}antilist`" + "Usage": [ + "{0}antilist" + ] }, "antispamignore": { "Cmd": "antispamignore", "Desc": "Toggles whether antispam ignores current channel. Antispam must be enabled.", - "Usage": "`{0}antispamignore`" + "Usage": [ + "{0}antispamignore" + ] }, "cmdcosts": { "Cmd": "cmdcosts", "Desc": "Shows a list of command costs. Paginated with 9 commands per page.", - "Usage": "`{0}cmdcosts` or `{0}cmdcosts 2`" + "Usage": [ + "{0}cmdcosts", + "{0}cmdcosts 2" + ] }, "commandcost": { "Cmd": "commandcost cmdcost", "Desc": "Sets a price for a command. Running that command will take currency from users. Set 0 to remove the price.", - "Usage": "`{0}cmdcost 0 !!q` or `{0}cmdcost 1 {0}8ball`" + "Usage": [ + "{0}cmdcost 0 !!q", + "{0}cmdcost 1 {0}8ball" + ] }, "startevent": { "Cmd": "startevent", "Desc": "Starts one of the events seen on public nadeko. `reaction` and `sneakygamestatus` are the only 2 available now.", - "Usage": "`{0}startevent reaction`" + "Usage": [ + "{0}startevent reaction" + ] }, "slotstats": { "Cmd": "slotstats", "Desc": "Shows the total stats of the slot command for this bot's session.", - "Usage": "`{0}slotstats`" + "Usage": [ + "{0}slotstats" + ] }, "slottest": { "Cmd": "slottest", "Desc": "Tests to see how much slots payout for X number of plays.", - "Usage": "`{0}slottest 1000`" + "Usage": [ + "{0}slottest 1000" + ] }, "slot": { "Cmd": "slot", "Desc": "Play Nadeko slots. Max bet is 9999. 1.5 second cooldown per user.", - "Usage": "`{0}slot 5`" + "Usage": [ + "{0}slot 5" + ] }, "waifuclaimeraffinity": { "Cmd": "affinity", "Desc": "Sets your affinity towards someone you want to be claimed by. Setting affinity will reduce their `{0}claim` on you by 20%. You can leave second argument empty to clear your affinity. 30 minutes cooldown.", - "Usage": "`{0}affinity @MyHusband` or `{0}affinity`" + "Usage": [ + "{0}affinity @MyHusband", + "{0}affinity" + ] }, "waifuclaim": { "Cmd": "claimwaifu claim", "Desc": "Claim a waifu for yourself by spending currency. You must spend at least 10% more than her current value unless she set `{0}affinity` towards you.", - "Usage": "`{0}claim 50 @Himesama`" + "Usage": [ + "{0}claim 50 @Himesama" + ] }, "waifugift": { "Cmd": "waifugift gift gifts", "Desc": "Gift an item to someone. This will increase their waifu value by 50% of the gifted item's value if they don't have affinity set towards you, or 100% if they do. Provide no arguments to see a list of items that you can gift.", - "Usage": "`{0}gifts` or `{0}gift Rose @Himesama`" + "Usage": [ + "{0}gifts", + "{0}gift Rose @Himesama" + ] }, "waifuleaderboard": { "Cmd": "waifus waifulb", "Desc": "Shows top 9 waifus. You can specify another page to show other waifus.", - "Usage": "`{0}waifus` or `{0}waifulb 3`" + "Usage": [ + "{0}waifus", + "{0}waifulb 3" + ] }, "divorce": { "Cmd": "divorce", "Desc": "Releases your claim on a specific waifu. You will get some of the money you've spent back unless that waifu has an affinity towards you. 6 hours cooldown.", - "Usage": "`{0}divorce @CheatingSloot`" + "Usage": [ + "{0}divorce @CheatingSloot" + ] }, "waifuinfo": { "Cmd": "waifuinfo waifustats", "Desc": "Shows waifu stats for a target person. Defaults to you if no user is provided.", - "Usage": "`{0}waifuinfo @MyCrush` or `{0}waifuinfo`" + "Usage": [ + "{0}waifuinfo @MyCrush", + "{0}waifuinfo" + ] }, "mal": { "Cmd": "mal", "Desc": "Shows basic info from a MyAnimeList profile.", - "Usage": "`{0}mal straysocks`" + "Usage": [ + "{0}mal straysocks" + ] }, "setmusicchannel": { "Cmd": "setmusicchannel smch", "Desc": "Sets the current channel as the default music output channel. This will output playing, finished, paused and removed songs to that channel instead of the channel where the first song was queued in.", - "Usage": "`{0}smch`" + "Usage": [ + "{0}smch" + ] }, "reloadimages": { "Cmd": "reloadimages", "Desc": "Reloads images bot is using. Safe to use even when bot is being used heavily.", - "Usage": "`{0}reloadimages`" + "Usage": [ + "{0}reloadimages" + ] }, "shardstats": { "Cmd": "shardstats", "Desc": "Stats for shards. Paginated with 25 shards per page.", - "Usage": "`{0}shardstats` or `{0}shardstats 2`" + "Usage": [ + "{0}shardstats", + "{0}shardstats 2" + ] }, "restartshard": { "Cmd": "restartshard", "Desc": "Try (re)connecting a shard with a certain shardid when it dies. No one knows will it work. Keep an eye on the console for errors.", - "Usage": "`{0}restartshard 2`" + "Usage": [ + "{0}restartshard 2" + ] }, "shardid": { "Cmd": "shardid", "Desc": "Shows which shard is a certain guild on, by guildid.", - "Usage": "`{0}shardid 117523346618318850`" + "Usage": [ + "{0}shardid 117523346618318850" + ] }, "tictactoe": { "Cmd": "tictactoe ttt", "Desc": "Starts a game of tic tac toe. Another user must run the command in the same channel in order to accept the challenge. Use numbers 1-9 to play. 15 seconds per move.", - "Usage": "{0}ttt" + "Usage": [ + "0}tt" + ] }, "timezones": { "Cmd": "timezones", "Desc": "Lists all timezones available on the system to be used with `{0}timezone`.", - "Usage": "`{0}timezones`" + "Usage": [ + "{0}timezones" + ] }, "timezone": { "Cmd": "timezone", "Desc": "Sets this guilds timezone. This affects bot's time output in this server (logs, etc..)", - "Usage": "`{0}timezone` or `{0}timezone GMT Standard Time`" + "Usage": [ + "{0}timezone", + "{0}timezone GMT Standard Time" + ] }, "languagesetdefault": { "Cmd": "langsetdefault langsetd", "Desc": "Sets the bot's default response language. All servers which use a default locale will use this one. Setting to `default` will use the host's current culture. Provide no arguments to see currently set language.", - "Usage": "`{0}langsetd en-US` or `{0}langsetd default`" + "Usage": [ + "{0}langsetd en-US", + "{0}langsetd default" + ] }, "languageset": { "Cmd": "languageset langset", "Desc": "Sets this server's response language. If bot's response strings have been translated to that language, bot will use that language in this server. Reset by using `default` as the locale name. Provide no arguments to see currently set language.", - "Usage": "`{0}langset de-DE ` or `{0}langset default`" + "Usage": [ + "{0}langset de-DE ", + "{0}langset default" + ] }, "languageslist": { "Cmd": "languageslist langli", "Desc": "List of languages for which translation (or part of it) exist atm.", - "Usage": "`{0}langli`" + "Usage": [ + "{0}langli" + ] }, "rategirl": { "Cmd": "rategirl", "Desc": "Use the universal hot-crazy wife zone matrix to determine the girl's worth. It is everything young men need to know about women. At any moment in time, any woman you have previously located on this chart can vanish from that location and appear anywhere else on the chart.", - "Usage": "`{0}rategirl @SomeGurl`" + "Usage": [ + "{0}rategirl @SomeGurl" + ] }, "lucky7test": { "Cmd": "lucky7test l7t", "Desc": "Tests the l7 command.", - "Usage": "`{0}l7t 10000`" + "Usage": [ + "{0}l7t 10000" + ] }, "lucky7": { "Cmd": "lucky7 l7", "Desc": "Bet currency on the game and start rolling 3 sided dice. At any point you can choose to [m]ove (roll again) or [s]tay (get the amount bet times the current multiplier).", - "Usage": "`{0}l7 10` or `{0}l7 move` or `{0}l7 s`" + "Usage": [ + "{0}l7 10", + "{0}l7 move", + "{0}l7 s" + ] }, "vcrolelist": { "Cmd": "vcrolelist", "Desc": "Shows a list of currently set voice channel roles.", - "Usage": "`{0}vcrolelist`" + "Usage": [ + "{0}vcrolelist" + ] }, "vcrole": { "Cmd": "vcrole", "Desc": "Sets or resets a role which will be given to users who join the voice channel you're in when you run this command. Provide no role name to disable. You must be in a voice channel to run this command.", - "Usage": "`{0}vcrole SomeRole` or `{0}vcrole`" + "Usage": [ + "{0}vcrole SomeRole", + "{0}vcrole" + ] }, "crad": { "Cmd": "crad", "Desc": "Toggles whether the message triggering the custom reaction will be automatically deleted.", - "Usage": "`{0}crad 59`" + "Usage": [ + "{0}crad 59" + ] }, "crdm": { "Cmd": "crdm", "Desc": "Toggles whether the response message of the custom reaction will be sent as a direct message.", - "Usage": "`{0}crdm 44`" + "Usage": [ + "{0}crdm 44" + ] }, "crca": { "Cmd": "crca", "Desc": "Toggles whether the custom reaction will trigger if the triggering message contains the keyword (instead of only starting with it).", - "Usage": "`{0}crca 44`" + "Usage": [ + "{0}crca 44" + ] }, "aliaslist": { "Cmd": "aliaslist cmdmaplist aliases", "Desc": "Shows the list of currently set aliases. Paginated.", - "Usage": "`{0}aliaslist` or `{0}aliaslist 3`" + "Usage": [ + "{0}aliaslist", + "{0}aliaslist 3" + ] }, "alias": { "Cmd": "alias cmdmap", "Desc": "Create a custom alias for a certain Nadeko command. Provide no alias to remove the existing one.", - "Usage": "`{0}alias allin $bf 100 h` or `{0}alias \"linux thingy\" >loonix Spyware Windows`" + "Usage": [ + "{0}alias allin $bf 100 h", + "{0}alias \"linux thingy\" >loonix Spyware Windows" + ] }, "warnlog": { "Cmd": "warnlog", "Desc": "See a list of warnings of a certain user.", - "Usage": "`{0}warnlog @b1nzy`" + "Usage": [ + "{0}warnlog @b1nzy" + ] }, "warnlogall": { "Cmd": "warnlogall", "Desc": "See a list of all warnings on the server. 15 users per page.", - "Usage": "`{0}warnlogall` or `{0}warnlogall 2`" + "Usage": [ + "{0}warnlogall", + "{0}warnlogall 2" + ] }, "warn": { "Cmd": "warn", "Desc": "Warns a user.", - "Usage": "`{0}warn @b1nzy Very rude person`" + "Usage": [ + "{0}warn @b1nzy Very rude person" + ] }, "startupcommandadd": { "Cmd": "scadd", "Desc": "Adds a command to the list of commands which will be executed automatically in the current channel, in the order they were added in, by the bot when it startups up.", - "Usage": "`{0}scadd .stats`" + "Usage": [ + "{0}scadd .stats" + ] }, "startupcommandremove": { "Cmd": "scrm", "Desc": "Removes a startup command with the provided command text.", - "Usage": "`{0}scrm .stats`" + "Usage": [ + "{0}scrm .stats" + ] }, "startupcommandsclear": { "Cmd": "scclr", "Desc": "Removes all startup commands.", - "Usage": "`{0}scclr`" + "Usage": [ + "{0}scclr" + ] }, "startupcommands": { "Cmd": "sclist", "Desc": "Lists all startup commands in the order they will be executed in.", - "Usage": "`{0}sclist`" + "Usage": [ + "{0}sclist" + ] }, "unban": { "Cmd": "unban", "Desc": "Unbans a user with the provided user#discrim or id.", - "Usage": "`{0}unban kwoth#1234` or `{0}unban 123123123`" + "Usage": [ + "{0}unban kwoth#1234", + "{0}unban 123123123" + ] }, "wait": { "Cmd": "wait", "Desc": "Used only as a startup command. Waits a certain number of miliseconds before continuing the execution of the following startup commands.", - "Usage": "`{0}wait 3000`" + "Usage": [ + "{0}wait 3000" + ] }, "warnclear": { "Cmd": "warnclear warnc", "Desc": "Clears all warnings from a certain user.", - "Usage": "`{0}warnclear @PoorDude`" + "Usage": [ + "{0}warnclear @PoorDude" + ] }, "warnpunishlist": { "Cmd": "warnpunishlist warnpl", "Desc": "Lists punishments for warnings.", - "Usage": "`{0}warnpunishlist`" + "Usage": [ + "{0}warnpunishlist" + ] }, "warnpunish": { "Cmd": "warnpunish warnp", "Desc": "Sets a punishment for a certain number of warnings. Provide no punishment to remove.", - "Usage": "`{0}warnpunish 5 Ban` or `{0}warnpunish 3`" + "Usage": [ + "{0}warnpunish 5 Ban", + "{0}warnpunish 3" + ] }, "claimpatreonrewards": { "Cmd": "clparew", "Desc": "Claim patreon rewards. If you're subscribed to bot owner's patreon you can use this command to claim your rewards - assuming bot owner did setup has their patreon key.", - "Usage": "`{0}clparew`" + "Usage": [ + "{0}clparew" + ] }, "ping": { "Cmd": "ping", "Desc": "Ping the bot to see if there are latency issues.", - "Usage": "`{0}ping`" + "Usage": [ + "{0}ping" + ] }, "slowmodewhitelist": { "Cmd": "slowmodewl", "Desc": "Ignores a role or a user from the slowmode feature.", - "Usage": "`{0}slowmodewl SomeRole` or `{0}slowmodewl AdminDude`" + "Usage": [ + "{0}slowmodewl SomeRole", + "{0}slowmodewl AdminDude" + ] }, "time": { "Cmd": "time", "Desc": "Shows the current time and timezone in the specified location.", - "Usage": "`{0}time London, UK`" + "Usage": [ + "{0}time London, UK" + ] }, "patreonrewardsreload": { "Cmd": "parewrel", "Desc": "Forces the update of the list of patrons who are eligible for the reward.", - "Usage": "`{0}parewrel`" + "Usage": [ + "{0}parewrel" + ] }, "shopadd": { "Cmd": "shopadd", "Desc": "Adds an item to the shop by specifying type price and name. Available types are role and list.", - "Usage": "`{0}shopadd role 1000 Rich`" + "Usage": [ + "{0}shopadd role 1000 Rich" + ] }, "shopremove": { "Cmd": "shoprem shoprm", "Desc": "Removes an item from the shop by its ID.", - "Usage": "`{0}shoprm 1`" + "Usage": [ + "{0}shoprm 1" + ] }, "shop": { "Cmd": "shop", "Desc": "Lists this server's administrators' shop. Paginated.", - "Usage": "`{0}shop` or `{0}shop 2`" + "Usage": [ + "{0}shop", + "{0}shop 2" + ] }, "rolehoist": { "Cmd": "rolehoist rh", "Desc": "Toggles whether this role is displayed in the sidebar or not.", - "Usage": "`{0}rh Guests` or `{0}rh \"Space Wizards\"`" + "Usage": [ + "{0}rh Guests", + "{0}rh \"Space Wizards\"" + ] }, "buy": { "Cmd": "buy", "Desc": "Buys an item from the shop on a given index. If buying items, make sure that the bot can DM you.", - "Usage": "`{0}buy 2`" + "Usage": [ + "{0}buy 2" + ] }, "gamevoicechannel": { "Cmd": "gvc", "Desc": "Toggles game voice channel feature in the voice channel you're currently in. Users who join the game voice channel will get automatically redirected to the voice channel with the name of their current game, if it exists. Can't move users to channels that the bot has no connect permission for. One per server.", - "Usage": "`{0}gvc`" + "Usage": [ + "{0}gvc" + ] }, "shoplistadd": { "Cmd": "shoplistadd", "Desc": "Adds an item to the list of items for sale in the shop entry given the index. You usually want to run this command in the secret channel, so that the unique items are not leaked.", - "Usage": "`{0}shoplistadd 1 Uni-que-Steam-Key`" + "Usage": [ + "{0}shoplistadd 1 Uni-que-Steam-Key" + ] }, "gcmd": { "Cmd": "globalcommand gcmd", "Desc": "Toggles whether a command can be used on any server.", - "Usage": "`{0}gcmd .stats`" + "Usage": [ + "{0}gcmd .stats" + ] }, "gmod": { "Cmd": "globalmodule gmod", "Desc": "Toggles whether a module can be used on any server.", - "Usage": "`{0}gmod nsfw`" + "Usage": [ + "{0}gmod nsfw" + ] }, "lgp": { "Cmd": "listglobalperms lgp", "Desc": "Lists global permissions set by the bot owner.", - "Usage": "`{0}lgp`" + "Usage": [ + "{0}lgp" + ] }, "resetglobalpermissions": { "Cmd": "resetglobalperms", "Desc": "Resets global permissions set by bot owner.", - "Usage": "`{0}resetglobalperms`" + "Usage": [ + "{0}resetglobalperms" + ] }, "prefix": { "Cmd": "prefix", "Desc": "Sets this server's prefix for all bot commands. Provide no arguments to see the current server prefix.", - "Usage": "`{0}prefix +`" + "Usage": [ + "{0}prefix +" + ] }, "defprefix": { "Cmd": "defprefix", "Desc": "Sets bot's default prefix for all bot commands. Provide no arguments to see the current default prefix. This will not change this server's current prefix.", - "Usage": "`{0}defprefix +`" + "Usage": [ + "{0}defprefix +" + ] }, "verboseerror": { "Cmd": "verboseerror ve", "Desc": "Toggles whether the bot should print command errors when a command is incorrectly used.", - "Usage": "`{0}ve`" + "Usage": [ + "{0}ve" + ] }, "streamrolekeyword": { "Cmd": "streamrolekw srkw", "Desc": "Sets keyword which is required in the stream's title in order for the streamrole to apply. Provide no keyword in order to reset.", - "Usage": "`{0}srkw` or `{0}srkw PUBG`" + "Usage": [ + "{0}srkw", + "{0}srkw PUBG" + ] }, "streamroleblacklist": { "Cmd": "streamrolebl srbl", "Desc": "Adds or removes a blacklisted user. Blacklisted users will never receive the stream role.", - "Usage": "`{0}srbl add @b1nzy#1234` or `{0}srbl rem @b1nzy#1234`" + "Usage": [ + "{0}srbl add @b1nzy#1234", + "{0}srbl rem @b1nzy#1234" + ] }, "streamrolewhitelist": { "Cmd": "streamrolewl srwl", "Desc": "Adds or removes a whitelisted user. Whitelisted users will receive the stream role even if they don't have the specified keyword in their stream title.", - "Usage": "`{0}srwl add @b1nzy#1234` or `{0}srwl rem @b1nzy#1234`" + "Usage": [ + "{0}srwl add @b1nzy#1234", + "{0}srwl rem @b1nzy#1234" + ] }, "botconfigedit": { "Cmd": "botconfigedit bce", "Desc": "Sets one of available bot config settings to a specified value. Use the command without any parameters to get a list of available settings.", - "Usage": "`{0}bce CurrencyName b1nzy` or `{0}bce`" + "Usage": [ + "{0}bce CurrencyName b1nzy", + "{0}bce" + ] }, "nsfwtagblacklist": { "Cmd": "nsfwtagbl nsfwtbl", "Desc": "Toggles whether the tag is blacklisted or not in nsfw searches. Provide no parameters to see the list of blacklisted tags.", - "Usage": "`{0}nsfwtbl poop`" + "Usage": [ + "{0}nsfwtbl poop" + ] }, "experience": { "Cmd": "experience xp", "Desc": "Shows your xp stats. Specify the user to show that user's stats instead.", - "Usage": "`{0}xp`" + "Usage": [ + "{0}xp" + ] }, "xpexclusionlist": { "Cmd": "xpexclusionlist xpexl", "Desc": "Shows the roles and channels excluded from the XP system on this server, as well as whether the whole server is excluded.", - "Usage": "`{0}xpexl`" + "Usage": [ + "{0}xpexl" + ] }, "xpexclude": { "Cmd": "xpexclude xpex", "Desc": "Exclude a channel, role or current server from the xp system.", - "Usage": "`{0}xpex Role Excluded-Role` `{0}xpex Server`" + "Usage": [ + "{0}xpex Role Excluded-Role` `{0}xpex Server" + ] }, "xpnotify": { "Cmd": "xpnotify xpn", "Desc": "Sets how the bot should notify you when you get a `server` or `global` level. You can set `dm` (for the bot to send a direct message), `channel` (to get notified in the channel you sent the last message in) or `none` to disable.", - "Usage": "`{0}xpn global dm` `{0}xpn server channel`" + "Usage": [ + "{0}xpn global dm` `{0}xpn server channel" + ] }, "xprolerewards": { "Cmd": "xprolerewards xprrs", "Desc": "Shows currently set role rewards.", - "Usage": "`{0}xprrs`" + "Usage": [ + "{0}xprrs" + ] }, "xprolereward": { "Cmd": "xprolereward xprr", "Desc": "Sets a role reward on a specified level. Provide no role name in order to remove the role reward.", - "Usage": "`{0}xprr 3 Social`" + "Usage": [ + "{0}xprr 3 Social" + ] }, "xpleaderboard": { "Cmd": "xpleaderboard xplb", "Desc": "Shows current server's xp leaderboard.", - "Usage": "`{0}xplb`" + "Usage": [ + "{0}xplb" + ] }, "xpgloballeaderboard": { "Cmd": "xpgleaderboard xpglb", "Desc": "Shows the global xp leaderboard.", - "Usage": "`{0}xpglb`" + "Usage": [ + "{0}xpglb" + ] }, "xpadd": { "Cmd": "xpadd", "Desc": "Adds xp to a user on the server. This does not affect their global ranking. You can use negative values.", - "Usage": "`{0}xpadd 100 @b1nzy`" + "Usage": [ + "{0}xpadd 100 @b1nzy" + ] }, "clubcreate": { "Cmd": "clubcreate", "Desc": "Creates a club. You must be atleast level 5 and not be in the club already.", - "Usage": "`{0}clubcreate b1nzy's friends`" + "Usage": [ + "{0}clubcreate b1nzy's friends" + ] }, "clubinformation": { "Cmd": "clubinfo", "Desc": "Shows information about the club.", - "Usage": "`{0}clubinfo b1nzy's friends#123`" + "Usage": [ + "{0}clubinfo b1nzy's friends#123" + ] }, "clubapply": { "Cmd": "clubapply", "Desc": "Apply to join a club. You must meet that club's minimum level requirement, and not be on its ban list.", - "Usage": "`{0}clubapply b1nzy's friends#123`" + "Usage": [ + "{0}clubapply b1nzy's friends#123" + ] }, "clubaccept": { "Cmd": "clubaccept", "Desc": "Accept a user who applied to your club.", - "Usage": "`{0}clubaccept b1nzy#1337`" + "Usage": [ + "{0}clubaccept b1nzy#1337" + ] }, "clubleave": { "Cmd": "clubleave", "Desc": "Leaves the club you're currently in.", - "Usage": "`{0}clubleave`" + "Usage": [ + "{0}clubleave" + ] }, "clubdisband": { "Cmd": "clubdisband", "Desc": "Disbands the club you're the owner of. This action is irreversible.", - "Usage": "`{0}clubdisband`" + "Usage": [ + "{0}clubdisband" + ] }, "clubkick": { "Cmd": "clubkick", "Desc": "Kicks the user from the club. You must be the club owner. They will be able to apply again.", - "Usage": "`{0}clubkick b1nzy#1337`" + "Usage": [ + "{0}clubkick b1nzy#1337" + ] }, "clubban": { "Cmd": "clubban", "Desc": "Bans the user from the club. You must be the club owner. They will not be able to apply again.", - "Usage": "`{0}clubban b1nzy#1337`" + "Usage": [ + "{0}clubban b1nzy#1337" + ] }, "clubunban": { "Cmd": "clubunban", "Desc": "Unbans the previously banned user from the club. You must be the club owner.", - "Usage": "`{0}clubunban b1nzy#1337`" + "Usage": [ + "{0}clubunban b1nzy#1337" + ] }, "clublevelreq": { "Cmd": "clublevelreq", "Desc": "Sets the club required level to apply to join the club. You must be club owner. You can't set this number below 5.", - "Usage": "`{0}clublevelreq 7`" + "Usage": [ + "{0}clublevelreq 7" + ] }, "clubicon": { "Cmd": "clubicon", "Desc": "Sets the club icon.", - "Usage": "`{0}clubicon https://i.imgur.com/htfDMfU.png`" + "Usage": [ + "{0}clubicon https://i.imgur.com/htfDMfU.png" + ] }, "clubapps": { "Cmd": "clubapps", "Desc": "Shows the list of users who have applied to your club. Paginated. You must be club owner to use this command.", - "Usage": "`{0}clubapps 2`" + "Usage": [ + "{0}clubapps 2" + ] }, "clubbans": { "Cmd": "clubbans", "Desc": "Shows the list of users who have banned from your club. Paginated. You must be club owner to use this command.", - "Usage": "`{0}clubbans 2`" + "Usage": [ + "{0}clubbans 2" + ] }, "clubleaderboard": { "Cmd": "clublb", "Desc": "Shows club rankings on the specified page.", - "Usage": "`{0}clublb 2`" + "Usage": [ + "{0}clublb 2" + ] }, "nsfwclearcache": { "Cmd": "nsfwcc", "Desc": "Clears nsfw cache.", - "Usage": "`{0}nsfwcc`" + "Usage": [ + "{0}nsfwcc" + ] }, "clubadmin": { "Cmd": "clubadmin", "Desc": "Assigns (or unassigns) staff role to the member of the club. Admins can ban, kick and accept applications.", - "Usage": "`{0}clubadmin`" + "Usage": [ + "{0}clubadmin" + ] }, "autoboobs": { "Cmd": "autoboobs", "Desc": "Posts a boobs every X seconds. 20 seconds minimum. Provide no arguments to disable.", - "Usage": "`{0}autoboobs 30` or `{0}autoboobs`" + "Usage": [ + "{0}autoboobs 30", + "{0}autoboobs" + ] }, "autobutts": { "Cmd": "autobutts", "Desc": "Posts a butts every X seconds. 20 seconds minimum. Provide no arguments to disable.", - "Usage": "`{0}autobutts 30` or `{0}autobutts`" + "Usage": [ + "{0}autobutts 30", + "{0}autobutts" + ] }, "eightball": { - "cmd": "8ball", - "desc": "Ask the 8ball a yes/no question.", - "usage": "`{0}8ball`" + "Cmd": "8ball", + "Desc": "Ask the 8ball a yes/no question.", + "Usage": [ + "{0}8ball" + ] } } \ No newline at end of file From 45b696bab8319e21009ec568fff58fb5b8729fa6 Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Fri, 22 Sep 2017 06:59:57 +0200 Subject: [PATCH 322/346] support for rss and atoms feeds added. --- .../20170921185313_feeds.Designer.cs | 1985 +++++++++++++++++ .../Migrations/20170921185313_feeds.cs | 59 + .../NadekoSqliteContextModelSnapshot.cs | 42 +- .../Modules/Music/Services/MusicService.cs | 56 - .../Modules/Searches/FeedCommands.cs | 106 + .../Modules/Searches/Services/FeedsService.cs | 213 ++ src/NadekoBot/NadekoBot.csproj | 1 + src/NadekoBot/Properties/launchSettings.json | 4 + .../Services/Database/Models/FeedSub.cs | 23 + .../Services/Database/Models/GuildConfig.cs | 1 + .../Services/Database/NadekoContext.cs | 4 + .../Impl/GuildConfigRepository.cs | 2 + src/NadekoBot/Services/Impl/Localization.cs | 2 +- src/NadekoBot/_Extensions/StringExtensions.cs | 5 + .../_strings/ResponseStrings.en-US.json | 7 +- src/NadekoBot/data/command_strings.json | 21 + 16 files changed, 2469 insertions(+), 62 deletions(-) create mode 100644 src/NadekoBot/Migrations/20170921185313_feeds.Designer.cs create mode 100644 src/NadekoBot/Migrations/20170921185313_feeds.cs create mode 100644 src/NadekoBot/Modules/Searches/FeedCommands.cs create mode 100644 src/NadekoBot/Modules/Searches/Services/FeedsService.cs create mode 100644 src/NadekoBot/Services/Database/Models/FeedSub.cs diff --git a/src/NadekoBot/Migrations/20170921185313_feeds.Designer.cs b/src/NadekoBot/Migrations/20170921185313_feeds.Designer.cs new file mode 100644 index 00000000..86ee94cc --- /dev/null +++ b/src/NadekoBot/Migrations/20170921185313_feeds.Designer.cs @@ -0,0 +1,1985 @@ +// +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage; +using Microsoft.EntityFrameworkCore.Storage.Internal; +using NadekoBot.Services.Database; +using NadekoBot.Services.Database.Models; +using System; + +namespace NadekoBot.Migrations +{ + [DbContext(typeof(NadekoContext))] + [Migration("20170921185313_feeds")] + partial class feeds + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "2.0.0-rtm-26452"); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.AntiRaidSetting", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("Action"); + + b.Property("DateAdded"); + + b.Property("GuildConfigId"); + + b.Property("Seconds"); + + b.Property("UserThreshold"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId") + .IsUnique(); + + b.ToTable("AntiRaidSetting"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.AntiSpamIgnore", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AntiSpamSettingId"); + + b.Property("ChannelId"); + + b.Property("DateAdded"); + + b.HasKey("Id"); + + b.HasIndex("AntiSpamSettingId"); + + b.ToTable("AntiSpamIgnore"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.AntiSpamSetting", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("Action"); + + b.Property("DateAdded"); + + b.Property("GuildConfigId"); + + b.Property("MessageThreshold"); + + b.Property("MuteTime"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId") + .IsUnique(); + + b.ToTable("AntiSpamSetting"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.BlacklistItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("BotConfigId"); + + b.Property("DateAdded"); + + b.Property("ItemId"); + + b.Property("Type"); + + b.HasKey("Id"); + + b.HasIndex("BotConfigId"); + + b.ToTable("BlacklistItem"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.BlockedCmdOrMdl", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("BotConfigId"); + + b.Property("BotConfigId1"); + + b.Property("DateAdded"); + + b.Property("Name"); + + b.HasKey("Id"); + + b.HasIndex("BotConfigId"); + + b.HasIndex("BotConfigId1"); + + b.ToTable("BlockedCmdOrMdl"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.BotConfig", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("BetflipMultiplier"); + + b.Property("Betroll100Multiplier"); + + b.Property("Betroll67Multiplier"); + + b.Property("Betroll91Multiplier"); + + b.Property("BufferSize"); + + b.Property("CurrencyDropAmount"); + + b.Property("CurrencyDropAmountMax"); + + b.Property("CurrencyGenerationChance"); + + b.Property("CurrencyGenerationCooldown"); + + b.Property("CurrencyName"); + + b.Property("CurrencyPluralName"); + + b.Property("CurrencySign"); + + b.Property("CustomReactionsStartWith"); + + b.Property("DMHelpString"); + + b.Property("DateAdded"); + + b.Property("DefaultPrefix"); + + b.Property("ErrorColor"); + + b.Property("ForwardMessages"); + + b.Property("ForwardToAllOwners"); + + b.Property("HelpString"); + + b.Property("Locale"); + + b.Property("MigrationVersion"); + + b.Property("MinimumBetAmount"); + + b.Property("OkColor"); + + b.Property("PermissionVersion"); + + b.Property("RemindMessageFormat"); + + b.Property("RotatingStatuses"); + + b.Property("TriviaCurrencyReward"); + + b.Property("XpMinutesTimeout") + .ValueGeneratedOnAdd() + .HasDefaultValue(5); + + b.Property("XpPerMessage") + .ValueGeneratedOnAdd() + .HasDefaultValue(3); + + b.HasKey("Id"); + + b.ToTable("BotConfig"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.ClashCaller", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("BaseDestroyed"); + + b.Property("CallUser"); + + b.Property("ClashWarId"); + + b.Property("DateAdded"); + + b.Property("SequenceNumber"); + + b.Property("Stars"); + + b.Property("TimeAdded"); + + b.HasKey("Id"); + + b.HasIndex("ClashWarId"); + + b.ToTable("ClashCallers"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.ClashWar", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ChannelId"); + + b.Property("DateAdded"); + + b.Property("EnemyClan"); + + b.Property("GuildId"); + + b.Property("Size"); + + b.Property("StartedAt"); + + b.Property("WarState"); + + b.HasKey("Id"); + + b.ToTable("ClashOfClans"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.ClubApplicants", b => + { + b.Property("ClubId"); + + b.Property("UserId"); + + b.HasKey("ClubId", "UserId"); + + b.HasIndex("UserId"); + + b.ToTable("ClubApplicants"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.ClubBans", b => + { + b.Property("ClubId"); + + b.Property("UserId"); + + b.HasKey("ClubId", "UserId"); + + b.HasIndex("UserId"); + + b.ToTable("ClubBans"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.ClubInfo", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("Discrim"); + + b.Property("ImageUrl"); + + b.Property("MinimumLevelReq"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(20); + + b.Property("OwnerId"); + + b.Property("Xp"); + + b.HasKey("Id"); + + b.HasAlternateKey("Name", "Discrim"); + + b.HasIndex("OwnerId") + .IsUnique(); + + b.ToTable("Clubs"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.CommandAlias", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("GuildConfigId"); + + b.Property("Mapping"); + + b.Property("Trigger"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.ToTable("CommandAlias"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.CommandCooldown", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("CommandName"); + + b.Property("DateAdded"); + + b.Property("GuildConfigId"); + + b.Property("Seconds"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.ToTable("CommandCooldown"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.CommandPrice", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("BotConfigId"); + + b.Property("CommandName"); + + b.Property("DateAdded"); + + b.Property("Price"); + + b.HasKey("Id"); + + b.HasIndex("BotConfigId"); + + b.HasIndex("Price") + .IsUnique(); + + b.ToTable("CommandPrice"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.ConvertUnit", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("InternalTrigger"); + + b.Property("Modifier"); + + b.Property("UnitType"); + + b.HasKey("Id"); + + b.ToTable("ConversionUnits"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.Currency", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("Amount"); + + b.Property("DateAdded"); + + b.Property("UserId"); + + b.HasKey("Id"); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("Currency"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.CurrencyTransaction", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("Amount"); + + b.Property("DateAdded"); + + b.Property("Reason"); + + b.Property("UserId"); + + b.HasKey("Id"); + + b.ToTable("CurrencyTransactions"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.CustomReaction", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AutoDeleteTrigger"); + + b.Property("ContainsAnywhere"); + + b.Property("DateAdded"); + + b.Property("DmResponse"); + + b.Property("GuildId"); + + b.Property("IsRegex"); + + b.Property("OwnerOnly"); + + b.Property("Response"); + + b.Property("Trigger"); + + b.HasKey("Id"); + + b.ToTable("CustomReactions"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.DiscordUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AvatarId"); + + b.Property("ClubId"); + + b.Property("DateAdded"); + + b.Property("Discriminator"); + + b.Property("IsClubAdmin"); + + b.Property("LastLevelUp") + .ValueGeneratedOnAdd() + .HasDefaultValue(new DateTime(2017, 9, 21, 20, 53, 13, 305, DateTimeKind.Local)); + + b.Property("LastXpGain"); + + b.Property("NotifyOnLevelUp"); + + b.Property("TotalXp"); + + b.Property("UserId"); + + b.Property("Username"); + + b.HasKey("Id"); + + b.HasAlternateKey("UserId"); + + b.HasIndex("ClubId"); + + b.ToTable("DiscordUser"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.Donator", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("Amount"); + + b.Property("DateAdded"); + + b.Property("Name"); + + b.Property("UserId"); + + b.HasKey("Id"); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("Donators"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.EightBallResponse", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("BotConfigId"); + + b.Property("DateAdded"); + + b.Property("Text"); + + b.HasKey("Id"); + + b.HasIndex("BotConfigId"); + + b.ToTable("EightBallResponses"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.ExcludedItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("ItemId"); + + b.Property("ItemType"); + + b.Property("XpSettingsId"); + + b.HasKey("Id"); + + b.HasIndex("XpSettingsId"); + + b.ToTable("ExcludedItem"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.FeedSub", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ChannelId"); + + b.Property("DateAdded"); + + b.Property("GuildConfigId"); + + b.Property("Url") + .IsRequired(); + + b.HasKey("Id"); + + b.HasAlternateKey("GuildConfigId", "Url"); + + b.ToTable("FeedSub"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.FilterChannelId", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ChannelId"); + + b.Property("DateAdded"); + + b.Property("GuildConfigId"); + + b.Property("GuildConfigId1"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.HasIndex("GuildConfigId1"); + + b.ToTable("FilterChannelId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.FilteredWord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("GuildConfigId"); + + b.Property("Word"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.ToTable("FilteredWord"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.FollowedStream", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ChannelId"); + + b.Property("DateAdded"); + + b.Property("GuildConfigId"); + + b.Property("GuildId"); + + b.Property("Type"); + + b.Property("Username"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.ToTable("FollowedStream"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.GCChannelId", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ChannelId"); + + b.Property("DateAdded"); + + b.Property("GuildConfigId"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.ToTable("GCChannelId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.GuildConfig", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AutoAssignRoleId"); + + b.Property("AutoDeleteByeMessages"); + + b.Property("AutoDeleteByeMessagesTimer"); + + b.Property("AutoDeleteGreetMessages"); + + b.Property("AutoDeleteGreetMessagesTimer"); + + b.Property("AutoDeleteSelfAssignedRoleMessages"); + + b.Property("ByeMessageChannelId"); + + b.Property("ChannelByeMessageText"); + + b.Property("ChannelGreetMessageText"); + + b.Property("CleverbotEnabled"); + + b.Property("DateAdded"); + + b.Property("DefaultMusicVolume"); + + b.Property("DeleteMessageOnCommand"); + + b.Property("DmGreetMessageText"); + + b.Property("ExclusiveSelfAssignedRoles"); + + b.Property("FilterInvites"); + + b.Property("FilterWords"); + + b.Property("GameVoiceChannel"); + + b.Property("GreetMessageChannelId"); + + b.Property("GuildId"); + + b.Property("Locale"); + + b.Property("LogSettingId"); + + b.Property("MuteRoleName"); + + b.Property("PermissionRole"); + + b.Property("Prefix"); + + b.Property("RootPermissionId"); + + b.Property("SendChannelByeMessage"); + + b.Property("SendChannelGreetMessage"); + + b.Property("SendDmGreetMessage"); + + b.Property("TimeZoneId"); + + b.Property("VerboseErrors"); + + b.Property("VerbosePermissions"); + + b.Property("VoicePlusTextEnabled"); + + b.Property("WarningsInitialized"); + + b.HasKey("Id"); + + b.HasIndex("GuildId") + .IsUnique(); + + b.HasIndex("LogSettingId"); + + b.HasIndex("RootPermissionId"); + + b.ToTable("GuildConfigs"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.GuildRepeater", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ChannelId"); + + b.Property("DateAdded"); + + b.Property("GuildConfigId"); + + b.Property("GuildId"); + + b.Property("Interval"); + + b.Property("Message"); + + b.Property("StartTimeOfDay"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.ToTable("GuildRepeater"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.IgnoredLogChannel", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ChannelId"); + + b.Property("DateAdded"); + + b.Property("LogSettingId"); + + b.HasKey("Id"); + + b.HasIndex("LogSettingId"); + + b.ToTable("IgnoredLogChannels"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.IgnoredVoicePresenceChannel", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ChannelId"); + + b.Property("DateAdded"); + + b.Property("LogSettingId"); + + b.HasKey("Id"); + + b.HasIndex("LogSettingId"); + + b.ToTable("IgnoredVoicePresenceCHannels"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.LogSetting", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ChannelCreated"); + + b.Property("ChannelCreatedId"); + + b.Property("ChannelDestroyed"); + + b.Property("ChannelDestroyedId"); + + b.Property("ChannelId"); + + b.Property("ChannelUpdated"); + + b.Property("ChannelUpdatedId"); + + b.Property("DateAdded"); + + b.Property("IsLogging"); + + b.Property("LogOtherId"); + + b.Property("LogUserPresence"); + + b.Property("LogUserPresenceId"); + + b.Property("LogVoicePresence"); + + b.Property("LogVoicePresenceId"); + + b.Property("LogVoicePresenceTTSId"); + + b.Property("MessageDeleted"); + + b.Property("MessageDeletedId"); + + b.Property("MessageUpdated"); + + b.Property("MessageUpdatedId"); + + b.Property("UserBanned"); + + b.Property("UserBannedId"); + + b.Property("UserJoined"); + + b.Property("UserJoinedId"); + + b.Property("UserLeft"); + + b.Property("UserLeftId"); + + b.Property("UserMutedId"); + + b.Property("UserPresenceChannelId"); + + b.Property("UserUnbanned"); + + b.Property("UserUnbannedId"); + + b.Property("UserUpdated"); + + b.Property("UserUpdatedId"); + + b.Property("VoicePresenceChannelId"); + + b.HasKey("Id"); + + b.ToTable("LogSettings"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.ModulePrefix", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("BotConfigId"); + + b.Property("DateAdded"); + + b.Property("ModuleName"); + + b.Property("Prefix"); + + b.HasKey("Id"); + + b.HasIndex("BotConfigId"); + + b.ToTable("ModulePrefixes"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.MusicPlaylist", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("Author"); + + b.Property("AuthorId"); + + b.Property("DateAdded"); + + b.Property("Name"); + + b.HasKey("Id"); + + b.ToTable("MusicPlaylists"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.MutedUserId", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("GuildConfigId"); + + b.Property("UserId"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.ToTable("MutedUserId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.NsfwBlacklitedTag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("GuildConfigId"); + + b.Property("Tag"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.ToTable("NsfwBlacklitedTag"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.Permission", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("NextId"); + + b.Property("PrimaryTarget"); + + b.Property("PrimaryTargetId"); + + b.Property("SecondaryTarget"); + + b.Property("SecondaryTargetName"); + + b.Property("State"); + + b.HasKey("Id"); + + b.HasIndex("NextId") + .IsUnique(); + + b.ToTable("Permission"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.Permissionv2", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("GuildConfigId"); + + b.Property("Index"); + + b.Property("PrimaryTarget"); + + b.Property("PrimaryTargetId"); + + b.Property("SecondaryTarget"); + + b.Property("SecondaryTargetName"); + + b.Property("State"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.ToTable("Permissionv2"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.PlayingStatus", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("BotConfigId"); + + b.Property("DateAdded"); + + b.Property("Status"); + + b.HasKey("Id"); + + b.HasIndex("BotConfigId"); + + b.ToTable("PlayingStatus"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.PlaylistSong", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("MusicPlaylistId"); + + b.Property("Provider"); + + b.Property("ProviderType"); + + b.Property("Query"); + + b.Property("Title"); + + b.Property("Uri"); + + b.HasKey("Id"); + + b.HasIndex("MusicPlaylistId"); + + b.ToTable("PlaylistSong"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.Quote", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AuthorId"); + + b.Property("AuthorName") + .IsRequired(); + + b.Property("DateAdded"); + + b.Property("GuildId"); + + b.Property("Keyword") + .IsRequired(); + + b.Property("Text") + .IsRequired(); + + b.HasKey("Id"); + + b.ToTable("Quotes"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.RaceAnimal", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("BotConfigId"); + + b.Property("DateAdded"); + + b.Property("Icon"); + + b.Property("Name"); + + b.HasKey("Id"); + + b.HasIndex("BotConfigId"); + + b.ToTable("RaceAnimals"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.Reminder", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ChannelId"); + + b.Property("DateAdded"); + + b.Property("IsPrivate"); + + b.Property("Message"); + + b.Property("ServerId"); + + b.Property("UserId"); + + b.Property("When"); + + b.HasKey("Id"); + + b.ToTable("Reminders"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.RewardedUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AmountRewardedThisMonth"); + + b.Property("DateAdded"); + + b.Property("LastReward"); + + b.Property("PatreonUserId"); + + b.Property("UserId"); + + b.HasKey("Id"); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("RewardedUsers"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.SelfAssignedRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("GuildId"); + + b.Property("RoleId"); + + b.HasKey("Id"); + + b.HasIndex("GuildId", "RoleId") + .IsUnique(); + + b.ToTable("SelfAssignableRoles"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.ShopEntry", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AuthorId"); + + b.Property("DateAdded"); + + b.Property("GuildConfigId"); + + b.Property("Index"); + + b.Property("Name"); + + b.Property("Price"); + + b.Property("RoleId"); + + b.Property("RoleName"); + + b.Property("Type"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.ToTable("ShopEntry"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.ShopEntryItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("ShopEntryId"); + + b.Property("Text"); + + b.HasKey("Id"); + + b.HasIndex("ShopEntryId"); + + b.ToTable("ShopEntryItem"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.SlowmodeIgnoredRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("GuildConfigId"); + + b.Property("RoleId"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.ToTable("SlowmodeIgnoredRole"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.SlowmodeIgnoredUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("GuildConfigId"); + + b.Property("UserId"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.ToTable("SlowmodeIgnoredUser"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.StartupCommand", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("BotConfigId"); + + b.Property("ChannelId"); + + b.Property("ChannelName"); + + b.Property("CommandText"); + + b.Property("DateAdded"); + + b.Property("GuildId"); + + b.Property("GuildName"); + + b.Property("Index"); + + b.Property("VoiceChannelId"); + + b.Property("VoiceChannelName"); + + b.HasKey("Id"); + + b.HasIndex("BotConfigId"); + + b.ToTable("StartupCommand"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.StreamRoleBlacklistedUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("StreamRoleSettingsId"); + + b.Property("UserId"); + + b.Property("Username"); + + b.HasKey("Id"); + + b.HasIndex("StreamRoleSettingsId"); + + b.ToTable("StreamRoleBlacklistedUser"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.StreamRoleSettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AddRoleId"); + + b.Property("DateAdded"); + + b.Property("Enabled"); + + b.Property("FromRoleId"); + + b.Property("GuildConfigId"); + + b.Property("Keyword"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId") + .IsUnique(); + + b.ToTable("StreamRoleSettings"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.StreamRoleWhitelistedUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("StreamRoleSettingsId"); + + b.Property("UserId"); + + b.Property("Username"); + + b.HasKey("Id"); + + b.HasIndex("StreamRoleSettingsId"); + + b.ToTable("StreamRoleWhitelistedUser"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.UnmuteTimer", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("GuildConfigId"); + + b.Property("UnmuteAt"); + + b.Property("UserId"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.ToTable("UnmuteTimer"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.UserPokeTypes", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("UserId"); + + b.Property("type"); + + b.HasKey("Id"); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("PokeGame"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.UserXpStats", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AwardedXp"); + + b.Property("DateAdded"); + + b.Property("GuildId"); + + b.Property("LastLevelUp") + .ValueGeneratedOnAdd() + .HasDefaultValue(new DateTime(2017, 9, 21, 20, 53, 13, 307, DateTimeKind.Local)); + + b.Property("NotifyOnLevelUp"); + + b.Property("UserId"); + + b.Property("Xp"); + + b.HasKey("Id"); + + b.HasIndex("UserId", "GuildId") + .IsUnique(); + + b.ToTable("UserXpStats"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.VcRoleInfo", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("GuildConfigId"); + + b.Property("RoleId"); + + b.Property("VoiceChannelId"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.ToTable("VcRoleInfo"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.WaifuInfo", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AffinityId"); + + b.Property("ClaimerId"); + + b.Property("DateAdded"); + + b.Property("Price"); + + b.Property("WaifuId"); + + b.HasKey("Id"); + + b.HasIndex("AffinityId"); + + b.HasIndex("ClaimerId"); + + b.HasIndex("WaifuId") + .IsUnique(); + + b.ToTable("WaifuInfo"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.WaifuItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("Item"); + + b.Property("ItemEmoji"); + + b.Property("Price"); + + b.Property("WaifuInfoId"); + + b.HasKey("Id"); + + b.HasIndex("WaifuInfoId"); + + b.ToTable("WaifuItem"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.WaifuUpdate", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("NewId"); + + b.Property("OldId"); + + b.Property("UpdateType"); + + b.Property("UserId"); + + b.HasKey("Id"); + + b.HasIndex("NewId"); + + b.HasIndex("OldId"); + + b.HasIndex("UserId"); + + b.ToTable("WaifuUpdates"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.Warning", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("Forgiven"); + + b.Property("ForgivenBy"); + + b.Property("GuildId"); + + b.Property("Moderator"); + + b.Property("Reason"); + + b.Property("UserId"); + + b.HasKey("Id"); + + b.ToTable("Warnings"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.WarningPunishment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("Count"); + + b.Property("DateAdded"); + + b.Property("GuildConfigId"); + + b.Property("Punishment"); + + b.Property("Time"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.ToTable("WarningPunishment"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.XpRoleReward", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("Level"); + + b.Property("RoleId"); + + b.Property("XpSettingsId"); + + b.HasKey("Id"); + + b.HasAlternateKey("Level"); + + b.HasIndex("XpSettingsId"); + + b.ToTable("XpRoleReward"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.XpSettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("GuildConfigId"); + + b.Property("NotifyMessage"); + + b.Property("ServerExcluded"); + + b.Property("XpRoleRewardExclusive"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId") + .IsUnique(); + + b.ToTable("XpSettings"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.AntiRaidSetting", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig", "GuildConfig") + .WithOne("AntiRaidSetting") + .HasForeignKey("NadekoBot.Services.Database.Models.AntiRaidSetting", "GuildConfigId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.AntiSpamIgnore", b => + { + b.HasOne("NadekoBot.Services.Database.Models.AntiSpamSetting") + .WithMany("IgnoredChannels") + .HasForeignKey("AntiSpamSettingId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.AntiSpamSetting", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig", "GuildConfig") + .WithOne("AntiSpamSetting") + .HasForeignKey("NadekoBot.Services.Database.Models.AntiSpamSetting", "GuildConfigId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.BlacklistItem", b => + { + b.HasOne("NadekoBot.Services.Database.Models.BotConfig") + .WithMany("Blacklist") + .HasForeignKey("BotConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.BlockedCmdOrMdl", b => + { + b.HasOne("NadekoBot.Services.Database.Models.BotConfig") + .WithMany("BlockedCommands") + .HasForeignKey("BotConfigId"); + + b.HasOne("NadekoBot.Services.Database.Models.BotConfig") + .WithMany("BlockedModules") + .HasForeignKey("BotConfigId1"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.ClashCaller", b => + { + b.HasOne("NadekoBot.Services.Database.Models.ClashWar", "ClashWar") + .WithMany("Bases") + .HasForeignKey("ClashWarId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.ClubApplicants", b => + { + b.HasOne("NadekoBot.Services.Database.Models.ClubInfo", "Club") + .WithMany("Applicants") + .HasForeignKey("ClubId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("NadekoBot.Services.Database.Models.DiscordUser", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.ClubBans", b => + { + b.HasOne("NadekoBot.Services.Database.Models.ClubInfo", "Club") + .WithMany("Bans") + .HasForeignKey("ClubId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("NadekoBot.Services.Database.Models.DiscordUser", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.ClubInfo", b => + { + b.HasOne("NadekoBot.Services.Database.Models.DiscordUser", "Owner") + .WithOne() + .HasForeignKey("NadekoBot.Services.Database.Models.ClubInfo", "OwnerId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.CommandAlias", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig") + .WithMany("CommandAliases") + .HasForeignKey("GuildConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.CommandCooldown", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig") + .WithMany("CommandCooldowns") + .HasForeignKey("GuildConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.CommandPrice", b => + { + b.HasOne("NadekoBot.Services.Database.Models.BotConfig") + .WithMany("CommandPrices") + .HasForeignKey("BotConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.DiscordUser", b => + { + b.HasOne("NadekoBot.Services.Database.Models.ClubInfo", "Club") + .WithMany("Users") + .HasForeignKey("ClubId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.EightBallResponse", b => + { + b.HasOne("NadekoBot.Services.Database.Models.BotConfig") + .WithMany("EightBallResponses") + .HasForeignKey("BotConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.ExcludedItem", b => + { + b.HasOne("NadekoBot.Services.Database.Models.XpSettings") + .WithMany("ExclusionList") + .HasForeignKey("XpSettingsId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.FeedSub", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig", "GuildConfig") + .WithMany("FeedSubs") + .HasForeignKey("GuildConfigId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.FilterChannelId", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig") + .WithMany("FilterInvitesChannelIds") + .HasForeignKey("GuildConfigId"); + + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig") + .WithMany("FilterWordsChannelIds") + .HasForeignKey("GuildConfigId1"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.FilteredWord", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig") + .WithMany("FilteredWords") + .HasForeignKey("GuildConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.FollowedStream", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig") + .WithMany("FollowedStreams") + .HasForeignKey("GuildConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.GCChannelId", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig") + .WithMany("GenerateCurrencyChannelIds") + .HasForeignKey("GuildConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.GuildConfig", b => + { + b.HasOne("NadekoBot.Services.Database.Models.LogSetting", "LogSetting") + .WithMany() + .HasForeignKey("LogSettingId"); + + b.HasOne("NadekoBot.Services.Database.Models.Permission", "RootPermission") + .WithMany() + .HasForeignKey("RootPermissionId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.GuildRepeater", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig") + .WithMany("GuildRepeaters") + .HasForeignKey("GuildConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.IgnoredLogChannel", b => + { + b.HasOne("NadekoBot.Services.Database.Models.LogSetting", "LogSetting") + .WithMany("IgnoredChannels") + .HasForeignKey("LogSettingId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.IgnoredVoicePresenceChannel", b => + { + b.HasOne("NadekoBot.Services.Database.Models.LogSetting", "LogSetting") + .WithMany("IgnoredVoicePresenceChannelIds") + .HasForeignKey("LogSettingId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.ModulePrefix", b => + { + b.HasOne("NadekoBot.Services.Database.Models.BotConfig") + .WithMany("ModulePrefixes") + .HasForeignKey("BotConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.MutedUserId", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig") + .WithMany("MutedUsers") + .HasForeignKey("GuildConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.NsfwBlacklitedTag", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig") + .WithMany("NsfwBlacklistedTags") + .HasForeignKey("GuildConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.Permission", b => + { + b.HasOne("NadekoBot.Services.Database.Models.Permission", "Next") + .WithOne("Previous") + .HasForeignKey("NadekoBot.Services.Database.Models.Permission", "NextId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.Permissionv2", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig") + .WithMany("Permissions") + .HasForeignKey("GuildConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.PlayingStatus", b => + { + b.HasOne("NadekoBot.Services.Database.Models.BotConfig") + .WithMany("RotatingStatusMessages") + .HasForeignKey("BotConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.PlaylistSong", b => + { + b.HasOne("NadekoBot.Services.Database.Models.MusicPlaylist") + .WithMany("Songs") + .HasForeignKey("MusicPlaylistId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.RaceAnimal", b => + { + b.HasOne("NadekoBot.Services.Database.Models.BotConfig") + .WithMany("RaceAnimals") + .HasForeignKey("BotConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.ShopEntry", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig") + .WithMany("ShopEntries") + .HasForeignKey("GuildConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.ShopEntryItem", b => + { + b.HasOne("NadekoBot.Services.Database.Models.ShopEntry") + .WithMany("Items") + .HasForeignKey("ShopEntryId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.SlowmodeIgnoredRole", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig") + .WithMany("SlowmodeIgnoredRoles") + .HasForeignKey("GuildConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.SlowmodeIgnoredUser", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig") + .WithMany("SlowmodeIgnoredUsers") + .HasForeignKey("GuildConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.StartupCommand", b => + { + b.HasOne("NadekoBot.Services.Database.Models.BotConfig") + .WithMany("StartupCommands") + .HasForeignKey("BotConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.StreamRoleBlacklistedUser", b => + { + b.HasOne("NadekoBot.Services.Database.Models.StreamRoleSettings") + .WithMany("Blacklist") + .HasForeignKey("StreamRoleSettingsId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.StreamRoleSettings", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig", "GuildConfig") + .WithOne("StreamRole") + .HasForeignKey("NadekoBot.Services.Database.Models.StreamRoleSettings", "GuildConfigId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.StreamRoleWhitelistedUser", b => + { + b.HasOne("NadekoBot.Services.Database.Models.StreamRoleSettings") + .WithMany("Whitelist") + .HasForeignKey("StreamRoleSettingsId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.UnmuteTimer", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig") + .WithMany("UnmuteTimers") + .HasForeignKey("GuildConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.VcRoleInfo", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig") + .WithMany("VcRoleInfos") + .HasForeignKey("GuildConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.WaifuInfo", b => + { + b.HasOne("NadekoBot.Services.Database.Models.DiscordUser", "Affinity") + .WithMany() + .HasForeignKey("AffinityId"); + + b.HasOne("NadekoBot.Services.Database.Models.DiscordUser", "Claimer") + .WithMany() + .HasForeignKey("ClaimerId"); + + b.HasOne("NadekoBot.Services.Database.Models.DiscordUser", "Waifu") + .WithOne() + .HasForeignKey("NadekoBot.Services.Database.Models.WaifuInfo", "WaifuId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.WaifuItem", b => + { + b.HasOne("NadekoBot.Services.Database.Models.WaifuInfo") + .WithMany("Items") + .HasForeignKey("WaifuInfoId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.WaifuUpdate", b => + { + b.HasOne("NadekoBot.Services.Database.Models.DiscordUser", "New") + .WithMany() + .HasForeignKey("NewId"); + + b.HasOne("NadekoBot.Services.Database.Models.DiscordUser", "Old") + .WithMany() + .HasForeignKey("OldId"); + + b.HasOne("NadekoBot.Services.Database.Models.DiscordUser", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.WarningPunishment", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig") + .WithMany("WarnPunishments") + .HasForeignKey("GuildConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.XpRoleReward", b => + { + b.HasOne("NadekoBot.Services.Database.Models.XpSettings") + .WithMany("RoleRewards") + .HasForeignKey("XpSettingsId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.XpSettings", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig", "GuildConfig") + .WithOne("XpSettings") + .HasForeignKey("NadekoBot.Services.Database.Models.XpSettings", "GuildConfigId") + .OnDelete(DeleteBehavior.Cascade); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/NadekoBot/Migrations/20170921185313_feeds.cs b/src/NadekoBot/Migrations/20170921185313_feeds.cs new file mode 100644 index 00000000..b4cbc41b --- /dev/null +++ b/src/NadekoBot/Migrations/20170921185313_feeds.cs @@ -0,0 +1,59 @@ +using Microsoft.EntityFrameworkCore.Migrations; +using System; +using System.Collections.Generic; + +namespace NadekoBot.Migrations +{ + public partial class feeds : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "FeedSub", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + ChannelId = table.Column(type: "INTEGER", nullable: false), + DateAdded = table.Column(type: "TEXT", nullable: true), + GuildConfigId = table.Column(type: "INTEGER", nullable: false), + Url = table.Column(type: "TEXT", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_FeedSub", x => x.Id); + table.UniqueConstraint("AK_FeedSub_GuildConfigId_Url", x => new { x.GuildConfigId, x.Url }); + table.ForeignKey( + name: "FK_FeedSub_GuildConfigs_GuildConfigId", + column: x => x.GuildConfigId, + principalTable: "GuildConfigs", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "FeedSub"); + + migrationBuilder.AlterColumn( + name: "LastLevelUp", + table: "UserXpStats", + nullable: false, + defaultValue: new DateTime(2017, 9, 15, 5, 48, 8, 665, DateTimeKind.Local), + oldClrType: typeof(DateTime), + oldType: "TEXT", + oldDefaultValue: new DateTime(2017, 9, 21, 20, 53, 13, 307, DateTimeKind.Local)); + + migrationBuilder.AlterColumn( + name: "LastLevelUp", + table: "DiscordUser", + nullable: false, + defaultValue: new DateTime(2017, 9, 15, 5, 48, 8, 660, DateTimeKind.Local), + oldClrType: typeof(DateTime), + oldType: "TEXT", + oldDefaultValue: new DateTime(2017, 9, 21, 20, 53, 13, 305, DateTimeKind.Local)); + } + } +} diff --git a/src/NadekoBot/Migrations/NadekoSqliteContextModelSnapshot.cs b/src/NadekoBot/Migrations/NadekoSqliteContextModelSnapshot.cs index cb6b041e..37c56969 100644 --- a/src/NadekoBot/Migrations/NadekoSqliteContextModelSnapshot.cs +++ b/src/NadekoBot/Migrations/NadekoSqliteContextModelSnapshot.cs @@ -1,10 +1,13 @@ -using System; +// using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Metadata; using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage; +using Microsoft.EntityFrameworkCore.Storage.Internal; using NadekoBot.Services.Database; using NadekoBot.Services.Database.Models; +using System; namespace NadekoBot.Migrations { @@ -13,8 +16,9 @@ namespace NadekoBot.Migrations { protected override void BuildModel(ModelBuilder modelBuilder) { +#pragma warning disable 612, 618 modelBuilder - .HasAnnotation("ProductVersion", "1.1.1"); + .HasAnnotation("ProductVersion", "2.0.0-rtm-26452"); modelBuilder.Entity("NadekoBot.Services.Database.Models.AntiRaidSetting", b => { @@ -466,7 +470,7 @@ namespace NadekoBot.Migrations b.Property("LastLevelUp") .ValueGeneratedOnAdd() - .HasDefaultValue(new DateTime(2017, 9, 15, 5, 48, 8, 660, DateTimeKind.Local)); + .HasDefaultValue(new DateTime(2017, 9, 21, 20, 53, 13, 305, DateTimeKind.Local)); b.Property("LastXpGain"); @@ -546,6 +550,27 @@ namespace NadekoBot.Migrations b.ToTable("ExcludedItem"); }); + modelBuilder.Entity("NadekoBot.Services.Database.Models.FeedSub", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ChannelId"); + + b.Property("DateAdded"); + + b.Property("GuildConfigId"); + + b.Property("Url") + .IsRequired(); + + b.HasKey("Id"); + + b.HasAlternateKey("GuildConfigId", "Url"); + + b.ToTable("FeedSub"); + }); + modelBuilder.Entity("NadekoBot.Services.Database.Models.FilterChannelId", b => { b.Property("Id") @@ -1366,7 +1391,7 @@ namespace NadekoBot.Migrations b.Property("LastLevelUp") .ValueGeneratedOnAdd() - .HasDefaultValue(new DateTime(2017, 9, 15, 5, 48, 8, 665, DateTimeKind.Local)); + .HasDefaultValue(new DateTime(2017, 9, 21, 20, 53, 13, 307, DateTimeKind.Local)); b.Property("NotifyOnLevelUp"); @@ -1693,6 +1718,14 @@ namespace NadekoBot.Migrations .HasForeignKey("XpSettingsId"); }); + modelBuilder.Entity("NadekoBot.Services.Database.Models.FeedSub", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig", "GuildConfig") + .WithMany("FeedSubs") + .HasForeignKey("GuildConfigId") + .OnDelete(DeleteBehavior.Cascade); + }); + modelBuilder.Entity("NadekoBot.Services.Database.Models.FilterChannelId", b => { b.HasOne("NadekoBot.Services.Database.Models.GuildConfig") @@ -1945,6 +1978,7 @@ namespace NadekoBot.Migrations .HasForeignKey("NadekoBot.Services.Database.Models.XpSettings", "GuildConfigId") .OnDelete(DeleteBehavior.Cascade); }); +#pragma warning restore 612, 618 } } } diff --git a/src/NadekoBot/Modules/Music/Services/MusicService.cs b/src/NadekoBot/Modules/Music/Services/MusicService.cs index 6b7fffc5..d0b00657 100644 --- a/src/NadekoBot/Modules/Music/Services/MusicService.cs +++ b/src/NadekoBot/Modules/Music/Services/MusicService.cs @@ -222,61 +222,5 @@ namespace NadekoBot.Modules.Music.Services if (MusicPlayers.TryRemove(id, out var mp)) await mp.Destroy(); } - - - - //public Task ResolveYoutubeSong(string query, string queuerName) - //{ - // _log.Info("Getting video"); - // //var (link, video) = await GetYoutubeVideo(query); - - // //if (video == null) // do something with this error - // //{ - // // _log.Info("Could not load any video elements based on the query."); - // // return null; - // //} - // ////var m = Regex.Match(query, @"\?t=(?\d*)"); - // ////int gotoTime = 0; - // ////if (m.Captures.Count > 0) - // //// int.TryParse(m.Groups["t"].ToString(), out gotoTime); - - // //_log.Info("Creating song info"); - // //var song = new SongInfo - // //{ - // // Title = video.Title.Substring(0, video.Title.Length - 10), // removing trailing "- You Tube" - // // Provider = "YouTube", - // // Uri = async () => { - // // var vid = await GetYoutubeVideo(query); - // // if (vid.Item2 == null) - // // throw new HttpRequestException(); - - // // return await vid.Item2.GetUriAsync(); - // // }, - // // Query = link, - // // ProviderType = MusicType.YouTube, - // // QueuerName = queuerName - // //}; - // return GetYoutubeVideo(query, queuerName); - //} - - //private async Task GetYoutubeVideo(string query, string queuerName) - //{ - - - // //if (string.IsNullOrWhiteSpace(link)) - // //{ - // // _log.Info("No song found."); - // // return (null, null); - // //} - // //_log.Info("Getting all videos"); - // //var allVideos = await Task.Run(async () => { try { return await _yt.GetAllVideosAsync(link).ConfigureAwait(false); } catch { return Enumerable.Empty(); } }).ConfigureAwait(false); - // //var videos = allVideos.Where(v => v.AdaptiveKind == AdaptiveKind.Audio); - // //var video = videos - // // .Where(v => v.AudioBitrate < 256) - // // .OrderByDescending(v => v.AudioBitrate) - // // .FirstOrDefault(); - - // //return (link, video); - //} } } \ No newline at end of file diff --git a/src/NadekoBot/Modules/Searches/FeedCommands.cs b/src/NadekoBot/Modules/Searches/FeedCommands.cs new file mode 100644 index 00000000..38fe6be6 --- /dev/null +++ b/src/NadekoBot/Modules/Searches/FeedCommands.cs @@ -0,0 +1,106 @@ +using Discord; +using Discord.Commands; +using Discord.WebSocket; +using Microsoft.SyndicationFeed.Rss; +using NadekoBot.Common.Attributes; +using NadekoBot.Extensions; +using NadekoBot.Modules.Searches.Services; +using System; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using System.Xml; + +namespace NadekoBot.Modules.Searches +{ + public partial class Searches + { + [Group] + public class FeedCommands : NadekoSubmodule + { + private readonly DiscordSocketClient _client; + + public FeedCommands(DiscordSocketClient client) + { + _client = client; + } + + [NadekoCommand, Usage, Description, Aliases] + [RequireContext(ContextType.Guild)] + [RequireUserPermission(GuildPermission.ManageMessages)] + public async Task Feed(string url, [Remainder] ITextChannel channel = null) + { + var success = Uri.TryCreate(url, UriKind.Absolute, out var uri) && + (uri.Scheme == Uri.UriSchemeHttp || uri.Scheme == Uri.UriSchemeHttps); + if (success) + { + channel = channel ?? (ITextChannel)Context.Channel; + using (var xmlReader = XmlReader.Create(url, new XmlReaderSettings() { Async = true })) + { + var reader = new RssFeedReader(xmlReader); + try + { + await reader.Read(); + } + catch { success = false; } + } + + if (success) + { + success = _service.AddFeed(Context.Guild.Id, channel.Id, url); + if (success) + { + await ReplyConfirmLocalized("feed_added").ConfigureAwait(false); + return; + } + } + } + + await ReplyErrorLocalized("feed_not_valid").ConfigureAwait(false); + } + + [NadekoCommand, Usage, Description, Aliases] + [RequireContext(ContextType.Guild)] + [RequireUserPermission(GuildPermission.ManageMessages)] + public async Task FeedRemove(int index) + { + if (_service.RemoveFeed(Context.Guild.Id, --index)) + { + await ReplyConfirmLocalized("feed_removed").ConfigureAwait(false); + } + else + await ReplyErrorLocalized("feed_out_of_range").ConfigureAwait(false); + } + + [NadekoCommand, Usage, Description, Aliases] + [RequireContext(ContextType.Guild)] + [RequireUserPermission(GuildPermission.ManageMessages)] + public async Task FeedList() + { + var feeds = _service.GetFeeds(Context.Guild.Id); + + if (!feeds.Any()) + { + await Context.Channel.EmbedAsync(new EmbedBuilder() + .WithOkColor() + .WithDescription(GetText("feed_no_feed"))) + .ConfigureAwait(false); + return; + } + + await Context.Channel.SendPaginatedConfirmAsync(_client, 0, (cur) => + { + var embed = new EmbedBuilder() + .WithOkColor(); + var i = 0; + var fs = string.Join("\n", feeds.Skip(cur * 10) + .Take(10) + .Select(x => $"`{(cur * 10) + (++i)}.` <#{x.ChannelId}> {x.Url}")); + + return embed.WithDescription(fs); + + }, feeds.Count / 10); + } + } + } +} diff --git a/src/NadekoBot/Modules/Searches/Services/FeedsService.cs b/src/NadekoBot/Modules/Searches/Services/FeedsService.cs new file mode 100644 index 00000000..c0eae9d5 --- /dev/null +++ b/src/NadekoBot/Modules/Searches/Services/FeedsService.cs @@ -0,0 +1,213 @@ +using Discord; +using Microsoft.SyndicationFeed; +using Microsoft.SyndicationFeed.Rss; +using NadekoBot.Extensions; +using NadekoBot.Services; +using System; +using System.Linq; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using System.Xml; +using System.Collections.Generic; +using NadekoBot.Services.Database.Models; +using Microsoft.EntityFrameworkCore; +using System.Collections.Concurrent; +using Discord.WebSocket; + +namespace NadekoBot.Modules.Searches.Services +{ + public class FeedsService : INService + { + private readonly DbService _db; + private readonly ConcurrentDictionary> _subs; + private readonly DiscordSocketClient _client; + private readonly ConcurrentDictionary _lastPosts = + new ConcurrentDictionary(); + + public FeedsService(IEnumerable gcs, DbService db, DiscordSocketClient client) + { + _db = db; + + _subs = gcs.SelectMany(x => x.FeedSubs) + .GroupBy(x => x.Url) + .ToDictionary(x => x.Key, x => x.ToHashSet()) + .ToConcurrent(); + + _client = client; + + foreach (var kvp in _subs) + { + // to make sure rss feeds don't post right away, but + // only the updates from after the bot has started + _lastPosts.AddOrUpdate(kvp.Key, DateTime.UtcNow, (k, old) => DateTime.UtcNow); + } + + var _ = Task.Run(TrackFeeds); + } + + public async Task TrackFeeds() + { + while (true) + { + foreach (var kvp in _subs) + { + if (kvp.Value.Count == 0) + continue; + + DateTime lastTime; + if (!_lastPosts.TryGetValue(kvp.Key, out lastTime)) + lastTime = _lastPosts.AddOrUpdate(kvp.Key, DateTime.UtcNow, (k, old) => DateTime.UtcNow); + + var rssUrl = kvp.Key; + try + { + using (var xmlReader = XmlReader.Create(rssUrl, new XmlReaderSettings() { Async = true })) + { + var feedReader = new RssFeedReader(xmlReader); + + var embed = new EmbedBuilder() + .WithAuthor(kvp.Key) + .WithOkColor(); + + while (await feedReader.Read() && feedReader.ElementType != SyndicationElementType.Item) + { + switch (feedReader.ElementType) + { + case SyndicationElementType.Link: + var uri = await feedReader.ReadLink(); + embed.WithAuthor(kvp.Key, url: uri.Uri.AbsoluteUri); + break; + case SyndicationElementType.Content: + var content = await feedReader.ReadContent(); + break; + case SyndicationElementType.Category: + break; + case SyndicationElementType.Image: + ISyndicationImage image = await feedReader.ReadImage(); + embed.WithThumbnailUrl(image.Url.AbsoluteUri); + break; + default: + break; + } + } + + ISyndicationItem item = await feedReader.ReadItem(); + if (item.Published.UtcDateTime <= lastTime) + continue; + + var desc = item.Description.StripHTML(); + + lastTime = item.Published.UtcDateTime; + var title = string.IsNullOrWhiteSpace(item.Title) ? "-" : item.Title; + desc = Format.Code(item.Published.ToString()) + Environment.NewLine + desc; + var link = item.Links.FirstOrDefault(); + if (link != null) + desc = $"[link]({link.Uri}) " + desc; + + var img = item.Links.FirstOrDefault(x => x.RelationshipType == "enclosure")?.Uri.AbsoluteUri + ?? Regex.Match(item.Description, @"src=""(?.*?)""").Groups["src"].ToString(); + + if (!string.IsNullOrWhiteSpace(img) && Uri.IsWellFormedUriString(img, UriKind.Absolute)) + embed.WithImageUrl(img); + + embed.AddField(title, desc); + + //send the created embed to all subscribed channels + var sendTasks = kvp.Value + .Where(x => x.GuildConfig != null) + .Select(x => _client.GetGuild(x.GuildConfig.GuildId) + ?.GetTextChannel(x.ChannelId)) + .Where(x => x != null) + .Select(x => x.EmbedAsync(embed)); + + _lastPosts.AddOrUpdate(kvp.Key, item.Published.UtcDateTime, (k, old) => item.Published.UtcDateTime); + + await Task.WhenAll(sendTasks).ConfigureAwait(false); + } + } + catch (Exception ex) { Console.WriteLine(ex); } + } + + await Task.Delay(10000); + } + } + + public List GetFeeds(ulong guildId) + { + using (var uow = _db.UnitOfWork) + { + return uow.GuildConfigs.For(guildId, set => set.Include(x => x.FeedSubs)) + .FeedSubs + .OrderBy(x => x.Id) + .ToList(); + } + } + + public bool AddFeed(ulong guildId, ulong channelId, string rssFeed) + { + rssFeed.ThrowIfNull(nameof(rssFeed)); + + var fs = new FeedSub() + { + ChannelId = channelId, + Url = rssFeed.Trim().ToLowerInvariant(), + }; + + using (var uow = _db.UnitOfWork) + { + var gc = uow.GuildConfigs.For(guildId, set => set.Include(x => x.FeedSubs)); + + if (gc.FeedSubs.Contains(fs)) + { + return false; + } + else if (gc.FeedSubs.Count >= 10) + { + return false; + } + + gc.FeedSubs.Add(fs); + + //adding all, in case bot wasn't on this guild when it started + foreach (var f in gc.FeedSubs) + { + _subs.AddOrUpdate(f.Url, new HashSet(), (k, old) => + { + old.Add(f); + return old; + }); + } + + uow.Complete(); + } + + return true; + } + + public bool RemoveFeed(ulong guildId, int index) + { + if (index < 0) + return false; + + using (var uow = _db.UnitOfWork) + { + var items = uow.GuildConfigs.For(guildId, set => set.Include(x => x.FeedSubs)) + .FeedSubs + .OrderBy(x => x.Id) + .ToList(); + + if (items.Count <= index) + return false; + var toRemove = items[index]; + _subs.AddOrUpdate(toRemove.Url, new HashSet(), (key, old) => + { + old.Remove(toRemove); + return old; + }); + uow._context.Remove(toRemove); + uow.Complete(); + } + return true; + } + } +} diff --git a/src/NadekoBot/NadekoBot.csproj b/src/NadekoBot/NadekoBot.csproj index da1a1e22..e031284d 100644 --- a/src/NadekoBot/NadekoBot.csproj +++ b/src/NadekoBot/NadekoBot.csproj @@ -61,6 +61,7 @@ + diff --git a/src/NadekoBot/Properties/launchSettings.json b/src/NadekoBot/Properties/launchSettings.json index b532770a..ac657813 100644 --- a/src/NadekoBot/Properties/launchSettings.json +++ b/src/NadekoBot/Properties/launchSettings.json @@ -2,6 +2,10 @@ "profiles": { "NadekoBot": { "commandName": "Project" + }, + "Watch": { + "executablePath": "C:\\Program Files\\dotnet\\dotnet.exe", + "commandLineArgs": "watch run" } } } \ No newline at end of file diff --git a/src/NadekoBot/Services/Database/Models/FeedSub.cs b/src/NadekoBot/Services/Database/Models/FeedSub.cs new file mode 100644 index 00000000..aabf455e --- /dev/null +++ b/src/NadekoBot/Services/Database/Models/FeedSub.cs @@ -0,0 +1,23 @@ +namespace NadekoBot.Services.Database.Models +{ + public class FeedSub : DbEntity + { + public int GuildConfigId { get; set; } + public GuildConfig GuildConfig { get; set; } + + public ulong ChannelId { get; set; } + public string Url { get; set; } + + public override int GetHashCode() + { + return Url.GetHashCode() ^ GuildConfigId.GetHashCode(); + } + + public override bool Equals(object obj) + { + return obj is FeedSub s + ? s.Url == Url && s.GuildConfigId == GuildConfigId + : false; + } + } +} diff --git a/src/NadekoBot/Services/Database/Models/GuildConfig.cs b/src/NadekoBot/Services/Database/Models/GuildConfig.cs index 82cc8bdf..501b9a64 100644 --- a/src/NadekoBot/Services/Database/Models/GuildConfig.cs +++ b/src/NadekoBot/Services/Database/Models/GuildConfig.cs @@ -87,6 +87,7 @@ namespace NadekoBot.Services.Database.Models public StreamRoleSettings StreamRole { get; set; } public XpSettings XpSettings { get; set; } + public List FeedSubs { get; set; } = new List(); //public List ProtectionIgnoredChannels { get; set; } = new List(); } diff --git a/src/NadekoBot/Services/Database/NadekoContext.cs b/src/NadekoBot/Services/Database/NadekoContext.cs index 5142e8e5..373ab756 100644 --- a/src/NadekoBot/Services/Database/NadekoContext.cs +++ b/src/NadekoBot/Services/Database/NadekoContext.cs @@ -142,6 +142,9 @@ namespace NadekoBot.Services.Database .HasOne(x => x.GuildConfig) .WithOne(x => x.AntiRaidSetting); + modelBuilder.Entity() + .HasAlternateKey(x => new { x.GuildConfigId, x.Url }); + //modelBuilder.Entity() // .HasAlternateKey(c => new { c.ChannelId, c.ProtectionType }); @@ -304,6 +307,7 @@ namespace NadekoBot.Services.Database .WithOne(x => x.XpSettings); #endregion + //todo major bug #region XpRoleReward modelBuilder.Entity() .HasAlternateKey(x => x.Level); diff --git a/src/NadekoBot/Services/Database/Repositories/Impl/GuildConfigRepository.cs b/src/NadekoBot/Services/Database/Repositories/Impl/GuildConfigRepository.cs index 720fd794..eac42399 100644 --- a/src/NadekoBot/Services/Database/Repositories/Impl/GuildConfigRepository.cs +++ b/src/NadekoBot/Services/Database/Repositories/Impl/GuildConfigRepository.cs @@ -44,6 +44,8 @@ namespace NadekoBot.Services.Database.Repositories.Impl .Include(gc => gc.SlowmodeIgnoredUsers) .Include(gc => gc.AntiSpamSetting) .ThenInclude(x => x.IgnoredChannels) + .Include(gc => gc.FeedSubs) + .ThenInclude(x => x.GuildConfig) .Include(gc => gc.FollowedStreams) .Include(gc => gc.StreamRole) .Include(gc => gc.NsfwBlacklistedTags) diff --git a/src/NadekoBot/Services/Impl/Localization.cs b/src/NadekoBot/Services/Impl/Localization.cs index dc3829ce..8bc0d257 100644 --- a/src/NadekoBot/Services/Impl/Localization.cs +++ b/src/NadekoBot/Services/Impl/Localization.cs @@ -20,7 +20,7 @@ namespace NadekoBot.Services.Impl public CultureInfo DefaultCultureInfo { get; private set; } = CultureInfo.CurrentCulture; private static readonly Dictionary _commandData; - + static Localization() { _commandData = JsonConvert.DeserializeObject>( diff --git a/src/NadekoBot/_Extensions/StringExtensions.cs b/src/NadekoBot/_Extensions/StringExtensions.cs index fb083882..0b7adf65 100644 --- a/src/NadekoBot/_Extensions/StringExtensions.cs +++ b/src/NadekoBot/_Extensions/StringExtensions.cs @@ -9,6 +9,11 @@ namespace NadekoBot.Extensions { public static class StringExtensions { + public static string StripHTML(this string input) + { + return Regex.Replace(input, "<.*?>", String.Empty); + } + /// /// Easy use of fast, efficient case-insensitive Contains check with StringComparison Member Types /// CurrentCulture, CurrentCultureIgnoreCase, InvariantCulture, InvariantCultureIgnoreCase, Ordinal, OrdinalIgnoreCase diff --git a/src/NadekoBot/_strings/ResponseStrings.en-US.json b/src/NadekoBot/_strings/ResponseStrings.en-US.json index 80389b23..fe0a5ef3 100644 --- a/src/NadekoBot/_strings/ResponseStrings.en-US.json +++ b/src/NadekoBot/_strings/ResponseStrings.en-US.json @@ -872,5 +872,10 @@ "xp_club_admin_remove": "{0} is no longer club admin.", "xp_club_admin_error": "Error. You are either not the owner of the club, or that user is not in your club.", "nsfw_started": "Started. Reposting every {0}s.", - "nsfw_stopped": "Stopped reposting." + "nsfw_stopped": "Stopped reposting.", + "searches_feed_added": "Feed added.", + "searches_feed_not_valid": "Invalid link, or you're already following that feed on this server, or you've reached maximum number of feeds allowed.", + "searches_feed_out_of_range": "Index out of range.", + "searches_feed_removed": "Feed removed.", + "searches_feed_no_feed": "You haven't subscribed to any feeds on this server." } \ No newline at end of file diff --git a/src/NadekoBot/data/command_strings.json b/src/NadekoBot/data/command_strings.json index ca64fa24..1640acb0 100644 --- a/src/NadekoBot/data/command_strings.json +++ b/src/NadekoBot/data/command_strings.json @@ -2965,5 +2965,26 @@ "Usage": [ "{0}8ball" ] + }, + "feed": { + "cmd": "feed feedadd", + "desc": "Subscribes to a feed. Bot will post an update up to once every 10 seconds. You can have up to 10 feeds on one server. All feeds must have unique URLs.", + "usage": [ + "{0}feed https://www.rt.com/rss/" + ] + }, + "feedremove": { + "cmd": "feedremove feedrm feeddel", + "desc": "Stops tracking a feed on the given index. Use `{0}feeds` command to see a list of feeds and their indexes.", + "usage": [ + "{0}feedremove 3" + ] + }, + "feedlist": { + "cmd": "feeds feedlist", + "desc": "Shows the list of feeds you've subscribed to on this server.", + "usage": [ + "{0}feeds" + ] } } \ No newline at end of file From 61496a7c19e1245ee3c8b7c474ba17177bb9da69 Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Fri, 22 Sep 2017 10:15:01 +0200 Subject: [PATCH 323/346] allow unsafe --- src/NadekoBot/NadekoBot.csproj | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/NadekoBot/NadekoBot.csproj b/src/NadekoBot/NadekoBot.csproj index e031284d..816e2916 100644 --- a/src/NadekoBot/NadekoBot.csproj +++ b/src/NadekoBot/NadekoBot.csproj @@ -9,6 +9,7 @@ win7-x64 Debug;Release;global_nadeko latest + true @@ -73,10 +74,6 @@ $(NoWarn);CS1573;CS1591 - - true - - From 7e4b55ccac053cf34652493370e1647c0fba2f30 Mon Sep 17 00:00:00 2001 From: shivaco Date: Fri, 22 Sep 2017 16:43:07 +0600 Subject: [PATCH 324/346] Make PRs to 1.9 branch? --- docs/Contribution Guide.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/Contribution Guide.md b/docs/Contribution Guide.md index 3dff9b35..ca0cd94f 100644 --- a/docs/Contribution Guide.md +++ b/docs/Contribution Guide.md @@ -1,6 +1,6 @@ ### How to contribute -1. Make Pull Requests to the [**dev BRANCH**](https://github.com/Kwoth/NadekoBot/tree/dev). +1. Make Pull Requests to the [**1.9 BRANCH**](https://github.com/Kwoth/NadekoBot/tree/1.9). 2. Keep 1 Pull Request to a single feature. 3. Explain what you did in the PR message. From b603bc3ea3cd61fbc4a5816b68f7bffe6f8f1690 Mon Sep 17 00:00:00 2001 From: shivaco Date: Fri, 22 Sep 2017 17:28:37 +0600 Subject: [PATCH 325/346] Some re-phrasing --- docs/guides/Docker Guide.md | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/docs/guides/Docker Guide.md b/docs/guides/Docker Guide.md index e249cbff..5c50e73d 100644 --- a/docs/guides/Docker Guide.md +++ b/docs/guides/Docker Guide.md @@ -1,5 +1,5 @@ # NadekoBot a Discord bot -Nadeko is written in C# and Discord.net for more information visit +Nadeko is written in C# and Discord.Net for more information visit ## Install Docker Follow the respective guide for your operating system found here [Docker Engine Install Guide](https://docs.docker.com/engine/installation/) @@ -14,14 +14,12 @@ docker create --name=nadeko -v /nadeko/conf/:/root/nadeko -v /nadeko/data:/opt/N -If you are making a fresh install, create your credentials.json from the following guide and place it in the /nadeko folder [Nadeko JSON Guide](http://nadekobot.readthedocs.io/en/latest/JSON%20Explanations/) -Next start the docker up with - -`docker start nadeko; docker logs -f nadeko` +Next start the docker up with `docker start nadeko; docker logs -f nadeko` The docker will start and the log file will start scrolling past. Depending on hardware the bot start can take up to 5 minutes on a small DigitalOcean droplet. Once the log ends with "NadekoBot | Starting NadekoBot v1.0-rc2" the bot is ready and can be invited to your server. Ctrl+C at this point to stop viewing the logs. -After a few moments you should be able to invite Nadeko to your server. If you cannot check the log file for errors. +After a few moments you should be able to invite Nadeko to your server. If you cannot, check the log file for errors. ## Monitoring @@ -45,16 +43,16 @@ docker create --name=nadeko -v /nadeko/conf/:/root/nadeko -v /nadeko/data:/opt/N # Automatic Updates -Automatic update are now handled by watchertower [WatchTower GitHub](https://github.com/CenturyLinkLabs/watchtower) -To setup watchtower to keep Nadeko up-to-date for you with the default settings use the following command +Automatic update are now handled by WatchTower [WatchTower GitHub](https://github.com/CenturyLinkLabs/watchtower) +To setup WatchTower to keep Nadeko up-to-date for you with the default settings, use the following command ```bash docker run -d --name watchtower -v /var/run/docker.sock:/var/run/docker.sock centurylink/watchtower --cleanup nadeko ``` -This will check for updates to the docker every 5 minutes and update immediately. Alternatively using the `--interval X` command to change the interval, where X is the amount of time in seconds to wait. eg 21600 for 6 hours. +This will check for updates to the docker every 5 minutes and update immediately. Alternatively using the `--interval X` command to change the interval, where X is the amount of time in seconds to wait. e.g 21600 for 6 hours. -If you have any issues with the docker setup, please ask in #help but indicate you are using the docker. +If you have any issues with the docker setup, please ask in #help channel on our [Discord server](https://discordapp.com/invite/nadekobot) but indicate you are using the docker. -For information about configuring your bot or its functionality, please check the guides. +For information about configuring your bot or its functionality, please check the [documentation](http://nadekobot.readthedocs.io/en/latest). From 469f2bdc233563017327307a8d430fa519150502 Mon Sep 17 00:00:00 2001 From: shivaco Date: Fri, 22 Sep 2017 17:38:42 +0600 Subject: [PATCH 326/346] Some messing around with command usages --- docs/Commands List.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/Commands List.md b/docs/Commands List.md index 6d6a6d9a..9a3624df 100644 --- a/docs/Commands List.md +++ b/docs/Commands List.md @@ -448,9 +448,9 @@ Commands and aliases | Description | Usage ----------------|--------------|------- `.experience` `.xp` | Shows your xp stats. Specify the user to show that user's stats instead. | `.xp` `.xprolerewards` `.xprrs` | Shows currently set role rewards. | `.xprrs` -`.xprolereward` `.xprr` | Sets a role reward on a specified level. Provide no role name in order to remove the role reward. **Requires ManageRoles server permission.** | `.xprr 3 Social` -`.xpnotify` `.xpn` | Sets how the bot should notify you when you get a `server` or `global` level. You can set `dm` (for the bot to send a direct message), `channel` (to get notified in the channel you sent the last message in) or `none` to disable. | `.xpn global dm` `.xpn server channel` -`.xpexclude` `.xpex` | Exclude a user or a role from the xp system, or whole current server. **Requires Administrator server permission.** | `.xpex Role Excluded-Role` `.xpex Server` +`.xprolereward` `.xprr` | Sets a role reward on a specified level. Provide no role name in order to remove the role reward. **Requires ManageRoles server permission.** | `.xprr 4 Social` or `.xprr 9 Active` +`.xpnotify` `.xpn` | Sets how the bot should notify you when you get a `server` or `global` level. You can set `dm` (for the bot to send a direct message), `channel` (to get notified in the channel you sent the last message in) or `none` to disable. | `.xpn global dm` or `.xpn server channel` +`.xpexclude` `.xpex` | Exclude a user or a role from the xp system, or whole current server. **Requires Administrator server permission.** | `.xpex Role Excluded-Role` or `.xpex Server` or `.xpex channel spam` `.xpexclusionlist` `.xpexl` | Shows the roles and channels excluded from the XP system on this server, as well as whether the whole server is excluded. | `.xpexl` `.xpleaderboard` `.xplb` | Shows current server's xp leaderboard. | `.xplb` `.xpgleaderboard` `.xpglb` | Shows the global xp leaderboard. | `.xpglb` From 3dfe5b8d55b35073d0d8e360c9778be3d070f48a Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Sat, 23 Sep 2017 00:42:15 +0200 Subject: [PATCH 327/346] Fixed game, and other redis events when multiple bots are hosted on the same machine --- .../Services/CustomReactionsService.cs | 8 ++--- .../Modules/Searches/JokeCommands.cs | 33 +++---------------- .../Modules/Searches/Services/FeedsService.cs | 2 +- .../Searches/Services/SearchesService.cs | 27 +++++++++++++++ src/NadekoBot/NadekoBot.cs | 11 +++---- src/NadekoBot/Services/Impl/RedisCache.cs | 5 ++- 6 files changed, 46 insertions(+), 40 deletions(-) diff --git a/src/NadekoBot/Modules/CustomReactions/Services/CustomReactionsService.cs b/src/NadekoBot/Modules/CustomReactions/Services/CustomReactionsService.cs index 4cada529..3f965dda 100644 --- a/src/NadekoBot/Modules/CustomReactions/Services/CustomReactionsService.cs +++ b/src/NadekoBot/Modules/CustomReactions/Services/CustomReactionsService.cs @@ -49,12 +49,12 @@ namespace NadekoBot.Modules.CustomReactions.Services _cache = cache; var sub = _cache.Redis.GetSubscriber(); - sub.Subscribe("gcr.added", (ch, msg) => + sub.Subscribe(_client.CurrentUser.Id + "_gcr.added", (ch, msg) => { Array.Resize(ref GlobalReactions, GlobalReactions.Length + 1); GlobalReactions[GlobalReactions.Length - 1] = JsonConvert.DeserializeObject(msg); }, StackExchange.Redis.CommandFlags.FireAndForget); - sub.Subscribe("gcr.deleted", (ch, msg) => + sub.Subscribe(_client.CurrentUser.Id + "_gcr.deleted", (ch, msg) => { var id = int.Parse(msg); GlobalReactions = GlobalReactions.Where(cr => cr?.Id != id).ToArray(); @@ -69,13 +69,13 @@ namespace NadekoBot.Modules.CustomReactions.Services public Task AddGcr(CustomReaction cr) { var sub = _cache.Redis.GetSubscriber(); - return sub.PublishAsync("gcr.added", JsonConvert.SerializeObject(cr)); + return sub.PublishAsync(_client.CurrentUser.Id + "_gcr.added", JsonConvert.SerializeObject(cr)); } public Task DelGcr(int id) { var sub = _cache.Redis.GetSubscriber(); - return sub.PublishAsync("gcr.deleted", id); + return sub.PublishAsync(_client.CurrentUser.Id + "_gcr.deleted", id); } public void ClearStats() => ReactionStats.Clear(); diff --git a/src/NadekoBot/Modules/Searches/JokeCommands.cs b/src/NadekoBot/Modules/Searches/JokeCommands.cs index 3565b482..c7db2dbd 100644 --- a/src/NadekoBot/Modules/Searches/JokeCommands.cs +++ b/src/NadekoBot/Modules/Searches/JokeCommands.cs @@ -1,10 +1,7 @@ -using AngleSharp; -using Discord.Commands; +using Discord.Commands; using NadekoBot.Extensions; using NadekoBot.Modules.Searches.Services; -using Newtonsoft.Json.Linq; using System.Linq; -using System.Net.Http; using System.Threading.Tasks; using NadekoBot.Common; using NadekoBot.Common.Attributes; @@ -20,40 +17,20 @@ namespace NadekoBot.Modules.Searches [NadekoCommand, Usage, Description, Aliases] public async Task Yomama() { - using (var http = new HttpClient()) - { - var response = await http.GetStringAsync("http://api.yomomma.info/").ConfigureAwait(false); - await Context.Channel.SendConfirmAsync(JObject.Parse(response)["joke"].ToString() + " 😆").ConfigureAwait(false); - } + await Context.Channel.SendConfirmAsync(await _service.GetYomamaJoke()).ConfigureAwait(false); } [NadekoCommand, Usage, Description, Aliases] public async Task Randjoke() { - using (var http = new HttpClient()) - { - http.AddFakeHeaders(); - - var config = Configuration.Default.WithDefaultLoader(); - var document = await BrowsingContext.New(config).OpenAsync("http://www.goodbadjokes.com/random"); - - var html = document.QuerySelector(".post > .joke-content"); - - var part1 = html.QuerySelector("dt").TextContent; - var part2 = html.QuerySelector("dd").TextContent; - - await Context.Channel.SendConfirmAsync("", part1 + "\n\n" + part2, footer: document.BaseUri).ConfigureAwait(false); - } + var jokeInfo = await _service.GetRandomJoke(); + await Context.Channel.SendConfirmAsync("", jokeInfo.Text, footer: jokeInfo.BaseUri).ConfigureAwait(false); } [NadekoCommand, Usage, Description, Aliases] public async Task ChuckNorris() { - using (var http = new HttpClient()) - { - var response = await http.GetStringAsync("http://api.icndb.com/jokes/random/").ConfigureAwait(false); - await Context.Channel.SendConfirmAsync(JObject.Parse(response)["value"]["joke"].ToString() + " 😆").ConfigureAwait(false); - } + await Context.Channel.SendConfirmAsync(await _service.GetChuckNorrisJoke()).ConfigureAwait(false); } [NadekoCommand, Usage, Description, Aliases] diff --git a/src/NadekoBot/Modules/Searches/Services/FeedsService.cs b/src/NadekoBot/Modules/Searches/Services/FeedsService.cs index c0eae9d5..33ac0ff9 100644 --- a/src/NadekoBot/Modules/Searches/Services/FeedsService.cs +++ b/src/NadekoBot/Modules/Searches/Services/FeedsService.cs @@ -125,7 +125,7 @@ namespace NadekoBot.Modules.Searches.Services await Task.WhenAll(sendTasks).ConfigureAwait(false); } } - catch (Exception ex) { Console.WriteLine(ex); } + catch { } } await Task.Delay(10000); diff --git a/src/NadekoBot/Modules/Searches/Services/SearchesService.cs b/src/NadekoBot/Modules/Searches/Services/SearchesService.cs index ed87452a..be785bf3 100644 --- a/src/NadekoBot/Modules/Searches/Services/SearchesService.cs +++ b/src/NadekoBot/Modules/Searches/Services/SearchesService.cs @@ -15,6 +15,8 @@ using System.Linq; using Microsoft.EntityFrameworkCore; using NadekoBot.Modules.NSFW.Exceptions; using System.Net.Http; +using Newtonsoft.Json.Linq; +using AngleSharp; namespace NadekoBot.Modules.Searches.Services { @@ -195,6 +197,31 @@ namespace NadekoBot.Modules.Searches.Services c.Value?.Clear(); } } + + public async Task GetYomamaJoke() + { + var response = await Http.GetStringAsync("http://api.yomomma.info/").ConfigureAwait(false); + return JObject.Parse(response)["joke"].ToString() + " 😆"; + } + + public async Task<(string Text, string BaseUri)> GetRandomJoke() + { + var config = Configuration.Default.WithDefaultLoader(); + var document = await BrowsingContext.New(config).OpenAsync("http://www.goodbadjokes.com/random"); + + var html = document.QuerySelector(".post > .joke-content"); + + var part1 = html.QuerySelector("dt").TextContent; + var part2 = html.QuerySelector("dd").TextContent; + + return (part1 + "\n\n" + part2, document.BaseUri); + } + + public async Task GetChuckNorrisJoke() + { + var response = await Http.GetStringAsync("http://api.icndb.com/jokes/random/").ConfigureAwait(false); + return JObject.Parse(response)["value"]["joke"].ToString() + " 😆"; + } } public struct UserChannelPair diff --git a/src/NadekoBot/NadekoBot.cs b/src/NadekoBot/NadekoBot.cs index f6a87c11..946ba5c3 100644 --- a/src/NadekoBot/NadekoBot.cs +++ b/src/NadekoBot/NadekoBot.cs @@ -152,7 +152,7 @@ namespace NadekoBot .AddManual>(AllGuildConfigs) //todo wrap this .AddManual(this) .AddManual(uow) - .AddManual(new RedisCache()) + .AddManual(new RedisCache(Client.CurrentUser.Id)) .LoadFrom(Assembly.GetEntryAssembly()) .Build(); @@ -167,7 +167,6 @@ namespace NadekoBot CommandService.AddTypeReader(new ModuleOrCrTypeReader(CommandService)); CommandService.AddTypeReader(new GuildTypeReader(Client)); CommandService.AddTypeReader(new GuildDateTimeTypeReader()); - } } @@ -330,7 +329,7 @@ namespace NadekoBot private void HandleStatusChanges() { var sub = Services.GetService().Redis.GetSubscriber(); - sub.Subscribe("status.game_set", async (ch, game) => + sub.Subscribe(Client.CurrentUser.Id + "_status.game_set", async (ch, game) => { try { @@ -344,7 +343,7 @@ namespace NadekoBot } }, CommandFlags.FireAndForget); - sub.Subscribe("status.stream_set", async (ch, streamData) => + sub.Subscribe(Client.CurrentUser.Id + "_status.stream_set", async (ch, streamData) => { try { @@ -363,14 +362,14 @@ namespace NadekoBot { var obj = new { Name = game }; var sub = Services.GetService().Redis.GetSubscriber(); - return sub.PublishAsync("status.game_set", JsonConvert.SerializeObject(obj)); + return sub.PublishAsync(Client.CurrentUser.Id + "_status.game_set", JsonConvert.SerializeObject(obj)); } public Task SetStreamAsync(string name, string url) { var obj = new { Name = name, Url = url }; var sub = Services.GetService().Redis.GetSubscriber(); - return sub.PublishAsync("status.game_set", JsonConvert.SerializeObject(obj)); + return sub.PublishAsync(Client.CurrentUser.Id + "_status.game_set", JsonConvert.SerializeObject(obj)); } } } diff --git a/src/NadekoBot/Services/Impl/RedisCache.cs b/src/NadekoBot/Services/Impl/RedisCache.cs index 84083ef8..e0951120 100644 --- a/src/NadekoBot/Services/Impl/RedisCache.cs +++ b/src/NadekoBot/Services/Impl/RedisCache.cs @@ -5,11 +5,14 @@ namespace NadekoBot.Services.Impl { public class RedisCache : IDataCache { + private ulong _botid; + public ConnectionMultiplexer Redis { get; } private readonly IDatabase _db; - public RedisCache() + public RedisCache(ulong botId) { + _botid = botId; Redis = ConnectionMultiplexer.Connect("127.0.0.1"); Redis.PreserveAsyncOrder = false; _db = Redis.GetDatabase(); From 4f49b81dc14d7931906681e3a9324bd413e0178e Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Sat, 23 Sep 2017 03:32:12 +0200 Subject: [PATCH 328/346] Fixed .xprr, closes #1608 --- .../20170923002439_xprr-fix.Designer.cs | 1985 +++++++++++++++++ .../Migrations/20170923002439_xprr-fix.cs | 47 + .../NadekoSqliteContextModelSnapshot.cs | 12 +- .../Services/Database/Models/XpSettings.cs | 7 +- .../Services/Database/NadekoContext.cs | 8 +- 5 files changed, 2048 insertions(+), 11 deletions(-) create mode 100644 src/NadekoBot/Migrations/20170923002439_xprr-fix.Designer.cs create mode 100644 src/NadekoBot/Migrations/20170923002439_xprr-fix.cs diff --git a/src/NadekoBot/Migrations/20170923002439_xprr-fix.Designer.cs b/src/NadekoBot/Migrations/20170923002439_xprr-fix.Designer.cs new file mode 100644 index 00000000..79e0e678 --- /dev/null +++ b/src/NadekoBot/Migrations/20170923002439_xprr-fix.Designer.cs @@ -0,0 +1,1985 @@ +// +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage; +using Microsoft.EntityFrameworkCore.Storage.Internal; +using NadekoBot.Services.Database; +using NadekoBot.Services.Database.Models; +using System; + +namespace NadekoBot.Migrations +{ + [DbContext(typeof(NadekoContext))] + [Migration("20170923002439_xprr-fix")] + partial class xprrfix + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "2.0.0-rtm-26452"); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.AntiRaidSetting", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("Action"); + + b.Property("DateAdded"); + + b.Property("GuildConfigId"); + + b.Property("Seconds"); + + b.Property("UserThreshold"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId") + .IsUnique(); + + b.ToTable("AntiRaidSetting"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.AntiSpamIgnore", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AntiSpamSettingId"); + + b.Property("ChannelId"); + + b.Property("DateAdded"); + + b.HasKey("Id"); + + b.HasIndex("AntiSpamSettingId"); + + b.ToTable("AntiSpamIgnore"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.AntiSpamSetting", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("Action"); + + b.Property("DateAdded"); + + b.Property("GuildConfigId"); + + b.Property("MessageThreshold"); + + b.Property("MuteTime"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId") + .IsUnique(); + + b.ToTable("AntiSpamSetting"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.BlacklistItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("BotConfigId"); + + b.Property("DateAdded"); + + b.Property("ItemId"); + + b.Property("Type"); + + b.HasKey("Id"); + + b.HasIndex("BotConfigId"); + + b.ToTable("BlacklistItem"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.BlockedCmdOrMdl", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("BotConfigId"); + + b.Property("BotConfigId1"); + + b.Property("DateAdded"); + + b.Property("Name"); + + b.HasKey("Id"); + + b.HasIndex("BotConfigId"); + + b.HasIndex("BotConfigId1"); + + b.ToTable("BlockedCmdOrMdl"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.BotConfig", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("BetflipMultiplier"); + + b.Property("Betroll100Multiplier"); + + b.Property("Betroll67Multiplier"); + + b.Property("Betroll91Multiplier"); + + b.Property("BufferSize"); + + b.Property("CurrencyDropAmount"); + + b.Property("CurrencyDropAmountMax"); + + b.Property("CurrencyGenerationChance"); + + b.Property("CurrencyGenerationCooldown"); + + b.Property("CurrencyName"); + + b.Property("CurrencyPluralName"); + + b.Property("CurrencySign"); + + b.Property("CustomReactionsStartWith"); + + b.Property("DMHelpString"); + + b.Property("DateAdded"); + + b.Property("DefaultPrefix"); + + b.Property("ErrorColor"); + + b.Property("ForwardMessages"); + + b.Property("ForwardToAllOwners"); + + b.Property("HelpString"); + + b.Property("Locale"); + + b.Property("MigrationVersion"); + + b.Property("MinimumBetAmount"); + + b.Property("OkColor"); + + b.Property("PermissionVersion"); + + b.Property("RemindMessageFormat"); + + b.Property("RotatingStatuses"); + + b.Property("TriviaCurrencyReward"); + + b.Property("XpMinutesTimeout") + .ValueGeneratedOnAdd() + .HasDefaultValue(5); + + b.Property("XpPerMessage") + .ValueGeneratedOnAdd() + .HasDefaultValue(3); + + b.HasKey("Id"); + + b.ToTable("BotConfig"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.ClashCaller", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("BaseDestroyed"); + + b.Property("CallUser"); + + b.Property("ClashWarId"); + + b.Property("DateAdded"); + + b.Property("SequenceNumber"); + + b.Property("Stars"); + + b.Property("TimeAdded"); + + b.HasKey("Id"); + + b.HasIndex("ClashWarId"); + + b.ToTable("ClashCallers"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.ClashWar", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ChannelId"); + + b.Property("DateAdded"); + + b.Property("EnemyClan"); + + b.Property("GuildId"); + + b.Property("Size"); + + b.Property("StartedAt"); + + b.Property("WarState"); + + b.HasKey("Id"); + + b.ToTable("ClashOfClans"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.ClubApplicants", b => + { + b.Property("ClubId"); + + b.Property("UserId"); + + b.HasKey("ClubId", "UserId"); + + b.HasIndex("UserId"); + + b.ToTable("ClubApplicants"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.ClubBans", b => + { + b.Property("ClubId"); + + b.Property("UserId"); + + b.HasKey("ClubId", "UserId"); + + b.HasIndex("UserId"); + + b.ToTable("ClubBans"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.ClubInfo", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("Discrim"); + + b.Property("ImageUrl"); + + b.Property("MinimumLevelReq"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(20); + + b.Property("OwnerId"); + + b.Property("Xp"); + + b.HasKey("Id"); + + b.HasAlternateKey("Name", "Discrim"); + + b.HasIndex("OwnerId") + .IsUnique(); + + b.ToTable("Clubs"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.CommandAlias", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("GuildConfigId"); + + b.Property("Mapping"); + + b.Property("Trigger"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.ToTable("CommandAlias"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.CommandCooldown", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("CommandName"); + + b.Property("DateAdded"); + + b.Property("GuildConfigId"); + + b.Property("Seconds"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.ToTable("CommandCooldown"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.CommandPrice", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("BotConfigId"); + + b.Property("CommandName"); + + b.Property("DateAdded"); + + b.Property("Price"); + + b.HasKey("Id"); + + b.HasIndex("BotConfigId"); + + b.HasIndex("Price") + .IsUnique(); + + b.ToTable("CommandPrice"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.ConvertUnit", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("InternalTrigger"); + + b.Property("Modifier"); + + b.Property("UnitType"); + + b.HasKey("Id"); + + b.ToTable("ConversionUnits"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.Currency", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("Amount"); + + b.Property("DateAdded"); + + b.Property("UserId"); + + b.HasKey("Id"); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("Currency"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.CurrencyTransaction", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("Amount"); + + b.Property("DateAdded"); + + b.Property("Reason"); + + b.Property("UserId"); + + b.HasKey("Id"); + + b.ToTable("CurrencyTransactions"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.CustomReaction", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AutoDeleteTrigger"); + + b.Property("ContainsAnywhere"); + + b.Property("DateAdded"); + + b.Property("DmResponse"); + + b.Property("GuildId"); + + b.Property("IsRegex"); + + b.Property("OwnerOnly"); + + b.Property("Response"); + + b.Property("Trigger"); + + b.HasKey("Id"); + + b.ToTable("CustomReactions"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.DiscordUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AvatarId"); + + b.Property("ClubId"); + + b.Property("DateAdded"); + + b.Property("Discriminator"); + + b.Property("IsClubAdmin"); + + b.Property("LastLevelUp") + .ValueGeneratedOnAdd() + .HasDefaultValue(new DateTime(2017, 9, 21, 20, 53, 13, 305, DateTimeKind.Local)); + + b.Property("LastXpGain"); + + b.Property("NotifyOnLevelUp"); + + b.Property("TotalXp"); + + b.Property("UserId"); + + b.Property("Username"); + + b.HasKey("Id"); + + b.HasAlternateKey("UserId"); + + b.HasIndex("ClubId"); + + b.ToTable("DiscordUser"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.Donator", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("Amount"); + + b.Property("DateAdded"); + + b.Property("Name"); + + b.Property("UserId"); + + b.HasKey("Id"); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("Donators"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.EightBallResponse", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("BotConfigId"); + + b.Property("DateAdded"); + + b.Property("Text"); + + b.HasKey("Id"); + + b.HasIndex("BotConfigId"); + + b.ToTable("EightBallResponses"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.ExcludedItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("ItemId"); + + b.Property("ItemType"); + + b.Property("XpSettingsId"); + + b.HasKey("Id"); + + b.HasIndex("XpSettingsId"); + + b.ToTable("ExcludedItem"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.FeedSub", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ChannelId"); + + b.Property("DateAdded"); + + b.Property("GuildConfigId"); + + b.Property("Url") + .IsRequired(); + + b.HasKey("Id"); + + b.HasAlternateKey("GuildConfigId", "Url"); + + b.ToTable("FeedSub"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.FilterChannelId", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ChannelId"); + + b.Property("DateAdded"); + + b.Property("GuildConfigId"); + + b.Property("GuildConfigId1"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.HasIndex("GuildConfigId1"); + + b.ToTable("FilterChannelId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.FilteredWord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("GuildConfigId"); + + b.Property("Word"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.ToTable("FilteredWord"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.FollowedStream", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ChannelId"); + + b.Property("DateAdded"); + + b.Property("GuildConfigId"); + + b.Property("GuildId"); + + b.Property("Type"); + + b.Property("Username"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.ToTable("FollowedStream"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.GCChannelId", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ChannelId"); + + b.Property("DateAdded"); + + b.Property("GuildConfigId"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.ToTable("GCChannelId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.GuildConfig", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AutoAssignRoleId"); + + b.Property("AutoDeleteByeMessages"); + + b.Property("AutoDeleteByeMessagesTimer"); + + b.Property("AutoDeleteGreetMessages"); + + b.Property("AutoDeleteGreetMessagesTimer"); + + b.Property("AutoDeleteSelfAssignedRoleMessages"); + + b.Property("ByeMessageChannelId"); + + b.Property("ChannelByeMessageText"); + + b.Property("ChannelGreetMessageText"); + + b.Property("CleverbotEnabled"); + + b.Property("DateAdded"); + + b.Property("DefaultMusicVolume"); + + b.Property("DeleteMessageOnCommand"); + + b.Property("DmGreetMessageText"); + + b.Property("ExclusiveSelfAssignedRoles"); + + b.Property("FilterInvites"); + + b.Property("FilterWords"); + + b.Property("GameVoiceChannel"); + + b.Property("GreetMessageChannelId"); + + b.Property("GuildId"); + + b.Property("Locale"); + + b.Property("LogSettingId"); + + b.Property("MuteRoleName"); + + b.Property("PermissionRole"); + + b.Property("Prefix"); + + b.Property("RootPermissionId"); + + b.Property("SendChannelByeMessage"); + + b.Property("SendChannelGreetMessage"); + + b.Property("SendDmGreetMessage"); + + b.Property("TimeZoneId"); + + b.Property("VerboseErrors"); + + b.Property("VerbosePermissions"); + + b.Property("VoicePlusTextEnabled"); + + b.Property("WarningsInitialized"); + + b.HasKey("Id"); + + b.HasIndex("GuildId") + .IsUnique(); + + b.HasIndex("LogSettingId"); + + b.HasIndex("RootPermissionId"); + + b.ToTable("GuildConfigs"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.GuildRepeater", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ChannelId"); + + b.Property("DateAdded"); + + b.Property("GuildConfigId"); + + b.Property("GuildId"); + + b.Property("Interval"); + + b.Property("Message"); + + b.Property("StartTimeOfDay"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.ToTable("GuildRepeater"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.IgnoredLogChannel", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ChannelId"); + + b.Property("DateAdded"); + + b.Property("LogSettingId"); + + b.HasKey("Id"); + + b.HasIndex("LogSettingId"); + + b.ToTable("IgnoredLogChannels"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.IgnoredVoicePresenceChannel", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ChannelId"); + + b.Property("DateAdded"); + + b.Property("LogSettingId"); + + b.HasKey("Id"); + + b.HasIndex("LogSettingId"); + + b.ToTable("IgnoredVoicePresenceCHannels"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.LogSetting", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ChannelCreated"); + + b.Property("ChannelCreatedId"); + + b.Property("ChannelDestroyed"); + + b.Property("ChannelDestroyedId"); + + b.Property("ChannelId"); + + b.Property("ChannelUpdated"); + + b.Property("ChannelUpdatedId"); + + b.Property("DateAdded"); + + b.Property("IsLogging"); + + b.Property("LogOtherId"); + + b.Property("LogUserPresence"); + + b.Property("LogUserPresenceId"); + + b.Property("LogVoicePresence"); + + b.Property("LogVoicePresenceId"); + + b.Property("LogVoicePresenceTTSId"); + + b.Property("MessageDeleted"); + + b.Property("MessageDeletedId"); + + b.Property("MessageUpdated"); + + b.Property("MessageUpdatedId"); + + b.Property("UserBanned"); + + b.Property("UserBannedId"); + + b.Property("UserJoined"); + + b.Property("UserJoinedId"); + + b.Property("UserLeft"); + + b.Property("UserLeftId"); + + b.Property("UserMutedId"); + + b.Property("UserPresenceChannelId"); + + b.Property("UserUnbanned"); + + b.Property("UserUnbannedId"); + + b.Property("UserUpdated"); + + b.Property("UserUpdatedId"); + + b.Property("VoicePresenceChannelId"); + + b.HasKey("Id"); + + b.ToTable("LogSettings"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.ModulePrefix", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("BotConfigId"); + + b.Property("DateAdded"); + + b.Property("ModuleName"); + + b.Property("Prefix"); + + b.HasKey("Id"); + + b.HasIndex("BotConfigId"); + + b.ToTable("ModulePrefixes"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.MusicPlaylist", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("Author"); + + b.Property("AuthorId"); + + b.Property("DateAdded"); + + b.Property("Name"); + + b.HasKey("Id"); + + b.ToTable("MusicPlaylists"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.MutedUserId", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("GuildConfigId"); + + b.Property("UserId"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.ToTable("MutedUserId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.NsfwBlacklitedTag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("GuildConfigId"); + + b.Property("Tag"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.ToTable("NsfwBlacklitedTag"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.Permission", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("NextId"); + + b.Property("PrimaryTarget"); + + b.Property("PrimaryTargetId"); + + b.Property("SecondaryTarget"); + + b.Property("SecondaryTargetName"); + + b.Property("State"); + + b.HasKey("Id"); + + b.HasIndex("NextId") + .IsUnique(); + + b.ToTable("Permission"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.Permissionv2", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("GuildConfigId"); + + b.Property("Index"); + + b.Property("PrimaryTarget"); + + b.Property("PrimaryTargetId"); + + b.Property("SecondaryTarget"); + + b.Property("SecondaryTargetName"); + + b.Property("State"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.ToTable("Permissionv2"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.PlayingStatus", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("BotConfigId"); + + b.Property("DateAdded"); + + b.Property("Status"); + + b.HasKey("Id"); + + b.HasIndex("BotConfigId"); + + b.ToTable("PlayingStatus"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.PlaylistSong", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("MusicPlaylistId"); + + b.Property("Provider"); + + b.Property("ProviderType"); + + b.Property("Query"); + + b.Property("Title"); + + b.Property("Uri"); + + b.HasKey("Id"); + + b.HasIndex("MusicPlaylistId"); + + b.ToTable("PlaylistSong"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.Quote", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AuthorId"); + + b.Property("AuthorName") + .IsRequired(); + + b.Property("DateAdded"); + + b.Property("GuildId"); + + b.Property("Keyword") + .IsRequired(); + + b.Property("Text") + .IsRequired(); + + b.HasKey("Id"); + + b.ToTable("Quotes"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.RaceAnimal", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("BotConfigId"); + + b.Property("DateAdded"); + + b.Property("Icon"); + + b.Property("Name"); + + b.HasKey("Id"); + + b.HasIndex("BotConfigId"); + + b.ToTable("RaceAnimals"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.Reminder", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ChannelId"); + + b.Property("DateAdded"); + + b.Property("IsPrivate"); + + b.Property("Message"); + + b.Property("ServerId"); + + b.Property("UserId"); + + b.Property("When"); + + b.HasKey("Id"); + + b.ToTable("Reminders"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.RewardedUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AmountRewardedThisMonth"); + + b.Property("DateAdded"); + + b.Property("LastReward"); + + b.Property("PatreonUserId"); + + b.Property("UserId"); + + b.HasKey("Id"); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("RewardedUsers"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.SelfAssignedRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("GuildId"); + + b.Property("RoleId"); + + b.HasKey("Id"); + + b.HasIndex("GuildId", "RoleId") + .IsUnique(); + + b.ToTable("SelfAssignableRoles"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.ShopEntry", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AuthorId"); + + b.Property("DateAdded"); + + b.Property("GuildConfigId"); + + b.Property("Index"); + + b.Property("Name"); + + b.Property("Price"); + + b.Property("RoleId"); + + b.Property("RoleName"); + + b.Property("Type"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.ToTable("ShopEntry"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.ShopEntryItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("ShopEntryId"); + + b.Property("Text"); + + b.HasKey("Id"); + + b.HasIndex("ShopEntryId"); + + b.ToTable("ShopEntryItem"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.SlowmodeIgnoredRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("GuildConfigId"); + + b.Property("RoleId"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.ToTable("SlowmodeIgnoredRole"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.SlowmodeIgnoredUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("GuildConfigId"); + + b.Property("UserId"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.ToTable("SlowmodeIgnoredUser"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.StartupCommand", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("BotConfigId"); + + b.Property("ChannelId"); + + b.Property("ChannelName"); + + b.Property("CommandText"); + + b.Property("DateAdded"); + + b.Property("GuildId"); + + b.Property("GuildName"); + + b.Property("Index"); + + b.Property("VoiceChannelId"); + + b.Property("VoiceChannelName"); + + b.HasKey("Id"); + + b.HasIndex("BotConfigId"); + + b.ToTable("StartupCommand"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.StreamRoleBlacklistedUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("StreamRoleSettingsId"); + + b.Property("UserId"); + + b.Property("Username"); + + b.HasKey("Id"); + + b.HasIndex("StreamRoleSettingsId"); + + b.ToTable("StreamRoleBlacklistedUser"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.StreamRoleSettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AddRoleId"); + + b.Property("DateAdded"); + + b.Property("Enabled"); + + b.Property("FromRoleId"); + + b.Property("GuildConfigId"); + + b.Property("Keyword"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId") + .IsUnique(); + + b.ToTable("StreamRoleSettings"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.StreamRoleWhitelistedUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("StreamRoleSettingsId"); + + b.Property("UserId"); + + b.Property("Username"); + + b.HasKey("Id"); + + b.HasIndex("StreamRoleSettingsId"); + + b.ToTable("StreamRoleWhitelistedUser"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.UnmuteTimer", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("GuildConfigId"); + + b.Property("UnmuteAt"); + + b.Property("UserId"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.ToTable("UnmuteTimer"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.UserPokeTypes", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("UserId"); + + b.Property("type"); + + b.HasKey("Id"); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("PokeGame"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.UserXpStats", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AwardedXp"); + + b.Property("DateAdded"); + + b.Property("GuildId"); + + b.Property("LastLevelUp") + .ValueGeneratedOnAdd() + .HasDefaultValue(new DateTime(2017, 9, 21, 20, 53, 13, 307, DateTimeKind.Local)); + + b.Property("NotifyOnLevelUp"); + + b.Property("UserId"); + + b.Property("Xp"); + + b.HasKey("Id"); + + b.HasIndex("UserId", "GuildId") + .IsUnique(); + + b.ToTable("UserXpStats"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.VcRoleInfo", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("GuildConfigId"); + + b.Property("RoleId"); + + b.Property("VoiceChannelId"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.ToTable("VcRoleInfo"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.WaifuInfo", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AffinityId"); + + b.Property("ClaimerId"); + + b.Property("DateAdded"); + + b.Property("Price"); + + b.Property("WaifuId"); + + b.HasKey("Id"); + + b.HasIndex("AffinityId"); + + b.HasIndex("ClaimerId"); + + b.HasIndex("WaifuId") + .IsUnique(); + + b.ToTable("WaifuInfo"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.WaifuItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("Item"); + + b.Property("ItemEmoji"); + + b.Property("Price"); + + b.Property("WaifuInfoId"); + + b.HasKey("Id"); + + b.HasIndex("WaifuInfoId"); + + b.ToTable("WaifuItem"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.WaifuUpdate", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("NewId"); + + b.Property("OldId"); + + b.Property("UpdateType"); + + b.Property("UserId"); + + b.HasKey("Id"); + + b.HasIndex("NewId"); + + b.HasIndex("OldId"); + + b.HasIndex("UserId"); + + b.ToTable("WaifuUpdates"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.Warning", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("Forgiven"); + + b.Property("ForgivenBy"); + + b.Property("GuildId"); + + b.Property("Moderator"); + + b.Property("Reason"); + + b.Property("UserId"); + + b.HasKey("Id"); + + b.ToTable("Warnings"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.WarningPunishment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("Count"); + + b.Property("DateAdded"); + + b.Property("GuildConfigId"); + + b.Property("Punishment"); + + b.Property("Time"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.ToTable("WarningPunishment"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.XpRoleReward", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("Level"); + + b.Property("RoleId"); + + b.Property("XpSettingsId"); + + b.HasKey("Id"); + + b.HasIndex("XpSettingsId", "Level") + .IsUnique(); + + b.ToTable("XpRoleReward"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.XpSettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("GuildConfigId"); + + b.Property("NotifyMessage"); + + b.Property("ServerExcluded"); + + b.Property("XpRoleRewardExclusive"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId") + .IsUnique(); + + b.ToTable("XpSettings"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.AntiRaidSetting", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig", "GuildConfig") + .WithOne("AntiRaidSetting") + .HasForeignKey("NadekoBot.Services.Database.Models.AntiRaidSetting", "GuildConfigId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.AntiSpamIgnore", b => + { + b.HasOne("NadekoBot.Services.Database.Models.AntiSpamSetting") + .WithMany("IgnoredChannels") + .HasForeignKey("AntiSpamSettingId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.AntiSpamSetting", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig", "GuildConfig") + .WithOne("AntiSpamSetting") + .HasForeignKey("NadekoBot.Services.Database.Models.AntiSpamSetting", "GuildConfigId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.BlacklistItem", b => + { + b.HasOne("NadekoBot.Services.Database.Models.BotConfig") + .WithMany("Blacklist") + .HasForeignKey("BotConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.BlockedCmdOrMdl", b => + { + b.HasOne("NadekoBot.Services.Database.Models.BotConfig") + .WithMany("BlockedCommands") + .HasForeignKey("BotConfigId"); + + b.HasOne("NadekoBot.Services.Database.Models.BotConfig") + .WithMany("BlockedModules") + .HasForeignKey("BotConfigId1"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.ClashCaller", b => + { + b.HasOne("NadekoBot.Services.Database.Models.ClashWar", "ClashWar") + .WithMany("Bases") + .HasForeignKey("ClashWarId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.ClubApplicants", b => + { + b.HasOne("NadekoBot.Services.Database.Models.ClubInfo", "Club") + .WithMany("Applicants") + .HasForeignKey("ClubId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("NadekoBot.Services.Database.Models.DiscordUser", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.ClubBans", b => + { + b.HasOne("NadekoBot.Services.Database.Models.ClubInfo", "Club") + .WithMany("Bans") + .HasForeignKey("ClubId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("NadekoBot.Services.Database.Models.DiscordUser", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.ClubInfo", b => + { + b.HasOne("NadekoBot.Services.Database.Models.DiscordUser", "Owner") + .WithOne() + .HasForeignKey("NadekoBot.Services.Database.Models.ClubInfo", "OwnerId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.CommandAlias", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig") + .WithMany("CommandAliases") + .HasForeignKey("GuildConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.CommandCooldown", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig") + .WithMany("CommandCooldowns") + .HasForeignKey("GuildConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.CommandPrice", b => + { + b.HasOne("NadekoBot.Services.Database.Models.BotConfig") + .WithMany("CommandPrices") + .HasForeignKey("BotConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.DiscordUser", b => + { + b.HasOne("NadekoBot.Services.Database.Models.ClubInfo", "Club") + .WithMany("Users") + .HasForeignKey("ClubId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.EightBallResponse", b => + { + b.HasOne("NadekoBot.Services.Database.Models.BotConfig") + .WithMany("EightBallResponses") + .HasForeignKey("BotConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.ExcludedItem", b => + { + b.HasOne("NadekoBot.Services.Database.Models.XpSettings") + .WithMany("ExclusionList") + .HasForeignKey("XpSettingsId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.FeedSub", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig", "GuildConfig") + .WithMany("FeedSubs") + .HasForeignKey("GuildConfigId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.FilterChannelId", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig") + .WithMany("FilterInvitesChannelIds") + .HasForeignKey("GuildConfigId"); + + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig") + .WithMany("FilterWordsChannelIds") + .HasForeignKey("GuildConfigId1"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.FilteredWord", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig") + .WithMany("FilteredWords") + .HasForeignKey("GuildConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.FollowedStream", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig") + .WithMany("FollowedStreams") + .HasForeignKey("GuildConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.GCChannelId", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig") + .WithMany("GenerateCurrencyChannelIds") + .HasForeignKey("GuildConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.GuildConfig", b => + { + b.HasOne("NadekoBot.Services.Database.Models.LogSetting", "LogSetting") + .WithMany() + .HasForeignKey("LogSettingId"); + + b.HasOne("NadekoBot.Services.Database.Models.Permission", "RootPermission") + .WithMany() + .HasForeignKey("RootPermissionId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.GuildRepeater", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig") + .WithMany("GuildRepeaters") + .HasForeignKey("GuildConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.IgnoredLogChannel", b => + { + b.HasOne("NadekoBot.Services.Database.Models.LogSetting", "LogSetting") + .WithMany("IgnoredChannels") + .HasForeignKey("LogSettingId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.IgnoredVoicePresenceChannel", b => + { + b.HasOne("NadekoBot.Services.Database.Models.LogSetting", "LogSetting") + .WithMany("IgnoredVoicePresenceChannelIds") + .HasForeignKey("LogSettingId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.ModulePrefix", b => + { + b.HasOne("NadekoBot.Services.Database.Models.BotConfig") + .WithMany("ModulePrefixes") + .HasForeignKey("BotConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.MutedUserId", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig") + .WithMany("MutedUsers") + .HasForeignKey("GuildConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.NsfwBlacklitedTag", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig") + .WithMany("NsfwBlacklistedTags") + .HasForeignKey("GuildConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.Permission", b => + { + b.HasOne("NadekoBot.Services.Database.Models.Permission", "Next") + .WithOne("Previous") + .HasForeignKey("NadekoBot.Services.Database.Models.Permission", "NextId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.Permissionv2", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig") + .WithMany("Permissions") + .HasForeignKey("GuildConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.PlayingStatus", b => + { + b.HasOne("NadekoBot.Services.Database.Models.BotConfig") + .WithMany("RotatingStatusMessages") + .HasForeignKey("BotConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.PlaylistSong", b => + { + b.HasOne("NadekoBot.Services.Database.Models.MusicPlaylist") + .WithMany("Songs") + .HasForeignKey("MusicPlaylistId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.RaceAnimal", b => + { + b.HasOne("NadekoBot.Services.Database.Models.BotConfig") + .WithMany("RaceAnimals") + .HasForeignKey("BotConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.ShopEntry", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig") + .WithMany("ShopEntries") + .HasForeignKey("GuildConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.ShopEntryItem", b => + { + b.HasOne("NadekoBot.Services.Database.Models.ShopEntry") + .WithMany("Items") + .HasForeignKey("ShopEntryId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.SlowmodeIgnoredRole", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig") + .WithMany("SlowmodeIgnoredRoles") + .HasForeignKey("GuildConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.SlowmodeIgnoredUser", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig") + .WithMany("SlowmodeIgnoredUsers") + .HasForeignKey("GuildConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.StartupCommand", b => + { + b.HasOne("NadekoBot.Services.Database.Models.BotConfig") + .WithMany("StartupCommands") + .HasForeignKey("BotConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.StreamRoleBlacklistedUser", b => + { + b.HasOne("NadekoBot.Services.Database.Models.StreamRoleSettings") + .WithMany("Blacklist") + .HasForeignKey("StreamRoleSettingsId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.StreamRoleSettings", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig", "GuildConfig") + .WithOne("StreamRole") + .HasForeignKey("NadekoBot.Services.Database.Models.StreamRoleSettings", "GuildConfigId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.StreamRoleWhitelistedUser", b => + { + b.HasOne("NadekoBot.Services.Database.Models.StreamRoleSettings") + .WithMany("Whitelist") + .HasForeignKey("StreamRoleSettingsId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.UnmuteTimer", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig") + .WithMany("UnmuteTimers") + .HasForeignKey("GuildConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.VcRoleInfo", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig") + .WithMany("VcRoleInfos") + .HasForeignKey("GuildConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.WaifuInfo", b => + { + b.HasOne("NadekoBot.Services.Database.Models.DiscordUser", "Affinity") + .WithMany() + .HasForeignKey("AffinityId"); + + b.HasOne("NadekoBot.Services.Database.Models.DiscordUser", "Claimer") + .WithMany() + .HasForeignKey("ClaimerId"); + + b.HasOne("NadekoBot.Services.Database.Models.DiscordUser", "Waifu") + .WithOne() + .HasForeignKey("NadekoBot.Services.Database.Models.WaifuInfo", "WaifuId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.WaifuItem", b => + { + b.HasOne("NadekoBot.Services.Database.Models.WaifuInfo") + .WithMany("Items") + .HasForeignKey("WaifuInfoId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.WaifuUpdate", b => + { + b.HasOne("NadekoBot.Services.Database.Models.DiscordUser", "New") + .WithMany() + .HasForeignKey("NewId"); + + b.HasOne("NadekoBot.Services.Database.Models.DiscordUser", "Old") + .WithMany() + .HasForeignKey("OldId"); + + b.HasOne("NadekoBot.Services.Database.Models.DiscordUser", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.WarningPunishment", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig") + .WithMany("WarnPunishments") + .HasForeignKey("GuildConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.XpRoleReward", b => + { + b.HasOne("NadekoBot.Services.Database.Models.XpSettings", "XpSettings") + .WithMany("RoleRewards") + .HasForeignKey("XpSettingsId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.XpSettings", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig", "GuildConfig") + .WithOne("XpSettings") + .HasForeignKey("NadekoBot.Services.Database.Models.XpSettings", "GuildConfigId") + .OnDelete(DeleteBehavior.Cascade); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/NadekoBot/Migrations/20170923002439_xprr-fix.cs b/src/NadekoBot/Migrations/20170923002439_xprr-fix.cs new file mode 100644 index 00000000..e8160e83 --- /dev/null +++ b/src/NadekoBot/Migrations/20170923002439_xprr-fix.cs @@ -0,0 +1,47 @@ +using Microsoft.EntityFrameworkCore.Migrations; +using System; +using System.Collections.Generic; + +namespace NadekoBot.Migrations +{ + public partial class xprrfix : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable("XpRoleReward"); + + migrationBuilder.CreateTable( + name: "XpRoleReward", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + DateAdded = table.Column(type: "TEXT", nullable: true), + Level = table.Column(type: "INTEGER", nullable: false), + RoleId = table.Column(type: "INTEGER", nullable: false), + XpSettingsId = table.Column(type: "INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_XpRoleReward", x => x.Id); + table.ForeignKey( + name: "FK_XpRoleReward_XpSettings_XpSettingsId", + column: x => x.XpSettingsId, + principalTable: "XpSettings", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_XpRoleReward_XpSettingsId_Level", + table: "XpRoleReward", + columns: new[] { "XpSettingsId", "Level" }, + unique: true); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + + } + } +} diff --git a/src/NadekoBot/Migrations/NadekoSqliteContextModelSnapshot.cs b/src/NadekoBot/Migrations/NadekoSqliteContextModelSnapshot.cs index 37c56969..dee868e0 100644 --- a/src/NadekoBot/Migrations/NadekoSqliteContextModelSnapshot.cs +++ b/src/NadekoBot/Migrations/NadekoSqliteContextModelSnapshot.cs @@ -1559,13 +1559,12 @@ namespace NadekoBot.Migrations b.Property("RoleId"); - b.Property("XpSettingsId"); + b.Property("XpSettingsId"); b.HasKey("Id"); - b.HasAlternateKey("Level"); - - b.HasIndex("XpSettingsId"); + b.HasIndex("XpSettingsId", "Level") + .IsUnique(); b.ToTable("XpRoleReward"); }); @@ -1966,9 +1965,10 @@ namespace NadekoBot.Migrations modelBuilder.Entity("NadekoBot.Services.Database.Models.XpRoleReward", b => { - b.HasOne("NadekoBot.Services.Database.Models.XpSettings") + b.HasOne("NadekoBot.Services.Database.Models.XpSettings", "XpSettings") .WithMany("RoleRewards") - .HasForeignKey("XpSettingsId"); + .HasForeignKey("XpSettingsId") + .OnDelete(DeleteBehavior.Cascade); }); modelBuilder.Entity("NadekoBot.Services.Database.Models.XpSettings", b => diff --git a/src/NadekoBot/Services/Database/Models/XpSettings.cs b/src/NadekoBot/Services/Database/Models/XpSettings.cs index cf89d611..fcc67fff 100644 --- a/src/NadekoBot/Services/Database/Models/XpSettings.cs +++ b/src/NadekoBot/Services/Database/Models/XpSettings.cs @@ -18,17 +18,20 @@ namespace NadekoBot.Services.Database.Models public class XpRoleReward : DbEntity { + public int XpSettingsId { get; set; } + public XpSettings XpSettings { get; set; } + public int Level { get; set; } public ulong RoleId { get; set; } public override int GetHashCode() { - return Level.GetHashCode() ^ RoleId.GetHashCode(); + return Level.GetHashCode() ^ XpSettingsId.GetHashCode(); } public override bool Equals(object obj) { - return obj is XpRoleReward xrr && xrr.Level == Level && xrr.RoleId == RoleId; + return obj is XpRoleReward xrr && xrr.Level == Level && xrr.XpSettingsId == XpSettingsId; } } diff --git a/src/NadekoBot/Services/Database/NadekoContext.cs b/src/NadekoBot/Services/Database/NadekoContext.cs index 373ab756..9b4c1e46 100644 --- a/src/NadekoBot/Services/Database/NadekoContext.cs +++ b/src/NadekoBot/Services/Database/NadekoContext.cs @@ -277,7 +277,7 @@ namespace NadekoBot.Services.Database modelBuilder.Entity() .Property(x => x.LastLevelUp) - .HasDefaultValue(DateTime.Now); + .HasDefaultValue(new DateTime(2017, 9, 21, 20, 53, 13, 305, DateTimeKind.Local)); #endregion @@ -298,7 +298,8 @@ namespace NadekoBot.Services.Database modelBuilder.Entity() .Property(x => x.LastLevelUp) - .HasDefaultValue(DateTime.Now); + .HasDefaultValue(new DateTime(2017, 9, 21, 20, 53, 13, 307, DateTimeKind.Local)); + #endregion #region XpSettings @@ -310,7 +311,8 @@ namespace NadekoBot.Services.Database //todo major bug #region XpRoleReward modelBuilder.Entity() - .HasAlternateKey(x => x.Level); + .HasIndex(x => new { x.XpSettingsId, x.Level }) + .IsUnique(); #endregion #region Club From 851452f950d92887a2cc59598fb208ea91680afd Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Sat, 23 Sep 2017 03:38:17 +0200 Subject: [PATCH 329/346] Made clubbans and clubapps error more specific. closes #1605 --- src/NadekoBot/Modules/Xp/Club.cs | 4 ++-- src/NadekoBot/_strings/ResponseStrings.en-US.json | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/NadekoBot/Modules/Xp/Club.cs b/src/NadekoBot/Modules/Xp/Club.cs index 6ef7b5b5..e62b458a 100644 --- a/src/NadekoBot/Modules/Xp/Club.cs +++ b/src/NadekoBot/Modules/Xp/Club.cs @@ -142,7 +142,7 @@ namespace NadekoBot.Modules.Xp var club = _service.GetClubWithBansAndApplications(Context.User.Id); if (club == null) - return ReplyErrorLocalized("club_not_exists"); + return ReplyErrorLocalized("club_not_exists_owner"); var bans = club .Bans @@ -174,7 +174,7 @@ namespace NadekoBot.Modules.Xp var club = _service.GetClubWithBansAndApplications(Context.User.Id); if (club == null) - return ReplyErrorLocalized("club_not_exists"); + return ReplyErrorLocalized("club_not_exists_owner"); var apps = club .Applicants diff --git a/src/NadekoBot/_strings/ResponseStrings.en-US.json b/src/NadekoBot/_strings/ResponseStrings.en-US.json index fe0a5ef3..081dc5c9 100644 --- a/src/NadekoBot/_strings/ResponseStrings.en-US.json +++ b/src/NadekoBot/_strings/ResponseStrings.en-US.json @@ -847,6 +847,7 @@ "xp_club_create_error": "Failed creating the club. Make sure you're above level 5 and not a member of a club already.", "xp_club_created": "Club {0} successfully created!", "xp_club_not_exists": "That club doesn't exist.", + "xp_club_not_exists_owner": "You are not the owner or the admin of the club.", "xp_club_applied": "You've applied for membership in {0} club.", "xp_club_apply_error": "Error applying. You are either already a member of the club, or you don't meet the minimum level requirement, or you've been banned from this one.", "xp_club_accepted": "Accepted user {0} to the club.", From 2d8b9d677c59b31c34dd2cacaf68c49195cdbc36 Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Sat, 23 Sep 2017 12:28:13 +0200 Subject: [PATCH 330/346] Fixed server and global ranking when the user didn't get any xp. close 1581 --- .../Modules/Searches/Common/SearchImageCacher.cs | 1 - .../Database/Repositories/Impl/DiscordUserRepository.cs | 8 ++++++-- .../Services/Database/Repositories/Impl/XpRepository.cs | 3 +++ 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/src/NadekoBot/Modules/Searches/Common/SearchImageCacher.cs b/src/NadekoBot/Modules/Searches/Common/SearchImageCacher.cs index 8bd5d919..20b06014 100644 --- a/src/NadekoBot/Modules/Searches/Common/SearchImageCacher.cs +++ b/src/NadekoBot/Modules/Searches/Common/SearchImageCacher.cs @@ -99,7 +99,6 @@ namespace NadekoBot.Modules.Searches.Common public async Task DownloadImages(string tag, bool isExplicit, DapiSearchType type) { - _log.Info($"Loading extra images from {type}"); tag = tag?.Replace(" ", "_").ToLowerInvariant(); if (isExplicit) tag = "rating%3Aexplicit+" + tag; diff --git a/src/NadekoBot/Services/Database/Repositories/Impl/DiscordUserRepository.cs b/src/NadekoBot/Services/Database/Repositories/Impl/DiscordUserRepository.cs index 16938a0d..5238198d 100644 --- a/src/NadekoBot/Services/Database/Repositories/Impl/DiscordUserRepository.cs +++ b/src/NadekoBot/Services/Database/Repositories/Impl/DiscordUserRepository.cs @@ -40,10 +40,14 @@ namespace NadekoBot.Services.Database.Repositories.Impl public int GetUserGlobalRanking(ulong id) { - return _set.Count(x => x.TotalXp > + if (!_set.Where(y => y.UserId == id).Any()) + { + return _set.Count(); + } + return _set.Count(x => x.TotalXp >= _set.Where(y => y.UserId == id) .DefaultIfEmpty() - .Sum(y => y.TotalXp)); + .Sum(y => y.TotalXp)) + 1; } public DiscordUser[] GetUsersXpLeaderboardFor(int page) diff --git a/src/NadekoBot/Services/Database/Repositories/Impl/XpRepository.cs b/src/NadekoBot/Services/Database/Repositories/Impl/XpRepository.cs index b833cb79..64c78413 100644 --- a/src/NadekoBot/Services/Database/Repositories/Impl/XpRepository.cs +++ b/src/NadekoBot/Services/Database/Repositories/Impl/XpRepository.cs @@ -39,6 +39,9 @@ namespace NadekoBot.Services.Database.Repositories.Impl public int GetUserGuildRanking(ulong userId, ulong guildId) { + if (!_set.Where(x => x.GuildId == guildId && x.UserId == userId).Any()) + return _set.Count(); + return _set .Where(x => x.GuildId == guildId) .Count(x => x.Xp > (_set From b30c672922f0e7b5a3953a7ca41868ef1558376c Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Sat, 23 Sep 2017 12:40:31 +0200 Subject: [PATCH 331/346] Update ResponseStrings.nb-NO.json (POEditor.com) --- .../_strings/ResponseStrings.nb-NO.json | 112 ++++++++++++++---- 1 file changed, 88 insertions(+), 24 deletions(-) diff --git a/src/NadekoBot/_strings/ResponseStrings.nb-NO.json b/src/NadekoBot/_strings/ResponseStrings.nb-NO.json index c28e563b..e79389e9 100644 --- a/src/NadekoBot/_strings/ResponseStrings.nb-NO.json +++ b/src/NadekoBot/_strings/ResponseStrings.nb-NO.json @@ -1,27 +1,4 @@ { - "clashofclans_base_already_claimed": "Den basen har allerede blitt tatt over eller ødelagt.", - "clashofclans_base_already_destroyed": "Den basen er allerede ødelagt.", - "clashofclans_base_already_unclaimed": "Ingen har hevdet denne basen.", - "clashofclans_base_destroyed": "**ØDELAGT** base #{0} i en krig mot {1}", - "clashofclans_base_unclaimed": "{0} har **hevdet** bare #{1} i en krig mot {2}", - "clashofclans_claimed_base": "{0} har hevdet en base #{1} i en krig mot {2}", - "clashofclans_claimed_other": "@{0} Du har allerede hevdet basen #{1}. Du kan ikke kreve en ny.", - "clashofclans_claim_expired": "Krav fra @{0} for en krig mot {1} har utløpt.", - "clashofclans_enemy": "Fiende", - "clashofclans_info_about_war": "Info om krigen mot {0}", - "clashofclans_invalid_base_number": "Ugyldig basenummer", - "clashofclans_invalid_size": "Ikke en gyldig krigstørrelse", - "clashofclans_list_active_wars": "Liste over aktive kriger", - "clashofclans_not_claimed": "ikke hevdet", - "clashofclans_not_partic": "Du er ikke en deltager i den krigen.", - "clashofclans_not_partic_or_destroyed": "@{0} Du deltar enten ikke i denne krigen, eller basen er allerede ødelagt.", - "clashofclans_no_active_wars": "Ingen aktive kriger.", - "clashofclans_size": "Størrelse", - "clashofclans_war_already_started": "Krig mot {0} har allerede begynt.", - "clashofclans_war_created": "Krig mot {0} opprettet.", - "clashofclans_war_ended": "Krig mot {0} avsluttet.", - "clashofclans_war_not_exist": "Den krigen eksisterer ikke.", - "clashofclans_war_started": "Krig mot {0} startet!", "customreactions_all_stats_cleared": "Alle tilpassede reaksjons-statistikker fjernet.", "customreactions_deleted": "Tilpasset reaksjon fjernet", "customreactions_insuff_perms": "Utilstrekkelig tilgang. Krever Bot-eierskap for globale tilpassede reaksjoner, og Administrator for server reaksjoner.", @@ -804,5 +781,92 @@ "music_songs_shuffle_enable": "Sanger vil spilles av i tilfeldig rekkefølge fra nå.", "music_songs_shuffle_disable": "Sanger vil ikke lenger spilles av i tilfeldig rekkefølge.", "music_song_skips_after": "Sanger vil hoppe etter {0}", - "administration_warnings_list": "Liste over advarte brukere på denne serveren." + "administration_warnings_list": "Liste over advarte brukere på denne serveren.", + "customreactions_redacted_too_long": "Fjernet fordi den var for lang.", + "nsfw_blacklisted_tag_list": "Svartelistede knagger.", + "nsfw_blacklisted_tag": "Én eller flere av knaggene du brukte er svartelistet.", + "nsfw_blacklisted_tag_add": "NSFW knagg {0} er nå svartelistet.", + "nsfw_blacklisted_tag_remove": "NSFW knagg {0} er ikke lenger svartelistet.", + "gambling_waifu_gift": "Ga {0} til {1].", + "gambling_waifu_gift_shop": "Waifu gavebutikk.", + "gambling_gifts": "Gaver.", + "games_connect4_created": "Startet et 4-på-rad spill. Venter på at spiller skal bli med.", + "games_connect4_player_to_move": "Spiller {0} sin tur.", + "games_connect4_failed_to_start": "4-på-rad kunne ikke starte fordi ingen ble med.", + "games_connect4_draw": "4-på-rad endte uavgjort.", + "games_connect4_won": "{0} vant 4-på-rad mot {1}", + "games_nunchi_joined": "Startet 'nunchi'. {0} spillere har blitt med.", + "games_nunchi_ended": "Nunchi avsluttet. {0} vant.", + "games_nunchi_ended_no_winner": "Nunchi avsluttet uten en vinner.", + "games_nunchi_started": "Nunchi startet med {0} deltakere.", + "games_nunchi_round_ended": "Nunchi runde avsluttet. {0} er ute av spillet.", + "games_nunchi_round_ended_boot": "Nunchi runded avsluttet fordi én eller flere spillere ikke reagerte i tide. Disse er fremdeles med i spillet: {0}", + "games_nunchi_round_started": "Nunchi startet med {0} spillere. Start med å telle fra {1}", + "games_nunchi_next_number": "Tall registrert. Siste nummer var {0}.", + "games_nunchi_failed_to_start": "Nunchi kunne ikke starte fordi det ikke var nok deltakere.", + "games_nunchi_created": "Nunchi startet. Venter på spillere.", + "music_sad_enabled": "Sanger vil bli fjernet fra lista når de er spilt av.", + "music_sad_disabled": "Sanger blir ikke lenger fjernet fra lista når de er spilt av.", + "utility_stream_role_enabled": "Når en bruker med rollen {0} strømmer, får de rollen {1}.", + "utility_stream_role_disabled": "Strømmerolle skrudd på.", + "utility_stream_role_kw_set": "Strømmere trenger nå nøkkelordet {0} for å få den rollen.", + "utility_stream_role_kw_reset": "Nøkkelord for strømmerolle er tilbakestilt.", + "utility_stream_role_bl_add": "Bruker {0} vil aldri få strømmerollen.", + "utility_stream_role_bl_add_fail": "Bruker {0} er allerede svartelistet.", + "utility_stream_role_bl_rem": "Bruker {0} er ikke lenger svartelistet.", + "utility_stream_role_bl_rem_fail": "Bruker {0} er ikke svartelistet.", + "utility_stream_role_wl_add": "Bruker {0} får strømmerollen selv om de ikke har nøkkelordet i strømmens tittel.", + "utility_stream_role_wl_add_fail": "Bruker {0} er allerede hvitelistet.", + "utility_stream_role_wl_rem": "Bruker {0} er ikke lenger hvitelistet.", + "utility_stream_role_wl_rem_fail": "Bruker {0} er ikke hvitelistet.", + "utility_bot_config_edit_fail": "Kunne ikke sette verdien {0} til {1}", + "utility_bot_config_edit_success": "Verdien {0} er satt til {1}", + "customreactions_crca_disabled": "Tilpasset reaksjon med ID {0} vil ikke lenger trigges med mindre triggerordet er i begynnelsen av en setning.", + "customreactions_crca_enabled": "Tilpasset reaksjon med ID {0} vil nå trigges uansett hvor i setningen triggerordet er.", + "xp_server_level": "Nivå i serveren.", + "xp_level": "Nivå", + "xp_club": "Klubb", + "xp_xp": "Erfaring", + "xp_excluded": "{0} er nå ekskludert fra nivå-systemet på denne serveren.", + "xp_not_excluded": "{0} er ikke lenger ekskludert fra nivå-systemet på denne serveren.", + "xp_exclusion_list": "Eksklusjonsliste", + "xp_server_is_excluded": "Denne serveren er ekskludert", + "xp_server_is_not_excluded": "Denne serveren er ikke ekskludert.", + "xp_excluded_roles": "Ekskluderte roller.", + "xp_excluded_channels": "Ekskluderte kanaler.", + "xp_level_up_channel": "Gratulerer {0}, du er nå nivå {1}!", + "xp_level_up_dm": "Gratulerer {0}, du er nå nivå {1} på serveren {2}.", + "xp_level_up_global": "Gratulerer {0}, ditt globale nivå er nå {1}!", + "xp_role_reward_cleared": "Nivå {0} vil ikke lenger gi noen belønning.", + "xp_role_reward_added": "Brukere som når nivå {0} vil få rollen {1}.", + "xp_role_rewards": "Rolle belønninger.", + "xp_level_x": "Nivå {0}", + "xp_no_role_rewards": "Ingen rollebelønninger på denne siden.", + "xp_server_leaderboard": "Server XP ledetavle.", + "xp_global_leaderboard": "Global XP ledetavle", + "xp_modified": "Redigerte server XP-nivået til {0} med {1}", + "xp_club_create_error": "Kunne ikke opprette klubb. Du må være minst nivå 5 og ikke være i en klubb allerede.", + "xp_club_created": "Klubben {0} ble suksessfult opprettet.", + "xp_club_not_exists": "Den klubben eksisterer ikke.", + "xp_club_applied": "Du har søkt om medlemskap i klubben {0}.", + "xp_club_apply_error": "Kunne ikke søke. Du er enten medlem i en annen klubb, møter ikke minimum nivå-krav, eller du er utestengt fra denne klubben.", + "xp_club_accepted": "Aksepterte {0} inn i klubben.", + "xp_club_accept_error": "Fant ikke bruker.", + "xp_club_left": "Du forlot klubben.", + "xp_club_not_in_club": "Du er ikke i en klubb", + "xp_club_user_kick": "{0} sparket deg fra klubben {1}", + "xp_club_user_kick_fail": "Kunne ikke sparke brukeren. Enten er du ikke klubbeieren, eller brukeren er ikke med i klubben.", + "xp_club_user_banned": "Utestengte {0} fra {1}", + "xp_club_user_ban_fail": "Kunne ikke utestenge. Enten er du ikke eier av klubben, eller den brukeren er ikke medlem av klubben.", + "xp_club_user_unbanned": "{0} er ikke lenger utestengt fra {1}", + "xp_club_user_unban_fail": "Kunne ikke fjerne utestenging. Enten er du ikke klubbeieren, eller brukeren har ikke søkt om medlemskap.", + "xp_club_level_req_changed": "Endret klubbens nivå-krav til {0}", + "xp_club_level_req_change_error": "Kunne ikke endre nivå-krav\n", + "xp_club_disbanded": "Klubben {0} er nå oppløst.\n", + "xp_club_disband_error": "Feil. Enten er du ikke i en klubb eller du er ikke eieren av klubben.", + "xp_club_icon_error": "Ikke en gyldig bilde-URL eller du er ikke eieren av klubben.", + "xp_club_icon_set": "Nytt klubb-bilde satt.", + "xp_club_bans_for": "Utestengelser for klubben {0}", + "xp_club_apps_for": "Søkere for klubben {0}", + "xp_club_leaderboard": "Ledetavle - side {0}" } \ No newline at end of file From 2acf623f8e5b50b4a7396b476f50d4664eb9aaf4 Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Sat, 23 Sep 2017 12:40:33 +0200 Subject: [PATCH 332/346] Update ResponseStrings.cs-CZ.json (POEditor.com) --- .../_strings/ResponseStrings.cs-CZ.json | 114 ++++++++++++++---- 1 file changed, 89 insertions(+), 25 deletions(-) diff --git a/src/NadekoBot/_strings/ResponseStrings.cs-CZ.json b/src/NadekoBot/_strings/ResponseStrings.cs-CZ.json index dc5d4f70..ea900b0f 100644 --- a/src/NadekoBot/_strings/ResponseStrings.cs-CZ.json +++ b/src/NadekoBot/_strings/ResponseStrings.cs-CZ.json @@ -1,27 +1,4 @@ { - "clashofclans_base_already_claimed": "Tato základna je již obsazena nebo zničena.", - "clashofclans_base_already_destroyed": "Tato základna je již zničena.", - "clashofclans_base_already_unclaimed": "Tato základna není obsazena.", - "clashofclans_base_destroyed": "**ZNIČENA** základna #{0} ve válce proti {1}", - "clashofclans_base_unclaimed": "{0} má **NEOBSAZENOU** základnu #{1} ve válce proti {2}", - "clashofclans_claimed_base": "{0} obsadil základnu #{1} ve válce proti {2}", - "clashofclans_claimed_other": "@{0} Už jsi obsadil základnu #{1}. Nemůžeš obsadit další.", - "clashofclans_claim_expired": "Nárok na @{0} ve válce proti {1} vypršel.", - "clashofclans_enemy": "Nepřítel", - "clashofclans_info_about_war": "Info o válce proti {0}", - "clashofclans_invalid_base_number": "Neplatné číslo základny.", - "clashofclans_invalid_size": "Neplatná velikost války.", - "clashofclans_list_active_wars": "Seznam aktivních válek.", - "clashofclans_not_claimed": "neobsazeno", - "clashofclans_not_partic": "Této války se neúčastníš.", - "clashofclans_not_partic_or_destroyed": "@{0} Buď se této války neúčastníš, nebo je základna již zničena.", - "clashofclans_no_active_wars": "Žádná aktivní válka.", - "clashofclans_size": "Velikost", - "clashofclans_war_already_started": "Válka proti {0} už začala.", - "clashofclans_war_created": "Válka proti {0} vytvořena.", - "clashofclans_war_ended": "Válka proti {0} skončila.", - "clashofclans_war_not_exist": "Tato válka neexistuje.", - "clashofclans_war_started": "Válka proti {0} začala!", "customreactions_all_stats_cleared": "Statistika všech vlastních reakcí byla vyčištěna.", "customreactions_deleted": "Vlastní reakce smazána", "customreactions_insuff_perms": "Nedostatečná oprávnění. Potřebuješ vlastnictví bota pro globální vlastní reakce a oprávnění Administrátor pro vlastní reakce na svém serveru.", @@ -657,7 +634,7 @@ "utility_server_info": "Informace o serveru", "utility_shard": "Shard", "utility_shard_stats": "Shard statistika", - "utility_shard_stats_txt": "Shard **#{0}** je ve stavu {1} na serverech {2}", + "utility_shard_stats_txt": "Shard **#{0}** je ve stavu {1} na serverech {2}- před {3}", "utility_showemojis": "**Jméno:** {0} **Odkaz:** {1}", "utility_showemojis_none": "Nepodařilo se najít žádné speciální emoji.", "utility_stats_songs": "Přehrávám {0} písní, {1} zařazena/o", @@ -804,5 +781,92 @@ "music_songs_shuffle_enable": "Písně se budou odteď přehrávat náhodně.", "music_songs_shuffle_disable": "Písně se odteď nebudou přehrávat náhodně.", "music_song_skips_after": "Písně se přeskočí po {0}", - "administration_warnings_list": "Seznam všech varovaných uživatelů na tomto serveru" + "administration_warnings_list": "Seznam všech varovaných uživatelů na tomto serveru", + "customreactions_redacted_too_long": "Redigováno, protože to trvalo dlouho.", + "nsfw_blacklisted_tag_list": "Černá listina tagů:", + "nsfw_blacklisted_tag": "Jeden nebo více tagů, které jsi použil jsou na černé listině.", + "nsfw_blacklisted_tag_add": "Nsfw tag {0} je nyní na černé listině.", + "nsfw_blacklisted_tag_remove": "Nsfw tag {0} již není na černé listině.", + "gambling_waifu_gift": "Darováno {0} {1}", + "gambling_waifu_gift_shop": "Obchod dárků pro waifu", + "gambling_gifts": "Dárky", + "games_connect4_created": "Hra Connect4 byla vytvořena. Čekám na připojení hráče.", + "games_connect4_player_to_move": "Hráč k posunutí: {0}", + "games_connect4_failed_to_start": "Connect4 hru se nepodařilo spustit, protože se nikdo nepřipojil.", + "games_connect4_draw": "Hra Connect4 skončila remízou.", + "games_connect4_won": "{0} vyhrál hru Connect4 proti {1}", + "games_nunchi_joined": "Připojen k nunchi hře.{0} uživatelů se již připojilo.", + "games_nunchi_ended": "Nunchi hra skončila. {0} vyhrál", + "games_nunchi_ended_no_winner": "Nunchi hra skončila bez vítěze.", + "games_nunchi_started": "Nunchi hra začala s {0} účastníky", + "games_nunchi_round_ended": "Kolo hry Nunchi skončilo. {0} je mimo hru.", + "games_nunchi_round_ended_boot": "Kolo hry Nunchi skončilo kvůli vypršení času pro některé uživatele. Tito uživatelé stále ještě hrají: {0}", + "games_nunchi_round_started": "Kolo hry Nunchi začalo s {0} uživateli. Začni počítat od čísla {1}.", + "games_nunchi_next_number": "Číslo registrováno. Poslední číslo bylo {0}.", + "games_nunchi_failed_to_start": "Hru Nunchi se nepodařilo spustit z důvodu nedostatku účastníků.", + "games_nunchi_created": "Hra Nunchi byla vytvořena. Čekám na připojení uživatelů.", + "music_sad_enabled": "Písně budou mazány ze seznamu přehrávání, když skončí.", + "music_sad_disabled": "Písně nebudou mazány ze seznamu přehrávání, když skončí.", + "utility_stream_role_enabled": "Když uživatel z role {0} začne streamovat, dám mu roli {1}.", + "utility_stream_role_disabled": "Funkce Role při streamování byla vypnuta.", + "utility_stream_role_kw_set": "Streameři nyní potřebují klíčové slovo {0}, aby dostali roli.", + "utility_stream_role_kw_reset": "Klíčové slovo pro streamování bylo resetováno.", + "utility_stream_role_bl_add": "Uživatel {0} nikdy nedostane streamovací roli.", + "utility_stream_role_bl_add_fail": "Uživatel {0} již je na černé listině.", + "utility_stream_role_bl_rem": "Uživatel {0} již není na černé listině.", + "utility_stream_role_bl_rem_fail": "Uživatel {0} není na černé listině.", + "utility_stream_role_wl_add": "Uživatel {0} získá streamovací roli, i když nemá klíčové slovo v názvu streamu.", + "utility_stream_role_wl_add_fail": "Uživatel {0} již je na bílé listině.", + "utility_stream_role_wl_rem": "Uživatel {0} již není na bílé listině.", + "utility_stream_role_wl_rem_fail": "Uživatel {0} není na bílé listině.", + "utility_bot_config_edit_fail": "Nepodařilo se nastavit {0} na hodnotu {1}", + "utility_bot_config_edit_success": "Hodnota {0} byla nastavena na {1}", + "customreactions_crca_disabled": "Vlastní realce s id{0} se nyní spustí, kdykoli se objeví na začátku věty.", + "customreactions_crca_enabled": "Vlastní realce s id{0} se nyní spustí, kdykoli se objeví kdekoli ve větě.", + "xp_server_level": "Serverová úroveň", + "xp_level": "Úroveň", + "xp_club": "Klub", + "xp_xp": "Zkušenost", + "xp_excluded": "{0} byl vyloučen z XP systému na tomto serveru.", + "xp_not_excluded": "{0} již není vyloučen z XP systému na tomto serveru.", + "xp_exclusion_list": "Seznam vyloučených", + "xp_server_is_excluded": "Tento server je vyloučen.", + "xp_server_is_not_excluded": "Tento server není vyloučen.", + "xp_excluded_roles": "Vyloučené role", + "xp_excluded_channels": "Vyloučené kanály", + "xp_level_up_channel": "Gratulace {0}, dosáhl jsi úrovně {1}!", + "xp_level_up_dm": "Gratulace {0}, dosáhl jsi úrovně {1} na serveru {2}!", + "xp_level_up_global": "Gratulace {0}, dosáhl jsi globální úrovně {1}!", + "xp_role_reward_cleared": "Za úroveň {0} již nikdo nedostane roli .", + "xp_role_reward_added": "Uživatelé, kteří dosáhnou úrovně {0} dostanou roli {1}.", + "xp_role_rewards": "Role za odměnu", + "xp_level_x": "Úroveň {0}", + "xp_no_role_rewards": "Na této straně není žádná role za odměnu.", + "xp_server_leaderboard": "Serverový XP žebříček", + "xp_global_leaderboard": "Globální XP žebříček", + "xp_modified": "Upraveno serverové XP uživatele {0} o {1}", + "xp_club_create_error": "Nepodařilo se založit klub. Ujisti se, že tvoje úroveň je alespoň 5, a že již nejsi členem jiného klubu.", + "xp_club_created": "Klub {0} byl úspěšně vytvořen.", + "xp_club_not_exists": "Tento klub neexistuje.", + "xp_club_applied": "Ucházíš se o místo v klubu {0}.", + "xp_club_apply_error": "Nepodařilo se ucházení o klub. Buď již jsi členem klubu nebo nesplňuješ minimální úroveň k přijetí nebo jsi z tohoto klubu byl zabanován.", + "xp_club_accepted": "Uživatel {0} byl přijat do klubu.", + "xp_club_accept_error": "Uživatel nenalezen.", + "xp_club_left": "Opustil jsi klub.", + "xp_club_not_in_club": "Nejsi v klubu nebo se snažíš opustit klub, který vlastníš.", + "xp_club_user_kick": "Uživatel {0} byl vyhozen z klubu {1}.", + "xp_club_user_kick_fail": "Nepodařilo se vyhodit. Buď nejsi majitelem klubu nebo tento uživatel není ve tvém klubu.", + "xp_club_user_banned": "Uživatel {0} byl zabanován v klubu {1}.", + "xp_club_user_ban_fail": "Nepodařilo se zabanovat. Buď nejsi majitelem klubu nebo tento uživatel není ve tvém klubu, ani se o něj neuchází.", + "xp_club_user_unbanned": "Uživatel {0} byl odbanován v klubu {1}.", + "xp_club_user_unban_fail": "Nepodařilo se odbanovat. Buď nejsi majitelem klubu nebo tento uživatel není ve tvém klubu, ani se o něj neuchází.", + "xp_club_level_req_changed": "Požadovaná úroveň pro vstup do klubu byla změněna na {0}", + "xp_club_level_req_change_error": "Nepodařilo se změnit požadovanou úroveň pro vstup.", + "xp_club_disbanded": "Klub {0} byl rozpuštěn.", + "xp_club_disband_error": "Chyba. Buď nejsi v klubu nebo nejsi jeho majitelem.", + "xp_club_icon_error": "Neplatné url obrázku nebo nejsi majitel klubu.", + "xp_club_icon_set": "Nová klubová ikona byla nastavena.", + "xp_club_bans_for": "Bany pro klub {0}", + "xp_club_apps_for": "Uchazeči o klub {0}", + "xp_club_leaderboard": "Klubový žebříček - strana {0}" } \ No newline at end of file From 81a75394ad7b015d340db2f31d756e27ae777cda Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Sat, 23 Sep 2017 12:40:36 +0200 Subject: [PATCH 333/346] Update ResponseStrings.nl-NL.json (POEditor.com) --- .../_strings/ResponseStrings.nl-NL.json | 118 ++++++++++++++---- 1 file changed, 91 insertions(+), 27 deletions(-) diff --git a/src/NadekoBot/_strings/ResponseStrings.nl-NL.json b/src/NadekoBot/_strings/ResponseStrings.nl-NL.json index 23a7e229..36900859 100644 --- a/src/NadekoBot/_strings/ResponseStrings.nl-NL.json +++ b/src/NadekoBot/_strings/ResponseStrings.nl-NL.json @@ -1,28 +1,5 @@ { - "clashofclans_base_already_claimed": "Die basis is al veroverd of vernietigd.", - "clashofclans_base_already_destroyed": "Die basis is al vernietigd.", - "clashofclans_base_already_unclaimed": "Die basis is nog niet veroverd.", - "clashofclans_base_destroyed": "**VERNIETIGD** basis #{0} in een oorlog tegen {1}", - "clashofclans_base_unclaimed": "{0} heeft **ONOVERWONNEN** basis #{1} in een oorlog tegen {2}", - "clashofclans_claimed_base": "{0} heeft basis #{1} overwonnen in een oorlog tegen {2}", - "clashofclans_claimed_other": "@{0} Je hebt al basis #{1} overwonnen. Je kunt er niet nog een nemen.", - "clashofclans_claim_expired": "De aanvraag van @{0} voor een oorlog tegen {1} is niet meer geldig.", - "clashofclans_enemy": "Vijand", - "clashofclans_info_about_war": "Informatie over de oorlog tegen {0}", - "clashofclans_invalid_base_number": "Ongeldige basis nummer", - "clashofclans_invalid_size": "Ongeldig oorlogsformaat.", - "clashofclans_list_active_wars": "Lijst van actieve oorlogen", - "clashofclans_not_claimed": "Niet veroverd.", - "clashofclans_not_partic": "Jij doet niet mee aan die oorlog.", - "clashofclans_not_partic_or_destroyed": "@{0} Je doet niet mee aan die oorlog, of die basis is al vernietigd.", - "clashofclans_no_active_wars": "Geen actieve oorlogen", - "clashofclans_size": "Grootte.", - "clashofclans_war_already_started": "Oorlog tegen {0} is al begonnen.", - "clashofclans_war_created": "Oorlog tegen {0} gecreëerd", - "clashofclans_war_ended": "De oorlog tegen {0} is beëindigd.", - "clashofclans_war_not_exist": "Die oorlog bestaat niet.", - "clashofclans_war_started": "De oorlog tegen {0} is begonnen!", - "customreactions_all_stats_cleared": "Alle speciale reactie statistieken zijn verwijderd.", + "customreactions_all_stats_cleared": "Alle speciale reactiestatistieken zijn verwijderd.", "customreactions_deleted": "Speciale Reactie verwijderd.", "customreactions_insuff_perms": "Onvoldoende rechten. Bot Eigendom is nodig voor globale speciale reacties, en Administrator voor speciale server-reacties.", "customreactions_list_all": "Lijst van alle zelf gemaakte reacties", @@ -629,7 +606,7 @@ "utility_owner": "Eigenaar", "utility_owner_ids": "Eigenaar IDs", "utility_presence": "Aanwezigheid", - "utility_presence_txt": "{0} Servers\n{1} Tekst Kanalen\n{2} Spraak Kanalen", + "utility_presence_txt": "{0} Server(s)\n{1} Tekstkanalen\n{2} Spraakkanalen", "utility_quotes_deleted": "Verwijder alle citaten met het trefwoord {0}", "utility_quotes_page": "Pagina {0} met citaten", "utility_quotes_page_none": "Geen citaten op die pagina.", @@ -798,11 +775,98 @@ "help_module": "Module: {0}", "games_hangman_stopped": "Galgje game gestopt.", "music_autoplaying": "Automatisch-afspelen.", - "music_queue_stopped": "Afspeler is gestopt. Hervat the afspelere door {0} commando.", + "music_queue_stopped": "Afspeler is gestopt. Hervat the afspeler door {0} commando.", "music_removed_song_error": "Liedje op die index bestaat niet", "music_shuffling_playlist": "Shuffling liedjes", "music_songs_shuffle_enable": "Liedjes worden vanaf nu geshuffeld.", "music_songs_shuffle_disable": "Liedjes worden niet langer meer geshuffeld.", "music_song_skips_after": "Liedjes worden overgeslagen na {0}", - "administration_warnings_list": "Lijst van alle gewaarschuwde gebruikers op deze server" + "administration_warnings_list": "Lijst van alle gewaarschuwde gebruikers op deze server", + "customreactions_redacted_too_long": "Redacted omdat het te lang is.", + "nsfw_blacklisted_tag_list": "De zwarte lijst van tags:", + "nsfw_blacklisted_tag": "Een of meerder tags die je hebt gebruikt staan op de zwarte lijst.", + "nsfw_blacklisted_tag_add": "Nsfw tag {0} is nu op de zwarte lijst.", + "nsfw_blacklisted_tag_remove": "Nsfw tag {0} is niet langer meer op de zwarte lijst.", + "gambling_waifu_gift": "Cadeau {0} gegeven aan {1}", + "gambling_waifu_gift_shop": "Waifu cadeauwinkel", + "gambling_gifts": "Cadeaus", + "games_connect4_created": "Connect4-spel gecreëerd. Wachten op een speler om mee te doen.", + "games_connect4_player_to_move": "Speler om te verplaatsen: {0}", + "games_connect4_failed_to_start": "Connect4-spel kan niet starten omdat niemand anders deelnam.", + "games_connect4_draw": "Connect4-spel is geëindigd in gelijkspel.", + "games_connect4_won": "{0} won het spel Connect4 tegen {1}.", + "games_nunchi_joined": "Aangesloten bij het nunchi spel. {0} gebruikers zijn tot nu toe aangesloten.", + "games_nunchi_ended": "Nunchi spel geëindigd. {0} heeft gewonnen", + "games_nunchi_ended_no_winner": "Nunchi spel geëindigd zonder winnaar.", + "games_nunchi_started": "Nunchi spel begonnen met {0} deelnemers.", + "games_nunchi_round_ended": "Nunchi ronde is geëindigd. {0} is uit het spel.", + "games_nunchi_round_ended_boot": "Nunchi ronde is afgelopen wegens een time-out van sommige gebruikers. Deze gebruikers zijn nog steeds in het spel: {0}", + "games_nunchi_round_started": "Nunchi ronde begonnen met {0} gebruikers. Begint met het nummer {1} te tellen.", + "games_nunchi_next_number": "Nummer geregistreerd. Laatste nummer was {0}.", + "games_nunchi_failed_to_start": "Nunchi kon niet starten omdat er niet genoeg deelnemers waren.", + "games_nunchi_created": "Nunchi spel is aangemaakt. Wachten op gebruikers om mee te spelen.", + "music_sad_enabled": "Liedjes worden verwijderd uit de muziekwachtrij wanneer ze afgelopen zijn.", + "music_sad_disabled": "Liedjes worden niet langer meer uit de muziekwachtrij verwijderd wanneer ze klaar zijn met afspelen.", + "utility_stream_role_enabled": "Wanneer een gebruiker van {0} rol begint te streaming, zal ik ze {1} rol geven.", + "utility_stream_role_disabled": "Stream rol functie is nu uitgeschakeld.", + "utility_stream_role_kw_set": "Streamers moeten nu {0} zoekwoord gebruiken om de rol te kunnen ontvangen.", + "utility_stream_role_kw_reset": "Stream rol zoekwoord gereset.", + "utility_stream_role_bl_add": "Gebruiker {0} ontvangt nooit de stream rol.", + "utility_stream_role_bl_add_fail": "Gebruiker {0} staat al op de zwarte lijst.", + "utility_stream_role_bl_rem": "Gebruiker {0} staat niet langer meer op de zwarte lijst.", + "utility_stream_role_bl_rem_fail": "Gebruiker {0} staat niet op de zwarte lijst.", + "utility_stream_role_wl_add": "Gebruiker {0} ontvangt de streamingrol, zelfs als ze het zoekwoord niet hebben in de streamtitel.", + "utility_stream_role_wl_add_fail": "Gebruiker {0} staat al op de witte lijst.", + "utility_stream_role_wl_rem": "Gebruiker {0} staat niet langer meer op de witte lijst.", + "utility_stream_role_wl_rem_fail": "Gebruiker {0} staat niet op de witte lijst.", + "utility_bot_config_edit_fail": "Mislukt bij het instellen van {0} naar de waarde {1}", + "utility_bot_config_edit_success": "De waarde van {0} is ingesteld op {1}", + "customreactions_crca_disabled": "Aangepaste reactie met id {0} zal niet langer worden geactiveerd, tenzij het triggerwoord in het begin van een zin staat.", + "customreactions_crca_enabled": "Aangepaste reactie met id {0} wordt nu geactiveerd als het ergens in de zin voorkomt.", + "xp_server_level": "Server niveau", + "xp_level": "Niveau", + "xp_club": "Vereniging", + "xp_xp": "Ervaring", + "xp_excluded": "{0} is uitgesloten van het XP-systeem op deze server.", + "xp_not_excluded": "{0} is niet meer uitgesloten van het XP-systeem op deze server.", + "xp_exclusion_list": "Uitsluitingslijst", + "xp_server_is_excluded": "Deze server is uitgesloten.", + "xp_server_is_not_excluded": "Deze server is niet uitgesloten.", + "xp_excluded_roles": "Uitgesloten Rollen", + "xp_excluded_channels": "Uitgesloten Kanalen", + "xp_level_up_channel": "Gefeliciteerd {0}, Je hebt niveau {1} bereikt!", + "xp_level_up_dm": "Gefeliciteerd {0}, je hebt niveau {1} bereikt op {2} server!", + "xp_level_up_global": "Gefeliciteerd {0}, je hebt globale niveau {1} bereikt!", + "xp_role_reward_cleared": "Niveau {0} wordt niet langer meer beloond met een rol.", + "xp_role_reward_added": "Gebruikers die niveau {0} bereiken, krijgen de rol {1}.", + "xp_role_rewards": "Rolbeloningen", + "xp_level_x": "Niveau {0}", + "xp_no_role_rewards": "Geen rolbeloning op deze pagina.", + "xp_server_leaderboard": "Server XP Scorebord", + "xp_global_leaderboard": "Global XP Scorebord", + "xp_modified": "Aangepaste server XP van de gebruiker {0} met {1}", + "xp_club_create_error": "Mislukt bij het maken van een club. Zorg ervoor dat je boven level 5 bent en niet al lid bent van een club.", + "xp_club_created": "Club {0} succesvol gemaakt!", + "xp_club_not_exists": "Die club bestaat niet.", + "xp_club_applied": "Je hebt je aangemeld om lid te worden bij {0} club.", + "xp_club_apply_error": "Fout bij aanmelden. Je bent al lid van de club of je voldoet niet aan het minimumniveau, of je bent bij deze club verbannen.", + "xp_club_accepted": "Gebruiker geaccepteerde bij {0} club.", + "xp_club_accept_error": "Gebruiker niet gevonden", + "xp_club_left": "Je hebt de club verlaten.", + "xp_club_not_in_club": "Je bent niet in een club, of je probeert de club te verlaten waar je de eigenaar van bent.", + "xp_club_user_kick": "Gebruiker {0} gekicked van {1} club.", + "xp_club_user_kick_fail": "Fout bij kicken. Je bent ook niet de club eigenaar, of die gebruiker zit niet in je club.", + "xp_club_user_banned": "Gebruiker {0} van {1} club verbannen.", + "xp_club_user_ban_fail": "Kan niet verbannen. Je bent ook niet de eigenaar van de club, of die gebruiker zit niet in jouw club of heeft nog niet gesolliciteerd.", + "xp_club_user_unbanned": "Gebruiker {0} van {1} club verbanning opgeheven.", + "xp_club_user_unban_fail": "Mislukt om verbanning op te heffen. Je bent niet de eigenaar van de club, of die gebruiker zit niet in jouw club, of heeft nog niet gesolliciteerd.", + "xp_club_level_req_changed": "Veranderd clubvereisten naar {0}", + "xp_club_level_req_change_error": "Vereiste niveau veranderen mislukt", + "xp_club_disbanded": "Club {0} is ontbonden", + "xp_club_disband_error": "Fout. Je bent niet in een club, of je bent niet de eigenaar van jouw club.", + "xp_club_icon_error": "Niet een geldige image url of je bent niet de eigenaar van de club.", + "xp_club_icon_set": "Nieuwe club icon set.", + "xp_club_bans_for": "Verbanne van {0} club", + "xp_club_apps_for": "Aanvragers voor {0} club", + "xp_club_leaderboard": "Club scorebord - pagina {0}" } \ No newline at end of file From 75d1732cedb76e570845f09c1acea3653e3f9bcd Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Sat, 23 Sep 2017 12:40:39 +0200 Subject: [PATCH 334/346] Update ResponseStrings.fr-FR.json (POEditor.com) --- .../_strings/ResponseStrings.fr-FR.json | 118 ++++++++++++++---- 1 file changed, 91 insertions(+), 27 deletions(-) diff --git a/src/NadekoBot/_strings/ResponseStrings.fr-FR.json b/src/NadekoBot/_strings/ResponseStrings.fr-FR.json index 38884372..ef17903d 100644 --- a/src/NadekoBot/_strings/ResponseStrings.fr-FR.json +++ b/src/NadekoBot/_strings/ResponseStrings.fr-FR.json @@ -1,27 +1,4 @@ { - "clashofclans_base_already_claimed": "Cette base a déjà été revendiquée ou détruite.", - "clashofclans_base_already_destroyed": "Cette base est déjà détruite.", - "clashofclans_base_already_unclaimed": "Cette base n'est pas revendiquée.", - "clashofclans_base_destroyed": "Base #{0} **DETRUITE** dans une guerre contre {1}", - "clashofclans_base_unclaimed": "{0} a **ABANDONNÉ** la base #{1} dans une guerre contre {2}", - "clashofclans_claimed_base": "{0} a revendiqué une base #{1} dans une guerre contre {2}", - "clashofclans_claimed_other": "@{0} Vous avez déjà revendiqué la base #{1}. Vous ne pouvez pas en revendiquer une nouvelle.", - "clashofclans_claim_expired": "La demande de la part de @{0} pour une guerre contre {1} a expiré.", - "clashofclans_enemy": "Ennemi", - "clashofclans_info_about_war": "Informations concernant la guerre contre {0}", - "clashofclans_invalid_base_number": "Numéro de base invalide.", - "clashofclans_invalid_size": "La taille de la guerre n'est pas valide.", - "clashofclans_list_active_wars": "Liste des guerres en cours", - "clashofclans_not_claimed": "non réclamé", - "clashofclans_not_partic": "Vous ne participez pas a cette guerre.", - "clashofclans_not_partic_or_destroyed": "@{0} Vous ne participez pas à cette guerre ou la base a déjà été détruite.", - "clashofclans_no_active_wars": "Aucune guerre en cours.", - "clashofclans_size": "Taille", - "clashofclans_war_already_started": "La guerre contre {0} a déjà commencé!", - "clashofclans_war_created": "La guerre contre {0} commence!", - "clashofclans_war_ended": "La guerre contre {0} est terminée.", - "clashofclans_war_not_exist": "Cette guerre n'existe pas.", - "clashofclans_war_started": "La guerre contre {0} a éclaté !", "customreactions_all_stats_cleared": "Statistiques de réactions personnalisées effacées.", "customreactions_deleted": "Réaction personnalisée supprimée", "customreactions_insuff_perms": "Permissions insuffisantes. Nécessite d'être le propriétaire du Bot pour avoir les réactions personnalisées globales, et Administrateur pour les réactions personnalisées du serveur.", @@ -32,16 +9,16 @@ "customreactions_no_found_id": "Aucune réaction personnalisée ne correspond à cet ID.", "customreactions_response": "Réponse", "customreactions_stats": "Statistiques des Réactions Personnalisées", - "customreactions_stats_cleared": "Statistiques effacées pour {0} réaction personnalisée.", + "customreactions_stats_cleared": "Statistiques effacées pour {0} réactions personnalisées.", "customreactions_stats_not_found": "Pas de statistiques pour ce déclencheur trouvées, aucune action effectuée.", "customreactions_trigger": "Déclencheur", "nsfw_autohentai_stopped": "Autohentai arrêté.", - "nsfw_not_found": "Aucun résultat trouvé.", + "nsfw_not_found": "Aucuns résultats trouvés.", "pokemon_already_fainted": "{0} est déjà inconscient.", "pokemon_already_full": "{0} a tous ses PV.", "pokemon_already_that_type": "Votre type est déjà {0}", "pokemon_attack": "Vous avez utilisé {0}{1} sur {2}{3} pour {4} dégâts.", - "pokemon_cant_attack_again": "Vous ne pouvez pas attaquer de nouveau sans représailles !", + "pokemon_cant_attack_again": "Vous ne pouvez pas attaquer de nouveau avant les représailles !", "pokemon_cant_attack_yourself": "Vous ne pouvez pas vous attaquer vous-même.", "pokemon_fainted": "{0} s'est évanoui!", "pokemon_healed": "Vous avez soigné {0} avec un {1}", @@ -804,5 +781,92 @@ "music_songs_shuffle_enable": "Les chansons seront désormais lues de façon aléatoire.", "music_songs_shuffle_disable": "Les chansons ne seront plus désormais lues de façon aléatoire.", "music_song_skips_after": "La chanson passera à la suivante après {0}", - "administration_warnings_list": "Liste de tous les utilisateurs ayant un avertissement sur le serveur." + "administration_warnings_list": "Liste de tous les utilisateurs ayant un avertissement sur le serveur.", + "customreactions_redacted_too_long": "Édité car c'est trop long.", + "nsfw_blacklisted_tag_list": "Liste des tags en liste noire:", + "nsfw_blacklisted_tag": "Un ou plusieurs tags que vous avez utilisés sont en liste noire", + "nsfw_blacklisted_tag_add": "Le tag nsfw {0} est maintenant en liste noire.", + "nsfw_blacklisted_tag_remove": "Le tag nsfw {0} n'est plus en liste noire.", + "gambling_waifu_gift": "Donné {0} à {1}", + "gambling_waifu_gift_shop": "Boutique cadeau pour Waifu", + "gambling_gifts": "Cadeaux", + "games_connect4_created": "Partie de Connect4 créée. En attente de joueurs.", + "games_connect4_player_to_move": "Joueur en cours: {0}", + "games_connect4_failed_to_start": "La partie de Connect4 est annulée car personne ne l'a rejoint.", + "games_connect4_draw": "La partie de Connect4 s'est terminée en égalitée.", + "games_connect4_won": "{0} a gagné la partie de Connect4 contre {1}.", + "games_nunchi_joined": "Partie de Nunchi rejoint. {0} joueurs l'ont rejoint jusqu'à présent.", + "games_nunchi_ended": "Partie de Nunchi terminée. {0} a gagné", + "games_nunchi_ended_no_winner": "La partie de Nunchi s'est terminée sans gagnant.", + "games_nunchi_started": "Partie de Nunchi démarrée avec {0} participants", + "games_nunchi_round_ended": "Tour de Nunchi est terminé. {0} est hors de la partie.", + "games_nunchi_round_ended_boot": "Le tour de Nunchi est terminé dut à l'inactivité de quelques joueurs. Ces joueurs sont toujours dans la partie: {0}", + "games_nunchi_round_started": "Tour de Nunchi démarré avec {0} joueurs. Commencez à compter a partir du nombre {1}", + "games_nunchi_next_number": "Nombre enregistré. Le dernier nombre était {0}.", + "games_nunchi_failed_to_start": "La partie de Nunchi n'a pas pu démarré car il n'y a pas assez de participants.", + "games_nunchi_created": "Partie de Nunchi créée. En attente de joueursé", + "music_sad_enabled": "Les pistes seront effacés de la liste de lecture lorsqu'elles seront terminés.", + "music_sad_disabled": "Les pistes ne seront plus effacés de la liste de lecture lorsqu'elles seront terminés.", + "utility_stream_role_enabled": "Lorsqu'un utilisateur du rôle {0} commencera à streamer, je lui donnerai le rôle {1}.", + "utility_stream_role_disabled": "La fonctionnalité du rôle streamer a été désactivée", + "utility_stream_role_kw_set": "Les streamers requièrent le mot clé {0} afin de recevoir le rôle.", + "utility_stream_role_kw_reset": "Mot clé de stream réinitialisé.", + "utility_stream_role_bl_add": "L'utilisateur {0} ne recevra jamais le rôle streameur.", + "utility_stream_role_bl_add_fail": "L'utilisateur {0} est déjà sur liste noire.", + "utility_stream_role_bl_rem": "L'utilisateur {0} n'est désormais plus sur liste noire.", + "utility_stream_role_bl_rem_fail": "L'utilisateur {0} n'est pas sur liste noire.", + "utility_stream_role_wl_add": "L'utilisateur {0} recevra le rôle streameur même s'ils n'ont pas le mot-clé dans le titre sur stream.", + "utility_stream_role_wl_add_fail": "L'utilisateur {0} est déjà sur liste blanche.", + "utility_stream_role_wl_rem": "L'utilisateur {0} n'est désormais plus sur liste blanche.", + "utility_stream_role_wl_rem_fail": "L'utilisateur {0} n'est pas sur liste blanche.", + "utility_bot_config_edit_fail": "Le réglage {0} à échoué à la valeur {1}", + "utility_bot_config_edit_success": "La valeur de {0} a été mise à {1}", + "customreactions_crca_disabled": "La réaction personnalisée avec l'id {0} ne se déclenchera plus sauf si le mot déclencheur est au début de la phrase.", + "customreactions_crca_enabled": "La réaction personnalisée avec l'id {0} se déclenchera si elle est à n'importe quel endroit dans la phrase.", + "xp_server_level": "Niveau sur le serveur", + "xp_level": "Niveau", + "xp_club": "Club", + "xp_xp": "Expérience", + "xp_excluded": "{0} a été exclu du système XP sur ce serveur.", + "xp_not_excluded": "{0} n'est plus exclu du système XP sur ce serveur.", + "xp_exclusion_list": "Liste des exclus", + "xp_server_is_excluded": "Ce serveur est exclu.", + "xp_server_is_not_excluded": "Ce serveur n'est pas exclu.", + "xp_excluded_roles": "Rôles exclus.", + "xp_excluded_channels": "Salons exclus.", + "xp_level_up_channel": "Félicitations {0}, vous avez atteint le niveau {1}!", + "xp_level_up_dm": "Félicitations {0}, vous avez atteint le niveau {1} sur le serveur {2}!", + "xp_level_up_global": "Félicitations {0}, vous avez atteint le niveau global {1}!", + "xp_role_reward_cleared": "Le niveau {0} ne récompensera plus d'un rôle.", + "xp_role_reward_added": "Les utilisateurs qui atteignent le niveau {0} recevront le rôle {1}.", + "xp_role_rewards": "Rôles en récompenses", + "xp_level_x": "Niveau {0}", + "xp_no_role_rewards": "Aucun rôle en récompense sur cette page.", + "xp_server_leaderboard": "Tableau de classement de l'XP serveur", + "xp_global_leaderboard": "Tableau de classement de l'XP global", + "xp_modified": "L'XP serveur a été modifiée pour l'utilisateur {0} par {1}", + "xp_club_create_error": "Échec de la création du club. Vérifiez que vous êtes supérieur au niveau 5 et n'êtes pas déjà membre d'un club.", + "xp_club_created": "Le club {0} a été créé avec succès!", + "xp_club_not_exists": "Ce club n'existe pas.", + "xp_club_applied": "Vous avez postulé pour être membre dans le club {0}.", + "xp_club_apply_error": "Erreur lors de la postulation. Vous êtes soit déjà membre du club, soit vous n'avez pas le niveau minimum requis pour postuler, soit vous avez été bannis de celui-ci.", + "xp_club_accepted": "L'utilisateur {0} a été accepté dans le club.", + "xp_club_accept_error": "Utilisateur non trouvé", + "xp_club_left": "Vous avez quitté le club.", + "xp_club_not_in_club": "Vous n'êtes pas dans un club, ou vous essayez de quitter le club dont vous êtes le propriétaire.", + "xp_club_user_kick": "L'utilisateur {0} a été expulsé du club {1}", + "xp_club_user_kick_fail": "Erreur lors de l'expulsion. Vous n'êtes soit pas le propriétaire du club, soit l'utilisateur n'est pas dans votre club.", + "xp_club_user_banned": "L'utilisateur {0} a été banni du club {1}", + "xp_club_user_ban_fail": "Échec du bannissement. Vous n'êtes soit pas le propriétaire du club, soit l'utilisateur n'est pas dans votre club ou n'y a pas postulé. ", + "xp_club_user_unbanned": "L'utilisateur {0} a été gracié du club {0}", + "xp_club_user_unban_fail": "Échec de la grâce. Vous n'êtes soit pas le propriétaire du club, soit l'utilisateur n'est pas dans votre club ou n'y a pas postulé.", + "xp_club_level_req_changed": "Changement du niveau requis pour le club à {0}", + "xp_club_level_req_change_error": "Échec dans le changement du niveau requis.", + "xp_club_disbanded": "Le club {0} a été dissous", + "xp_club_disband_error": "Erreur. Vous n'êtes soit pas dans un club, soit pas le propriétaire de votre club.", + "xp_club_icon_error": "L'url de l'image n'est pas valide ou vous n'êtes pas le propriétaire du club.", + "xp_club_icon_set": "Nouvelle icône du club définie.", + "xp_club_bans_for": "Bannis du club {0}", + "xp_club_apps_for": "Postulants pour le club {0}", + "xp_club_leaderboard": "Tableau de classement du club - page {0}" } \ No newline at end of file From 2c59babcd015fda28bdc35a8351495771f9e643d Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Sat, 23 Sep 2017 12:40:42 +0200 Subject: [PATCH 335/346] Update ResponseStrings.zh-TW.json (POEditor.com) --- .../_strings/ResponseStrings.zh-TW.json | 112 ++++++++++++++---- 1 file changed, 88 insertions(+), 24 deletions(-) diff --git a/src/NadekoBot/_strings/ResponseStrings.zh-TW.json b/src/NadekoBot/_strings/ResponseStrings.zh-TW.json index 50d61c97..3cb5c124 100644 --- a/src/NadekoBot/_strings/ResponseStrings.zh-TW.json +++ b/src/NadekoBot/_strings/ResponseStrings.zh-TW.json @@ -1,27 +1,4 @@ { - "clashofclans_base_already_claimed": "該基地已被佔領或摧毀。", - "clashofclans_base_already_destroyed": "該基地已摧毀。", - "clashofclans_base_already_unclaimed": "該基地尚未佔領", - "clashofclans_base_destroyed": "在與 {1} 對戰時 **摧毀** 了基地 #{0}", - "clashofclans_base_unclaimed": "{0} 在與 {2} 對戰時 **解放** 基地 #{1}", - "clashofclans_claimed_base": "{0} 在與 {2} 對戰時 **佔領** 基地 #{1}", - "clashofclans_claimed_other": "@{0} 您已佔領基地 #{1}。您不能再佔領一個新的。", - "clashofclans_claim_expired": "在 @{0} 與 {1} 對戰後佔領的基地已失效。", - "clashofclans_enemy": "敵人", - "clashofclans_info_about_war": "與 {0} 對戰的信息", - "clashofclans_invalid_base_number": "無效的基地編號。", - "clashofclans_invalid_size": "無效的戰爭大小。", - "clashofclans_list_active_wars": "目前正在進行的戰爭", - "clashofclans_not_claimed": "未佔領", - "clashofclans_not_partic": "您並未参與此戰爭。", - "clashofclans_not_partic_or_destroyed": "@{0} 您並未参與此戰爭或此基地已被毁壞。", - "clashofclans_no_active_wars": "目前尚無戰爭進行中。", - "clashofclans_size": "大小", - "clashofclans_war_already_started": "與 {0} 的戰爭已經開始了。", - "clashofclans_war_created": "與 {0} 的戰爭已創建。", - "clashofclans_war_ended": "與 {0} 的戰爭已結束。", - "clashofclans_war_not_exist": "此戰爭不存在。", - "clashofclans_war_started": "與 {0} 的戰爭已開始!", "customreactions_all_stats_cleared": "所有自訂回應統計已刪除。", "customreactions_deleted": "自訂回應已刪除", "customreactions_insuff_perms": "權限不足。伺服器自訂回應需要管理員權限,而只有 Bot 擁有者才能設定全域自訂回應。", @@ -804,5 +781,92 @@ "music_songs_shuffle_enable": "開始隨機播放清單。", "music_songs_shuffle_disable": "停止隨機播放清單。", "music_song_skips_after": "音樂將在 {0} 後跳過", - "administration_warnings_list": "伺服器上所有被警告成員的列表" + "administration_warnings_list": "伺服器上所有被警告成員的列表", + "customreactions_redacted_too_long": "字串因太長而裁短。", + "nsfw_blacklisted_tag_list": "黑名單關鍵字:", + "nsfw_blacklisted_tag": "你所使用的部分關鍵字已被黑名單。", + "nsfw_blacklisted_tag_add": "{0}標籤已被新增至黑名單。", + "nsfw_blacklisted_tag_remove": "{0}標籤已被移除黑名單。", + "gambling_waifu_gift": "贈與了{0}給{1}", + "gambling_waifu_gift_shop": "後宮精品店", + "gambling_gifts": "獲得禮物", + "games_connect4_created": "建立了一個四子棋遊戲。等待另一位玩家的加入中。", + "games_connect4_player_to_move": "輪到玩家: {0}", + "games_connect4_failed_to_start": "因為沒有對手而無法開始四子棋遊戲。", + "games_connect4_draw": "平局。", + "games_connect4_won": "{0} 從和 {1} 的四子棋比賽中勝出。", + "games_nunchi_joined": "加入了心臟病遊戲。目前共有{0}位玩家。", + "games_nunchi_ended": "心臟病遊戲結束。{0}獲勝", + "games_nunchi_ended_no_winner": "心臟病遊戲在沒有勝利者的狀況下結束。", + "games_nunchi_started": "{0}位玩家開始了心臟病遊戲。", + "games_nunchi_round_ended": "心臟病本回合結束。{0}已遭淘汰。", + "games_nunchi_round_ended_boot": "心臟病回合已將部分沒有回應玩家剔除。目前還在遊戲內的玩家為: {0}", + "games_nunchi_round_started": "{0}位玩家的心臟病回合開始。請往上+1數右邊的數字: {1}。", + "games_nunchi_next_number": "號碼以註冊。目前號碼為{0}。", + "games_nunchi_failed_to_start": "因人數不足而無法開始心臟病遊戲 (最少要三位)。", + "games_nunchi_created": "心臟病遊戲開始了。等待其他玩家的加入。", + "music_sad_enabled": "歌曲會在撥放完畢後從清單中移除。", + "music_sad_disabled": "歌曲會在撥放完畢後保留在清單中。", + "utility_stream_role_enabled": "當擁有{0}身分組的使用者開始實況時,我將會給予他們{0}身分組。", + "utility_stream_role_disabled": "實況身分組已被停用。", + "utility_stream_role_kw_set": "實況主現在必須要包含 {0} 關鍵字才能獲得身分組。", + "utility_stream_role_kw_reset": "實況身分組關鍵字以重設。", + "utility_stream_role_bl_add": "{0} 使用者將永不獲得實況者群組", + "utility_stream_role_bl_add_fail": "使用者{0}已經在黑名單上了。", + "utility_stream_role_bl_rem": "使用者{0}不再會被黑名單。", + "utility_stream_role_bl_rem_fail": "使用者{0}並不在黑名單上。", + "utility_stream_role_wl_add": "使用者{0}將會獲得實況身放組,即便實況標題內並沒有任何關鍵字。", + "utility_stream_role_wl_add_fail": "使用者{0}已經在白名單上了。", + "utility_stream_role_wl_rem": "使用者{0}不再會被白名單了。", + "utility_stream_role_wl_rem_fail": "使用者{0}並不在白名單上。", + "utility_bot_config_edit_fail": "{0}的值在設定成{1}時失敗", + "utility_bot_config_edit_success": "{0}的值已被設定為{1}", + "customreactions_crca_disabled": "自訂回應編號{0}現在只會在訊息開頭包含關鍵字時觸發。", + "customreactions_crca_enabled": "自訂回應編號{0}現在會在訊息中有包含關鍵字時觸發。", + "xp_server_level": "伺服器等級", + "xp_level": "等級", + "xp_club": "公會", + "xp_xp": "經驗", + "xp_excluded": "{0}已被排除XP系統", + "xp_not_excluded": "{0} 不再從伺服器上的經驗值系統中排除。", + "xp_exclusion_list": "排除名單", + "xp_server_is_excluded": "這個伺服器已被排除在外。", + "xp_server_is_not_excluded": "這個伺服器不被排除在外。", + "xp_excluded_roles": "排除身分組", + "xp_excluded_channels": "排除頻道", + "xp_level_up_channel": "恭喜{0},您的等級已提升為{1}!", + "xp_level_up_dm": "恭喜{0},您的等級已於{2}伺服器上提升為{1}!", + "xp_level_up_global": "恭喜{0},您的全域等級提升為{1}!", + "xp_role_reward_cleared": "等級{0}不再會給予身分組。", + "xp_role_reward_added": "使用者達到等級{0}時將會獲得{1}身分組。", + "xp_role_rewards": "身分組獎勵", + "xp_level_x": "等級 {0}", + "xp_no_role_rewards": "這一頁上沒有身分組獎勵。", + "xp_server_leaderboard": "伺服器經驗值排名", + "xp_global_leaderboard": "全局經驗值排名", + "xp_modified": "修改成員 {0} 的經驗值為 {1}", + "xp_club_create_error": "公會創建失敗。請確保你已經超過等級5,而且不是其他公會的成員。", + "xp_club_created": "{0}已被成功建立!", + "xp_club_not_exists": "找不到此公會。", + "xp_club_applied": "你已經向{0}公會申請了會員。", + "xp_club_apply_error": "無法申請。你要不是已經是此公會的成員,要不就是你還沒達到最低入會等級需求。或你已經被此公會黑單了。", + "xp_club_accepted": "已批准成員 {0} 加入公會。.", + "xp_club_accept_error": "找不到使用者", + "xp_club_left": "你已離開公會。", + "xp_club_not_in_club": "你要不是目前沒有工會,或者就是你嘗試想要離開一個由你創立的工會。", + "xp_club_user_kick": "成員 {0} 已從 {1} 公會中被踢出。.", + "xp_club_user_kick_fail": "在踢除時發生錯誤。您不是公會的擁有者,或該用戶不在您的公會。", + "xp_club_user_banned": "已將{0}逐出{1}公會並設定黑名單。", + "xp_club_user_ban_fail": "無法設定黑名單。您要不是並非貴會會長,就是您想要設定黑名單的對象並不在您的公會內。", + "xp_club_user_unbanned": "將成員 {0} 從 {1} 俱樂部解除封鎖。", + "xp_club_user_unban_fail": "無法解除黑名單。您要不是並非貴會會長,就是您想要解除黑名單的對象並不在您的公會內。", + "xp_club_level_req_changed": "已設定本公會的最低需求等級為 {0}", + "xp_club_level_req_change_error": "變更等級需求失敗。", + "xp_club_disbanded": "{0} 已遭解散。", + "xp_club_disband_error": "錯誤: 你要不是沒有在公會,就是你並不是你所在公會的會長。", + "xp_club_icon_error": "圖示網址不正確或者你不是公會會長。", + "xp_club_icon_set": "已設定新的公會圖示。", + "xp_club_bans_for": "{0} 公會黑名單", + "xp_club_apps_for": "{0} 公會會員申請名單", + "xp_club_leaderboard": "公會排行榜 - 第 {0} 頁" } \ No newline at end of file From 7588ad58403fed5ed1b5c854bba8634ea2f45393 Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Sat, 23 Sep 2017 12:40:45 +0200 Subject: [PATCH 336/346] Update ResponseStrings.tr-TR.json (POEditor.com) --- .../_strings/ResponseStrings.tr-TR.json | 148 ++++++++++++++---- 1 file changed, 117 insertions(+), 31 deletions(-) diff --git a/src/NadekoBot/_strings/ResponseStrings.tr-TR.json b/src/NadekoBot/_strings/ResponseStrings.tr-TR.json index 8ed3478a..e2254327 100644 --- a/src/NadekoBot/_strings/ResponseStrings.tr-TR.json +++ b/src/NadekoBot/_strings/ResponseStrings.tr-TR.json @@ -1,27 +1,4 @@ { - "clashofclans_base_already_claimed": "Bu köy daha önceden alınmış veya yok edilmiş.", - "clashofclans_base_already_destroyed": "Bu köy önceden yok edilmiş.", - "clashofclans_base_already_unclaimed": "Bu köy alınmamış.", - "clashofclans_base_destroyed": " {1}'ye karşı savaşta köy #{0} **BERTARAF EDİLDİ**", - "clashofclans_base_unclaimed": "{0},{2} karşı savaşta #{1} üssünü alamadı.", - "clashofclans_claimed_base": "{0}, {2} ile savaşan bir #{1} temel hak iddia etti.", - "clashofclans_claimed_other": "@{0} zaten #{1} aldın. Yeni bir tane daha alamazsın.", - "clashofclans_claim_expired": "{1}'a karşı savaşta @{0} tarafından yapılan hak talebinin süresi dolmuştur.", - "clashofclans_enemy": "Düşman", - "clashofclans_info_about_war": "{0} ile savaşa ilişkin bilgi", - "clashofclans_invalid_base_number": "Hatalı köy numarası.", - "clashofclans_invalid_size": "Savaş büyüklüğü geçerli değil.", - "clashofclans_list_active_wars": "Aktif olan savaşlar", - "clashofclans_not_claimed": "Alınmamış", - "clashofclans_not_partic": "Bu savaşa katılmadınız.", - "clashofclans_not_partic_or_destroyed": "@{0} Bu savaşa katılmadınız veya köyünüz yok edildi.", - "clashofclans_no_active_wars": "Aktif olan savaş yok.", - "clashofclans_size": "Büyüklük", - "clashofclans_war_already_started": "{0} ile olan savaş başladı.", - "clashofclans_war_created": "{0} ile şavaş çıkardın.", - "clashofclans_war_ended": "{0} ile olan savaş sona erdi.", - "clashofclans_war_not_exist": "Böyle bir savaş bulunamadı.", - "clashofclans_war_started": "{0} ile olan savaş başladı.", "customreactions_all_stats_cleared": "Tüm özel reaksiyon istatistikleri temizlendi.", "customreactions_deleted": "Özel reaksiyon silindi", "customreactions_insuff_perms": "Yetersiz yetki.Global özel reaksiyonlar için bot sahibi yetkisi gerkirken, server tabanlı özel reaksiyonlar için Administrator yetkisi gerekir.", @@ -151,7 +128,6 @@ "administration_old_nick": "Eski takma ad", "administration_old_topic": "Eski konu", "administration_perms": "Hata.Yüksek ihtimalle yeterli yetki'ye sahip değilim.", - "administration_perms_reset": "Bu server için yetkiler sıfırlandı.", "administration_prot_active": "Koruma Aktif", "administration_prot_disable": "{0} Bu sunucudaki {0} **Devre Dışı**.", "administration_prot_enable": "{0} Etkin", @@ -243,7 +219,6 @@ "administration_sbdm": "{0} sunucusundan hafif bir şekilde yasaklandınız.\nSebep: {1}", "administration_user_unbanned": "Kullanıcı yasağı kaldırıldı", "administration_migration_done": "Eski kayıtlar taşındı!", - "administration_migration_error": "Taşıma işlemi sırasında hata oluştu, daha fazla bilgi için bot konsolunu kontrol edin.", "administration_presence_updates": "Durum güncellemeleri", "administration_sb_user": "Kullanıcı hafif bir şekilde yasaklandı.", "gambling_awarded": "{1} 'e {0} ödül vermiş", @@ -439,7 +414,6 @@ "music_rpl_enabled": "Çalma listesi tekrarı etkin.", "music_set_music_channel": "Şimdi bu kanaldaki şarkıların çalınması, bitmesi, duraklatılması ve çıkarılmasını yapacağım.", "music_skipped_to": "`{0}:{1}` atlandı", - "music_songs_shuffled": "Şarkılar karıştırıldı", "music_song_moved": "Şarkı taşındı", "music_time_format": "{0} saat {1} dakika {2} saniye", "music_to_position": "Konumlandır", @@ -605,9 +579,6 @@ "utility_convert_not_found": "{0} 'ı {1}' e dönüştüremem: birimleri bulunamadım", "utility_convert_type_error": "{0} 'ı {1}' e dönüştüremiyorum: birim türleri eşit değil.", "utility_created_at": "Tarihinde oluşturuldu", - "utility_csc_join": "Çapraz sunucu kanalına katıldı.", - "utility_csc_leave": "Çapraz sunucu kanalından ayrıldı.", - "utility_csc_token": "Bu,senin CSC anahtarın", "utility_custom_emojis": "Özel emojiler", "utility_error": "Hata", "utility_features": "Özellikler", @@ -754,7 +725,6 @@ "gambling_shop_role": "{0} rol alacaksınız.", "gambling_type": "Tip", "utility_clpa_next_update": "{0} 'da sonraki güncelleme", - "administration_global_perms_reset": "Genel izinler sıfırlandı.", "administration_gvc_disabled": "Oyun Ses Kanalı özelliği bu sunucuda devre dışı bırakıldı.", "administration_gvc_enabled": "{0} şimdi bir Oyun Sesli Kanal.", "administration_not_in_voice": "Bu sunucuda sesli kanalda değilsiniz.", @@ -782,5 +752,121 @@ "permissions_lgp_none": "Bloke edilmiş komut veya modül yok.", "gambling_animal_race_no_race": "Bu Hayvan Yarışı dolu!", "utility_cant_read_or_send": "Bu kanalda mesaj okuyamaz veya bu kanala mesaj gönderemezsiniz.", - "utility_quotes_notfound": "Belirtilen teklif kimliğine uyan teklif bulunamadı." + "utility_quotes_notfound": "Belirtilen teklif kimliğine uyan teklif bulunamadı.", + "administration_prefix_current": "Bu sunucudaki önek {0}", + "administration_prefix_new": "Bu sunucudaki bot önekini {0} 'ten {1} 'e değiştirildi", + "administration_defprefix_current": "Varsayılan bot öneki {0}", + "administration_defprefix_new": "Varsayılan bot önekini {0} 'ten {1} 'e değiştirildi", + "administration_bot_nick": "Bot'un takma adı {0} olarak değiştirildi", + "administration_user_nick": "{0} kullanıcısının kullanıcı adı {1} olarak değiştirildi", + "administration_timezone_guild": "Bu lonca için saat dilimi `{0}`", + "administration_timezone_not_found": "Saat dilimi bulunamadı. Kullanılabilen saat dilimlerinin listesini görmek için \"timezones\" komutunu kullanın", + "administration_timezones_available": "Kullanılabilir Saat Dilimleri", + "music_song_not_found": "Hiç şarkı bulunamadı.", + "searches_define_unknown": "Bu terim için tanım bulamıyor.", + "utility_repeater_initial": "İlk tekrarlanan mesaj {0} saat ve {1} dakika sonra gönderilecektir.", + "utility_verbose_errors_enabled": "Yanlış kullanılan komutlar artık hataları gösterecektir.", + "utility_verbose_errors_disabled": "Yanlış kullanılan komutlar artık hataları göstermeyecek.", + "permissions_perms_reset": "Bu sunucu için izinler sıfırlandı.", + "permissions_trigger": "{0} {1} izin numarası, bu işlemi önlüyor.", + "administration_migration_error": "Taşıma işlemi sırasında hata oluştu, daha fazla bilgi için bot konsolunu kontrol edin.", + "searches_hex_invalid": "Geçersiz renk belirtildi.", + "permissions_global_perms_reset": "Genel izinler sıfırlandı.", + "help_module": "Modül: {0}", + "games_hangman_stopped": "Hangman oyunu durdu.", + "music_autoplaying": "Otomatik-oynatılıyor.", + "music_queue_stopped": "Oynatıcı durduruldu. Oynatmaya başlamak için {0} komutunu kullanın.", + "music_removed_song_error": "Bu dizinde şarkı mevcut değil.", + "music_shuffling_playlist": "Şarkılar karıştırılıyor.", + "music_songs_shuffle_enable": "Şarkılar bundan sonra karıştırılacak.", + "music_songs_shuffle_disable": "Şarkılar artık karıştırılmayacak.", + "music_song_skips_after": "Şarkılar {0} sonrasında atlanacak", + "administration_warnings_list": "Sunucuda uyarılan tüm kullanıcıların listesi", + "customreactions_redacted_too_long": "Kırılmış çünkü çok uzun.", + "nsfw_blacklisted_tag_list": "Kara listeye alınmış etiketler listesi:", + "nsfw_blacklisted_tag": "Kullandığınız bir veya daha fazla etiket kara listeye alındı", + "nsfw_blacklisted_tag_add": "Nsfw etiketi {0} şimdi kara listeye alındı.", + "nsfw_blacklisted_tag_remove": "Nsfw etiketi {0} artık kara listeye alınmayacak.", + "gambling_waifu_gift": "{0} {1}'den daha yetenekli", + "gambling_waifu_gift_shop": "Waifu hediye dükkanı", + "gambling_gifts": "Hediyeler", + "games_connect4_created": "Connect4 oyunu kuruldu. Bir oyuncunun katılmasını bekliyorum.", + "games_connect4_player_to_move": "Taşınacak oyuncu: {0}", + "games_connect4_failed_to_start": "Connect4 oyunu başlayamadı, çünkü kimse katılmadı.", + "games_connect4_draw": "Connect4 oyunu berabere bitti.", + "games_connect4_won": "{0}, {1} a karşı Connect4 oyununu kazandı.", + "games_nunchi_joined": "Nunchi oyununa katıldın. şimdiye kadar {0} kullanıcı katıldı.", + "games_nunchi_ended": "Nunchi oyunu sona erdi. {0} kazandı", + "games_nunchi_ended_no_winner": "Nunchi oyunu kazanan olmaksızın bitti.", + "games_nunchi_started": "Nunchi oyunu {0} katılımcılarla başladı.", + "games_nunchi_round_ended": "Nunchi turu sona erdi. {0} oyun dışı.", + "games_nunchi_round_ended_boot": "Bazı kullanıcıların zaman aşımı nedeniyle Nunchi turu sona erdi. Bu kadar kullanıcı hala oyunda: {0}", + "games_nunchi_round_started": "Nunchi turu {0} kullanıcılarıyla başladı. {1} numarasından saymaya başlayın.", + "games_nunchi_next_number": "Numara kaydedildi. Son sayı {0} idi.", + "games_nunchi_failed_to_start": "Nunchi başlayamadı, çünkü yeterli katılımcı yoktu.", + "games_nunchi_created": "Nunchi oyunu kuruldu. Kullanıcıların katılmasını bekliyorum.", + "music_sad_enabled": "Şarkılar, çalmayı bitirince müzik kuyruğundan silinir.", + "music_sad_disabled": "Şarkılar, çalmayı bitirdiğinde artık müzik kuyruğundan silinmeyecek.", + "utility_stream_role_enabled": "{0} rolündeki bir kullanıcı yayın başlattığında, onlara {1} rol vereceğim.", + "utility_stream_role_disabled": "Yayın rolü özelliği devre dışı bırakıldı.", + "utility_stream_role_kw_set": "Yayıncılar artık rol alabilmek için {0} anahtar kelimesine ihtiyaç duyuyorlar.", + "utility_stream_role_kw_reset": "Yayın rolü anahtar kelimesini sıfırla.", + "utility_stream_role_bl_add": "{0} kullanıcısı asla yayın rolünü almaz.", + "utility_stream_role_bl_add_fail": "{0} kullanıcısı zaten kara listeye alındı.", + "utility_stream_role_bl_rem": "{0} kullanıcısı artık kara listede değil.", + "utility_stream_role_bl_rem_fail": "{0} kullanıcısı kara listeye alınmadı.", + "utility_stream_role_wl_add": "Kullanıcı {0}, yayın başlığında anahtar kelime olmasa da yayın rolünü alacak.", + "utility_stream_role_wl_add_fail": "{0} kullanıcısı zaten beyaz listeye alındı.", + "utility_stream_role_wl_rem": "{0} kullanıcısı artık beyaz listede değil.", + "utility_stream_role_wl_rem_fail": "{0} kullanıcısı beyaz listeye alınmadı.", + "utility_bot_config_edit_fail": "{0} değerini {1} değerine ayarlama başarısız oldu", + "utility_bot_config_edit_success": "{0} değeri {1} olarak ayarlandı.", + "customreactions_crca_disabled": "{0} kimlikli özel reaksiyon, tetikleyici kelime cümlenin başında olmadığı sürece tetiklenmeyecektir.", + "customreactions_crca_enabled": "{0} kimlikli özel reaksiyon, cümledeki herhangi bir yerde bulunursa tetiklenecektir.", + "xp_server_level": "Sunucu Seviyesi", + "xp_level": "Seviye", + "xp_club": "Kulüp", + "xp_xp": "Tecrübe", + "xp_excluded": "{0}, bu sunucu XP sisteminden çıkarıldı.", + "xp_not_excluded": "{0} artık bu sunucu XP sisteminden çıkartılmıyor.", + "xp_exclusion_list": "Dışlanmış listesi", + "xp_server_is_excluded": "Bu sunucu dışlanmıştır.", + "xp_server_is_not_excluded": "Bu sunucu dışlanmamıştır.", + "xp_excluded_roles": "Dışlanmış Roller", + "xp_excluded_channels": "Dışlanmış Kanallar", + "xp_level_up_channel": "Tebrikler {0}, {1} seviyesine ulaştınız!", + "xp_level_up_dm": "Tebrikler {0}, {2} sunucusunda {1} seviyesine ulaştınız!", + "xp_level_up_global": "Tebrikler {0}, {1} genel seviyesine ulaştınız!", + "xp_role_reward_cleared": "{0} seviyesi artık bir rolü ödüllendirmeyecek.", + "xp_role_reward_added": "{0} düzeyine ulaşan kullanıcılar {1} rol alacaktır.", + "xp_role_rewards": "Rol Ödülleri", + "xp_level_x": "Seviye {0}", + "xp_no_role_rewards": "Bu sayfada rol ödülü yok.", + "xp_server_leaderboard": "Sunucu XP Lider Tablosu", + "xp_global_leaderboard": "Genel XP Lider Tablosu", + "xp_modified": "{0} kullanıcısının {1} tarafından değiştirilen sunucu XP'si", + "xp_club_create_error": "Kulüp oluşturulamadı. Halihazırda bir kulübün üyesi olmadığınızdan ve 5. seviyenin üstünde olduğunuzdan emin olun.", + "xp_club_created": "Kulüp {0} başarıyla oluşturuldu!", + "xp_club_not_exists": "Bu kulüp yok.", + "xp_club_applied": "{0} kulübüne üyelik başvurusunda bulundunuz.", + "xp_club_apply_error": "Başvurularda hata oluştu. Siz zaten ya bir kulübün üyesiydiniz ya da minimum seviye gereksinimini karşılamıyorsunuz ya da bu kulüpten yasaklandınız.", + "xp_club_accepted": "{0} kullanıcısı kulübe kabul edildi.", + "xp_club_accept_error": "Kullanıcı bulunamadı.", + "xp_club_left": "Kulübü terk ettin.", + "xp_club_not_in_club": "Bir kulüpte değilsin veya kulübün sahibi olduğun kulüpten ayrılmaya çalışıyorsun.", + "xp_club_user_kick": "{0} kullanıcısı {1} kulübünden atıldı.", + "xp_club_user_kick_fail": "Atma hatası. Siz ya kulüp sahibi değilsiniz ya da o kullanıcı kulübünüzde değil.", + "xp_club_user_banned": "{0} kullanıcısı {1} kulübünden yasaklandı.", + "xp_club_user_ban_fail": "Yasaklama hatası. Siz ya kulüp sahibi değilsiniz ya da o kullanıcı kulübünüzde değil ya da kulübe başvurmuş.", + "xp_club_user_unbanned": "{0} kullanıcısı {1} kulübündeki yasak kaldırıldı.", + "xp_club_user_unban_fail": "Yasak kaldırmada hata. Siz ya kulüp sahibi değilsiniz ya da o kullanıcı kulübünüzde değil ya da kulübe başvurmuş.", + "xp_club_level_req_changed": "Kulübün seviye gereksinimini {0} olarak değiştirildi", + "xp_club_level_req_change_error": "Gerekli seviye değişimi başarısız.", + "xp_club_disbanded": "{0} kulübü dağıtıldı", + "xp_club_disband_error": "Hata. Ya bir kulüpte değilsin ya da kulübün sahibi değilsin.", + "xp_club_icon_error": "Geçersiz bir resim URL'si veya kulüp sahibi değilsiniz.", + "xp_club_icon_set": "Yeni kulüp simgesi ayarla.", + "xp_club_bans_for": "{0} kulübü için yasaklar", + "xp_club_apps_for": "{0} kulübü için başvuranlar", + "xp_club_leaderboard": "Kulüp lider tablosu - sayfa {0}" } \ No newline at end of file From 270b614f28a70e7f7383298cf731ca9c424408dc Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Sat, 23 Sep 2017 12:40:47 +0200 Subject: [PATCH 337/346] Update ResponseStrings.es-ES.json (POEditor.com) --- .../_strings/ResponseStrings.es-ES.json | 148 ++++++++++++++---- 1 file changed, 117 insertions(+), 31 deletions(-) diff --git a/src/NadekoBot/_strings/ResponseStrings.es-ES.json b/src/NadekoBot/_strings/ResponseStrings.es-ES.json index 06f38e5f..6853bb38 100644 --- a/src/NadekoBot/_strings/ResponseStrings.es-ES.json +++ b/src/NadekoBot/_strings/ResponseStrings.es-ES.json @@ -1,27 +1,4 @@ { - "clashofclans_base_already_claimed": "Esa base ya fue reclamada o destruida.", - "clashofclans_base_already_destroyed": "Esa base ya fue destruida.", - "clashofclans_base_already_unclaimed": "Esa base no ha sido reclamada.", - "clashofclans_base_destroyed": "**Destruida** la base #{0} en la guerrra contra {1}", - "clashofclans_base_unclaimed": "{0} ha **liberado** la base #{1} en la guerra contra {2}", - "clashofclans_claimed_base": "{0} reclamó la base #{1} en la guerra contra {2}", - "clashofclans_claimed_other": "@{0} Ya has reclamado la base #{1}. No puedes reclamar otra.", - "clashofclans_claim_expired": "La reclamación de @{0} en la guerra contra {1} ha expirado.", - "clashofclans_enemy": "Enemigo", - "clashofclans_info_about_war": "Información sobre la guerra contra {0}", - "clashofclans_invalid_base_number": "Número de base inválido.", - "clashofclans_invalid_size": "No es un tamaño de guerra válido.", - "clashofclans_list_active_wars": "Lista de guerras activas", - "clashofclans_not_claimed": "no reclamada", - "clashofclans_not_partic": "No estás participando en esa guerra.", - "clashofclans_not_partic_or_destroyed": "@{0} No estás participando en esa guerra o esa base ya fue destruida.", - "clashofclans_no_active_wars": "No hay guerra activa.", - "clashofclans_size": "Tamaño", - "clashofclans_war_already_started": "La guerra contra {0} ya ha iniciado.", - "clashofclans_war_created": "La guerra contra {0} ha sido creada.", - "clashofclans_war_ended": "La guerra contra {0} ha terminado.", - "clashofclans_war_not_exist": "Esa guerra no existe.", - "clashofclans_war_started": "¡La guerra contra {0} ha iniciado!", "customreactions_all_stats_cleared": "Las estadísticas de los comandos personalizados han sido borradas.", "customreactions_deleted": "Comando personalizado eliminado", "customreactions_insuff_perms": "Insuficientes permisos. Necesitas administrar propiamente el Bot para comandos globales y ser administrador del servidor para locales.", @@ -151,7 +128,6 @@ "administration_old_nick": "Apodo anterior", "administration_old_topic": "Tema anterior", "administration_perms": "Error. Creo que no tengo suficientes permisos.", - "administration_perms_reset": "Los permisos de este servidor han sido reiniciados.", "administration_prot_active": "Protecciones activas", "administration_prot_disable": "{0} ha sido **desactivado** en este servidor.", "administration_prot_enable": "{0} activado", @@ -243,7 +219,6 @@ "administration_sbdm": "Has sido advertido en el servidor {0}. \nRazón: {1}", "administration_user_unbanned": "Usuario desbloqueado:", "administration_migration_done": "¡Migración terminada!", - "administration_migration_error": "Error al migrar, revisa la consola para más información.", "administration_presence_updates": "Actualizaciones de presencia", "administration_sb_user": "Usuario advertido", "gambling_awarded": "le ha regalado {0} a {1}", @@ -439,7 +414,6 @@ "music_rpl_enabled": "Repetición de listas de reproducción activada.", "music_set_music_channel": "Desde ahora publicaré la reproducción, finalización, pausa y eliminación de canciones en este canal.", "music_skipped_to": "Saltado a `{0}:{1}`", - "music_songs_shuffled": "Canciones reorganizadas.", "music_song_moved": "Canción movida", "music_time_format": "{0}h {1}m {2}s", "music_to_position": "A la posición", @@ -605,9 +579,6 @@ "utility_convert_not_found": "No pude convertir {0} a {1}: Unidades no encontradas", "utility_convert_type_error": "No pude convertir {0} a {1}: el tipo de unidad no es igual", "utility_created_at": "Creada el", - "utility_csc_join": "Entró al canal del cruce de servidor.", - "utility_csc_leave": "Salió del canal del cruce de servidor.", - "utility_csc_token": "Este es tu token CSC", "utility_custom_emojis": "Emoticones personales", "utility_error": "Error", "utility_features": "Características", @@ -754,7 +725,6 @@ "gambling_shop_role": "Obtendrás el rol {0}.", "gambling_type": "Tipo", "utility_clpa_next_update": "Próxima actualización en {0}", - "administration_global_perms_reset": "Los permisos globales han sido reiniciados.", "administration_gvc_disabled": "El canal de juego por voz ha sido desactivado.", "administration_gvc_enabled": "{0} ahora es un canal de juego por voz.", "administration_not_in_voice": "No estás en un canal de voz en este servidor.", @@ -782,5 +752,121 @@ "permissions_lgp_none": "No hay comandos o módulos bloqueados.", "gambling_animal_race_no_race": "¡Esta carrera de animales está llena!", "utility_cant_read_or_send": "No puedes leer ni enviar mensajes a ese canal.", - "utility_quotes_notfound": "No se han encontrado citas con la ID especificada." + "utility_quotes_notfound": "No se han encontrado citas con la ID especificada.", + "administration_prefix_current": "El prefijo de este servidor es {0}", + "administration_prefix_new": "Cambiado el prefijo en este servidor de {0} a {1}", + "administration_defprefix_current": "El prefijo por defecto es {0}", + "administration_defprefix_new": "Cambiado el prefijo por defecto de {0} a {1}", + "administration_bot_nick": "Apodo del bot cambiado a {0}", + "administration_user_nick": "Apodo del usuario {0} cambiado a {1}", + "administration_timezone_guild": "La zona horaria de este servidor es `{0}`", + "administration_timezone_not_found": "Zona horaria no encontrada. Usa \".timezones\" para ver la lista de husos horarios disponibles.", + "administration_timezones_available": "Zonas horarias disponibles", + "music_song_not_found": "No encontré esa canción.", + "searches_define_unknown": "No pude encontrar una definición para ese término.", + "utility_repeater_initial": "El mensaje de repetición inicial será enviado en {0}h y {1}m.", + "utility_verbose_errors_enabled": "Desde ahora el uso incorrecto de comandos mostrará errores.", + "utility_verbose_errors_disabled": "El uso incorrecto de comandos ya no mostrará errores.", + "permissions_perms_reset": "Los permisos de este servidor se han reiniciado.", + "permissions_trigger": "El permiso número #{0} {1} está impidiendo esta acción.", + "administration_migration_error": "Error al migrar, revisa la consola para más información.", + "searches_hex_invalid": "Color especificado no válido.", + "permissions_global_perms_reset": "Los permisos globales se han reiniciado.", + "help_module": "Módulo: {0}", + "games_hangman_stopped": "Juego del ahorcado detenido.", + "music_autoplaying": "Reproducción automática.", + "music_queue_stopped": "Reproducción detenida. Usa el comando {0} para resumir.", + "music_removed_song_error": "No existe una canción relacionada.", + "music_shuffling_playlist": "Mezclando canciones", + "music_songs_shuffle_enable": "Las canciones se mezclarán desde ahora.", + "music_songs_shuffle_disable": "Las canciones ya no se mezclarán.", + "music_song_skips_after": "Las canciones se omitirán después de {0}", + "administration_warnings_list": "Lista de usuarios advertidos en el servidor", + "customreactions_redacted_too_long": "Editado porque es muy largo.", + "nsfw_blacklisted_tag_list": "Lista de etiquetas bloqueadas:", + "nsfw_blacklisted_tag": "Una o más etiquetas usadas están en la lista negra", + "nsfw_blacklisted_tag_add": "La etiqueta NSFW {0} ahora está bloqueada.", + "nsfw_blacklisted_tag_remove": "La etiqueta NSFW {0} ya no está bloqueada.", + "gambling_waifu_gift": "Le regaló {0} a {1}", + "gambling_waifu_gift_shop": "Tienda de regalos para waifus", + "gambling_gifts": "Regalos", + "games_connect4_created": "Juego Conecta 4 creado. Esperando usuarios", + "games_connect4_player_to_move": "El movimiento es de: {0}", + "games_connect4_failed_to_start": "El juego no pudo iniciarse porque nadie ingresó.", + "games_connect4_draw": "Es un empate.", + "games_connect4_won": "{0} ganó el juego de Conecta 4 contra {1}.", + "games_nunchi_joined": "Inicia el juego de Nunchi. {0} usuarios en cola.", + "games_nunchi_ended": "Ha finalizado el juego de Nunchi. {0} gana.", + "games_nunchi_ended_no_winner": "El juego terminó sin ganadores.", + "games_nunchi_started": "Empieza el juego con {0} participantes.", + "games_nunchi_round_ended": "Ronda terminada. {0} queda fuera.", + "games_nunchi_round_ended_boot": "Terminó la ronda debido a que varios usuarios tardaron. Estos usuarios siguen en juego: {0}", + "games_nunchi_round_started": "La ronda de Nunchi empezó con {0} usuarios.Empiecen contando desde el {1}.", + "games_nunchi_next_number": "Número registrado. El último número fue {0}.", + "games_nunchi_failed_to_start": "No se pudo iniciar el juego debido a que no hay suficientes participantes.", + "games_nunchi_created": "Juego de Nunchi creado. Esperando usuarios.", + "music_sad_enabled": "Las canciones serán eliminadas de la cola cuando la reproducción termine.", + "music_sad_disabled": "Las canciones ya no serán eliminadas de la cola cuando la reproducción termine.", + "utility_stream_role_enabled": "Cuando un usuario del rol {0} empiece a transmitir, les daré el rol {1}", + "utility_stream_role_disabled": "Rol de transmisión desactivado.", + "utility_stream_role_kw_set": "Los que transmitan ahora requerirán la clave {0} para recibir el rol.", + "utility_stream_role_kw_reset": "Clave de rol de transmisión reiniciada.", + "utility_stream_role_bl_add": "El usuario {0} no recibirá jamás el rol de transmisor.", + "utility_stream_role_bl_add_fail": "El usuario {0} ya está en la lista negra.", + "utility_stream_role_bl_rem": "El usuario {0} ya no está en la lista negra.", + "utility_stream_role_bl_rem_fail": "El usuario {0} no está en la lista negra.", + "utility_stream_role_wl_add": "El usuario {0} recibirá el rol de transmisor aunque no tengan la clave en el título de la transmisión.", + "utility_stream_role_wl_add_fail": "El usuario {0} ya está en la lista blanca.", + "utility_stream_role_wl_rem": "El usuario {0} ya no está en la lista blanca.", + "utility_stream_role_wl_rem_fail": "El usuario {0} no está en la lista blanca.", + "utility_bot_config_edit_fail": "Configuración {0} fallida en el valor {1}.", + "utility_bot_config_edit_success": "El valor de {0} ha sido configurado a {1}.", + "customreactions_crca_disabled": "El comando personalizado con la ID {0} no se ejecutará a menos que la palabra esté al principio de la oración.", + "customreactions_crca_enabled": "El comando personalizado con la ID {0} se ejecutará si está en cualquier parte de la oración.\n", + "xp_server_level": "Nivel en el servidor", + "xp_level": "Nivel\n", + "xp_club": "Club\n", + "xp_xp": "Experiencia", + "xp_excluded": "{0} ha sido excluido del sistema de XP en este servidor.", + "xp_not_excluded": "{0} ya no será excluido del sistema de XP en este servidor.", + "xp_exclusion_list": "Lista de excluidos", + "xp_server_is_excluded": "Este servidor está excluido.", + "xp_server_is_not_excluded": "Este servidor no está excluido.", + "xp_excluded_roles": "Roles excluidos", + "xp_excluded_channels": "Canales excluidos", + "xp_level_up_channel": "¡Felicitaciones {0}, has alcanzado el nivel {1}!", + "xp_level_up_dm": "¡Felicitaciones {0}, has alcanzado el nivel {1} en el servidor {2}!", + "xp_level_up_global": "¡Felicitaciones {0}, has alcanzado el nivel global {1}!", + "xp_role_reward_cleared": "El nivel {0} ya no recompensará un rol.", + "xp_role_reward_added": "Los usuarios que alcancen el nivel {0} recibirán el rol {1}.", + "xp_role_rewards": "Roles de recompensa", + "xp_level_x": "Nivel {0}", + "xp_no_role_rewards": "No hay roles en esta página.", + "xp_server_leaderboard": "Marcador de XP del servidor", + "xp_global_leaderboard": "Marcador de XP global", + "xp_modified": "Modificada la XP del servidor del usuario {0} por {1}", + "xp_club_create_error": "No pude crear el club. Asegúrate de que eres al menos nivel 5 y que no estés ya en un club.", + "xp_club_created": "¡Club {0} creado!", + "xp_club_not_exists": "Ese club no existe.", + "xp_club_applied": "Has aplicado al club {0}.", + "xp_club_apply_error": "Error aplicando. O ya eres un miembro del club, o no cumples con el nivel mínimo, o has sido bloqueado de este club.", + "xp_club_accepted": "Se ha aceptado al usuario {0} en el club.", + "xp_club_accept_error": "No encuentro ese usuario.", + "xp_club_left": "Has salido del club.", + "xp_club_not_in_club": "No estás en ningún club o estás tratando de dejar un club que administras.", + "xp_club_user_kick": "El usuario {0} ha sido expulsado del club {1}.", + "xp_club_user_kick_fail": "Error expulsando. No eres el administrador del club o ese usuario no está en tu club.", + "xp_club_user_banned": "El usuario {0} ha sido bloqueado del club {1}.", + "xp_club_user_ban_fail": "No pude bloquearlo. No eres el administrador del club, o ese usuario no está en tu club.", + "xp_club_user_unbanned": "El usuario {0} ha sido desbloqueado en el club {1}.", + "xp_club_user_unban_fail": "No pude desbloquearlo. No eres el administrador del club, o ese usuario no está en tu club.", + "xp_club_level_req_changed": "Modificado el nivel requerido para ingresar al club a {0}", + "xp_club_level_req_change_error": "No pude cambiar el nivel requerido.", + "xp_club_disbanded": "El club Club {0} ha sido disuelto", + "xp_club_disband_error": "Error. No estás en un club o no eres el administrador de ese club.", + "xp_club_icon_error": "Esa imagen no es válida o no eres el administrador del club.", + "xp_club_icon_set": "Nuevo avatar de club configurado.", + "xp_club_bans_for": "Bloqueos en el club {0}", + "xp_club_apps_for": "Aplicantes al club {0}", + "xp_club_leaderboard": "Marcador de clubes - página {0}" } \ No newline at end of file From f826c9a36814077a5f53bdc8763b90eb84a46614 Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Sat, 23 Sep 2017 12:40:50 +0200 Subject: [PATCH 338/346] Update ResponseStrings.ru-RU.json (POEditor.com) --- .../_strings/ResponseStrings.ru-RU.json | 114 ++++++++++++++---- 1 file changed, 89 insertions(+), 25 deletions(-) diff --git a/src/NadekoBot/_strings/ResponseStrings.ru-RU.json b/src/NadekoBot/_strings/ResponseStrings.ru-RU.json index f0ec42bc..99a3f46f 100644 --- a/src/NadekoBot/_strings/ResponseStrings.ru-RU.json +++ b/src/NadekoBot/_strings/ResponseStrings.ru-RU.json @@ -1,27 +1,4 @@ { - "clashofclans_base_already_claimed": "Эта база уже захвачена или разрушена.", - "clashofclans_base_already_destroyed": "Эта база уже разрушена", - "clashofclans_base_already_unclaimed": "Эта база не захвачена.", - "clashofclans_base_destroyed": "**РАЗРУШЕННАЯ** база #{0} ведёт войну против {1}.", - "clashofclans_base_unclaimed": "У {0} есть **НЕ ЗАХВАЧЕННАЯ** база #{1}, ведущая войну против {2}", - "clashofclans_claimed_base": "{0} захватил базу #{1}, ведя войну против {2}", - "clashofclans_claimed_other": "@{0} Вы уже захватили базу #{1}. Вы не можете захватить новую базу.", - "clashofclans_claim_expired": "Время действия запроса от @{0} на войну против {1} истекло.", - "clashofclans_enemy": "Враг", - "clashofclans_info_about_war": "Информация о войне против {0}", - "clashofclans_invalid_base_number": "Неправильный номер базы.", - "clashofclans_invalid_size": "Неправильный размер войны.", - "clashofclans_list_active_wars": "Список активных войн", - "clashofclans_not_claimed": "не захвачена", - "clashofclans_not_partic": "Вы не участвуете в этой войне.", - "clashofclans_not_partic_or_destroyed": "@{0} Вы либо не участвуете в этой войне, либо эта база уже разрушена.", - "clashofclans_no_active_wars": "Нет активных войн.", - "clashofclans_size": "Размер", - "clashofclans_war_already_started": "Война против {0} уже началась.", - "clashofclans_war_created": "Война против {0} начата.", - "clashofclans_war_ended": "Война против {0} закончилась.", - "clashofclans_war_not_exist": "Этой войны не существует.", - "clashofclans_war_started": "Война против {0} началась!", "customreactions_all_stats_cleared": "Вся статистика пользовательских реакций стёрта.", "customreactions_deleted": "Пользовательская реакция удалена", "customreactions_insuff_perms": "Недостаточно разрешений. Необходимо иметь разрешение владельца бота для глобальных пользовательских реакций, и разрешение Администратора для реакций на сервере.", @@ -396,7 +373,7 @@ "music_autoplay_enabled": "Автовоспроизведение включено.", "music_defvol_set": "Громкость по умолчанию выставлена на {0}%", "music_dir_queue_complete": "Папка успешно добавлена в очередь воспроизведения.", - "music_fairplay": "Справедливое воспроизведение", + "music_fairplay": "Честное воспроизведение", "music_finished_song": "Песня завершилась.", "music_fp_disabled": "Отключено честное воспроизведение.", "music_fp_enabled": "Включено честное воспроизведение.", @@ -804,5 +781,92 @@ "music_songs_shuffle_enable": "Песни будут воспроизводиться в случайном порядке.", "music_songs_shuffle_disable": "Песни больше не будут воспроизводиться в случайном порядке.", "music_song_skips_after": "Песни будут пропущены после {0}", - "administration_warnings_list": "Список всех предупреждённых пользователей на сервере" + "administration_warnings_list": "Список всех предупреждённых пользователей на сервере", + "customreactions_redacted_too_long": "Текст пользовательской реакции изменён, поскольку он был слишком длинным.", + "nsfw_blacklisted_tag_list": "Чёрный список тэгов:", + "nsfw_blacklisted_tag": "Один или несколько использованных тэгов в чёрном списке:", + "nsfw_blacklisted_tag_add": "Небезопасный для работы тэг {0} добавлен в чёрный список.", + "nsfw_blacklisted_tag_remove": "Небезопасный для работы тэг {0} исключён из чёрного списка.", + "gambling_waifu_gift": "Подарили {0} пользователю {1}", + "gambling_waifu_gift_shop": "Список подарков для вайфу", + "gambling_gifts": "Подарки", + "games_connect4_created": "Создана игра \"Четыре в ряд\". Ожидание игроков.", + "games_connect4_player_to_move": "Игроки, которым осталось сделать ход: {0}", + "games_connect4_failed_to_start": "Игра \"Четыре в ряд\" не началась, поскольку не присоединился ни один игрок.", + "games_connect4_draw": "Игра \"Четыре в ряд\" завершилась ничьёй.", + "games_connect4_won": "{0} выиграл игру \"Четыре в ряд\" против {1}.", + "games_nunchi_joined": "Началась игра в нунчи. {0} пользователей присоединились к игре.", + "games_nunchi_ended": "Игра в нунчи завершилась. {0} выиграл", + "games_nunchi_ended_no_winner": "Игра в нунчи закончилась, никто не выиграл.", + "games_nunchi_started": "Игра в нунчи началась с {0} участниками.", + "games_nunchi_round_ended": "Раунд нунчи закончился. {0} выбывает из игры.", + "games_nunchi_round_ended_boot": "Раунд нунчи закончился, поскольку у игроков закончилось время. Игроки, оставшиеся в игре: {0}", + "games_nunchi_round_started": "Раунд нунчи начался с {0} пользователями. Отсчёт начинается с номера {1}.", + "games_nunchi_next_number": "Число записано. Предыдущее число —{0}.", + "games_nunchi_failed_to_start": "Игра в нунчи не началась, поскольку было недостаточно участников.", + "games_nunchi_created": "Игра в нунчи создана. Ожидание игроков.", + "music_sad_enabled": "Песни будут удаляться из очереди после завершения воспроизведения.", + "music_sad_disabled": "Песни больше не будут удаляться из очереди после завершения воспроизведения.", + "utility_stream_role_enabled": "Когда пользователь с ролью {0} начинает трансляцию, им будет добавлена роль {1}.", + "utility_stream_role_disabled": "Функция добавления специальной роли стримерам отключена.", + "utility_stream_role_kw_set": "Стримерам теперь будет требоваться ключевое слово {0} для получения роли.", + "utility_stream_role_kw_reset": "Ключевое слово для роли стримеров сброшено.", + "utility_stream_role_bl_add": "Пользователь {0} не будет получать роль стримера.", + "utility_stream_role_bl_add_fail": "Пользователь {0} уже в чёрном списке.", + "utility_stream_role_bl_rem": "Пользователь {0} больше не в чёрном списке.", + "utility_stream_role_bl_rem_fail": "Пользователь {0} не в чёрном списке.", + "utility_stream_role_wl_add": "Пользователь {0} будет получать роль стримера, даже если в названии их трансляции нет ключевого слова.", + "utility_stream_role_wl_add_fail": "Пользователь {0} уже в белом списке.", + "utility_stream_role_wl_rem": "Пользователь {0} больше не в белом списке.", + "utility_stream_role_wl_rem_fail": "Пользователь {0} не в белом списке.", + "utility_bot_config_edit_fail": "Не удалось выставить величину {0} равной {1}", + "utility_bot_config_edit_success": "Величина {0} была выставлена равной {1}", + "customreactions_crca_disabled": "Пользовательская реакция с id {0} будет вызываться, только если сообщение начинается со слова-триггера.", + "customreactions_crca_enabled": "Пользовательская реакция с id {0} будет теперь вызываться, если слово-триггер содержится в любой части сообщения.", + "xp_server_level": "Серверный уровень", + "xp_level": "Уровень", + "xp_club": "Клуб", + "xp_xp": "Опыт", + "xp_excluded": "{0} исключён из системы опыта на данном сервере.", + "xp_not_excluded": "{0} больше не исключён из системы опыта на данном сервере.", + "xp_exclusion_list": "Список исключённых серверов", + "xp_server_is_excluded": "Данный сервер исключён.", + "xp_server_is_not_excluded": "Данный сервер не исключён.", + "xp_excluded_roles": "Исключённые роли", + "xp_excluded_channels": "Исключённые текстовые каналы", + "xp_level_up_channel": "Поздравляем, {0}, Вы достигли уровень {1}!", + "xp_level_up_dm": "Поздравляем, {0}, Вы достигли уровень {1} на сервере {2}!", + "xp_level_up_global": "Поздравляем, {0}, Вы достигли глобальный уровень {1}!", + "xp_role_reward_cleared": "Пользователи больше не будут получать роль за уровень {0} .", + "xp_role_reward_added": "Пользователи, достигающие уровня {0}, будут получать роль {1}.", + "xp_role_rewards": "Награды для ролей", + "xp_level_x": "Уровень {0}", + "xp_no_role_rewards": "На этой странице нет наград для ролей.", + "xp_server_leaderboard": "Серверный рейтинг опыта", + "xp_global_leaderboard": "Глобальный рейтинг опыта", + "xp_modified": "Изменено количество серверного опыта на {1} для пользователя {0}", + "xp_club_create_error": "Не удалось создать клуб. Убедитесь, что Вы 5-го уровня или выше, и что Вы не состоите в клубе на настоящий момент.", + "xp_club_created": "Клуб {0} создан!", + "xp_club_not_exists": "Данный клуб не существует.", + "xp_club_applied": "Вы подали заявку на вступление в клуб {0}.", + "xp_club_apply_error": "Не удалось подать заявку. Либо Вы уже состоите в клубе, либо у Вас недостаточный уровень, либо Вас забанили в этом клубе.", + "xp_club_accepted": "Пользователь {0} принят в клуб.", + "xp_club_accept_error": "Пользователь не найден", + "xp_club_left": "Вы покинули клуб.", + "xp_club_not_in_club": "Вы либо не состоите в клубе, либо пытаетесь покинуть клуб, владельцем которого Вы являетесь.", + "xp_club_user_kick": "Пользователь {0} выгнан из клуба {1}.", + "xp_club_user_kick_fail": "Не удалось выгнать пользователя. Либо вы не являетесь владельцем клуба, либо данный пользователь не состоит в вашем клубе.", + "xp_club_user_banned": "Пользователь {0} забанен в клубе {1}.", + "xp_club_user_ban_fail": "Не удалось забанить. Вы либо не являетесь владельцем клуба, либо этот пользователь не состоит в клубе и не подавал заявку на вступление.", + "xp_club_user_unbanned": "Пользователь {0} разбанен в клубе {1}.", + "xp_club_user_unban_fail": "Не удалось разбанить. Вы либо не являетесь владельцем клуба, либо этот пользователь не состоит в клубе и не подавал заявку на вступление.", + "xp_club_level_req_changed": "Минимальный уровень для клуба изменён на {0}.", + "xp_club_level_req_change_error": "Не удалось изменить минимальный уровень для клуба.", + "xp_club_disbanded": "Клуб {0} распущен.", + "xp_club_disband_error": "Ошибка. Вы либо не состоите в клубе, либо не являетесь владельцем клуба.", + "xp_club_icon_error": "Неправильный URL для изображения, или Вы не являетесь владельцем клуба.", + "xp_club_icon_set": "Выставлен новый аватар клуба.", + "xp_club_bans_for": "Баны клуба {0}", + "xp_club_apps_for": "Заявки на вступление в клуб {0}", + "xp_club_leaderboard": "Рейтинг клуба - страница {0}" } \ No newline at end of file From 8235a5998331fc32149eb977bfc20ad5f82ec61c Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Sat, 23 Sep 2017 12:40:53 +0200 Subject: [PATCH 339/346] Update ResponseStrings.ko-KR.json (POEditor.com) --- .../_strings/ResponseStrings.ko-KR.json | 114 ++++++++++++++---- 1 file changed, 89 insertions(+), 25 deletions(-) diff --git a/src/NadekoBot/_strings/ResponseStrings.ko-KR.json b/src/NadekoBot/_strings/ResponseStrings.ko-KR.json index e812bf90..60c4cac6 100644 --- a/src/NadekoBot/_strings/ResponseStrings.ko-KR.json +++ b/src/NadekoBot/_strings/ResponseStrings.ko-KR.json @@ -1,27 +1,4 @@ { - "clashofclans_base_already_claimed": " 기지가 이미 요청되었거나 파괴되었습니다.", - "clashofclans_base_already_destroyed": "기지가 이미 파괴되었습니다.", - "clashofclans_base_already_unclaimed": "기지가 요청당하지 않았습니다.", - "clashofclans_base_destroyed": "{1}와의 전쟁에서 #{0}번 기지가 **파괴**되었습니다.", - "clashofclans_base_unclaimed": "{2}와의 전쟁에서 {0}가 #{1}번 기지를 **요청**하지 못했습니다.", - "clashofclans_claimed_base": "{2}와의 전쟁에서 #{1}번 기지를 {0}가 요청하였습니다.", - "clashofclans_claimed_other": "@{0} 이미 #{1}번 기지를 요청했습니다. 새로운 기지를 요청 할 수 없습니다.", - "clashofclans_claim_expired": "{1}와의 전쟁에서 @{0}의 요청이 만료되었습니다.", - "clashofclans_enemy": "적", - "clashofclans_info_about_war": "{0}와의 전쟁에 대한 정보", - "clashofclans_invalid_base_number": "유효하지 않은 기지 숫자입니다.", - "clashofclans_invalid_size": "유효하지 않은 전쟁 크기입니다.", - "clashofclans_list_active_wars": "활성화된 전쟁 목록", - "clashofclans_not_claimed": "요청되지 않았습니다.", - "clashofclans_not_partic": "당신은 이 전쟁에 참여하고 있지 않습니다.", - "clashofclans_not_partic_or_destroyed": "@{0} 당신은 그 전쟁에 참여하지 않거나 그 기지가 이미 파괴되었습니다.", - "clashofclans_no_active_wars": "활성화된 전쟁이 없습니다.", - "clashofclans_size": "크기", - "clashofclans_war_already_started": "{0}에 대한 전쟁이 이미 시작되었습니다.", - "clashofclans_war_created": "{0}에 대한 전쟁이 생성되었습니다.", - "clashofclans_war_ended": "{0}와의 전쟁이 종료되었습니다.", - "clashofclans_war_not_exist": "전쟁이 존재하지않습니다.", - "clashofclans_war_started": "{0} 와의 전쟁이 시작되었습니다.", "customreactions_all_stats_cleared": "모든 커스텀 리액션에 대한 통계가 삭제되었습니다.", "customreactions_deleted": "커스텀 리액션이 삭제되었습니다.", "customreactions_insuff_perms": "권한이 부족합니다. 전체 커스텀 리액션을 관리하기 위해서는 봇의 소유권과 서버의 관리자 권한이 필요합니다.", @@ -368,7 +345,7 @@ "games_hangman_running": "행맨 게임이 이미 시작되었습니다", "games_hangman_start_errored": "행맨 게임 시작에 오류가 발생하였습니다.", "games_hangman_types": "\"{0}hangman\" 용어 종류 목록", - "games_leaderboard": "리더 보드", + "games_leaderboard": "리더보드", "games_not_enough": "당신은 충분한 {0}이(가) 없습니다.", "games_no_results": "결과가 없습니다.", "games_picked": "이(가) {0}을(를) 뽑았습니다.", @@ -804,5 +781,92 @@ "music_songs_shuffle_enable": "지금부터 노래들을 섞습니다.", "music_songs_shuffle_disable": "노래들을 더 이상 섞지 않습니다.", "music_song_skips_after": "노래들을 {0}초 후에 스킵합니다.", - "administration_warnings_list": "이 서버의 경고받은 사용자 목록" + "administration_warnings_list": "이 서버의 경고받은 사용자 목록", + "customreactions_redacted_too_long": "너무 길기 때문에 편집했습니다.", + "nsfw_blacklisted_tag_list": "블랙리스트 된 태그 목록", + "nsfw_blacklisted_tag": "당신이 입력한 태그 중 하나 이상의 태그가 블랙리스트에 있습니다.", + "nsfw_blacklisted_tag_add": "NSFW 태그 {0}이(가) 블랙리스트에 추가 됬습니다.", + "nsfw_blacklisted_tag_remove": "NSFW 태그 {0}은(는) 더 이상 블랙리스트가 아닙니다.", + "gambling_waifu_gift": "{1}에게 {0}개를 선물하셨습니다", + "gambling_waifu_gift_shop": "와이프 선물 가게", + "gambling_gifts": "선물", + "games_connect4_created": "Connect4 게임을 생성했습니다. 플레이어의 참여를 기다리는 중입니다.", + "games_connect4_player_to_move": "", + "games_connect4_failed_to_start": "아무도 게임에 참여하지 않아서 Connect4 게임 시작에 실패했습니다.", + "games_connect4_draw": "Connect4 게임이 무승부로 끝났습니다.", + "games_connect4_won": "{0}님이 {1}을 상대로 Connect4 게임을 이겼습니다.", + "games_nunchi_joined": "눈치 게임에 참가했습니다. {0} 명의 사용자가 지금까지 참가했습니다.", + "games_nunchi_ended": "눈치 게임이 종료되었습니다. {0}님이 이겼습니다", + "games_nunchi_ended_no_winner": "눈치 게임이 승자없이 종료되었습니다.", + "games_nunchi_started": "{0}명의 참가자와 함께 눈치 게임을 시작했습니다.", + "games_nunchi_round_ended": "눈치 게임 라운드가 종료되었습니다. {0}님이 탈락했습니다.", + "games_nunchi_round_ended_boot": "눈치 게임 라운드는 일부 사용자의 시간 초과로 종료되었습니다. 이 사용자는 여전히 게임 중입니다: {0}", + "games_nunchi_round_started": "{0}명의 참가자와 함께 눈치 게임 라운드가 시작되었습니다. 숫자 {1}부터 시작합니다.", + "games_nunchi_next_number": "숫자가 등록되었습니다. 마지막 숫자는 {0} 입니다.", + "games_nunchi_failed_to_start": "충분한 플레이어가 없어서 눈치게임 시작에 실패했습니다.", + "games_nunchi_created": "눈치게임이 생성되었습니다. 플레이어의 참여를 기다리는 중입니다.", + "music_sad_enabled": "음악 재생이 완료되면 대기열에서 삭제됩니다.", + "music_sad_disabled": "", + "utility_stream_role_enabled": "", + "utility_stream_role_disabled": "스트림 역할 기능이 비활성화되었습니다.", + "utility_stream_role_kw_set": "이제 스트리머는 역할을 얻기위해 {0} 키워드가 필요합니다.", + "utility_stream_role_kw_reset": "스트림 역할 키워드가 초기화되었습니다.", + "utility_stream_role_bl_add": "{0}은(는) 절대 스트림 역할을 얻을 수 없습니다.", + "utility_stream_role_bl_add_fail": "{0}은(는) 이미 블랙리스트에 추가됬습니다.", + "utility_stream_role_bl_rem": "{0}은(는) 더 이상 블랙리스트에 속해있지 않습니다.", + "utility_stream_role_bl_rem_fail": "{0}은(는) 블랙리스트에 속해있지 않습니다.", + "utility_stream_role_wl_add": "", + "utility_stream_role_wl_add_fail": "{0}은(는) 이미 화이트리스트에 추가되었습니다.", + "utility_stream_role_wl_rem": "", + "utility_stream_role_wl_rem_fail": "", + "utility_bot_config_edit_fail": "", + "utility_bot_config_edit_success": "", + "customreactions_crca_disabled": "", + "customreactions_crca_enabled": "", + "xp_server_level": "서버 레벨", + "xp_level": "레벨", + "xp_club": "클럽", + "xp_xp": "경험치", + "xp_excluded": "", + "xp_not_excluded": "", + "xp_exclusion_list": "제외 목록", + "xp_server_is_excluded": "이 서버가 제외 됐습니다", + "xp_server_is_not_excluded": "이 서버가 제외되지 않습니다", + "xp_excluded_roles": "", + "xp_excluded_channels": "", + "xp_level_up_channel": "", + "xp_level_up_dm": "", + "xp_level_up_global": "{0}님 축하드립니다, 글로벌 레벨 {1}을(를) 달성했습니다.", + "xp_role_reward_cleared": "{0} 레벨에 도달해도 역할을 얻지않습니다.", + "xp_role_reward_added": "{0} 레벨에 도달한 유저는 {1} 역할을 얻게됩니다.", + "xp_role_rewards": "역할 보상", + "xp_level_x": "레벨 {0}", + "xp_no_role_rewards": "", + "xp_server_leaderboard": "서버 XP 리더보드", + "xp_global_leaderboard": "글로벌 XP 리더보드", + "xp_modified": "", + "xp_club_create_error": "", + "xp_club_created": "", + "xp_club_not_exists": "", + "xp_club_applied": "", + "xp_club_apply_error": "", + "xp_club_accepted": "", + "xp_club_accept_error": "사용자를 찾을 수 없습니다", + "xp_club_left": "", + "xp_club_not_in_club": "", + "xp_club_user_kick": "", + "xp_club_user_kick_fail": "", + "xp_club_user_banned": "", + "xp_club_user_ban_fail": "", + "xp_club_user_unbanned": "", + "xp_club_user_unban_fail": "", + "xp_club_level_req_changed": "", + "xp_club_level_req_change_error": "", + "xp_club_disbanded": "", + "xp_club_disband_error": "", + "xp_club_icon_error": "", + "xp_club_icon_set": "", + "xp_club_bans_for": "{0} 클럽 밴 목록", + "xp_club_apps_for": "", + "xp_club_leaderboard": "클럽 리더보드 - 페이지 {0}" } \ No newline at end of file From d34cf0d6547e1c51ca91e0b2090a9c19ba889dd1 Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Sat, 23 Sep 2017 15:28:01 +0200 Subject: [PATCH 340/346] Update ResponseStrings.de-DE.json (POEditor.com) --- .../_strings/ResponseStrings.de-DE.json | 150 +++++++++++++----- 1 file changed, 107 insertions(+), 43 deletions(-) diff --git a/src/NadekoBot/_strings/ResponseStrings.de-DE.json b/src/NadekoBot/_strings/ResponseStrings.de-DE.json index f155cb6e..455ca29d 100644 --- a/src/NadekoBot/_strings/ResponseStrings.de-DE.json +++ b/src/NadekoBot/_strings/ResponseStrings.de-DE.json @@ -1,27 +1,4 @@ { - "clashofclans_base_already_claimed": "Diese Basis wurde bereits beansprucht oder zerstört.", - "clashofclans_base_already_destroyed": "Diese Basis ist bereits zerstört.", - "clashofclans_base_already_unclaimed": "Diese Basis ist nicht beansprucht.", - "clashofclans_base_destroyed": "Basis #{0} im Krieg gegen {1} **ZERSTÖRT** ", - "clashofclans_base_unclaimed": "{0} hat die Basis #{1} **UNBEANSPRUCHT** im Krieg gegen {2}", - "clashofclans_claimed_base": "{0} hat die Basis #{1} beansprucht im Krieg gegen {2}", - "clashofclans_claimed_other": "@{0} sie haben die Basis #{1} bereits beansprucht. Sie können keine weitere beanspruchen.", - "clashofclans_claim_expired": "Beanspruchung von @{0} für den Krieg gegen {1} ist abgelaufen.", - "clashofclans_enemy": "Gegner", - "clashofclans_info_about_war": "informationen über den Krieg mit {0}", - "clashofclans_invalid_base_number": "Ungültige Basisnummer.", - "clashofclans_invalid_size": "Keine gültige Kriegsgröße.", - "clashofclans_list_active_wars": "Liste der aktiven Kriege", - "clashofclans_not_claimed": "nicht beansprucht", - "clashofclans_not_partic": "Sie nehmen nicht an diesem Krieg teil.", - "clashofclans_not_partic_or_destroyed": "@{0} Sie nehmen nicht an diesem Krieg teil, oder diese Basis ist bereits zerstört.", - "clashofclans_no_active_wars": "Keine aktiven Kriege.", - "clashofclans_size": "Größe", - "clashofclans_war_already_started": "Krieg gegen {0} wurde schon gestartet.", - "clashofclans_war_created": "Krieg gegen {0} erstellt.", - "clashofclans_war_ended": "Krieg gegen {0} ist beendet.", - "clashofclans_war_not_exist": "Dieser Krieg existiert nicht.", - "clashofclans_war_started": "Krieg gegen {0} gestartet!", "customreactions_all_stats_cleared": "Reaktionsstatistiken gelöscht.", "customreactions_deleted": "Benutzerdefinierte Reaktion gelöscht.", "customreactions_insuff_perms": "Unzureichende Rechte. Dies erfordert das Sie den Bot besitzen für globale Reaktionen, und Administratorrechte für serverseitige Reaktionen.", @@ -97,7 +74,7 @@ "administration_fwdm_start": "Ich werde nun DNs weiterleiten.", "administration_fwdm_stop": "Ich werde aufhören DNs weiterzuleiten.", "administration_greetdel_off": "Automatisches löschen der Begrüßungsnachrichten wurde deaktiviert.", - "administration_greetdel_on": "Begrüßungsnachrichten werden gelöscht nach {0} sekunden.", + "administration_greetdel_on": "Begrüßungsnachrichten werden gelöscht nach {0} Sekunden.", "administration_greetdmmsg_cur": "Aktuelle DN Begrüßungsnachricht: {0}", "administration_greetdmmsg_enable": "Aktiviere DN Begrüßungsnachrichten durch schreiben von: {0}", "administration_greetdmmsg_new": "Neue DN Begrüßungsnachricht wurde gesetzt.", @@ -109,7 +86,7 @@ "administration_greet_off": "Begrüßungsankündigungen wurden deaktiviert.", "administration_greet_on": "Begrüßungsankündigungen wurden für diesen Kanal aktiviert.", "administration_hierarchy": "Sie können diesen befehl nicht an Benutzern mit einer Rolle über oder gleich zu Ihrer in der Rangordnung benutzen.", - "administration_images_loaded": "Bilder wurden geladen nach {0} sekunden!", + "administration_images_loaded": "Bilder wurden geladen nach {0} Sekunden!", "administration_invalid_format": "Ungültiges Eingabeformat.", "administration_invalid_params": "Ungültige Übergabevariable.", "administration_joined": "{0} ist {1} beigetreten", @@ -283,7 +260,7 @@ "help_cmdlist_donate": "Sie können das Projekt auf Patreon: <{0}> oder Paypal: <{1}> unterstützen.", "help_cmd_and_alias": "Befehle und Alias", "help_commandlist_regen": "Befehlsliste neu generiert.", - "help_commands_instr": "Gebe `{0}h NameDesBefehls` ein, um die Hilfestellung für diesen Befehl zu sehen. Z.B. `{0}h >8ball`", + "help_commands_instr": "Gebe `{0}h NameDesBefehls` ein, um die Hilfestellung für diesen Befehl zu sehen. Z.B. `{0}h {0}8ball`", "help_command_not_found": "Ich konnte diesen Befehl nicht finden. Bitte stelle sicher das dieser Befehl existiert bevor Sie es erneut versuchen.", "help_desc": "Beschreibung", "help_donate": "Sie können das NadekoBot-Projekt über \nPatreon <{0}> oder \nPayPal <{1}> unterstützen.\nVergessen Sie bitte nicht, Ihren Discord-Namen oder Ihre ID in der Nachricht zu hinterlassen.\n\n**Vielen Dank**♥️", @@ -657,7 +634,7 @@ "utility_server_info": "Server Info", "utility_shard": "Shard", "utility_shard_stats": "Shard Statistiken", - "utility_shard_stats_txt": "Shard **#{0}** ist im {1} status mit {2} Servern", + "utility_shard_stats_txt": "Shard **#{0}** ist im {1} Status mit {2} Servern - vor {3}", "utility_showemojis": "**Name:** {0} **Link:** {1}", "utility_showemojis_none": "Keine speziellen emoji gefunden.", "utility_stats_songs": "Wiedergabe von {0} Liedern, {1} in der Musikliste.", @@ -684,7 +661,7 @@ "gambling_page": "Seite {0}", "administration_must_be_in_voice": "Sie müssen in einem Sprachkanal auf diesem Server sein.", "administration_no_vcroles": "Keine Sprachkanal Rollen gefunden.", - "administration_user_muted_time": "{0} wurde **stummgeschaltet** im Text- und Sprachchat für {1} minuten.", + "administration_user_muted_time": "{0} wurde **stummgeschaltet** im Text- und Sprachchat für {1} Minuten.", "administration_vcrole_added": "Benutzer die dem Sprachkanal {0} beitreten werden der Rolle {1} zugeweist.", "administration_vcrole_removed": "Benutzer die den Sprachkanal {0} beitreten werden nicht länger einer Rolle zugeweist.", "administration_vc_role_list": "Rollen für Sprachkanäle", @@ -720,7 +697,7 @@ "administration_warnings_none": "Keine Warnungen auf dieser Seite.", "administration_warnlog_for": "Warnlog für {0}", "administration_warnpl_none": "Keine Bestrafungen gesetzt.", - "administration_warn_cleared_by": "Bereinigt bei {0}", + "administration_warn_cleared_by": "Gelöscht von {0}", "administration_warn_punish_list": "Warnungs Straf Liste", "administration_warn_punish_rem": "{0} Warnungen werden nicht mehr eine Bestrafung auslösen.", "administration_warn_punish_set": "Ich werde die Bestrafung {0} an Benutzern mit {1} Warnungen ausführen.", @@ -748,7 +725,7 @@ "gambling_shop_role": "Du wirst die Rolle {0} erhalten.", "gambling_type": "Art", "utility_clpa_next_update": "Nächste Aktualisierung in {0}", - "administration_gvc_disabled": "Spiel-Sprachkanal-Feature ist nicht eingeschaltet auf diesem Server.", + "administration_gvc_disabled": "Die Spiel-Sprachkanal-Funktion ist nicht eingeschaltet auf diesem Server.", "administration_gvc_enabled": "{0} ist nun ein Spiel-Sprachkanal", "administration_not_in_voice": "Du bist in keinem Sprachkanal auf diesem Server.", "gambling_item": "Gegenstand", @@ -787,22 +764,109 @@ "administration_timezones_available": "Verfügbare Zeitzonen", "music_song_not_found": "Kein Lied gefunden", "searches_define_unknown": "Konnte keine Definition für diesen Term finden.", - "utility_repeater_initial": "", - "utility_verbose_errors_enabled": "", - "utility_verbose_errors_disabled": "", - "permissions_perms_reset": "", - "permissions_trigger": "", - "administration_migration_error": "", + "utility_repeater_initial": "Ursprüngliche Nachricht wird nochmal gesendet in {0}Stunden und {1}Minuten.", + "utility_verbose_errors_enabled": "Falsch genutzte Befehle werden jetzt Fehlermeldungen zeigen.", + "utility_verbose_errors_disabled": "Falsch genutzte Befehle werden keine Fehlermeldungen mehr zeigen.", + "permissions_perms_reset": "Rechte für diesen Server wurden zurückgesetzt.", + "permissions_trigger": "Berechtigung Nummer #{0} {1} verhindert diese Aktion.", + "administration_migration_error": "Fehler während der Übernahme, überprüfe die Bot Console für mehr Informationen.", "searches_hex_invalid": "Ungültige Farbe angegeben", - "permissions_global_perms_reset": "", + "permissions_global_perms_reset": "Globale Rechte wurden zurückgesetzt.", "help_module": "Modul: {0}", - "games_hangman_stopped": "", - "music_autoplaying": "", - "music_queue_stopped": "", + "games_hangman_stopped": "Galgenmännchenspiel wurde gestoppt.", + "music_autoplaying": "Auto-Musik wird gespielt.", + "music_queue_stopped": "Wiedergabe gestoppt. Benutze {0} Befehl um die Wiedergabe fortzusetzen.", "music_removed_song_error": "Lied an diesem Index existiert nicht", - "music_shuffling_playlist": "", + "music_shuffling_playlist": "Mische Lieder", "music_songs_shuffle_enable": "Lieder werden jetzt zufällig wiedergegeben.", - "music_songs_shuffle_disable": "", + "music_songs_shuffle_disable": "Lieder werden nicht mehr gemischt.", "music_song_skips_after": "Lieder werden übersprungen nach {0}", - "administration_warnings_list": "Liste aller gewarnten Benutzer auf diesem Server" + "administration_warnings_list": "Liste aller gewarnten Benutzer auf diesem Server", + "customreactions_redacted_too_long": "Bearbeitet da es zu lang ist.", + "nsfw_blacklisted_tag_list": "Liste von Stichwörtern auf der Sperrliste:", + "nsfw_blacklisted_tag": "Ein oder mehrere Stichwörter die du benutzt hast sind auf der Sperrliste", + "nsfw_blacklisted_tag_add": "Nsfw Stichwort {0} ist jetzt auf der Sperrliste.", + "nsfw_blacklisted_tag_remove": "Nsfw Stichwort {0} ist nicht mehr auf der Sperrliste.", + "gambling_waifu_gift": "Schenkte {0} an {1}", + "gambling_waifu_gift_shop": "Waifu Geschenke Laden", + "gambling_gifts": "Geschenke", + "games_connect4_created": "Connect4 Spiel wurde erstellt. Warte auf Spieler.", + "games_connect4_player_to_move": "Nächster Spieler an der Reihe: {0}", + "games_connect4_failed_to_start": "Connect4 Spiel konnte nicht gestartet werden weil niemand beigetreten ist.", + "games_connect4_draw": "Connect4 Spiel endete mit Gleichstand.", + "games_connect4_won": "{0} hat das Connect4 Spiel gewonnen gegen {1}.", + "games_nunchi_joined": "Nunchi Spiel beigetreten. {0} Benutzer sind bis jetzt eingetreten.", + "games_nunchi_ended": "Nunchi Spiel beendet. {0} hat gewonnen", + "games_nunchi_ended_no_winner": "Nunchi Spiel hat ohne Gewinner geendet.", + "games_nunchi_started": "Nunchi Spiel startet mit {0} Teilnehmern.", + "games_nunchi_round_ended": "Nunchi Runde ist beendet. {0} ist aus dem Spiel.", + "games_nunchi_round_ended_boot": "Nunchi Runde endet aufgrund von Zeitüberschreitung einiger Nutzer. Diese Nutzer sind noch im Spiel: {0}", + "games_nunchi_round_started": "Nunchi Runde startete mit {0} Nutzern. Beginnt ab Nummer {1} zu zählen.", + "games_nunchi_next_number": "Nummer registriert. Die letzte Nummer war {0}.", + "games_nunchi_failed_to_start": "Nunchi Spiel konnte nicht starten, da es nicht genügend Teilnehmer gibt.", + "games_nunchi_created": "Nunchi Spiel erstellt. Warte auf das Beitreten von Nutzern.", + "music_sad_enabled": "Lieder werden von der Musik-Warteschlange gelöscht sobald die Wiedergabe beendet wurde.", + "music_sad_disabled": "Lieder werden nun nicht mehr von der Musik-Warteschlange gelöscht sobald die Wiedergabe beendet wurde.", + "utility_stream_role_enabled": "Wenn ein Benutzer mit der {0} Rolle anfängt zu streamen, werde ich ihm die {1} Rolle geben.", + "utility_stream_role_disabled": "Das Stream Rollen Funtion wurde deaktiviert.", + "utility_stream_role_kw_set": "Streamer benötigen nun {0} Stichwort um die Rolle zu erhalten", + "utility_stream_role_kw_reset": "Stream Rollen Stichwort wurde zurückgesetzt.", + "utility_stream_role_bl_add": "Benutzer {0} wird nie die Stream Rolle erhalten.", + "utility_stream_role_bl_add_fail": "Nutzer {0} ist bereits auf der Sperrliste.", + "utility_stream_role_bl_rem": "Nutzer {0} ist nicht mehr auf der Sperrliste.", + "utility_stream_role_bl_rem_fail": "Nutzer {0} ist nicht auf der Sperrliste.", + "utility_stream_role_wl_add": "Benutzer {0} wird nun die Stream Rolle erhalten, auch wenn das Stichwort nicht im Stream Titel ist.", + "utility_stream_role_wl_add_fail": "Nutzer {0} ist bereits auf der weißen Liste.", + "utility_stream_role_wl_rem": "Nutzer {0} ist nicht mehr auf der weißen Liste.", + "utility_stream_role_wl_rem_fail": "Nutzer {0} ist nicht auf der weißen Liste.", + "utility_bot_config_edit_fail": "Fehlschlag der Änderung {0} zu dem Wert {1}", + "utility_bot_config_edit_success": "Der Wert von {0} wurde auf {1} gestellt", + "customreactions_crca_disabled": "Benutzerdefinierte Reaktion mit der ID {0} wird nicht mehr auslösen, außer der Auslöser ist am Anfang des Satzes.", + "customreactions_crca_enabled": "Benutzerdefinierte Reaktion mit der ID {0} wird nun auslösen wenn der Auslöser ein Teil des Satzes ist.", + "xp_server_level": "Server Level", + "xp_level": "Level", + "xp_club": "Club", + "xp_xp": "Erfahrungspunkte", + "xp_excluded": "{0} wurde vom XP System in diesem Server ausgeschlossen.", + "xp_not_excluded": "{0} ist nun nicht mehr vom XP System in diesem Server ausgeschlossen.", + "xp_exclusion_list": "Ausschluß Liste", + "xp_server_is_excluded": "Dieser Server ist von xp ausgeschlossen", + "xp_server_is_not_excluded": "Dieser Server ist nicht von xp ausgeschlossen", + "xp_excluded_roles": "Ausgeschlossene Rollen", + "xp_excluded_channels": "Ausgeschlossene Kanäle.", + "xp_level_up_channel": "Glückwunsch {0}, du hast Level {1} erreicht!", + "xp_level_up_dm": "Glückwunsch {0}, du hast Level {1} auf {2} Servern erreicht!", + "xp_level_up_global": "Glückwunsch {0}, du hast das globale Level {1} erreicht!", + "xp_role_reward_cleared": "Level {0} wird keine Rollen-Belohnung mehr erhalten.", + "xp_role_reward_added": "benutzer die Level {0} werden die {1} Rolle erhalten.", + "xp_role_rewards": "Rollen Belohnungen", + "xp_level_x": "Level {0}", + "xp_no_role_rewards": "Keine Rollen Belohnungen auf dieser Seite.", + "xp_server_leaderboard": "Server XP Bestenliste", + "xp_global_leaderboard": "Global XP Bestenliste", + "xp_modified": "Die Server XP dieses Nutzers wurde von {0} auf {1} eingestellt", + "xp_club_create_error": "Gründung des Clubs fehlgeschlagen. Stelle sicher, dass du mindestens Level 5 und nicht bereits Mitglied eines Clubs bist.", + "xp_club_created": "Club {0} wurde erfolgreich erstellt!", + "xp_club_not_exists": "Diesen Club gibt es nicht.", + "xp_club_applied": "Du hast dich für eine Mitgliedschaft im {0} Club beworben.", + "xp_club_apply_error": "Fehler beim Bewerben. Du bist entweder bereits ein Mitglied eines Clubs, oder du erfüllst nicht die Level-Anforderung, oder du wurdest vom Club gebannt.", + "xp_club_accepted": "Nutzer {0} wurde in den Club aufgenommen.", + "xp_club_accept_error": "Nutzer nicht gefunden", + "xp_club_left": "Du hast den Club verlassen.", + "xp_club_not_in_club": "Du bist nicht in einem Club oder du versuchst den Club zu verlassen den du besitzt.", + "xp_club_user_kick": "Nutzer {0} von {1} Club gekickt.", + "xp_club_user_kick_fail": "Fehler bei Kicken. Du bist entweder nicht Club-Besitzer oder dieser Nutzer ist nicht in deinem Club.", + "xp_club_user_banned": "Nutzer {0} wurde von {1} Club gebannt.", + "xp_club_user_ban_fail": "Bann fehlgeschlagen. Entweder bist du nicht Club-Besitzer, oder der Nutzer ist nicht im Club oder hat sich beworben.", + "xp_club_user_unbanned": "Der Bann des Nutzers {0} im {1} Club wurde aufgehoben.", + "xp_club_user_unban_fail": "Entbannung fehlgeschlagen. Du bist entweder nicht der Besitzer des Clubs, oder der Nutzer ist nicht in deinem Club oder hat sich nicht beworben.", + "xp_club_level_req_changed": "Änderte Level Anforderung des Clubs auf {0}", + "xp_club_level_req_change_error": "Änderung der Level Voraussetzung fehlgeschlagen.", + "xp_club_disbanded": "Club {0} wurde aufgelöst", + "xp_club_disband_error": "Fehler. Du bist entweder nicht in einem Club, oder du bist nicht der Besitzer des Clubs.", + "xp_club_icon_error": "Keine gültige Bild url oder du bist nicht der Besitzer des Clubs.", + "xp_club_icon_set": "Neues Club Bild eingestellt.", + "xp_club_bans_for": "Banne des {0} Clubs", + "xp_club_apps_for": "Bewerber für {0} Club", + "xp_club_leaderboard": "Club Bestenliste - Seite {0}" } \ No newline at end of file From 21f2bba73d80db79dc963eca0106c2a85e28bd3d Mon Sep 17 00:00:00 2001 From: shivaco Date: Sat, 23 Sep 2017 21:56:38 +0600 Subject: [PATCH 341/346] Revert commandlist --- docs/Commands List.md | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/docs/Commands List.md b/docs/Commands List.md index 9a3624df..b5b615f6 100644 --- a/docs/Commands List.md +++ b/docs/Commands List.md @@ -447,10 +447,9 @@ Commands and aliases | Description | Usage Commands and aliases | Description | Usage ----------------|--------------|------- `.experience` `.xp` | Shows your xp stats. Specify the user to show that user's stats instead. | `.xp` -`.xprolerewards` `.xprrs` | Shows currently set role rewards. | `.xprrs` -`.xprolereward` `.xprr` | Sets a role reward on a specified level. Provide no role name in order to remove the role reward. **Requires ManageRoles server permission.** | `.xprr 4 Social` or `.xprr 9 Active` -`.xpnotify` `.xpn` | Sets how the bot should notify you when you get a `server` or `global` level. You can set `dm` (for the bot to send a direct message), `channel` (to get notified in the channel you sent the last message in) or `none` to disable. | `.xpn global dm` or `.xpn server channel` -`.xpexclude` `.xpex` | Exclude a user or a role from the xp system, or whole current server. **Requires Administrator server permission.** | `.xpex Role Excluded-Role` or `.xpex Server` or `.xpex channel spam` +`.xprolereward` `.xprr` | Sets a role reward on a specified level. Provide no role name in order to remove the role reward. **Requires ManageRoles server permission.** | `.xprr 3 Social` +`.xpnotify` `.xpn` | Sets how the bot should notify you when you get a `server` or `global` level. You can set `dm` (for the bot to send a direct message), `channel` (to get notified in the channel you sent the last message in) or `none` to disable. | `.xpn global dm` `.xpn server channel` +`.xpexclude` `.xpex` | Exclude a user or a role from the xp system, or whole current server. **Requires Administrator server permission.** | `.xpex Role Excluded-Role` `.xpex Server` `.xpexclusionlist` `.xpexl` | Shows the roles and channels excluded from the XP system on this server, as well as whether the whole server is excluded. | `.xpexl` `.xpleaderboard` `.xplb` | Shows current server's xp leaderboard. | `.xplb` `.xpgleaderboard` `.xpglb` | Shows the global xp leaderboard. | `.xpglb` From f0eed2ab4e8a6176e485905c889377345570ef64 Mon Sep 17 00:00:00 2001 From: shivaco Date: Sat, 23 Sep 2017 22:00:53 +0600 Subject: [PATCH 342/346] I've lost `.xprrs` --- docs/Commands List.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/Commands List.md b/docs/Commands List.md index b5b615f6..6d6a6d9a 100644 --- a/docs/Commands List.md +++ b/docs/Commands List.md @@ -447,6 +447,7 @@ Commands and aliases | Description | Usage Commands and aliases | Description | Usage ----------------|--------------|------- `.experience` `.xp` | Shows your xp stats. Specify the user to show that user's stats instead. | `.xp` +`.xprolerewards` `.xprrs` | Shows currently set role rewards. | `.xprrs` `.xprolereward` `.xprr` | Sets a role reward on a specified level. Provide no role name in order to remove the role reward. **Requires ManageRoles server permission.** | `.xprr 3 Social` `.xpnotify` `.xpn` | Sets how the bot should notify you when you get a `server` or `global` level. You can set `dm` (for the bot to send a direct message), `channel` (to get notified in the channel you sent the last message in) or `none` to disable. | `.xpn global dm` `.xpn server channel` `.xpexclude` `.xpex` | Exclude a user or a role from the xp system, or whole current server. **Requires Administrator server permission.** | `.xpex Role Excluded-Role` `.xpex Server` From 763d529c7424855b667419b7c20523626eb0d4b4 Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Sat, 23 Sep 2017 18:51:51 +0200 Subject: [PATCH 343/346] Fixed extremly slow .clubinfo, however they're case sensitive now. Just target a user for best experience. --- src/NadekoBot/Modules/Xp/Club.cs | 4 ++-- src/NadekoBot/Modules/Xp/Services/ClubService.cs | 2 +- .../Services/Database/Repositories/Impl/ClubRepository.cs | 8 +++++--- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/src/NadekoBot/Modules/Xp/Club.cs b/src/NadekoBot/Modules/Xp/Club.cs index e62b458a..d6b5df5c 100644 --- a/src/NadekoBot/Modules/Xp/Club.cs +++ b/src/NadekoBot/Modules/Xp/Club.cs @@ -117,12 +117,12 @@ namespace NadekoBot.Modules.Xp .AddField("Owner", club.Owner.ToString(), true) .AddField("Level Req.", club.MinimumLevelReq.ToString(), true) .AddField("Members", string.Join("\n", club.Users + .OrderByDescending(x => x.IsClubAdmin || club.OwnerId == x.Id) .Skip(page * 10) .Take(10) - .OrderByDescending(x => x.IsClubAdmin) .Select(x => { - if (x.IsClubAdmin) + if (x.IsClubAdmin || club.OwnerId == x.Id) return x.ToString() + "⭐"; return x.ToString(); })), false); diff --git a/src/NadekoBot/Modules/Xp/Services/ClubService.cs b/src/NadekoBot/Modules/Xp/Services/ClubService.cs index 362067eb..4c61d7ab 100644 --- a/src/NadekoBot/Modules/Xp/Services/ClubService.cs +++ b/src/NadekoBot/Modules/Xp/Services/ClubService.cs @@ -113,7 +113,7 @@ namespace NadekoBot.Modules.Xp.Services using (var uow = _db.UnitOfWork) { - club = uow.Clubs.GetByName(name.Trim().ToLowerInvariant(), discrim); + club = uow.Clubs.GetByName(name, discrim); if (club == null) return false; else diff --git a/src/NadekoBot/Services/Database/Repositories/Impl/ClubRepository.cs b/src/NadekoBot/Services/Database/Repositories/Impl/ClubRepository.cs index 7eabbb1c..0977e3bf 100644 --- a/src/NadekoBot/Services/Database/Repositories/Impl/ClubRepository.cs +++ b/src/NadekoBot/Services/Database/Repositories/Impl/ClubRepository.cs @@ -52,10 +52,11 @@ namespace NadekoBot.Services.Database.Repositories.Impl { if (func == null) return _set + .Where(x => x.Name == name && x.Discrim == discrim) + .Include(x => x.Users) .Include(x => x.Bans) .Include(x => x.Applicants) - .Include(x => x.Users) - .FirstOrDefault(x => x.Name.ToLowerInvariant() == name && x.Discrim == discrim); + .FirstOrDefault(); return func(_set).FirstOrDefault(x => x.Name == name && x.Discrim == discrim); } @@ -72,7 +73,8 @@ namespace NadekoBot.Services.Database.Repositories.Impl public ClubInfo GetByMember(ulong userId, Func, IQueryable> func = null) { if (func == null) - return _set.Include(x => x.Users) + return _set + .Include(x => x.Users) .Include(x => x.Bans) .Include(x => x.Applicants) .FirstOrDefault(x => x.Users.Any(y => y.UserId == userId)); From 8c8fed98d81ee81d222e27885bbbf854e55ba008 Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Sat, 23 Sep 2017 18:56:04 +0200 Subject: [PATCH 344/346] A bit nicer clubinfo. --- src/NadekoBot/Modules/Xp/Club.cs | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/NadekoBot/Modules/Xp/Club.cs b/src/NadekoBot/Modules/Xp/Club.cs index d6b5df5c..20a6e97a 100644 --- a/src/NadekoBot/Modules/Xp/Club.cs +++ b/src/NadekoBot/Modules/Xp/Club.cs @@ -117,12 +117,21 @@ namespace NadekoBot.Modules.Xp .AddField("Owner", club.Owner.ToString(), true) .AddField("Level Req.", club.MinimumLevelReq.ToString(), true) .AddField("Members", string.Join("\n", club.Users - .OrderByDescending(x => x.IsClubAdmin || club.OwnerId == x.Id) + .OrderByDescending(x => { + if (club.OwnerId == x.Id) + return 2; + else if (x.IsClubAdmin) + return 1; + else + return 0; + }) .Skip(page * 10) .Take(10) .Select(x => { - if (x.IsClubAdmin || club.OwnerId == x.Id) + if (club.OwnerId == x.Id) + return x.ToString() + "🌟"; + else if (x.IsClubAdmin) return x.ToString() + "⭐"; return x.ToString(); })), false); From 8b6bf2f2690e6da0857ac32fd007456145c20264 Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Sun, 24 Sep 2017 06:55:17 +0200 Subject: [PATCH 345/346] Fixed owners not being able to ban/kick users from clubs. --- .../Services/Database/Repositories/Impl/ClubRepository.cs | 1 + .../Database/Repositories/Impl/DiscordUserRepository.cs | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/NadekoBot/Services/Database/Repositories/Impl/ClubRepository.cs b/src/NadekoBot/Services/Database/Repositories/Impl/ClubRepository.cs index 0977e3bf..8caa1838 100644 --- a/src/NadekoBot/Services/Database/Repositories/Impl/ClubRepository.cs +++ b/src/NadekoBot/Services/Database/Repositories/Impl/ClubRepository.cs @@ -32,6 +32,7 @@ namespace NadekoBot.Services.Database.Repositories.Impl .Include(x => x.Applicants) .ThenInclude(x => x.User) .Include(x => x.Owner) + .Include(x => x.Users) .FirstOrDefault(x => x.Owner.UserId == userId) ?? _context.Set() .Include(x => x.Club) diff --git a/src/NadekoBot/Services/Database/Repositories/Impl/DiscordUserRepository.cs b/src/NadekoBot/Services/Database/Repositories/Impl/DiscordUserRepository.cs index 5238198d..933d583b 100644 --- a/src/NadekoBot/Services/Database/Repositories/Impl/DiscordUserRepository.cs +++ b/src/NadekoBot/Services/Database/Repositories/Impl/DiscordUserRepository.cs @@ -42,12 +42,12 @@ namespace NadekoBot.Services.Database.Repositories.Impl { if (!_set.Where(y => y.UserId == id).Any()) { - return _set.Count(); + return _set.Count() + 1; } return _set.Count(x => x.TotalXp >= _set.Where(y => y.UserId == id) .DefaultIfEmpty() - .Sum(y => y.TotalXp)) + 1; + .Sum(y => y.TotalXp)); } public DiscordUser[] GetUsersXpLeaderboardFor(int page) From 37106e5613757d72cb963a4ebae7802d53f781f6 Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Sun, 24 Sep 2017 06:56:05 +0200 Subject: [PATCH 346/346] Version upped to 1.9.1 --- src/NadekoBot/Services/Impl/StatsService.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/NadekoBot/Services/Impl/StatsService.cs b/src/NadekoBot/Services/Impl/StatsService.cs index 24bc273e..e484bfb4 100644 --- a/src/NadekoBot/Services/Impl/StatsService.cs +++ b/src/NadekoBot/Services/Impl/StatsService.cs @@ -20,7 +20,7 @@ namespace NadekoBot.Services.Impl private readonly IBotCredentials _creds; private readonly DateTime _started; - public const string BotVersion = "1.9.0"; + public const string BotVersion = "1.9.1"; public string Author => "Kwoth#2560"; public string Library => "Discord.Net";

$`D@28hX$4 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