diff --git a/src/NadekoBot/Attributes/NadekoModuleAttribute.cs b/src/NadekoBot/Attributes/NadekoModuleAttribute.cs index fe09d3e5..dc0d21b7 100644 --- a/src/NadekoBot/Attributes/NadekoModuleAttribute.cs +++ b/src/NadekoBot/Attributes/NadekoModuleAttribute.cs @@ -6,7 +6,7 @@ namespace NadekoBot.Attributes [AttributeUsage(AttributeTargets.Class)] sealed class NadekoModuleAttribute : GroupAttribute { - public NadekoModuleAttribute(string moduleName) : base("") + public NadekoModuleAttribute(string moduleName) : base(moduleName) { } } diff --git a/src/NadekoBot/DataStructures/TypeReaders/BotCommandTypeReader.cs b/src/NadekoBot/DataStructures/TypeReaders/BotCommandTypeReader.cs index 1e2ad01d..629c7df3 100644 --- a/src/NadekoBot/DataStructures/TypeReaders/BotCommandTypeReader.cs +++ b/src/NadekoBot/DataStructures/TypeReaders/BotCommandTypeReader.cs @@ -15,6 +15,11 @@ namespace NadekoBot.TypeReaders public override Task Read(ICommandContext context, string input) { input = input.ToUpperInvariant(); + if (!input.StartsWith(NadekoBot.Prefix.ToUpperInvariant())) + return Task.FromResult(TypeReaderResult.FromError(CommandError.ParseFailed, "No such command found.")); + + input = input.Substring(NadekoBot.Prefix.Length); + var cmd = _cmds.Commands.FirstOrDefault(c => c.Aliases.Select(a => a.ToUpperInvariant()).Contains(input)); if (cmd == null) diff --git a/src/NadekoBot/Modules/ClashOfClans/ClashOfClans.cs b/src/NadekoBot/Modules/ClashOfClans/ClashOfClans.cs index 53380a86..b650304d 100644 --- a/src/NadekoBot/Modules/ClashOfClans/ClashOfClans.cs +++ b/src/NadekoBot/Modules/ClashOfClans/ClashOfClans.cs @@ -1,81 +1,23 @@ using Discord.Commands; using System; -using System.Collections.Concurrent; using System.Collections.Generic; using System.Text; using System.Threading.Tasks; using Discord; -using NadekoBot.Services; using NadekoBot.Attributes; using NadekoBot.Services.Database.Models; -using System.Linq; using NadekoBot.Extensions; -using System.Threading; +using NadekoBot.Services.ClashOfClans; namespace NadekoBot.Modules.ClashOfClans { - [NadekoModule("ClashOfClans", ",")] public class ClashOfClans : NadekoTopLevelModule { - public static ConcurrentDictionary> ClashWars { get; set; } + private readonly ClashOfClansService _service; - private static Timer checkWarTimer { get; } - - static ClashOfClans() + public ClashOfClans(ClashOfClansService service) { - using (var uow = DbHandler.UnitOfWork()) - { - ClashWars = new ConcurrentDictionary>( - uow.ClashOfClans - .GetAllWars() - .Select(cw => - { - cw.Channel = NadekoBot.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 static 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(GetTextStatic("claim_expired", - NadekoBot.Localization.GetCultureInfo(war.Channel.GuildId), - typeof(ClashOfClans).Name.ToLowerInvariant(), - Format.Bold(Bases[i].CallUser), - war.ShortPrint())); - } - catch { } - } - } + _service = service; } [NadekoCommand, Usage, Description, Aliases] @@ -92,18 +34,18 @@ namespace NadekoBot.Modules.ClashOfClans return; } List wars; - if (!ClashWars.TryGetValue(Context.Guild.Id, out wars)) + if (!_service.ClashWars.TryGetValue(Context.Guild.Id, out wars)) { wars = new List(); - if (!ClashWars.TryAdd(Context.Guild.Id, wars)) + if (!_service.ClashWars.TryAdd(Context.Guild.Id, wars)) return; } - var cw = await CreateWar(enemyClan, size, Context.Guild.Id, Context.Channel.Id); + var cw = await _service.CreateWar(enemyClan, size, Context.Guild.Id, Context.Channel.Id); wars.Add(cw); - await ReplyErrorLocalized("war_created", cw.ShortPrint()).ConfigureAwait(false); + await ReplyErrorLocalized("war_created", _service.ShortPrint(cw)).ConfigureAwait(false); } [NadekoCommand, Usage, Description, Aliases] @@ -113,7 +55,7 @@ namespace NadekoBot.Modules.ClashOfClans int num = 0; int.TryParse(number, out num); - var warsInfo = GetWarInfo(Context.Guild, num); + var warsInfo = _service.GetWarInfo(Context.Guild, num); if (warsInfo == null) { await ReplyErrorLocalized("war_not_exist").ConfigureAwait(false); @@ -123,13 +65,13 @@ namespace NadekoBot.Modules.ClashOfClans try { war.Start(); - await ReplyConfirmLocalized("war_started", war.ShortPrint()).ConfigureAwait(false); + await ReplyConfirmLocalized("war_started", _service.ShortPrint(war)).ConfigureAwait(false); } catch { - await ReplyErrorLocalized("war_already_started", war.ShortPrint()).ConfigureAwait(false); + await ReplyErrorLocalized("war_already_started", _service.ShortPrint(war)).ConfigureAwait(false); } - SaveWar(war); + _service.SaveWar(war); } [NadekoCommand, Usage, Description, Aliases] @@ -142,7 +84,7 @@ namespace NadekoBot.Modules.ClashOfClans { //check if there are any wars List wars = null; - ClashWars.TryGetValue(Context.Guild.Id, out wars); + _service.ClashWars.TryGetValue(Context.Guild.Id, out wars); if (wars == null || wars.Count == 0) { await ReplyErrorLocalized("no_active_wars").ConfigureAwait(false); @@ -163,21 +105,21 @@ namespace NadekoBot.Modules.ClashOfClans var num = 0; int.TryParse(number, out num); //if number is not null, print the war needed - var warsInfo = GetWarInfo(Context.Guild, 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]; - await Context.Channel.SendConfirmAsync(war.Localize("info_about_war", $"`{war.EnemyClan}` ({war.Size} v {war.Size})"), war.ToPrettyString()).ConfigureAwait(false); + 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 Claim(int number, int baseNumber, [Remainder] string other_name = null) { - var warsInfo = GetWarInfo(Context.Guild, number); + var warsInfo = _service.GetWarInfo(Context.Guild, number); if (warsInfo == null || warsInfo.Item1.Count == 0) { await ReplyErrorLocalized("war_not_exist").ConfigureAwait(false); @@ -190,9 +132,9 @@ namespace NadekoBot.Modules.ClashOfClans try { var war = warsInfo.Item1[warsInfo.Item2]; - war.Call(usr, baseNumber - 1); - SaveWar(war); - await ConfirmLocalized("claimed_base", Format.Bold(usr.ToString()), baseNumber, war.ShortPrint()).ConfigureAwait(false); + _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) { @@ -225,7 +167,7 @@ namespace NadekoBot.Modules.ClashOfClans [RequireContext(ContextType.Guild)] public async Task EndWar(int number) { - var warsInfo = GetWarInfo(Context.Guild, number); + var warsInfo = _service.GetWarInfo(Context.Guild, number); if (warsInfo == null) { await ReplyErrorLocalized("war_not_exist").ConfigureAwait(false); @@ -233,8 +175,8 @@ namespace NadekoBot.Modules.ClashOfClans } var war = warsInfo.Item1[warsInfo.Item2]; war.End(); - SaveWar(war); - await ReplyConfirmLocalized("war_ended", warsInfo.Item1[warsInfo.Item2].ShortPrint()).ConfigureAwait(false); + _service.SaveWar(war); + await ReplyConfirmLocalized("war_ended", _service.ShortPrint(warsInfo.Item1[warsInfo.Item2])).ConfigureAwait(false); warsInfo.Item1.RemoveAt(warsInfo.Item2); } @@ -243,7 +185,7 @@ namespace NadekoBot.Modules.ClashOfClans [RequireContext(ContextType.Guild)] public async Task Unclaim(int number, [Remainder] string otherName = null) { - var warsInfo = GetWarInfo(Context.Guild, number); + var warsInfo = _service.GetWarInfo(Context.Guild, number); if (warsInfo == null || warsInfo.Item1.Count == 0) { await ReplyErrorLocalized("war_not_exist").ConfigureAwait(false); @@ -256,9 +198,9 @@ namespace NadekoBot.Modules.ClashOfClans try { var war = warsInfo.Item1[warsInfo.Item2]; - var baseNumber = war.Uncall(usr); - SaveWar(war); - await ReplyConfirmLocalized("base_unclaimed", usr, baseNumber + 1, war.ShortPrint()).ConfigureAwait(false); + var baseNumber = _service.Uncall(war, usr); + _service.SaveWar(war); + await ReplyConfirmLocalized("base_unclaimed", usr, baseNumber + 1, _service.ShortPrint(war)).ConfigureAwait(false); } catch (Exception ex) { @@ -268,7 +210,7 @@ namespace NadekoBot.Modules.ClashOfClans private async Task FinishClaim(int number, int baseNumber, int stars = 3) { - var warInfo = GetWarInfo(Context.Guild, number); + var warInfo = _service.GetWarInfo(Context.Guild, number); if (warInfo == null || warInfo.Item1.Count == 0) { await ReplyErrorLocalized("war_not_exist").ConfigureAwait(false); @@ -279,87 +221,19 @@ namespace NadekoBot.Modules.ClashOfClans { if (baseNumber == -1) { - baseNumber = war.FinishClaim(Context.User.Username, stars); - SaveWar(war); + baseNumber = _service.FinishClaim(war, Context.User.Username, stars); + _service.SaveWar(war); } else { - war.FinishClaim(baseNumber, stars); + _service.FinishClaim(war, baseNumber, stars); } - await ReplyConfirmLocalized("base_destroyed", baseNumber +1, war.ShortPrint()).ConfigureAwait(false); + await ReplyConfirmLocalized("base_destroyed", baseNumber + 1, _service.ShortPrint(war)).ConfigureAwait(false); } catch (Exception ex) { await Context.Channel.SendErrorAsync($"🔰 {ex.Message}").ConfigureAwait(false); } } - - private static 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 static async Task CreateWar(string enemyClan, int size, ulong serverId, ulong channelId) - { - var channel = NadekoBot.Client.GetGuild(serverId)?.GetTextChannel(channelId); - using (var uow = DbHandler.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, - }); - } - Console.WriteLine(cw.Bases.Capacity); - uow.ClashOfClans.Add(cw); - await uow.CompleteAsync(); - return cw; - } - } - - public static void SaveWar(ClashWar cw) - { - if (cw.WarState == ClashWar.StateOfWar.Ended) - { - using (var uow = DbHandler.UnitOfWork()) - { - uow.ClashOfClans.Remove(cw); - uow.CompleteAsync(); - } - return; - } - - - using (var uow = DbHandler.UnitOfWork()) - { - uow.ClashOfClans.Update(cw); - uow.CompleteAsync(); - } - } } } \ No newline at end of file diff --git a/src/NadekoBot/Modules/ClashOfClans/Extensions.cs b/src/NadekoBot/Modules/ClashOfClans/Extensions.cs deleted file mode 100644 index 5756d3db..00000000 --- a/src/NadekoBot/Modules/ClashOfClans/Extensions.cs +++ /dev/null @@ -1,148 +0,0 @@ -using NadekoBot.Services.Database.Models; -using System; -using System.Linq; -using System.Text; -using static NadekoBot.Services.Database.Models.ClashWar; - -namespace NadekoBot.Modules.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 Call(this ClashWar cw, string u, int baseNumber) - { - if (baseNumber < 0 || baseNumber >= cw.Bases.Count) - throw new ArgumentException(cw.Localize("invalid_base_number")); - if (cw.Bases[baseNumber].CallUser != null && cw.Bases[baseNumber].Stars == 3) - throw new ArgumentException(cw.Localize("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(cw.Localize("claimed_other", u, i + 1)); - } - - var cc = cw.Bases[baseNumber]; - cc.CallUser = u.Trim(); - cc.TimeAdded = DateTime.UtcNow; - cc.BaseDestroyed = false; - } - - 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(); - } - } - - public static int Uncall(this 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(cw.Localize("not_partic")); - } - - public static string ShortPrint(this ClashWar cw) => - $"`{cw.EnemyClan}` ({cw.Size} v {cw.Size})"; - - public static string ToPrettyString(this 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}.` ❌*{cw.Localize("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 static int FinishClaim(this 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(cw.Localize("not_partic_or_destroyed", user)); - } - - public static void FinishClaim(this 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(cw.Localize("base_already_destroyed")); - if (toFinish.CallUser == null) throw new InvalidOperationException(cw.Localize("base_already_unclaimed")); - toFinish.BaseDestroyed = true; - toFinish.Stars = stars; - } - - public static string Localize(this ClashWar cw, string key) - { - return NadekoTopLevelModule.GetTextStatic(key, - NadekoBot.Localization.GetCultureInfo(cw.Channel?.GuildId), - typeof(ClashOfClans).Name.ToLowerInvariant()); - } - - public static string Localize(this ClashWar cw, string key, params object[] replacements) - { - return string.Format(cw.Localize(key), replacements); - } - } -} \ No newline at end of file diff --git a/src/NadekoBot/Modules/CustomReactions/CustomReactions.cs b/src/NadekoBot/Modules/CustomReactions/CustomReactions.cs index b250c510..e9b8827f 100644 --- a/src/NadekoBot/Modules/CustomReactions/CustomReactions.cs +++ b/src/NadekoBot/Modules/CustomReactions/CustomReactions.cs @@ -17,97 +17,12 @@ namespace NadekoBot.Modules.CustomReactions { public static class CustomReactionExtensions { - public static async Task Send(this CustomReaction cr, IUserMessage context) - { - var channel = cr.DmResponse ? await context.Author.CreateDMChannelAsync() : context.Channel; - - CustomReactions.ReactionStats.AddOrUpdate(cr.Trigger, 1, (k, old) => ++old); - - CREmbed crembed; - if (CREmbed.TryParse(cr.Response, out crembed)) - { - return await channel.EmbedAsync(crembed.ToEmbed(), crembed.PlainText ?? ""); - } - return await channel.SendMessageAsync(cr.ResponseWithContext(context).SanitizeMentions()); - } + } [NadekoModule("CustomReactions", ".")] public class CustomReactions : NadekoTopLevelModule { - private static CustomReaction[] _globalReactions = new CustomReaction[] { }; - public static CustomReaction[] GlobalReactions => _globalReactions; - public static ConcurrentDictionary GuildReactions { get; } = new ConcurrentDictionary(); - - public static ConcurrentDictionary ReactionStats { get; } = new ConcurrentDictionary(); - - private new static readonly Logger _log; - - static CustomReactions() - { - _log = LogManager.GetCurrentClassLogger(); - var sw = Stopwatch.StartNew(); - using (var uow = DbHandler.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(); - - public static CustomReaction TryGetCustomReaction(IUserMessage umsg) - { - var channel = umsg.Channel as SocketTextChannel; - if (channel == null) - return null; - - var content = umsg.Content.Trim().ToLowerInvariant(); - CustomReaction[] reactions; - - GuildReactions.TryGetValue(channel.Guild.Id, out reactions); - if (reactions != null && reactions.Any()) - { - var rs = reactions.Where(cr => - { - if (cr == null) - return false; - - var hasTarget = cr.Response.ToLowerInvariant().Contains("%target%"); - var trigger = cr.TriggerWithContext(umsg).Trim().ToLowerInvariant(); - return ((hasTarget && content.StartsWith(trigger + " ")) || content == trigger); - }).ToArray(); - - if (rs.Length != 0) - { - var reaction = rs[new NadekoRandom().Next(0, rs.Length)]; - if (reaction != null) - { - if (reaction.Response == "-") - return null; - return reaction; - } - } - } - - var grs = GlobalReactions.Where(cr => - { - if (cr == null) - return false; - var hasTarget = cr.Response.ToLowerInvariant().Contains("%target%"); - var trigger = cr.TriggerWithContext(umsg).Trim().ToLowerInvariant(); - return ((hasTarget && content.StartsWith(trigger + " ")) || content == trigger); - }).ToArray(); - if (grs.Length == 0) - return null; - var greaction = grs[new NadekoRandom().Next(0, grs.Length)]; - - return greaction; - } - [NadekoCommand, Usage, Description, Aliases] public async Task AddCustReact(string key, [Remainder] string message) { @@ -131,7 +46,7 @@ namespace NadekoBot.Modules.CustomReactions Response = message, }; - using (var uow = DbHandler.UnitOfWork()) + using (var uow = _db.UnitOfWork) { uow.CustomReactions.Add(cr); @@ -309,7 +224,7 @@ namespace NadekoBot.Modules.CustomReactions var success = false; CustomReaction toDelete; - using (var uow = DbHandler.UnitOfWork()) + using (var uow = _db.UnitOfWork) { toDelete = uow.CustomReactions.Get(id); if (toDelete == null) //not found @@ -381,7 +296,7 @@ namespace NadekoBot.Modules.CustomReactions var setValue = reaction.DmResponse = !reaction.DmResponse; - using (var uow = DbHandler.UnitOfWork()) + using (var uow = _db.UnitOfWork) { uow.CustomReactions.Get(id).DmResponse = setValue; uow.Complete(); @@ -432,7 +347,7 @@ namespace NadekoBot.Modules.CustomReactions var setValue = reaction.AutoDeleteTrigger = !reaction.AutoDeleteTrigger; - using (var uow = DbHandler.UnitOfWork()) + using (var uow = _db.UnitOfWork) { uow.CustomReactions.Get(id).AutoDeleteTrigger = setValue; uow.Complete(); diff --git a/src/NadekoBot/Modules/Help/Help.cs b/src/NadekoBot/Modules/Help/Help.cs index c0948041..59764b93 100644 --- a/src/NadekoBot/Modules/Help/Help.cs +++ b/src/NadekoBot/Modules/Help/Help.cs @@ -9,19 +9,27 @@ using System; using System.IO; using System.Text; using System.Collections.Generic; +using NadekoBot.Services.Database.Models; namespace NadekoBot.Modules.Help { - [NadekoModule("Help", "-")] public class Help : NadekoTopLevelModule { - private static string helpString { get; } = NadekoBot.BotConfig.HelpString; - public static string HelpString => String.Format(helpString, NadekoBot.Credentials.ClientId, NadekoBot.ModulePrefixes[typeof(Help).Name]); - - public static string DMHelpString { get; } = NadekoBot.BotConfig.DMHelpString; - 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 CommandService _cmds; + + public string HelpString => String.Format(_config.HelpString, _creds.ClientId, NadekoBot.Prefix); + public string DMHelpString => _config.DMHelpString; + + public Help(IBotCredentials creds, BotConfig config, CommandService cmds) + { + _creds = creds; + _config = config; + _cmds = cmds; + } [NadekoCommand, Usage, Description, Aliases] public async Task Modules() @@ -30,8 +38,9 @@ namespace NadekoBot.Modules.Help .WithFooter(efb => efb.WithText("ℹ️" + GetText("modules_footer", Prefix))) .WithTitle(GetText("list_of_modules")) .WithDescription(string.Join("\n", - NadekoBot.CommandService.Modules.GroupBy(m => m.GetTopLevelModule()) - .Where(m => !Permissions.Permissions.GlobalPermissionCommands.BlockedModules.Contains(m.Key.Name.ToLowerInvariant())) + _cmds.Modules.GroupBy(m => m.GetTopLevelModule()) + //todo perms + //.Where(m => !Permissions.Permissions.GlobalPermissionCommands.BlockedModules.Contains(m.Key.Name.ToLowerInvariant())) .Select(m => "• " + m.Key.Name) .OrderBy(s => s))); await Context.Channel.EmbedAsync(embed).ConfigureAwait(false); @@ -45,8 +54,9 @@ namespace NadekoBot.Modules.Help module = module?.Trim().ToUpperInvariant(); if (string.IsNullOrWhiteSpace(module)) return; - var cmds = NadekoBot.CommandService.Commands.Where(c => c.Module.GetTopLevelModule().Name.ToUpperInvariant().StartsWith(module)) - .Where(c => !Permissions.Permissions.GlobalPermissionCommands.BlockedCommands.Contains(c.Aliases.First().ToLowerInvariant())) + var cmds = _cmds.Commands.Where(c => c.Module.GetTopLevelModule().Name.ToUpperInvariant().StartsWith(module)) + //todo perms + //.Where(c => !Permissions.Permissions.GlobalPermissionCommands.BlockedCommands.Contains(c.Aliases.First().ToLowerInvariant())) .OrderBy(c => c.Aliases.First()) .Distinct(new CommandTextEqualityComparer()) .AsEnumerable(); @@ -62,36 +72,33 @@ namespace NadekoBot.Modules.Help for (int i = 0; i < groups.Count(); i++) { - await channel.SendTableAsync(i == 0 ? $"📃 **{GetText("list_of_commands")}**\n" : "", groups.ElementAt(i), el => $"{el.Aliases.First(),-15} {"[" + el.Aliases.Skip(1).FirstOrDefault() + "]",-8}").ConfigureAwait(false); + await channel.SendTableAsync(i == 0 ? $"📃 **{GetText("list_of_commands")}**\n" : "", groups.ElementAt(i), el => $"{Prefix + el.Aliases.First(),-15} {"[" + el.Aliases.Skip(1).FirstOrDefault() + "]",-8}").ConfigureAwait(false); } - await ConfirmLocalized("commands_instr", Prefix).ConfigureAwait(false); } [NadekoCommand, Usage, Description, Aliases] - public async Task H([Remainder] string comToFind = null) + public async Task H([Remainder] CommandInfo com = null) { var channel = Context.Channel; - comToFind = comToFind?.ToLowerInvariant(); - if (string.IsNullOrWhiteSpace(comToFind)) + if (com == null) { IMessageChannel ch = channel is ITextChannel ? await ((IGuildUser)Context.User).CreateDMChannelAsync() : channel; await ch.SendMessageAsync(HelpString).ConfigureAwait(false); return; } - var com = NadekoBot.CommandService.Commands.FirstOrDefault(c => c.Aliases.Select(a=>a.ToLowerInvariant()).Contains(comToFind)); if (com == null) { await ReplyErrorLocalized("command_not_found").ConfigureAwait(false); return; } - var str = string.Format("**`{0}`**", com.Aliases.First()); + var str = string.Format("**`{0}`**", Prefix + com.Aliases.First()); var alias = com.Aliases.Skip(1).FirstOrDefault(); if (alias != null) - str += string.Format(" **/ `{0}`**", alias); + str += string.Format(" **/ `{0}`**", Prefix + alias); var embed = new EmbedBuilder() .AddField(fb => fb.WithName(str).WithValue($"{com.RealSummary()} {GetCommandRequirements(com)}").WithIsInline(true)) .AddField(fb => fb.WithName(GetText("usage")).WithValue(com.RealRemarks()).WithIsInline(false)) @@ -122,7 +129,7 @@ namespace NadekoBot.Modules.Help var helpstr = new StringBuilder(); helpstr.AppendLine(GetText("cmdlist_donate", PatreonUrl, PaypalUrl) + "\n"); helpstr.AppendLine("##"+ GetText("table_of_contents")); - helpstr.AppendLine(string.Join("\n", NadekoBot.CommandService.Modules.Where(m => m.GetTopLevelModule().Name.ToLowerInvariant() != "help") + helpstr.AppendLine(string.Join("\n", _cmds.Modules.Where(m => m.GetTopLevelModule().Name.ToLowerInvariant() != "help") .Select(m => m.GetTopLevelModule().Name) .Distinct() .OrderBy(m => m) @@ -130,7 +137,7 @@ namespace NadekoBot.Modules.Help .Select(m => string.Format("- [{0}](#{1})", m, m.ToLowerInvariant())))); helpstr.AppendLine(); string lastModule = null; - foreach (var com in NadekoBot.CommandService.Commands.OrderBy(com => com.Module.GetTopLevelModule().Name).GroupBy(c => c.Aliases.First()).Select(g => g.First())) + foreach (var com in _cmds.Commands.OrderBy(com => com.Module.GetTopLevelModule().Name).GroupBy(c => c.Aliases.First()).Select(g => g.First())) { var module = com.Module.GetTopLevelModule(); if (module.Name != lastModule) @@ -147,10 +154,9 @@ namespace NadekoBot.Modules.Help lastModule = module.Name; } helpstr.AppendLine($"{string.Join(" ", com.Aliases.Select(a => "`" + a + "`"))} |" + - $" {string.Format(com.Summary, com.Module.GetPrefix())} {GetCommandRequirements(com)} |" + - $" {string.Format(com.Remarks, com.Module.GetPrefix())}"); + $" {string.Format(com.Summary, NadekoBot.Prefix)} {GetCommandRequirements(com)} |" + + $" {string.Format(com.Remarks, NadekoBot.Prefix)}"); } - helpstr = helpstr.Replace(NadekoBot.Client.CurrentUser.Username , "@BotName"); File.WriteAllText("../../docs/Commands List.md", helpstr.ToString()); await ReplyConfirmLocalized("commandlist_regen").ConfigureAwait(false); } diff --git a/src/NadekoBot/Modules/Music/Music.cs b/src/NadekoBot/Modules/Music/Music.cs index 9bca0a41..4c798d39 100644 --- a/src/NadekoBot/Modules/Music/Music.cs +++ b/src/NadekoBot/Modules/Music/Music.cs @@ -1,6 +1,4 @@ using Discord.Commands; -using NadekoBot.Modules.Music.Classes; -using System.Collections.Concurrent; using Discord.WebSocket; using NadekoBot.Services; using System.IO; @@ -19,26 +17,28 @@ using NadekoBot.Services.Music; namespace NadekoBot.Modules.Music { - [NadekoModule("Music", "!!")] - [DontAutoLoad] public class Music : NadekoTopLevelModule { - private static MusicService music; + private static MusicService _music; + private readonly DiscordShardedClient _client; + private readonly IBotCredentials _creds; + private readonly IGoogleApiService _google; + private readonly DbHandler _db; - static Music() + public Music(DiscordShardedClient client, IBotCredentials creds, IGoogleApiService google, + DbHandler db, MusicService music) { + _client = client; + _creds = creds; + _google = google; + _db = db; + _music = music; //it can fail if its currenctly opened or doesn't exist. Either way i don't care - try { Directory.Delete(MusicDataPath, true); } catch { } - NadekoBot.Client.UserVoiceStateUpdated += Client_UserVoiceStateUpdated; - - Directory.CreateDirectory(MusicDataPath); - - //todo move to service - music = NadekoBot.MusicService; + _client.UserVoiceStateUpdated += Client_UserVoiceStateUpdated; } - private static 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 || @@ -46,14 +46,14 @@ namespace NadekoBot.Modules.Music return Task.CompletedTask; MusicPlayer player; - if ((player = music.GetPlayer(usr.Guild.Id)) == null) + if ((player = _music.GetPlayer(usr.Guild.Id)) == null) return Task.CompletedTask; try { //if bot moved if ((player.PlaybackVoiceChannel == oldState.VoiceChannel) && - usr.Id == NadekoBot.Client.CurrentUser.Id) + usr.Id == _client.CurrentUser.Id) { if (player.Paused && newState.VoiceChannel.Users.Count > 1) //unpause if there are people in the new channel player.TogglePause(); @@ -92,7 +92,7 @@ namespace NadekoBot.Modules.Music return Task.CompletedTask; MusicPlayer musicPlayer; - if ((musicPlayer = music.GetPlayer(Context.Guild.Id)) == null) + if ((musicPlayer = _music.GetPlayer(Context.Guild.Id)) == null) return Task.CompletedTask; if (musicPlayer.PlaybackVoiceChannel == ((IGuildUser)Context.User).VoiceChannel) { @@ -110,7 +110,7 @@ namespace NadekoBot.Modules.Music public Task Stop() { MusicPlayer musicPlayer; - if ((musicPlayer = music.GetPlayer(Context.Guild.Id)) == null) + if ((musicPlayer = _music.GetPlayer(Context.Guild.Id)) == null) return Task.CompletedTask; if (((IGuildUser)Context.User).VoiceChannel == musicPlayer.PlaybackVoiceChannel) { @@ -125,10 +125,10 @@ namespace NadekoBot.Modules.Music public Task Destroy() { MusicPlayer musicPlayer; - if ((musicPlayer = music.GetPlayer(Context.Guild.Id)) == null) + if ((musicPlayer = _music.GetPlayer(Context.Guild.Id)) == null) return Task.CompletedTask; if (((IGuildUser)Context.User).VoiceChannel == musicPlayer.PlaybackVoiceChannel) - music.DestroyPlayer(Context.Guild.Id); + _music.DestroyPlayer(Context.Guild.Id); return Task.CompletedTask; @@ -139,7 +139,7 @@ namespace NadekoBot.Modules.Music public Task Pause() { MusicPlayer musicPlayer; - if ((musicPlayer = music.GetPlayer(Context.Guild.Id)) == null) + if ((musicPlayer = _music.GetPlayer(Context.Guild.Id)) == null) return Task.CompletedTask; if (((IGuildUser)Context.User).VoiceChannel != musicPlayer.PlaybackVoiceChannel) return Task.CompletedTask; @@ -153,7 +153,7 @@ namespace NadekoBot.Modules.Music { var channel = (ITextChannel)Context.Channel; MusicPlayer musicPlayer; - if ((musicPlayer = music.GetPlayer(Context.Guild.Id)) == null) + if ((musicPlayer = _music.GetPlayer(Context.Guild.Id)) == null) return; if (((IGuildUser)Context.User).VoiceChannel != musicPlayer.PlaybackVoiceChannel) return; @@ -173,7 +173,7 @@ 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); + await _music.QueueSong(((IGuildUser)Context.User), (ITextChannel)Context.Channel, ((IGuildUser)Context.User).VoiceChannel, query).ConfigureAwait(false); if ((await Context.Guild.GetCurrentUserAsync()).GetPermissions((IGuildChannel)Context.Channel).ManageMessages) { Context.Message.DeleteAfter(10); @@ -184,7 +184,7 @@ namespace NadekoBot.Modules.Music [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); + 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); @@ -197,7 +197,7 @@ namespace NadekoBot.Modules.Music { Song currentSong; MusicPlayer musicPlayer; - if ((musicPlayer = music.GetPlayer(Context.Guild.Id)) == null) + if ((musicPlayer = _music.GetPlayer(Context.Guild.Id)) == null) return; if ((currentSong = musicPlayer?.CurrentSong) == null) { @@ -250,7 +250,7 @@ namespace NadekoBot.Modules.Music return embed; }; - await Context.Channel.SendPaginatedConfirmAsync(page, printAction, lastPage, false).ConfigureAwait(false); + await Context.Channel.SendPaginatedConfirmAsync(_client, page, printAction, lastPage, false).ConfigureAwait(false); } [NadekoCommand, Usage, Description, Aliases] @@ -258,7 +258,7 @@ namespace NadekoBot.Modules.Music public async Task NowPlaying() { MusicPlayer musicPlayer; - if ((musicPlayer = music.GetPlayer(Context.Guild.Id)) == null) + if ((musicPlayer = _music.GetPlayer(Context.Guild.Id)) == null) return; var currentSong = musicPlayer.CurrentSong; if (currentSong == null) @@ -279,7 +279,7 @@ namespace NadekoBot.Modules.Music public async Task Volume(int val) { MusicPlayer musicPlayer; - if ((musicPlayer = music.GetPlayer(Context.Guild.Id)) == null) + if ((musicPlayer = _music.GetPlayer(Context.Guild.Id)) == null) return; if (((IGuildUser)Context.User).VoiceChannel != musicPlayer.PlaybackVoiceChannel) return; @@ -301,7 +301,7 @@ namespace NadekoBot.Modules.Music await ReplyErrorLocalized("volume_input_invalid").ConfigureAwait(false); return; } - using (var uow = DbHandler.UnitOfWork()) + using (var uow = _db.UnitOfWork) { uow.GuildConfigs.For(Context.Guild.Id, set => set).DefaultMusicVolume = val / 100.0f; uow.Complete(); @@ -314,7 +314,7 @@ namespace NadekoBot.Modules.Music public async Task ShufflePlaylist() { MusicPlayer musicPlayer; - if ((musicPlayer = music.GetPlayer(Context.Guild.Id)) == null) + if ((musicPlayer = _music.GetPlayer(Context.Guild.Id)) == null) return; if (((IGuildUser)Context.User).VoiceChannel != musicPlayer.PlaybackVoiceChannel) return; @@ -338,13 +338,13 @@ namespace NadekoBot.Modules.Music await ReplyErrorLocalized("must_be_in_voice").ConfigureAwait(false); return; } - var plId = (await NadekoBot.Google.GetPlaylistIdsByKeywordsAsync(arg).ConfigureAwait(false)).FirstOrDefault(); + var plId = (await _google.GetPlaylistIdsByKeywordsAsync(arg).ConfigureAwait(false)).FirstOrDefault(); if (plId == null) { await ReplyErrorLocalized("no_search_results").ConfigureAwait(false); return; } - var ids = await NadekoBot.Google.GetPlaylistTracksAsync(plId, 500).ConfigureAwait(false); + var ids = await _google.GetPlaylistTracksAsync(plId, 500).ConfigureAwait(false); if (!ids.Any()) { await ReplyErrorLocalized("no_search_results").ConfigureAwait(false); @@ -366,7 +366,7 @@ namespace NadekoBot.Modules.Music return; try { - await music.QueueSong(gusr, (ITextChannel)Context.Channel, gusr.VoiceChannel, id, true).ConfigureAwait(false); + await _music.QueueSong(gusr, (ITextChannel)Context.Channel, gusr.VoiceChannel, id, true).ConfigureAwait(false); } catch (SongNotFoundException) { } catch { try { cancelSource.Cancel(); } catch { } } @@ -391,11 +391,11 @@ namespace NadekoBot.Modules.Music using (var http = new HttpClient()) { - var scvids = JObject.Parse(await http.GetStringAsync($"http://api.soundcloud.com/resolve?url={pl}&client_id={NadekoBot.Credentials.SoundCloudClientId}").ConfigureAwait(false))["tracks"].ToObject(); - await music.QueueSong(((IGuildUser)Context.User), (ITextChannel)Context.Channel, ((IGuildUser)Context.User).VoiceChannel, scvids[0].TrackLink).ConfigureAwait(false); + var scvids = JObject.Parse(await http.GetStringAsync($"http://api.soundcloud.com/resolve?url={pl}&client_id={_creds.SoundCloudClientId}").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) + if ((musicPlayer = _music.GetPlayer(Context.Guild.Id)) == null) return; foreach (var svideo in scvids.Skip(1)) @@ -406,7 +406,7 @@ namespace NadekoBot.Modules.Music { Title = svideo.FullName, Provider = "SoundCloud", - Uri = svideo.StreamLink, + Uri = svideo.GetStreamLink(_creds), ProviderType = MusicType.Normal, Query = svideo.TrackLink, }), ((IGuildUser)Context.User).Username); @@ -433,7 +433,7 @@ namespace NadekoBot.Modules.Music { try { - await music.QueueSong(gusr, (ITextChannel)Context.Channel, gusr.VoiceChannel, file.FullName, true, MusicType.Local).ConfigureAwait(false); + await _music.QueueSong(gusr, (ITextChannel)Context.Channel, gusr.VoiceChannel, file.FullName, true, MusicType.Local).ConfigureAwait(false); } catch (PlaylistFullException) { @@ -457,7 +457,7 @@ namespace NadekoBot.Modules.Music 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); + 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); @@ -473,7 +473,7 @@ namespace NadekoBot.Modules.Music 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); + await _music.QueueSong(((IGuildUser)Context.User), (ITextChannel)Context.Channel, ((IGuildUser)Context.User).VoiceChannel, path, musicType: MusicType.Local).ConfigureAwait(false); } @@ -495,7 +495,7 @@ namespace NadekoBot.Modules.Music public Task Remove(int num) { MusicPlayer musicPlayer; - if ((musicPlayer = music.GetPlayer(Context.Guild.Id)) == null) + if ((musicPlayer = _music.GetPlayer(Context.Guild.Id)) == null) return Task.CompletedTask; if (((IGuildUser)Context.User).VoiceChannel != musicPlayer.PlaybackVoiceChannel) return Task.CompletedTask; @@ -512,7 +512,7 @@ namespace NadekoBot.Modules.Music if (all.Trim().ToUpperInvariant() != "ALL") return; MusicPlayer musicPlayer; - if ((musicPlayer = music.GetPlayer(Context.Guild.Id)) == null) + if ((musicPlayer = _music.GetPlayer(Context.Guild.Id)) == null) return; musicPlayer.ClearQueue(); await ReplyConfirmLocalized("queue_cleared").ConfigureAwait(false); @@ -526,7 +526,7 @@ namespace NadekoBot.Modules.Music return; MusicPlayer musicPlayer; - if ((musicPlayer = music.GetPlayer(Context.Guild.Id)) == null) + if ((musicPlayer = _music.GetPlayer(Context.Guild.Id)) == null) return; fromto = fromto?.Trim(); @@ -569,7 +569,7 @@ namespace NadekoBot.Modules.Music public async Task SetMaxQueue(uint size = 0) { MusicPlayer musicPlayer; - if ((musicPlayer = music.GetPlayer(Context.Guild.Id)) == null) + if ((musicPlayer = _music.GetPlayer(Context.Guild.Id)) == null) return; musicPlayer.MaxQueueSize = size; @@ -589,7 +589,7 @@ namespace NadekoBot.Modules.Music var channel = (ITextChannel)Context.Channel; MusicPlayer musicPlayer; - if ((musicPlayer = music.GetPlayer(Context.Guild.Id)) == null) + if ((musicPlayer = _music.GetPlayer(Context.Guild.Id)) == null) return; musicPlayer.MaxPlaytimeSeconds = seconds; if (seconds == 0) @@ -603,7 +603,7 @@ namespace NadekoBot.Modules.Music public async Task ReptCurSong() { MusicPlayer musicPlayer; - if ((musicPlayer = music.GetPlayer(Context.Guild.Id)) == null) + if ((musicPlayer = _music.GetPlayer(Context.Guild.Id)) == null) return; var currentSong = musicPlayer.CurrentSong; if (currentSong == null) @@ -626,7 +626,7 @@ namespace NadekoBot.Modules.Music public async Task RepeatPl() { MusicPlayer musicPlayer; - if ((musicPlayer = music.GetPlayer(Context.Guild.Id)) == null) + if ((musicPlayer = _music.GetPlayer(Context.Guild.Id)) == null) return; var currentValue = musicPlayer.ToggleRepeatPlaylist(); if(currentValue) @@ -640,7 +640,7 @@ namespace NadekoBot.Modules.Music public async Task Save([Remainder] string name) { MusicPlayer musicPlayer; - if ((musicPlayer = music.GetPlayer(Context.Guild.Id)) == null) + if ((musicPlayer = _music.GetPlayer(Context.Guild.Id)) == null) return; var curSong = musicPlayer.CurrentSong; @@ -655,7 +655,7 @@ namespace NadekoBot.Modules.Music }).ToList(); MusicPlaylist playlist; - using (var uow = DbHandler.UnitOfWork()) + using (var uow = _db.UnitOfWork) { playlist = new MusicPlaylist { @@ -679,7 +679,7 @@ namespace NadekoBot.Modules.Music public async Task Load([Remainder] int id) { MusicPlaylist mpl; - using (var uow = DbHandler.UnitOfWork()) + using (var uow = _db.UnitOfWork) { mpl = uow.MusicPlaylists.GetWithSongs(id); } @@ -696,7 +696,7 @@ namespace NadekoBot.Modules.Music var usr = (IGuildUser)Context.User; try { - await music.QueueSong(usr, (ITextChannel)Context.Channel, usr.VoiceChannel, item.Query, true, item.ProviderType).ConfigureAwait(false); + await _music.QueueSong(usr, (ITextChannel)Context.Channel, usr.VoiceChannel, item.Query, true, item.ProviderType).ConfigureAwait(false); } catch (SongNotFoundException) { } catch { break; } @@ -714,7 +714,7 @@ namespace NadekoBot.Modules.Music List playlists; - using (var uow = DbHandler.UnitOfWork()) + using (var uow = _db.UnitOfWork) { playlists = uow.MusicPlaylists.GetPlaylistsOnPage(num); } @@ -735,13 +735,13 @@ namespace NadekoBot.Modules.Music var success = false; try { - using (var uow = DbHandler.UnitOfWork()) + using (var uow = _db.UnitOfWork) { var pl = uow.MusicPlaylists.Get(id); if (pl != null) { - if (NadekoBot.Credentials.IsOwner(Context.User) || pl.AuthorId == Context.User.Id) + if (_creds.IsOwner(Context.User) || pl.AuthorId == Context.User.Id) { uow.MusicPlaylists.Remove(pl); await uow.CompleteAsync().ConfigureAwait(false); @@ -766,7 +766,7 @@ namespace NadekoBot.Modules.Music public async Task Goto(int time) { MusicPlayer musicPlayer; - if ((musicPlayer = music.GetPlayer(Context.Guild.Id)) == null) + if ((musicPlayer = _music.GetPlayer(Context.Guild.Id)) == null) return; if (((IGuildUser)Context.User).VoiceChannel != musicPlayer.PlaybackVoiceChannel) return; @@ -801,7 +801,7 @@ namespace NadekoBot.Modules.Music public async Task Autoplay() { MusicPlayer musicPlayer; - if ((musicPlayer = music.GetPlayer(Context.Guild.Id)) == null) + if ((musicPlayer = _music.GetPlayer(Context.Guild.Id)) == null) return; if (!musicPlayer.ToggleAutoplay()) @@ -816,7 +816,7 @@ namespace NadekoBot.Modules.Music public async Task SetMusicChannel() { MusicPlayer musicPlayer; - if ((musicPlayer = music.GetPlayer(Context.Guild.Id)) == null) + if ((musicPlayer = _music.GetPlayer(Context.Guild.Id)) == null) { await ReplyErrorLocalized("no_player").ConfigureAwait(false); return; diff --git a/src/NadekoBot/Modules/NSFW/NSFW.cs b/src/NadekoBot/Modules/NSFW/NSFW.cs index 16834288..8a5182d2 100644 --- a/src/NadekoBot/Modules/NSFW/NSFW.cs +++ b/src/NadekoBot/Modules/NSFW/NSFW.cs @@ -15,7 +15,6 @@ using NadekoBot.Services.Searches; namespace NadekoBot.Modules.NSFW { - [NadekoModule("NSFW")] public class NSFW : NadekoTopLevelModule { private static readonly ConcurrentDictionary _autoHentaiTimers = new ConcurrentDictionary(); diff --git a/src/NadekoBot/Modules/NadekoModule.cs b/src/NadekoBot/Modules/NadekoModule.cs index 6310f574..6fc26e2a 100644 --- a/src/NadekoBot/Modules/NadekoModule.cs +++ b/src/NadekoBot/Modules/NadekoModule.cs @@ -17,7 +17,6 @@ namespace NadekoBot.Modules public readonly string ModuleTypeName; public readonly string LowerModuleTypeName; - //todo :thinking: public NadekoStrings _strings { get; set; } public ILocalization _localization { get; set; } diff --git a/src/NadekoBot/Modules/Utility/Utility.cs b/src/NadekoBot/Modules/Utility/Utility.cs index 928c304f..102f717a 100644 --- a/src/NadekoBot/Modules/Utility/Utility.cs +++ b/src/NadekoBot/Modules/Utility/Utility.cs @@ -20,7 +20,6 @@ using System.Diagnostics; namespace NadekoBot.Modules.Utility { - [NadekoModule("Utility")] public partial class Utility : NadekoTopLevelModule { private static ConcurrentDictionary _rotatingRoleColors = new ConcurrentDictionary(); diff --git a/src/NadekoBot/NadekoBot.cs b/src/NadekoBot/NadekoBot.cs index 52b491d5..bb105ec1 100644 --- a/src/NadekoBot/NadekoBot.cs +++ b/src/NadekoBot/NadekoBot.cs @@ -18,6 +18,8 @@ using NadekoBot.Services.Database.Models; using System.Threading; using NadekoBot.Modules.Utility; using NadekoBot.Services.Searches; +using NadekoBot.Services.ClashOfClans; +using NadekoBot.Services.Music; namespace NadekoBot { @@ -75,7 +77,6 @@ namespace NadekoBot var greetSettingsService = new GreetSettingsService(AllGuildConfigs, db); var localization = new Localization(BotConfig.Locale, AllGuildConfigs.ToDictionary(x => x.GuildId, x => x.Locale), db); - //var musicService = new MusicService(google, strings, localization); var commandService = new CommandService(new CommandServiceConfig() { @@ -89,9 +90,15 @@ namespace NadekoBot var images = new ImagesService(); + var currencyHandler = new CurrencyHandler(BotConfig, db); + + var soundcloud = new SoundCloudApiService(credentials); + //module services var utilityService = new UtilityService(AllGuildConfigs, Client, BotConfig, db); var searchesService = new SearchesService(); + var clashService = new ClashOfClansService(Client, db, localization, strings); + var musicService = new MusicService(google, strings, localization, db, soundcloud, credentials); //initialize Services Services = new NServiceProvider.ServiceProviderBuilder() //todo all Adds should be interfaces @@ -105,12 +112,15 @@ namespace NadekoBot .Add(strings) .Add(Client) .Add(greetSettingsService) - //.Add(musicService) + .Add(BotConfig) + .Add(currencyHandler) + .Add(musicService) .Add(commandHandler) .Add(db) //modules .Add(utilityService) .Add(searchesService) + .Add(clashService) .Build(); commandHandler.AddServices(Services); diff --git a/src/NadekoBot/NadekoBot.csproj b/src/NadekoBot/NadekoBot.csproj index b829edd5..2e78356c 100644 --- a/src/NadekoBot/NadekoBot.csproj +++ b/src/NadekoBot/NadekoBot.csproj @@ -29,35 +29,26 @@ - - - + - - - - + - - - - + - PreserveNewest @@ -105,4 +96,8 @@ + + + + diff --git a/src/NadekoBot/Services/ClashOfClans/ClashOfClansService.cs b/src/NadekoBot/Services/ClashOfClans/ClashOfClansService.cs new file mode 100644 index 00000000..fadbf637 --- /dev/null +++ b/src/NadekoBot/Services/ClashOfClans/ClashOfClansService.cs @@ -0,0 +1,263 @@ +using Discord; +using Discord.WebSocket; +using NadekoBot.Extensions; +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, 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 everything that has to do with strings + // shouldn't be here + public class ClashOfClansService + { + private readonly DiscordShardedClient _client; + private readonly DbHandler _db; + private readonly ILocalization _localization; + private readonly NadekoStrings _strings; + private readonly Timer checkWarTimer; + + public ConcurrentDictionary> ClashWars { get; set; } + + public ClashOfClansService(DiscordShardedClient client, DbHandler db, ILocalization localization, NadekoStrings strings) + { + _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())); + } + + 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, + }); + } + Console.WriteLine(cw.Bases.Capacity); + 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 new file mode 100644 index 00000000..635557cf --- /dev/null +++ b/src/NadekoBot/Services/ClashOfClans/Extensions.cs @@ -0,0 +1,40 @@ +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/CustomReactions/CustomReactions.cs b/src/NadekoBot/Services/CustomReactions/CustomReactions.cs new file mode 100644 index 00000000..d2a3e08e --- /dev/null +++ b/src/NadekoBot/Services/CustomReactions/CustomReactions.cs @@ -0,0 +1,88 @@ +using Discord; +using Discord.WebSocket; +using NadekoBot.Services.Database.Models; +using NLog; +using System.Collections.Concurrent; +using System.Diagnostics; +using System.Linq; + +namespace NadekoBot.Services +{ + public class CustomReactions + { + private CustomReaction[] _globalReactions = new CustomReaction[] { }; + public CustomReaction[] GlobalReactions => _globalReactions; + public ConcurrentDictionary GuildReactions { get; } = new ConcurrentDictionary(); + + public ConcurrentDictionary ReactionStats { get; } = new ConcurrentDictionary(); + + private readonly Logger _log; + private readonly DbHandler _db; + + public CustomReactions(DbHandler db) + { + _log = LogManager.GetCurrentClassLogger(); + _db = db; + 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(); + + public CustomReaction TryGetCustomReaction(IUserMessage umsg) + { + var channel = umsg.Channel as SocketTextChannel; + if (channel == null) + return null; + + var content = umsg.Content.Trim().ToLowerInvariant(); + CustomReaction[] reactions; + + GuildReactions.TryGetValue(channel.Guild.Id, out reactions); + if (reactions != null && reactions.Any()) + { + var rs = reactions.Where(cr => + { + if (cr == null) + return false; + + var hasTarget = cr.Response.ToLowerInvariant().Contains("%target%"); + var trigger = cr.TriggerWithContext(umsg).Trim().ToLowerInvariant(); + return ((hasTarget && content.StartsWith(trigger + " ")) || content == trigger); + }).ToArray(); + + if (rs.Length != 0) + { + var reaction = rs[new NadekoRandom().Next(0, rs.Length)]; + if (reaction != null) + { + if (reaction.Response == "-") + return null; + return reaction; + } + } + } + + var grs = GlobalReactions.Where(cr => + { + if (cr == null) + return false; + var hasTarget = cr.Response.ToLowerInvariant().Contains("%target%"); + var trigger = cr.TriggerWithContext(umsg).Trim().ToLowerInvariant(); + return ((hasTarget && content.StartsWith(trigger + " ")) || content == trigger); + }).ToArray(); + if (grs.Length == 0) + return null; + var greaction = grs[new NadekoRandom().Next(0, grs.Length)]; + + return greaction; + } + } +} diff --git a/src/NadekoBot/Modules/CustomReactions/Extensions.cs b/src/NadekoBot/Services/CustomReactions/Extensions.cs similarity index 84% rename from src/NadekoBot/Modules/CustomReactions/Extensions.cs rename to src/NadekoBot/Services/CustomReactions/Extensions.cs index b40e3fa8..fdcd9f54 100644 --- a/src/NadekoBot/Modules/CustomReactions/Extensions.cs +++ b/src/NadekoBot/Services/CustomReactions/Extensions.cs @@ -1,13 +1,12 @@ using Discord; -using Discord.WebSocket; +using NadekoBot.DataStructures; using NadekoBot.Extensions; using NadekoBot.Services; using NadekoBot.Services.Database.Models; using System; using System.Collections.Generic; -using System.Diagnostics; -using System.Linq; using System.Text.RegularExpressions; +using System.Threading.Tasks; namespace NadekoBot.Modules.CustomReactions { @@ -103,5 +102,19 @@ namespace NadekoBot.Modules.CustomReactions public static string ResponseWithContext(this CustomReaction cr, IUserMessage ctx) => cr.Response.ResolveResponseString(ctx, cr.Trigger.ResolveTriggerString(ctx)); + + public static async Task Send(this CustomReaction cr, IUserMessage context) + { + var channel = cr.DmResponse ? await context.Author.CreateDMChannelAsync() : context.Channel; + + CustomReactions.ReactionStats.AddOrUpdate(cr.Trigger, 1, (k, old) => ++old); + + CREmbed crembed; + if (CREmbed.TryParse(cr.Response, out crembed)) + { + return await channel.EmbedAsync(crembed.ToEmbed(), crembed.PlainText ?? ""); + } + return await channel.SendMessageAsync(cr.ResponseWithContext(context).SanitizeMentions()); + } } } diff --git a/src/NadekoBot/Services/Database/Models/ClashWar.cs b/src/NadekoBot/Services/Database/Models/ClashWar.cs index e38f6337..aa1baca1 100644 --- a/src/NadekoBot/Services/Database/Models/ClashWar.cs +++ b/src/NadekoBot/Services/Database/Models/ClashWar.cs @@ -7,15 +7,6 @@ namespace NadekoBot.Services.Database.Models { public class ClashWar : DbEntity { - public enum DestroyStars - { - One, Two, Three - } - public enum StateOfWar - { - Started, Ended, Created - } - public string EnemyClan { get; set; } public int Size { get; set; } public StateOfWar WarState { get; set; } = StateOfWar.Created; @@ -29,4 +20,13 @@ namespace NadekoBot.Services.Database.Models public List Bases { get; set; } = new List(); } + + public enum DestroyStars + { + One, Two, Three + } + public enum StateOfWar + { + Started, Ended, Created + } } diff --git a/src/NadekoBot/Services/Music/MusicControls.cs b/src/NadekoBot/Services/Music/MusicControls.cs index 2687e99b..705a224a 100644 --- a/src/NadekoBot/Services/Music/MusicControls.cs +++ b/src/NadekoBot/Services/Music/MusicControls.cs @@ -8,12 +8,10 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; using NLog; -using NadekoBot.Services.Music; -using NadekoBot.Services; +using NadekoBot.Services.Database.Models; namespace NadekoBot.Services.Music { - public enum StreamState { Resolving, @@ -24,7 +22,6 @@ namespace NadekoBot.Services.Music public class MusicPlayer { - public const string MusicDataPath = "data/musicdata"; private IAudioClient AudioClient { get; set; } /// diff --git a/src/NadekoBot/Services/Music/MusicService.cs b/src/NadekoBot/Services/Music/MusicService.cs index 3c85607b..258c436d 100644 --- a/src/NadekoBot/Services/Music/MusicService.cs +++ b/src/NadekoBot/Services/Music/MusicService.cs @@ -6,23 +6,45 @@ using Discord; using NadekoBot.Extensions; using NadekoBot.Modules; using NadekoBot.Services.Impl; +using NadekoBot.Services.Database.Models; +using System.Text.RegularExpressions; +using NLog; +using System.IO; +using VideoLibrary; +using System.Net.Http; namespace NadekoBot.Services.Music { public class MusicService { + public const string MusicDataPath = "data/musicdata"; + private readonly IGoogleApiService _google; private readonly NadekoStrings _strings; private readonly ILocalization _localization; private GoogleApiService google; + private readonly DbHandler _db; + private readonly Logger _log; + private readonly SoundCloudApiService _sc; + private readonly IBotCredentials _creds; public ConcurrentDictionary MusicPlayers { get; } = new ConcurrentDictionary(); - public MusicService(IGoogleApiService google, NadekoStrings strings, ILocalization localization) + public MusicService(IGoogleApiService google, + NadekoStrings strings, ILocalization localization, DbHandler db, + SoundCloudApiService sc, IBotCredentials creds) { _google = google; _strings = strings; _localization = localization; + _db = db; + _sc = sc; + _creds = creds; + _log = LogManager.GetCurrentClassLogger(); + + try { Directory.Delete(MusicDataPath, true); } catch { } + + Directory.CreateDirectory(MusicDataPath); } public MusicPlayer GetPlayer(ulong guildId) @@ -39,7 +61,7 @@ namespace NadekoBot.Services.Music return MusicPlayers.GetOrAdd(guildId, server => { float vol;// SpecificConfigurations.Default.Of(server.Id).DefaultMusicVolume; - using (var uow = DbHandler.UnitOfWork()) + using (var uow = _db.UnitOfWork) { //todo move to cached variable vol = uow.GuildConfigs.For(guildId, set => set).DefaultMusicVolume; @@ -168,7 +190,7 @@ namespace NadekoBot.Services.Music try { musicPlayer.ThrowIfQueueFull(); - resolvedSong = await SongHandler.ResolveSong(query, musicType).ConfigureAwait(false); + resolvedSong = await ResolveSong(query, musicType).ConfigureAwait(false); if (resolvedSong == null) throw new SongNotFoundException(); @@ -213,5 +235,202 @@ namespace NadekoBot.Services.Music } + 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 = svideo.GetStreamLink(_creds), + 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 = svideo.GetStreamLink(_creds), + ProviderType = MusicType.Soundcloud, + Query = svideo.TrackLink, + AlbumArt = svideo.artwork_url, + }) + { TotalTime = TimeSpan.FromMilliseconds(svideo.Duration) }; + } + + var link = (await _google.GetVideosByKeywordsAsync(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")); } } diff --git a/src/NadekoBot/Services/Music/Song.cs b/src/NadekoBot/Services/Music/Song.cs index 77394349..ea94508b 100644 --- a/src/NadekoBot/Services/Music/Song.cs +++ b/src/NadekoBot/Services/Music/Song.cs @@ -10,6 +10,7 @@ using System.Threading; using System.Threading.Tasks; using System.Net; using Discord; +using NadekoBot.Services.Database.Models; namespace NadekoBot.Services.Music { @@ -144,7 +145,7 @@ namespace NadekoBot.Services.Music public async Task Play(IAudioClient voiceClient, CancellationToken cancelToken) { BytesSent = (ulong) SkipTo * 3840 * 50; - var filename = Path.Combine(MusicPlayer.MusicDataPath, DateTime.Now.UnixTimestamp().ToString()); + var filename = Path.Combine(MusicService.MusicDataPath, DateTime.Now.UnixTimestamp().ToString()); var inStream = new SongBuffer(MusicPlayer, filename, SongInfo, SkipTo, _frameBytes * 100); var bufferTask = inStream.BufferSong(cancelToken).ConfigureAwait(false); diff --git a/src/NadekoBot/Services/Music/SongHandler.cs b/src/NadekoBot/Services/Music/SongHandler.cs index 9ef4aae0..f716196b 100644 --- a/src/NadekoBot/Services/Music/SongHandler.cs +++ b/src/NadekoBot/Services/Music/SongHandler.cs @@ -1,214 +1,9 @@ -using System; -using System.IO; -using System.Linq; -using System.Net.Http; -using System.Text.RegularExpressions; -using System.Threading.Tasks; -using NLog; -using VideoLibrary; +using NLog; namespace NadekoBot.Services.Music { public static class SongHandler { private static readonly Logger _log = LogManager.GetCurrentClassLogger(); - public static 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 }; - } - var sc = SoundCloud.GetInstance(_creds); - if (SoundCloud.Default.IsSoundCloudLink(query)) - { - var svideo = await SoundCloud.Default.ResolveVideoAsync(query).ConfigureAwait(false); - return new Song(new SongInfo - { - Title = svideo.FullName, - Provider = "SoundCloud", - Uri = svideo.StreamLink, - ProviderType = musicType, - Query = svideo.TrackLink, - AlbumArt = svideo.artwork_url, - }) - { TotalTime = TimeSpan.FromMilliseconds(svideo.Duration) }; - } - - if (musicType == MusicType.Soundcloud) - { - var svideo = await SoundCloud.Default.GetVideoByQueryAsync(query).ConfigureAwait(false); - return new Song(new SongInfo - { - Title = svideo.FullName, - Provider = "SoundCloud", - Uri = svideo.StreamLink, - ProviderType = MusicType.Soundcloud, - Query = svideo.TrackLink, - AlbumArt = svideo.artwork_url, - }) - { TotalTime = TimeSpan.FromMilliseconds(svideo.Duration) }; - } - - var link = (await NadekoBot.Google.GetVideosByKeywordsAsync(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 static 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 static bool IsRadioLink(string query) => - (query.StartsWith("http") || - query.StartsWith("ww")) - && - (query.Contains(".pls") || - query.Contains(".m3u") || - query.Contains(".asx") || - query.Contains(".xspf")); } } diff --git a/src/NadekoBot/Services/Music/SoundCloud.cs b/src/NadekoBot/Services/Music/SoundCloudApiService.cs similarity index 90% rename from src/NadekoBot/Services/Music/SoundCloud.cs rename to src/NadekoBot/Services/Music/SoundCloudApiService.cs index 9d18dc2f..34240970 100644 --- a/src/NadekoBot/Services/Music/SoundCloud.cs +++ b/src/NadekoBot/Services/Music/SoundCloudApiService.cs @@ -6,16 +6,11 @@ using System.Threading.Tasks; namespace NadekoBot.Services.Music { - public class SoundCloud + public class SoundCloudApiService { private readonly IBotCredentials _creds; - //todo make a service - private static SoundCloud _instance = null; - public static SoundCloud GetInstance(IBotCredentials creds) => _instance ?? (_instance = new SoundCloud(creds)); - - static SoundCloud() { } - public SoundCloud(IBotCredentials creds) + public SoundCloudApiService(IBotCredentials creds) { _creds = creds; } @@ -36,7 +31,7 @@ namespace NadekoBot.Services.Music } - var responseObj = Newtonsoft.Json.JsonConvert.DeserializeObject(response); + var responseObj = JsonConvert.DeserializeObject(response); if (responseObj?.Kind != "track") throw new InvalidOperationException("Url is either not a track, or it doesn't exist."); @@ -84,7 +79,7 @@ namespace NadekoBot.Services.Music } public class SoundCloudUser { - [Newtonsoft.Json.JsonProperty("username")] + [JsonProperty("username")] public string Name { get; set; } } /*